6

We want to have a collection of controllers where we route logger output from all actions and downstream methods to a separate log file. This is a Rails 3 project. In Rails 2 we did this by redefining the "logger" method but in Rails 3, the way to log is using "Rails.logger". I tried putting

Rails::logger = Logger.new(File.join(Rails.root, 'log', "reports_controller.log"), 10, 1000000) 

at the top of the controller, but only the specific cases where Rails.logger is used specifically in the action is getting sent to the specified log file, all of the default log output for the controller is still routing to the main log file.

How can we route all log output for a specific controller to a specific log file to include all default controller output?

By default controller output, I am referring to all of the messages that start with the very beginning of the inbound request

Started POST "/api/v1/reports.json" for 10.XXX.XX.XX at 2015-03-07 01:30:22 +0000
Processing by Api::V1::ReportsController#create as JSON
  Parameters: {"report"=>{"cloud_file_path"=>"report.zip", "company_id"=>nil, "created_at"=>"2015-03-07T01:30:17Z", "type_id"=>"2", "updated_at"=>"2015-03-07T01:30:17Z", "master"=>"1"}}

and all log output that might follow from the inbound request and outbound response in the controller, etc.

Basically I want all logging for the reports controller to be in the reports_controller.log and I don't want any messages for traffic to the reports controller to show up in the main log (i.e. production.log if in production)

Update:

Thanks to @mudasobwa's help with his answer and chat, I was able to solve this using middleware as his answer depicts (although I had to change my insert_before to be before Rails::Rack::Logger)

the revised answer from him that solved it for me is below and it lives in config/initializers/logger_middleware.rb

module MyApp
  class LoggerMiddleware

    REPORTS_API_CONTROLLER_PATH = %r|\A/api/v.*/reports/.*|
    REPORTS_API_CONTROLLER_LOGFILE = "reports_controller.log"

    def initialize(app)
      @app, @logger = app, Rails::logger.instance_variable_get(:@logger).instance_variable_get(:@log)
      @reports_api_controller_logger = Logger.new(Rails.root.join('log', REPORTS_API_CONTROLLER_LOGFILE), 10, 1000000)
    end

    def call(env)
      Rails::logger
           .instance_variable_get(:@logger)
           .instance_variable_set(:@log,
               case env['PATH_INFO']
               when REPORTS_API_CONTROLLER_PATH then
                 @reports_api_controller_logger
               else
                 @logger
               end
           )
      @app.call(env)
    end
  end
end

Rails.application.middleware.insert_before Rails::Rack::Logger, MyApp::LoggerMiddleware
Streamline
  • 2,040
  • 4
  • 37
  • 56

3 Answers3

4

Have you tried prepending an around_filter?

class MyController < ApplicationController
  prepend_around_filter :set_logger

  private

  def set_logger
    old_logger = Rails::logger
    Rails::logger = Logger.new(Rails.root.join('log', "reports_controller.log"), 10, 1000000) 
    yield
    Rails.logger = old_logger
  end
end
messanjah
  • 8,977
  • 4
  • 27
  • 40
  • The result of this is the same as my code example, the explicit logger calls do get routed to the new controller log file however, all of the default logger messages for the controller action still get routed to the environment default log file. Again by default I mean messages that rails writes on its own such as "Started...", "Processing by..." and including database (i.e. if environment is development) messages, etc. – Streamline Mar 14 '15 at 22:10
  • 1
    Hmm - this response inspired me to tinker more and I extended the method to include include `ActiveRecord::Base.logger = new_logger` (and resetting after yield) and this routes the active record database logging to the new specific logger - but I can't seem to get the `Started...` and `Processing by...` and `Completed...` messages to route to the specific log file – Streamline Mar 14 '15 at 23:14
  • @Streamline - did you ever figure out, how to also log the "Started..." and "Processing by..." in the other log-file? – Matthias Oct 21 '15 at 20:05
  • @Mattherick - Unfortunately no. I hope to revisit this soon because the production log is so overloaded its not useful. This answer I am commenting on really only aids in the base level logger redirection not the rest of the mentioned items that are still routed to production.log – Streamline Jan 19 '16 at 00:08
3

The reason that not all the stuff is redirected by controller filter, is that these “Started...” etc are written by rack middleware, that is executed before the controller was even instantiated.

So, to grab and redirect everything related to some condition, one should interfere deeper. Below is a [possibly incomplete] example on how to hack into the pipeline.

Define the middleware to switch loggers

module MyApp
  class MyMiddleware

    def initialize(app)
      @app, @logger = app, Rails.logger
                                .instance_variable_get(:@logger)
                                .instance_variable_get(:@log)
      @my_logger = Logger.new('reports_controller.log', ...)
    end

    def call(env)
      # env['action_dispatch.logger'].inspect
      #⇒ <TaggedLogging... @logger=#<BufferedLogger...> @log_dest=...>

      # here we do not have a controller name
      Rails.logger
           .instance_variable_get(:@logger)
           .instance_variable_set(:@log,
               case env['PATH_INFO'] # or PATH_INFO, or whatever
               when %r|\A/api/v1/| then @my_logger
               else @logger
               end
           )

      @app.call(env)
    end
  end
end

Add an initializer somewhere in config/initializers/my_logger.rb

Rails.application.middleware.insert_before \
    Rails::Rack::Logger, MyApp::MyMiddleware

Please note, that Rails’ logger is a nested beast:

Rails::logger
#⇒ #<ActiveSupport::TaggedLogging:0x000000020f5ad0 @logger=...
Rails::logger.instance_variable_get(:@logger)
#⇒ #<ActiveSupport::BufferedLogger:0x000000020f6188 @log_dest=...
Rails::logger.instance_variable_get(:@logger)
             .instance_variable_get(:@log)
#⇒ #<Logger:0x000000020f6138 @progname=nil, @level=0, @default_formatter=...

One might want to set a specific formatter on the logger, or even filter messages using regular expression there (though it should not be considered a good practice.)

Community
  • 1
  • 1
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • So is this approach by setting the Rails.logger in the middleware based evaluating the REQUEST_URI going to affect all logging from that point forward for that one call? Or is this just one more additional location to catch more? For example after the other answer I noticed I had to redirect ActiveRecord::Base.logger separately if I wanted db logging also routed. Does the rack middleware override and set the logger for that and other places where logger is defined also or will I need to enhance that to include more loggers at that point (or further upstream?). – Streamline Jan 19 '16 at 17:07
  • Setting `Rails::logger.instance_variable_get(:@logger).instance_variable_get(:@log)` affects everything (I do use this in production.) Setting just `Rails::logger` depends on smth I did not dig deeply into (in short: I suggest to set the underlying `Logger`.) – Aleksei Matiushkin Jan 19 '16 at 17:27
  • When you say are setting Rails::logger.instance_variable_get(:@logger).instance_variable_get(:@log) now in production, are you doing that in the same manner as you have posted via middleware and just replacing this line `when %r|\A/api/v1/| then Rails::logger = @my_logger` above with this `when %r|\A/api/v1/| then Rails::logger.instance_variable_get(:@logger).instance_variable_set(:@log,@my_logger)`? – Streamline Jan 19 '16 at 17:56
  • Exactly. Frankly, I do not remember why I am doing so. Maybe I met some glitches with just setting `Rails::logger`, maybe there were other reasons. I reset formatters as well, maybe that’s why. Anyway, I would suggest you to try as above and if it will fail, try with the `logger.logger.log`. – Aleksei Matiushkin Jan 19 '16 at 18:12
  • I think I am close to a testable situation however 1) I don't have Rack::Cache in my list of middlewares (maybe because this is Rails 3.2?) so instead of insert_before Rack::Cache I changed it to insert_after Rails::Rack::Logger like this `Rails.application.middleware.insert_after Rails::Rack::Logger, MyApp::LoggerMiddleWare, :my_app` 2) I can't find in the docs what the last argument should be and its throwing exceptions like `undefined method 'info' for :my_app:Symbol` - so what is that 3rd argument supposed to be where you have :my_app above and where is it documented? – Streamline Jan 19 '16 at 22:32
  • The last argument is the symbolized name of the topmost module in your `config/application.rb`. I tried to google the documentation, but without any success; sorry for that, I did not touch that piece of code for a while already. I have 3.2 as well, inserting after `Rails::Rack::Logger` is fine, but I would suggest you to read about caching and turn one on on your prod. – Aleksei Matiushkin Jan 20 '16 at 08:07
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/101207/discussion-between-streamline-and-mudasobwa). – Streamline Jan 20 '16 at 16:40
  • Thanks @mudasobwa! Per our chat and all of the tinkering, the main change still needed in you post needs to be to insert **before** Rails::Rack::Logger (not Rack::Cache in my case) and to remove that extra unused logger argument in the intiialize method for clarity so the new initializer call is `Rails.application.middleware.insert_before Rails::Rack::Logger, MyApp::MyMiddleware` – Streamline Jan 21 '16 at 00:41
  • I tested this on a rails4 project and it appears the get/set of the default rails logger is different. `Rails::logger.instance_variable_get(:@logger).instance_variable_get(:@log)` returns nil in rails4. Do you know what the equivalent low level setting/getting of the rails logger is for rails4? – Streamline Jan 21 '16 at 20:51
  • I've surfaced this Rails 4 specific twist as a new question http://stackoverflow.com/q/34936358/56082 – Streamline Jan 21 '16 at 23:28
  • I have no Rails4 here by me, but expect my answer during this weekend. I am interested in such kind of research. – Aleksei Matiushkin Jan 22 '16 at 06:59
0

To add on top of @messanjah's answer, you can overwrite these loggers: Rails::logger, ActionView::Base.logger, ActionController::Base.logger, ActiveRecord::Base.logger.

By suppressing these loggers you get rid of most messages. But as @mudasobwa pointed Started ... messages are written before controller.

If you don't want to use a middleware you can always use the hammer and monkeypatch Rails::Rack::Logger to use your logger aplication wide. Or you can check for path and reject certain messages there.

hamdiakoguz
  • 15,795
  • 9
  • 33
  • 27
  • When you say monkeypatch Rails::Rack::Logger application wide, how would that happen 1) differently than the way @mudasobwa is suggesting by using middleware and 2) how to be selective per controller call even though it is before the controller is called other than parsing the REQUEST_URI? – Streamline Jan 19 '16 at 17:09