It looks like Rails4's logger, unlike Rails3's, finally supports a custom formatter, like the ruby stdlib logger again.
Rails.logger.formatter # => #<ActiveSupport::Logger::SimpleFormatter:0x007ff81757d890 #datetime_format=nil>
Rails.logger.formatter = SomeFormatterClass
However, when I try to give it a formatter class that would be sufficient for stdlib Logger formatter:
[2014-03-12 16:23:27] ERROR NoMethodError: undefined method `tagged' for #<FormattedRailsLoggerFormatter:0x007fd816545ad8>
/Users/jrochkind/.gem/ruby/1.9.3/gems/activesupport-4.0.3/lib/active_support/tagged_logging.rb:67:in `tagged'
/Users/jrochkind/.gem/ruby/1.9.3/gems/railties-4.0.3/lib/rails/rack/logger.rb:20:in `call'
Does anyone know, is a custom formatter actually a supported feature of Rails4? And is how you are meant to do it documented anywhere?
Answering my own question, I've figured it out.
Rails4 provides a config variable, config.log_formatter. You would probably set it in your config/application.rb, along with other application config.
You should set it to an object implementing the stdlib Logger Formatter interface: Basically, you have to provide a method call(severity, time, progname, msg) that returns the formatted log line.
Note you set it to an object, NOT the class name, eg:
config.log_formatter = MyFormatter.new
You should not try to set Rails.logger.formatter directly -- Rails expects you to set it via config, and does some tricky stuff to make your formatter and logger work properly with Rails when you use the config. You can see that, as well as see that indeed config.log_formatter is used, in Rails source code here. (Thanks to github and it's awesome code search and display ui, is how I tracked this down and figured out the existence of config.log_formatter)
In Rails4, you should not need to monkey-patch any parts of Rails just to use a custom log formatter, just use config.log_formatter instead. (In Rails3 you did need to monkey-patch one or another to get custom log formatting).
In case it helps anyone else, in Rails 4.2.6 and Ruby 2.3.0, this is what worked for me:
# config/application.rb
module MyRailsApp
class Application < Rails::Application
require 'my_log_formatter'
...
# Custom log formatter that uses JSON
config.log_formatter = MyLogFormatter.new
end
end
To make this happen:
# lib/my_log_formatter.rb
class MyLogFormatter < ActiveSupport::Logger::SimpleFormatter
def call(severity, timestamp, progname, message)
if message.present? && message.exclude?('Started GET "/assets')
{
app: Rails.application.class.parent_name,
process_id: Process.pid,
level: severity,
time: timestamp,
progname: progname,
message: message
}.to_json + "\r\n"
else
''
end
end
end
Some notes:
The if statement prevents log entries with no message, and log messages that log the serving of asset files.
The + "\r\n" adds a CR+LF to the end of the line, so it remains somewhat human-readable in the rails server console.
I added app because I am sharing a single log file among many apps. You could delete this in a system that uses just one Rails app.
I find process_id comes in handy when you are reviewing logs on a busy server.
Related
My Rails application runs in Heroku; recently, we have changed the Heroku LOG_LEVEL to WARN as the system logs flooded with so many unwanted information. But still, in some of the areas, I wanted to use Rails.logger.info;
Currently, in Heroku we have this:
LOG_LEVEL = WARN
And in production.rb, still that is
config.log_level = :info
config.log_formatter = ::Logger::Formatter.new
The above configuration we didn't change it, as the precedence is for LOG_LEVEL if we set that. So with the above configuration, if we put Rails.logger.info "Hello world," that will not work because the logger will only handle the logs equal or higher to warn in importance.
So we have tried one other way.
Created a new initializer called custom_logger.rb; we put
$INFO_LOGGER = Rails.logger.dup
$INFO_LOGGER.level = :info
then wherever we wanted to use info, we just called $INFO_LOGGER.info "Hello World," this prints
Is this a correct approach, like using the global variable?
Is this a correct approach, like using the global variable?
This question could be considered opinion based, so in my opinion I would not recommend it. Additionally the question posed in the title is regarding a custom logger and while we could implement one to facilitate the request I would propose a simpler solution that will still log exactly what you want, to the current log file, and without the need for a custom logger, a secondary logger, or any kind of global variable.
My Suggestion:
Rails uses ActiveSupport::Logger by default which is essentially just a ruby Logger.
If there are messages you always want logged regardless of the level you can use Logger#unknown.
Per the Documents for the Logger class:
Levels:
UNKNOWN - An unknown message that should always be logged.
Logger#unknown
Log an UNKNOWN message. This will be printed no matter what the logger's level is.
So You can use this to your advantage for the messages that you always want to show while still avoiding the noise of the standard info messages:
For Example:
Rails.logger.info "Noisy message" # won't show when LOG_LEVEL > INFO
Rails.logger.unknown "VERY IMPORTANT MESSAGE" # will show no matter what the level is set to
You don't want to use global variables. Instead create a custom class for example
class MyLogger
class << self
def info(*args)
new.info(*args)
end
end
delegate :info, to: :logger
attr_reader :logger
def initialize
#logger = ActiveSupport::Logger.new(Rails.root.join("log/my_logger.#{Rails.env}.log"))
#logger.formatter = ::Logger::Formatter.new
end
end
Now you can call MyLogger.info("this is a test message") and it will output the message in the log file regardless of the LOG_LEVEL or config.log_level = :info
I am using the Apartment gem for a multi tenant Rails 5.2 app. I'm not sure that this even matters for my question but just giving some context.
Is there a way to override the Rails logger and redirect every single log entry to a file based on the tenant (database) that is being used?
Thinking... is there a method I can monkeypatch in Logger that will change the file written to dynamically?
Example: I want every error message directed to a file for that day. So at the end of a week there will be 7 dynamically generated files for errors that occurred on each specific day.
Another example: Before you write any server log message check if it is before 1pm. If it is before 1pm write it to /log/before_1.log ... if it is after 1pm write it to /log/after_1.log
Silly examples... but I want that kind of dynamic control before any line of log is written.
Thank you!
Usually the logger is usually configured per server (or per environment really) while apartment sets tenants per request - which means that in practice its not really going to work that well.
You can set the logger at any point by assigning Rails.logger to a logger instance.
Rails.logger = Logger.new(Rails.root.join('log/foo.log'), File::APPEND)
# or for multiple loggers
Rails.logger.extend(Logger.new(Rails.root.join('log/foo.log'), File::APPEND))
However its not that simple - you can't just throw that into ApplicationController and think that everything is hunky-dory - it will be called way to late and most of the entries with important stuff like the request or any errors that pop up before the controller will end up in the default log.
What you could do is write a custom piece of middleware that switches out the log:
# app/middleware/tenant_logger.rb
class TenantLogger
def initialize app
#app = app
end
def call(env)
file_name = "#{Appartment::Tenant.current}.log"
Rails.logger = Logger.new(Rails.root.join('log', file_name), File::APPEND)
#app.call(env)
end
end
And mount it after the "elevator" in the middleware stack:
Rails.application.config.middleware.insert_after Apartment::Elevators::Subdomain, TenantLogger
However as this is pretty far down in the middleware stack you will still miss quite a lot of important info logged by the middleware such as Rails::Rack::Logger.
Using the tagged logger as suggested by the Rails guides with a single file is a much better solution.
I have this custom exception handler:
module I18n
class MissingTanslationsCollectorExceptionHandler < I18n::ExceptionHandler
# Handles exceptions from I18n
def call(exception, locale, key, options)
if exception.is_a?(I18n::MissingTranslation)
binding.pry
missing_translations << I18n.normalize_keys(locale, key, options[:scope])
super
end
end
end
end
I assign it like this:
I18n.exception_handler = I18n::MissingTanslationsCollectorExceptionHandler.new
When using the console, it seems to work:
$ rails c
Loading development environment (Rails 4.2.0)
[1] base » I18n.exception_handler
=> #<I18n::MissingTanslationsCollectorExceptionHandler:0x00000101c121e8>
[2] base » I18n.translate 'unknown'
From: /Users/josh/Documents/Work/MuheimWebdesign/base/src/lib/missing_i18n_exception_handler.rb # line 11 I18n::MissingTanslationsCollectorExceptionHandler#call:
10: if exception.is_a?(I18n::MissingTranslation)
=> 11: binding.pry
12: missing_translations << I18n.normalize_keys(locale, key, options[:scope])
[1] base(#<I18n::MissingTanslationsCollectorExceptionHandler>) » c
=> "translation missing: en.unknown"
But when starting the server and hitting a missing translation with t 'unknown', binding.pry isn't called. Only when doing a I18n.translateunknown`, it is called. Why?
Maybe it has to do with the fact that the feature specs are run inside their own process using Capybara?
Update
Here is the Rails app in question:
https://github.com/jmuheim/base/tree/features/custom-i18n-exception-handler
I have added the custom i18n exception handler here:
https://github.com/jmuheim/base/commit/f2aff30046c7a9f38c4a1faed0953e474099120c
And I have added code which should demonstrate the issue here:
https://github.com/jmuheim/base/commit/af484b6b96f41194043e0ad0668a5c288d4a0af3
Simply go the to root_path, then it should be triggered (once, not twice!).
This happens because ActionView's t isn't a simple alias to I18n.translate. Among other things, it converts any MissingTranslation exceptions to spans in your HTML.
See the code for details.
I wanted exceptions to bubble up, but I didn't want to have to specify rescue_format: true every time I called t in my views, so I overrode Rails's helper to always pass rescue_format: true as an option.
Another thing to note: the translation helper has changed a bit from one Rails version to the next, so make sure you read the actual code for your version to see what to do.
If you are using locale language in your application it could be a syntax error in your {locale}.yml file.
You can validate your yml files in the below link.
http://www.yamllint.com/
I hope it may help.
I have some helper methods on my ActiveSupport::BufferedLogger such as displaying KeyValue pairs, it all works fine from rails s but fails in rails c
In rails s, I can see that my Logger extends BufferedLogger and that is the class I have chosen to MonkeyPatch, I have also tested this out with ActiveSupport::Logger as well.
I had thought that rails c ran through the same initializers that rails s used, am I wrong in thinking that?
Do I need to run some sort of initializer when starting rails c?
Location of my file is:
config/initializers/active_support_logger.rb
Error is here:
Sample Snippet listed here
class ActiveSupport::BufferedLogger
def kv(key, value)
info('%-50s %s' % ["#{key}: ".brown, value])
end
def line
info(##l.brown)
end
def block(message)
line
info(message)
line
end
end
I recommend subclassing ActiveSupport::Logger (or ActiveSupport::BufferedLogger if you really want though it's deprecated in Rails 4) instead of monkey-patching. Rails provides a configuration option to override the logger instance used by the app. This works in any context that loads the Rails environment, including the server and console.
There are a number of ways you can do this; a quick and dirty way to get you started is to just define the class and set the logger instance in an initializer that will get loaded during the initialization process of the Rails environment:
# config/initializers/my_logger.rb
class MyLogger < ::ActiveSupport::Logger
# additional methods and overrides
end
Rails.logger = MyLogger.new(Rails.root.join("log", "#{Rails.env}.log"))
For more info, check out the Rails guide to debugging applications: http://guides.rubyonrails.org/debugging_rails_applications.html#the-logger
I am using the ruby gem rest-client with rest-client-components.
Rest-client-components enables request logging with Rack::CommonLogger.
The instructions for enabling it make use of STDOUT:
require 'restclient/components'
RestClient.enable Rack::CommonLogger, STDOUT
This works fine in development, but when I'm in production with Apache/Passenger (mod_rails), I don't see any messages from rest-client in production.log. Is there a way to integrate Rack::CommonLogger with the Rails log? Or at least to write it to a file? The former is more useful because it's easy to see the context, but the latter is better than nothing.
Thanks.
Here's the solution I came up with.
Thanks to #crohr for pointing me in the right direction.
First, create a new Logger class. Rails defaults to ActiveSupport::BufferedLogger, so we'll extend that.
# lib/rest_client_logger.rb
class RestClientLogger < ActiveSupport::BufferedLogger
def write(msg)
add(INFO, msg)
end
end
Then tell Rails to use your new logger.
# application.rb
log_file = File.open("log/#{Rails.env}.log", 'a')
log_file.sync = true # turn on auto-flushing at the file level so we get complete messages
config.logger = RestClientLogger.new(log_file)
config.logger.auto_flushing = !Rails.env.production? # turn off auto-flushing at the logger level in production for better performance
Finally, tell rest-client to use your new logger.
# config/initializers/rest_client.rb
RestClient.enable Rack::CommonLogger, Rails.logger
Limitations:
If you're using Rack::Cache with rest-client-components, this doesn't capture the cache messages.