Logging, monitoring and the collection of metrics, be it performance metrics or important business metrics, is usually a topic that is not often discussed when building and scaling an application from garage-hack to the startup on the front page of TechCrunch. It is essential to be be aware of the current situation of the system and the product performance to prevent FailWhales and maximize revenue and growth.
Having a good logging and monitoring system allows reacting to errors as quickly as possible, helps correlating data to track down issues, gives an overview over performance and allows to predict future performance. Business metrics allow tracking the impact of new features and help understanding users and customers.
Solutions like NewRelic are a good start at high-level monitoring of the application stack and help to detect failures and monitor overall performance. However the data is not detailed enough to help with debugging more complex errors or i.e. figuring out that the reason for the increased response time was a few really long running requests or something that resulted in every request taking longer, for example due to a failing database node.
The goal of this series is to develop a robust system for logging, monitoring and collection of metrics that can easily scale in terms of throughput – i.e. adding more application servers – but is also easy to expand to new types of log data – i.e. by adding a new database or importing external data sources – and makes it easy to modify and analyze the data exactly as needed.
(Subscribe via Email to get informed as soon as the next post in this series is published! Sign up now on top of the sidebar to your right!)
Scalable and Robust Logging for Web Applications
- Log4r for Ruby on Rails
- Log Transport and Distribution with Kafka
- How to Pre-Process Logs with Logstash
- Real Time Analytics with Storm
- Server Monitoring with Sensu
- Metrics with Graphite
- Storing Metrics and Logs
- Visualization
Contents
Rails’ standard logger
Ruby on Rails is great to build things fast. You have an idea today, you can launch next week. You grow to a million users in two months. But debugging errors in production is not always easy, especially when using the default logger.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Started GET "/assets/rails.png" for 127.0.0.1 at 2013-07-18 01:18:09 -0700 Served asset /rails.png - 304 Not Modified (0ms) [2013-07-18 01:18:09] WARN Could not determine content-length of response body. Set content-length of the response or set Response#chunked = true Started GET "/foo" for 127.0.0.1 at 2013-07-18 01:25:07 -0700 Processing by FooController#index as HTML Rendered text template (0.0ms) Completed 503 Service Unavailable in 1ms (Views: 0.4ms | ActiveRecord: 0.0ms) Started GET "/foo/2" for 127.0.0.1 at 2013-07-18 01:25:11 -0700 Processing by FooController#show as HTML Parameters: {"id"=>"2"} Rendered text template (0.0ms) Completed 200 OK in 1ms (Views: 0.6ms | ActiveRecord: 0.0ms) SQL (0.4ms) INSERT INTO `billing_infos` (`billing_method`, `city`, `company_id`, `country`, `created_at`, `email`, `fax_number`, `phone_number`, `postal_code`, `province`, `street`, `updated_at`) VALUES (NULL, 'Montreal', NULL, 'Canada', '2011-10-26 20:53:45', NULL, NULL, '(123) 456 7890', 'H1H1H1', 'QC', '1111 Temp', '2011-10-26 20:53:45') (0.3ms) COMMIT (0.2ms) BEGIN (0.1ms) COMMIT (0.1ms) BEGIN (0.4ms) SELECT 1 FROM `sectors` WHERE (`sectors`.`name` = BINARY 'General 25' AND `sectors`.`id` != 1) LIMIT 1 |
(SQL log output example taken from StackOverflow)
So how can you work with these logs? One approach is just to ssh into the machine and do some shell scripting magic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ ssh devops@amazonServer #get average response time of this server cat log/development.log | grep "^Completed .* in [0-9]*ms .*" | sed "s/.* in \([0-9]*\)ms.*/\1/" | awk '{ s += $1 } END { print "sum: ", s, " average: ", s/NR, " samples: ", NR }' sum: 63 average: 10.5 samples: 6 #one-liner for copy&paste: cat log/development.log| grep "^Completed .* in [0-9]*ms .*" | sed "s/.* in \([0-9]*\)ms.*/\1/" | awk '{ s += $1 } END { print "sum: ", s, " average: ", s/NR, " samples: ", NR }' #Count all 500s on this server: cat log/development.log| grep "^Completed 500 .* in [0-9]*ms .*" 4 |
(The awk part is taken from logfile analysis with awk. Alternatives include more sophisticated gems like request-log-analyzer, however these also do not attack the root cause of the issues at hand – the log format is hard to parse for machines)
These simple metrics are doable via command line, but it still requires connecting to each server and execute these commands. It gets even worse for such simple information like “which URLs did result in 503s”? The url is at the top and the status at the end – in between are the view renderings and SQL queries – which makes it hard to get even to this simple piece of information.
The logfiles are becoming even harder to understand when using a server that spawns multiple instances of a rails app, like Unicorn, and logs all output to the same file. Worst case each thread could have written each line without a prefix. What belongs to which thread now?
Introducing Log4r
Log4r is modeled after the popular Java logging library log4j and allows a lot of customization and fine-tuning of your logging.
“Log4r is a comprehensive and flexible logging library for use in Ruby programs. It features a hierarchical logging system of any number of levels, custom level names, multiple output destinations per log event, custom formatting, and more”
( log4r project description ).
The official log4r website is a little outdated (v 1.1.7) but most of the content still applies to the latest version (1.1.10) that can be found here on github. The rest of the article will detail how to set up Log4r with Ruby on Rails 3.x and two common database adapters, ActiveRecord and mongoid.
Edit:
- Log4r seems to be no longer active. The Logging gem seems to be actively maintained and very close to Log4r, as pointed out in the HackerNews comments.
- Reportedly this article applies to Rails 4 as well.
Log4r Configuration
Basic Setup
Let’s get started with a plain new Ruby on Rails installation and change the default logging step by step to log4r:
1 2 3 4 5 6 7 8 |
$ rails new testApp ... $ cd testApp $ echo "gem 'log4r', '1.1.10'" >> Gemfile $ bundle install ... |
The first step to set up Log4r is to create a config file at config/log4r.yml . It describes the global log levels that the app supports and specifies a basic Outputter, a definition of how the log data is transferred out of the application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
log4r_config: # define all loggers: loggers: - name : rails level : DEBUG trace : 'true' outputters : - console # define all outputters (incl. formatters) outputters: - type: StdoutOutputter name: console formatter: date_pattern: '%H:%M:%S' pattern : '%d %l: %m' type : PatternFormatter |
This basic configuration writes all logs messages with loglevel DEBUG and above to the standard output, usually the console you started rails server in. The next step is initializing the logger in the application. This can be done in config/application.rb . First add the log4r requirements below the existing ones and load the yaml configuration file inside the Application class and set the new log4r logger as default rails logger.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#log4r requirements require 'log4r' require 'log4r/yamlconfigurator' require 'log4r/outputter/datefileoutputter' include Log4r ..... class Application < Rails::Application ... # assign log4r's logger as rails' logger. log4r_config= YAML.load_file(File.join(File.dirname(__FILE__),"log4r.yml")) log_cfg = YamlConfigurator log_cfg.decode_yaml( log4r_config['log4r_config'] ) config.logger = Log4r::Logger['rails'] config.log_level = DEBUG ActiveRecord::Base.logger = Log4r::Logger['rails'] |
Mongoid 3
In oder to direct the logging from mongoid to Log4r, use the following code in an initializer:
1 2 3 4 |
Mongoid.logger = Log4r::Logger['rails'] Moped.logger = Log4r::Logger['rails'] |
Loggers
Right now every log-message is handled through the same logger with the same loglevel. This does not allow for selectively changing the log level based on what has to be debugged. Maybe you want to optimize a mysql query but are not interested in seeing all the rails and mongo logs while you are working on this. Log4r allows you to use multiple loggers which can all be set to operate at a different loglevel.
Let’s create a separate logger for rails, mysql and mongoid:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
log4r_config: # define all loggers: loggers: - name : rails level : DEBUG trace : 'false' outputters : - console - name : mysql level : DEBUG trace : 'false' outputters : - console - name : mongoid level : DEBUG trace : 'false' outputters : - console # define all outputters (incl. formatters) outputters: - type: StdoutOutputter name: console formatter: date_pattern: '%H:%M:%S' pattern : '%d %l: %m' type : PatternFormatter |
Now you can set the loggers in your config/application.rb like this:
1 2 3 4 5 6 7 8 |
#no change to log4r loading code necessary config.logger = Log4r::Logger['rails'] config.log_level = DEBUG ActiveRecord::Base.logger = Log4r::Logger['mysql'] Mongoid.logger = Log4r::Logger['mongoid'] Moped.logger = Log4r::Logger['mongoid'] |
Log Levels
Besides the standard log levels FATAL > ERROR > WARN > INFO > DEBUG log4r allows defining custom log levels on a global basis. Adding developer log-levels is a great way to debug new features and show partial results or log the current value of variables. In the following example the additional loglevels DEV0, PARAMS, DEV1 and EXCEPTION are added. They can be used like the standard loglevels and a call to logger.dev0 "Log this to dev0!" will log the message with loglevel DEV0.
Warning: Expensive calls, processing of data and database calls should be wrapped in a Proc, so that they are only evaluated when the loglevel is set to output the message. logger.dev0 { Data.all.count() }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
log4r_config: pre_config: custom_levels: - DEV0 - DEBUG - PARAMS - DEV1 - INFO - WARN - ERROR - EXCEPTION - FATAL # define all loggers ... loggers: ... |
Custom Formatting
Rails and ActiveRecord are currently logging to stdout using Log4r. The logs do not look significantly different yet, except that they are now prefixed with the current time and the log level. Another advantage is that individual loggers can be set to different log levels. This is already an improvement over the standard rails logger. The next step is incorporating some of the advanced features of log4r to further improve the output.
Log4r
Log4r can prefix each log message using the PatternFormatter. Currently each log message is prepended with the time and the log level. However the time resolution is only one second and multiple processes that are writing to the same logfile cannot be distinguished. More useful information like hostname and application name can be added to make it easier to filter the logs down the road.
Let’s add another Outputter railsfile that writes the logs to a file, formats it and takes care of the limitations listed above:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
log4r_config: # define all loggers: loggers: - name : rails level : DEBUG trace : 'true' outputters : - console - railsfile # ... # define all outputters (incl. formatters) outputters: - type: StdoutOutputter name: console formatter: date_pattern: '%H:%M:%S' pattern : '%d %l: %m' type : PatternFormatter - type: FileOutputter name: railsfile filename: "log/#{ENV}.log" # notice the file extension is needed! trunc: false formatter: date_pattern: '%Y %m %d %H:%M:%S.%L %z' pattern : '[#{EC2_INSTANCE}][%p][#{APPNAME}]|[RAILS][%d][%l]: %m ' type : PatternFormatter |
The type: FileOutputter defines that the logs should be written to the file specified through the filename: attribute. The FileOutputter is thread-safe, so multiple processes can write to the same file as well as multiple loggers within one process.
The date format was appended with
%L to print milliseconds and
%z to log the timezone to allow easy synchronization of data points from data centers across the world.
The
pattern attribute supports some built-in variables of which the following were used in this example:
- %p : process id (pid)
- %d : the date
- %l : the log level
- %m : log message
You probably already noticed #{ENV} , #{EC2_INSTANCE} and #{APPNAME}. These are variables that can be defined before loading the configuration and will be substituted. In oder to use these variables, they must be defined in the log_cfg variable in application.rb before decoding the yaml.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#log4r requirements require 'log4r' require 'log4r/yamlconfigurator' require 'log4r/outputter/datefileoutputter' include Log4r ..... class Application < Rails::Application ... # assign log4r's logger as rails' logger. log4r_config= YAML.load_file(File.join(File.dirname(__FILE__),"log4r.yml")) log_cfg = YamlConfigurator log_cfg["ENV"] = Rails.env log_cfg["EC2_INSTANCE"] = ENV["EC2_INSTANCE"].nil? ? `hostname`.to_s.gsub(/\n$/, "") : ENV["EC2_INSTANCE"] log_cfg["APPNAME"] = Rails.application.class.parent_name log_cfg.decode_yaml( log4r_config['log4r_config'] ) # disable standard Rails logging config.log_level = DEBUG ActiveRecord::Base.logger = Log4r::Logger['rails'] #nice for multiple-instance webservers like unicorn #to monitor (re-)starts #log whenever a worker (re-)started Log4r::Logger['rails'].info "LAUNCH worker" |
Rails
Rails is still printing multiple lines per request. To change this, first turn off the default Rails logger.
1 2 3 4 |
# disable standard Rails logging config.log_level = :unknown |
Rails 3.0 introduced ActiveSupport::Notifications which allows subscribing to many events that happen throughout the lifecycle of a request and also supports sending your own notifications. The most important type of notification for logging is
process_action.action_controller . It contains the information about the processing of the request, the time it took and every parameter associated with the request. A typical notification of this type looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |name, start, finish, id, payload| name "process_action.action_controller" end - start 0.109161 id "9ecf6f39160896d40130" payload { :controller => "MysqltestController", :action => "index", :params => {"action"=>"index", "controller"=>"mysqltest"}, :format => :html, :method => "GET", :path => "/mysqltest", :status => 200, :view_runtime => 44.013, :db_runtime => 5.72 } |
The following example will output the most important facts of a request at the INFO log level and WARN on requests that take longer than 200ms or return a http status code different from 200 or 500. An ERROR will be logged when an exception is thrown or the request returned a status of 500. The DEBUG level will print the timings of view renderings and database calls and the parameters are printed at the PARAMS log level.
1 2 3 4 5 6 7 8 |
INFO: GET 200 /object/24 34.59ms WARN: POST 200 /object/25 450.22ms ERROR: GET 500 /breakme 30.0ms EXCEPTION: ERROR: GET 403 /object/notThere 24.4ms EXCEPTION: "The Object could not be found in the Database" PARAMS: {"action"=>"index", "controller"=>"mysqltest"} DEBUG: TIMING[ms]: sum:130ms db:50ms view:50ms |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
def time_in_ms(start, finish) return ( ((finish - start).to_f * 100000).round / 100.0 ).to_s end ActiveSupport::Notifications.subscribe("process_action.action_controller") do |name, start, finish, id, payload| logger = Log4r::Logger['rails'] controller_format = "@method @status @path @durationms" duration = time_in_ms(start, finish) if !payload[:exception].nil? || payload[:status] == 500 #["process_action.action_controller", 0.033505, "b4ebe16ef3da4c5eb902", {:controller=>"MongotestController", :action=>"index", :params=>{"action"=>"index", "controller"=>"mongotest"}, :format=>:html, :method=>"GET", :path=>"/mongotest", :exception=>["Mongoid::Errors::DocumentNotFound", "\nPro ... "]} logger.error { message = controller_format.clone message.sub!(/@status/, payload[:status].to_s) message.sub!(/@method/, payload[:method]) message.sub!(/@path/, payload[:path]) message.sub!(/@duration/, duration) message += " EXCEPTION: #{payload[:exception]}" message } end if payload[:status] != 200 && payload[:status] != 500 && !payload[:exception].nil? logger.warn { message = controller_format.clone message.sub!(/@status/, payload[:status].to_s) message.sub!(/@method/, payload[:method]) message.sub!(/@path/, payload[:path]) message.sub!(/@duration/, duration) message += " EXCEPTION: #{payload[:exception]}" message } end if payload[:status] == 200 if duration >= 500 logger.warn { message = controller_format.clone message.sub!(/@status/, payload[:status].to_s) message.sub!(/@method/, payload[:method]) message.sub!(/@path/, payload[:path]) message.sub!(/@duration/, duration) message } else logger.info { message = controller_format.clone message.sub!(/@status/, payload[:status].to_s) message.sub!(/@method/, payload[:method]) message.sub!(/@path/, payload[:path]) message.sub!(/@duration/, duration) message } end end logger.params { "PARAMS: #{payload[:params].to_json }" } logger.debug { db = (payload[:db_runtime] * 100).round/100.0 rescue "-" view = (payload[:view_runtime] * 100).round/100.0 rescue "-" "TIMING[ms]: sum:#{duration} db:#{db} view:#{view}" } end |
ActiveRecord
The same principle applies for ActiveRecord. This is just a simple example that prints all SQL queries with the time it took them to execute.
More complex filtering and formatting is possible, like assigning different log levels depending on how long the query took or the type of the query.
1 2 3 |
DEBUG: (23.45ms) SELECT * FROM test; |
1 2 3 4 5 6 |
ActiveSupport::Notifications.subscribe "sql.active_record" do |name, start, finish, id, payload| logger = Log4r::Logger["mysql"] logger.debug { "(#{time_in_ms(start,finish)}) #{payload[:sql]}" } end |
You can turn off the default logging of ActiveRecord by putting this into your config/application.rb :
ActiveRecord::Base.logger = Logger.new('/dev/null')
Exceptions and Stack Traces
The
process_action.action_controller’s payload contains an exception message but no stack trace. To also be able to log stack traces, add this to the
app/controllers/application_controller.rb
1 2 3 4 5 6 7 8 9 10 |
class ApplicationController < ActionController::Base #Logging exceptions if Log4r::Logger['rails'].exception? rescue_from Exception do |ex| ActiveSupport::Notifications.instrument "exception.action_controller", message: ex.message, inspect: ex.inspect, backtrace: ex.backtrace raise ex end end |
and this to the config/initializers/logger.rb
1 2 3 4 5 6 |
ActiveSupport::Notifications.subscribe "exception.action_controller" do |name, start, finish, id, payload| logger = Log4r::Logger['rails'] logger.exception { "msg:#{payload[:message]} - inspect:#{payload[:inspect]} - backtrace:#{payload[:backtrace].to_json}" } end |
It is also possible to directly log in the ApplicationController and avoid sending a message if the exception log level is not active, however I decided to keep all the formatting in one place.
Changing Log Levels During Runtime
Most of the time logging everything is not necessary and slows down the application for no reason, therefore setting the log level to INFO or WARN is preferrable. However imagine you see strange behavior, customers are reporting issues and inconsistencies or the response times spike. In these times it is very helpful to see much more verbose logging and maybe even each SQL command and the results of it.
Changing the log level in rails requires a restart of the webserver in the best case – when the log level is set based on, for example, an environment variable – or redeploy the application in the worst case – the log levels are hard-coded into, i.e. in the production.rb .
Setting the log level via a request – for example http://mmlac.com/logs/set?level=DEBUG – is also not going to help because webservers like unicorn create a new process for each instance they run. These instances are isolated and do not propagate state between each other. This means the log level will only be changed at the rails process that this particular request was handled by.
One solution to this problem is saving the log level for each logger in a file and periodically check for changes. Doing this i.e. every 20 seconds does not impact performance and allows a reasonable response time to issues. Assuming the file $RAILS_ROOT/loglevels.txt contains the log levels in JSON format, you can check and reset the log levels the following way in the logging initializer file config/initializers/logger.rb . Every time you call a logging function, like in ActiveSupport.Notification.subscribe , call log_time_check at the beginning to ensure the log levels are up to date.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$LOG_CHECK_TIME = Time.now def log_time_check if Time.now - $LOG_CHECK_TIME > 20 #update log levels begin f = File.open("loglevels.txt", "r") level = JSON.parse f.read f.close eval("Log4r::Logger['rails'].level = #{level['rails']}") if level['rails'] eval("Log4r::Logger['mysql'].level = #{level['mysql']}") if level['mysql'] eval("Log4r::Logger['mongo'].level = #{level['mongo']}") if level['mongo'] rescue Exception => e Log4r::Logger['rails'].error "Setting the logging level failed: #{e.message}" end $LOG_CHECK_TIME = Time.now end end |
1 2 3 |
{"rails":"DEV0","mysql":"INFO","mongo":"INFO"} |
1 2 3 4 5 6 7 |
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |name, start, finish, id, payload| log_time_check logger = Log4r::Logger['rails'] logger.error "This is a message to log..." end |
Silencing Memcached
If you use memcached you may notice a lot of cache-miss logs that do not seem to go through log4r.
1 2 3 4 |
cache: [GET /login] miss cache: [GET /javascripts/jquery.min.js?1317513028] miss |
The reason for this is that the memcache gem – in this case dalli – is Rack middleware and therefore does not use the Rails logger but the Rack logger. To prevent the verbose Rack logger output, put the following into your development.rb and production.rb :
1 2 3 4 |
#disable Rack::Cache verbose logging config.action_dispatch.rack_cache = {:metastore => "rails:/", :entitystore => "rails:/", :verbose => false} |
Also add the command Rails.cache.silence! in an initializer.
Nginx Queueing Time
If you use nginx or another reverse proxy it is good to know how much time the request spent in the queue before the application server started processing it. Webservers like Unicorn have a central queue (the length of it can also be monitored separately) and assign each request to the next free worker available. Long queueing times are a sign that more application servers are needed to handle the current load.
In order to log the queuing duration, nginx has to set a header with the time it processed the request. The application can read out this header at the beginning and log the time elapsed. If you use NewRelic’s patched version of nginx, the format is slightly different. If you set the header yourself, use the $msec variable:
1 2 3 4 5 6 7 8 9 10 11 |
location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://upstream_server; proxy_set_header X-Queue-Start 't=$msec'; break; } |
1 2 3 4 5 6 7 8 9 10 |
#Logging queue start time before_filter :queuestart def queuestart $HTTP_X_QUEUE_START = nil begin $HTTP_X_QUEUE_START = request.headers["HTTP_X_QUEUE_START"] if request.headers["HTTP_X_QUEUE_START"] end rescue nil end |
Then add the following code to the beginning of the process_action.action_controller subscription:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
ActiveSupport::Notifications.subscribe("process_action.action_controller") do |name, start, finish, id, payload| queue_time = 0 #FOR $msec #queue_time = start.to_f - ($HTTP_X_QUEUE_START.sub(/^t=/, "").to_f) rescue 0 #For $start_time (new_relic patched nginx) queue_time = start.to_f - ($HTTP_X_QUEUE_START.sub(/^t=/, "").to_f/1000000.0) rescue 0 queue_time = (queue_time * 1000000).round / 100.0 logger = Log4r::Logger['rails'] controller_format = "@method @status @path @durationms" ...... |
I hope this article gave you a good introduction into how to get started with improving your logging for Ruby on Rails using Log4R.
What do you think? Do you have any comments, improvements, questions or just enjoyed the read? Go write a comment below, at reddit or HackerNews and I try to respond as quickly as possible :)
Help others discover this article as well: Share it with your connections and upvote it on reddit and HackerNews!
Subscribe to the blog to get informed immediately when the next post in this series is published!
Great blog! These are great tips. I like how the exception is getting logged via log4r, but when I configure it as you have it setup, I get:
ActionController::RoutingError (undefined method `exception?’ for #):
I’m currently using log4r 1.1.11 with Rails 4. I’ve looked at docs around Log4r::Logger and I don’t see it having an exception? method. May I ask where it’s from? Thanks again for this great post!
I come from Java/Scala land so picking up these handy Ruby tips are extremely useful.
Erick
Ah, exception? is the log type. I guess my version of log4r doesn’t have exception? but it does have error? so I’ll use that for now I suppose.
Oh exception? will map to the custom logger.exception you added? Didn’t realize ‘?’ came with it.
Erick, Marcus:
I see the same thing Erick is seeing:
ActionController::RoutingError – undefined method `exception?’ for #:
I’m not getting what you meant if you figured it out.
Nathan, in the “Log Levels” section that Marcus provided above, notice that he defined the custom level of
EXCEPTION
. When you do that, then you’ll get theexception?
method available.Any idea how to get log4r working with delayed_job?
I haven’t had any success on stackoverflow: Getting error using log4r with delayed job
Hi Edward,
The issue is that DelayedJob expects the logger to be Rails::Logger and makes calls to it that log4r does not support. In this article the logger variable is overwritten to use Log4r::Logger, which does not support the .add call as intended. Unlike in Java there are no packages to determine which logger class to use or slf4j that unites different loggers to a standard interface.
There is no simple solution to this issue. On the one hand you can overwrite the affected parts of delayedJob. On the other hand you can prevent loading the log4r when delayedJob is running rails, i.e. by using a custom environment:
This is also not guaranteed to work everywhere (anything that checks the rails env == “production” will have issues).
Another way is to use environment variables and check those in the application.rb:
if (ENV["log4rlogger"] == "true") config.logger = Log4r::Logger["rails"]
I hope that helps,
Best Regards,
Markus
Thank you for a great article!
Just to mention: didn’t you forget to add (start, finish) to time_in_ms here:
if payload[:status] == 200 && time_in_ms >= 500
?
You are absolutely correct! Thanks for mentioning this, it’s fixed now :)
Still can’t run this :(
It doesn’t accept any constant defined in application.rb.
Do you have any idea?
(eval):2:in
format': uninitialized constant Log4r::PatternFormatter::HOSTNAME (NameError)
format’from C:/VirVit/Ruby200/lib/ruby/gems/2.0.0/gems/log4r-1.1.10/lib/log4r/outputter/outputter.rb:120:in
from C:/VirVit/Ruby200/lib/ruby/gems/2.0.0/gems/log4r-1.1.10/lib/log4r/outputter/outputter.rb:110:in `block in canonical_log’
Hey VirVit,
it looks like log4r is looking for a constant in the PatternFormatter class, are you sure you want it to look there and not under another scope?
Very good write-up. I absolutely love this site.
Keep writing!
Very useful article. There is also a project lumber (https://github.com/wr0ngway/lumber) addressing this problem.
Very useful blog, thank very much! :)