Logger for Sinatra/Rack apps
2022-07-13
While developing an application on Sinatra, I ran into a problem: how to combine
- application logs (or http access logs)
- Sinatra logs (Rack errors)
- manually created events through info/warn/error methods
to the single output (file) controlled by a built-in ruby class Logger.
There are no actual information on this topic on the net, so I had to collect it bit by bit. The completed solution is to write custom middleware:
class LoggerMiddleware
FORMAT = %{"%s %s%s%s %s" %d}
def initialize(app, app_logger, error_logger)
@app, @app_logger, @error_logger = app, app_logger, error_logger
end
def call(env)
env['rack.logger'] = @app_logger
env['rack.errors'] = @error_logger
status, headers, body = @app.call(env)
body = Rack::BodyProxy.new(body) { log(env, status) }
[status, headers, body]
end
private
def log(env, status, _headers = nil)
data = [
env[Rack::REQUEST_METHOD],
env[Rack::SCRIPT_NAME],
env[Rack::PATH_INFO],
env[Rack::QUERY_STRING].empty? ? '' : "?#{env[Rack::QUERY_STRING]}",
env[Rack::SERVER_PROTOCOL],
status.to_s[0..3]
]
@app_logger.add(Logger::Severity::INFO, FORMAT % data)
end
end
The sinatra application configuration block will look like:
configure do
::Logger.class_eval { alias :puts :error }
$app_logger = ::Logger.new("log/production.log")
$error_logger = ::Logger.new('log/error.log')
set :logging, false
use LoggerMiddleware, $app_logger, $error_logger
end
Alias puts-error added for compatibility with Rack errors processing.
If you need to manually create log events, use the previously declared variables:
get '/after_login' do
$app_logger.info("Login from ip #{request.ip}")
end
And one last note: to rotate old log files I was going to use the parameter shift_age in Logger class builder. However, it is much easier to leave this task to the Linux logrotate system [1][2].
That's all, I hope you found this article useful.