Approach to a custom rails logger - ruby-on-rails

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

Related

Dynamically override destination file of all Logger output in Rails

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.

What is the difference between using 'config.log_level' and 'Rails.logger.level'?

I've seen examples of both statements, and both control the level of logging whether using descriptive values (:debug, :info, :warn, :error, :fatal, :unknown) or numeral (1-5).
config.log_level is only used in environment initializer files.
Rails.logger.level can be used almost anywhere.
See this documentation.
I have never set log-level anywhere else than in my env.-initializer files. I guess maybe one possible usecase for using Rails.logger.level could be when for ex. you have a development environment so it has a log-level of :debug but you don't want to bloat you log files with a lot of db-querys and unneccessary debug-info- then you can filter out only info-level log:
Rails.logger.level = 1
# Some code that gives out too much debug information
Rails.logger.level = 0
# Code that's ok for debug information
And respectively in prod-env. if you, for some reason, want to print out debug info in a certain place of the code while environment log level is set to :info.

custom logger in rails4?

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.

Logging in Ruby on Rails in Production Mode

I would like to view some variables in controller, it tried the following:
Rails.logger.debug "Year: #{Time.now.year}"
puts "Year: #{Time.now.year}, Month: #{#month}"
where can I see the output for Logger or Puts in production mode? Do I need so set something up to view these somewhere?
The normal log level in production is info, so debug logs are not shown.
Change your logging to
Rails.logger.info "Year: #{Time.now.year}"
to show it in production.log.
Alternatively (but not a good idea) you can raise the logging level in /config/environments/production.rb:
config.log_level = :debug
Update Rails 4.2:
Now the default debug level in all environments is :debug (as #nabilh mentioned).
If you want you production environment less chattery, you can reset your log level in /config/environments/production.rb to the former :info:
config.log_level = :info
While I believe #martin-m is right that you probably don't want to clutter your logs by using config.log_level = :debug in /config/environments/production.rb, I believe the default logging level in all environments is debug as of Rails 4.2. So debug logs (and all levels up) will be shown in production unless you specify otherwise.
So in response to your question, you can now write:
Rails.logger.debug "Year: #{Time.now.year}" and see the results in /log/production.log.
See here for more detailed documentation. Here is the documentation for Rails 4.1 for comparison.
If you have a production app that you may need to temporarily change log levels, you might want to use an environment variable to be able to set level without modifying code and redeployment, though your app would need to be restarted, you could take this approach:
config/environment/production.rb
MyApp::Application.configure do
#...
log_level = :info #default state
[:debug, :info, :warn, :error, :fatal, :unknown].each do |level|
if ENV['LOG_LEVEL']&.to_sym == level
log_level = level
end
end
config.log_level = log_level
#...
end

Getting "Server Error 500" when using logger.debug in production

I'm trying to find a little bug and need the logger inside one of my models for it. Whenever I use
logger.debug "something"
inside a model, the message is put in the log but I get a 500 Server Error page. Everything is normal without the logger.debug line.
I set
config.log_level = :debug
inside the environments/production.rb file and set the correct file permissions for the log but can't seem to find the problem.
Inside the model the logger object is not available by default. Use
Rails.logger.debug "something"
instead...

Resources