48

I have a Rails application that has an action invoked frequently enough to be inconvenient when I am developing, as it results in a lot of extra log output I don't care about. How can I get rails not to log anything (controller, action, parameters, completion time, etc.) for just this one action? I'd like to conditionalize it on RAILS_ENV as well, so logs in production are complete.

Thanks!

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
archbishop
  • 1,077
  • 1
  • 10
  • 15
  • Wonder if you could use rack middleware somehow - so you could throw the "Rails.logger.silence" block around the complete request-response when the request matches your pattern. –  Mar 10 '10 at 02:03
  • I looked briefly at this. I might have missed something, but it looked like at the time rack middleware is involved, it doesn't know what controller/action is being called. – archbishop Apr 04 '10 at 17:48
  • http://edgeguides.rubyonrails.org/4_0_release_notes.html ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1. – Anatortoise House Jul 24 '14 at 15:48

10 Answers10

29

You can silence the Rails logger object:

def action
  Rails.logger.silence do
    # Things within this block will not be logged...
  end
end
Josh Delsman
  • 3,022
  • 1
  • 18
  • 28
  • 10
    That's a generally useful pattern, but ActionController::Base logs stuff before and after the action method executes, so that doesn't solve my particular problem. – archbishop Feb 10 '10 at 23:21
  • 2
    This throws a deprecation warning in Rails 3. You can use `silence do ... end` instead – Simon Woker Mar 13 '13 at 15:44
20

Use lograge gem.

Gemfile:

gem 'lograge'

config/application.rb:

config.lograge.enabled = true
config.lograge.ignore_actions = ['StatusController#nginx', ...]
Tombart
  • 30,520
  • 16
  • 123
  • 136
merqlove
  • 3,674
  • 1
  • 23
  • 22
14

The following works with at least Rails 3.1.0:

Make a custom logger that can be silenced:

# selective_logger.rb
class SelectiveLogger < Rails::Rack::Logger

  def initialize  app, opts = {}
    @app = app
    @opts = opts
    @opts[:silenced] ||= []
  end

  def call  env
    if @opts[:silenced].include?(env['PATH_INFO']) || @opts[:silenced].any? {|silencer| silencer.is_a?( Regexp) && silencer.match( env['PATH_INFO']) }
      Rails.logger.silence do
        @app.call env
      end
    else
      super env
    end                        
  end

end

Tell Rails to use it:

# application.rb
config.middleware.swap Rails::Rack::Logger, SelectiveLogger, :silenced => ["/remote/every_minute", %r"^/assets/"]

The example above shows silencing asset serving requests, which in the development environment means less ( and sometimes no) scrolling back is required to see the actual request.

  • 2
    Unfortunately this seems to conflict with the quiet_assets gem (https://github.com/evrone/quiet_assets). With both enabled I get errors like ```NoMethodError: undefined method `collect' for nil:NilClass```, coming from logger.rb ```compute_tags```. – gmcnaughton Dec 01 '12 at 00:20
10

The answer turns out to be a lot harder than I expected, since rails really does provide no hook to do this. Instead, you need to wrap some of the guts of ActionController::Base. In the common base class for my controllers, I do

def silent?(action)
  false
end

# this knows more than I'd like about the internals of process, but
# the other options require knowing even more.  It would have been
# nice to be able to use logger.silence, but there isn't a good
# method to hook that around, due to the way benchmarking logs.

def log_processing_with_silence_logs
  if logger && silent?(action_name) then
    @old_logger_level, logger.level = logger.level, Logger::ERROR
  end

  log_processing_without_silence_logs
end

def process_with_silence_logs(request, response, method = :perform_action, *arguments)
  ret = process_without_silence_logs(request, response, method, *arguments)
  if logger && silent?(action_name) then
    logger.level = @old_logger_level
  end
  ret
end

alias_method_chain :log_processing, :silence_logs
alias_method_chain :process, :silence_logs

then, in the controller with the method I want to suppress logging on:

def silent?(action)
  RAILS_ENV == "development" && ['my_noisy_action'].include?(action)
end
archbishop
  • 1,077
  • 1
  • 10
  • 15
5

You can add the gem to the Gemfile silencer.

gem 'silencer', '>= 1.0.1'

And in your config/initializers/silencer.rb :

  require 'silencer/logger'

  Rails.application.configure do
    config.middleware.swap Rails::Rack::Logger, Silencer::Logger, silence: ['/api/notifications']
  end
micred
  • 1,482
  • 1
  • 19
  • 19
  • For some reason, I could not make this work in Rails 5.0.0.1. I get: No such middleware to insert before: Rails::Rack::Logger (RuntimeError) – Daniel Dec 03 '16 at 09:52
  • Updated to support Rails 5.x @pguardiario. Now it should work ;-) – micred Jun 22 '18 at 09:53
1

With Rails 5 it gets more complicated request processing is logged in several classes. Firstly we need to override call_app in Logger class, let's call this file lib/logger.rb:

# original class:
# https://github.com/rails/rails/blob/master/railties/lib/rails/rack/logger.rb
require 'rails/rack/logger'
module Rails
  module Rack
    class Logger < ActiveSupport::LogSubscriber

      def call_app(request, env) # :doc:
        unless Rails.configuration.logger_exclude.call(request.filtered_path)
          instrumenter = ActiveSupport::Notifications.instrumenter
          instrumenter.start "request.action_dispatch", request: request
          logger.info { started_request_message(request) }
        end
        status, headers, body = @app.call(env)
        body = ::Rack::BodyProxy.new(body) { finish(request) }
        [status, headers, body]
      rescue Exception
        finish(request)
        raise
      ensure
        ActiveSupport::LogSubscriber.flush_all!
      end

    end
  end
end

Then follow with lib/silent_log_subscriber.rb:

require 'active_support/log_subscriber'
require 'action_view/log_subscriber'
require 'action_controller/log_subscriber'
# original class:
# https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/log_subscriber.rb
class SilentLogSubscriber < ActiveSupport::LogSubscriber

  def start_processing(event)
    return unless logger.info?

    payload = event.payload
    return if Rails.configuration.logger_exclude.call(payload[:path])

    params  = payload[:params].except(*ActionController::LogSubscriber::INTERNAL_PARAMS)
    format  = payload[:format]
    format  = format.to_s.upcase if format.is_a?(Symbol)
    info "Processing by #{payload[:controller]}##{payload[:action]} as #{format}"
    info "  Parameters: #{params.inspect}" unless params.empty?
  end

  def process_action(event)
    return if Rails.configuration.logger_exclude.call(event.payload[:path])

    info do
      payload = event.payload
      additions = ActionController::Base.log_process_action(payload)
      status = payload[:status]

      if status.nil? && payload[:exception].present?
        exception_class_name = payload[:exception].first
        status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
      end

      additions << "Allocations: #{event.allocations}" if event.respond_to? :allocations

      message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
      message << " (#{additions.join(" | ")})" unless additions.empty?
      message << "\n\n" if defined?(Rails.env) && Rails.env.development?

      message
    end
  end

  def self.setup
    # unsubscribe default processors
    ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
      case subscriber
      when ActionView::LogSubscriber
        self.unsubscribe(:action_view, subscriber)
      when ActionController::LogSubscriber
        self.unsubscribe(:action_controller, subscriber)
      end
    end
  end

  def self.unsubscribe(component, subscriber)
    events = subscriber.public_methods(false).reject { |method| method.to_s == 'call' }
    events.each do |event|
      ActiveSupport::Notifications.notifier.listeners_for("#{event}.#{component}").each do |listener|
        if listener.instance_variable_get('@delegate') == subscriber
          ActiveSupport::Notifications.unsubscribe listener
        end
      end
    end
  end
end
# subscribe this class
SilentLogSubscriber.attach_to :action_controller
SilentLogSubscriber.setup

Make sure to load modified modules e.g. in config/application.rb after loading rails:

require_relative '../lib/logger'
require_relative '../lib/silent_log_subscriber'

Finally configure excluded paths:

Rails.application.configure do
  config.logger_exclude = ->(path) { path == "/health" }
end

As we're modifying core code of Rails it's always good idea to check original classes in Rails version you're using.

If this looks like too many modifications, you can simply use lograge gem which does pretty much the same with few other modifications. Although the Rack::Loggger code has changed since Rails 3, so you might be loosing some functionality.

Tombart
  • 30,520
  • 16
  • 123
  • 136
  • This options works great, though something is off with importing correct libs when running tests. it constsantly can't find some action_dispatch modules (have tried several different requires for action_dispatch) – Kkulikovskis Mar 09 '20 at 15:30
  • Never mind my comment, found the problem. The problem is that by calling the modified logger class file `logger.rb` and then requiring it in application, it causes some conflicts. I renamed it to `silence_logger.rb` and all problems were resolved – Kkulikovskis Mar 09 '20 at 15:34
1

@neil-stockbridge 's answer not worked for Rails 6.0, I edit some to make it work

# selective_logger.rb
class SelectiveLogger

  def initialize  app, opts = {}
    @app = app
    @opts = opts
    @opts[:silenced] ||= []
  end

  def call  env
    if @opts[:silenced].include?(env['PATH_INFO']) || @opts[:silenced].any? {|silencer| silencer.is_a?( Regexp) && silencer.match( env['PATH_INFO']) }
      Rails.logger.silence do
        @app.call env
      end
    else
        @app.call env
    end                        
  end

end

Test rails app to use it:

# application.rb
config.middleware.swap Rails::Rack::Logger, SelectiveLogger, :silenced => ["/remote/every_minute", %r"^/assets/"]
qichunren
  • 4,405
  • 7
  • 37
  • 48
1

The following works with Rails 2.3.14:

Make a custom logger that can be silenced:

#selective_logger.rb  
require "active_support"

class SelectiveLogger < ActiveSupport::BufferedLogger

  attr_accessor :silent

  def initialize path_to_log_file
    super path_to_log_file
  end

  def add severity, message = nil, progname = nil, &block
    super unless @silent
  end
end

Tell Rails to use it:

#environment.rb
  config.logger = SelectiveLogger.new  config.log_path

Intercept the log output at the beginning of each action and (re)configure the logger depending on whether the action should be silent or not:

#application_controller.rb
  # This method is invoked in order to log the lines that begin "Processing..."
  # for each new request.
  def log_processing
    logger.silent = %w"ping time_zone_table".include? params[:action]
    super
  end
0

Sprockets-rails gem starting from version 3.1.0 introduces implementation of quiet assets. Unfortunately it's not flexible at this moment, but can be extended easy enough.

Create config/initializers/custom_quiet_assets.rb file:

class CustomQuietAssets < ::Sprockets::Rails::QuietAssets
  def initialize(app)
    super
    @assets_regex = %r(\A/{0,2}#{quiet_paths})
  end

  def quiet_paths
    [
      ::Rails.application.config.assets.prefix, # remove if you don't need to quiet assets
      '/ping',
    ].join('|')
  end
end

Add it to middleware in config/application.rb:

# NOTE: that config.assets.quiet must be set to false (its default value).
initializer :quiet_assets do |app|
  app.middleware.insert_before ::Rails::Rack::Logger, CustomQuietAssets
end

Tested with Rails 4.2

0

Rails 6. I had to put this in config/application.rb, inside my app's class definition:

require 'silencer/logger'

initializer 'my_app_name.silence_health_check_request_logging' do |app|
  app.config.middleware.swap(
    Rails::Rack::Logger,
    Silencer::Logger,
    app.config.log_tags,
    silence: %w[/my_health_check_path /my_other_health_check_path],
  )
end

That leaves the log_tags config intact and modifies the middleware before it gets frozen. I would like to put it in config/initializers/ somewhere tucked away but haven't figured out how to do that yet.

Gregory McIntyre
  • 2,051
  • 1
  • 12
  • 7