Eric
Eric

Reputation: 2705

Dynamically changing log level for Sinatra without restarting the server

I have a Sinatra server running in production environment.

Its logger level is set to INFO, but I want to occasionally change the level to DEBUG for a specific deployment when I am debugging.

Is there a way to do this dynamically without killing the Sinatra server and restarting?

Upvotes: 0

Views: 115

Answers (1)

berkes
berkes

Reputation: 27553

Sinatra defers most of its logging to Racks' Rack::CommonLogger.

This is pluggable, so you can put your own loggers in there.

Sinatra, and thus rack, run in a long thread. The logger is initialized on first run and cannot be changed in a running app, like almost all settings in Sinatra.

So, what you need is a logger has a specific API to switch logs after initializing. Rack::Commonlogger has no such interface to switch after initializing.

This leaves three directions to solve this (well, other than hacking Sinatra or such):

  • Find a logger that has an API to configure it in runtime, after initializing.
  • Use a logger outside of Sinatra that offers such a feature.
  • Introduce a pattern that allows you to switch loggers at runtime.

Runtime configurable loggers

Benefit: Keeps it all in one place.
Downside: Your adding a lot of complexity, albeit in a lib, to your runtime and web-app. Complexity that has little to do with your business-logic.
Other major downside same as below with "Adapter": it must be toggled per running instance.

Outside of your app

E.g. Elastic Logstash or, far simpler, one of many syslogd(8) servers, severa of which are already present on your system. If you stick to Ruby, FluentD is an option.

Benefit: Single Responsibility Principle: your web-app should best keep far from writing logs to files etc, but stick to handling web-requests for your business-logic.
Downside: Adds dependency, making testing etc more complex. Can be solved by reducing the coupling with adapters, see below.

A pattern. Probably Adapter and Proxy Pattern.

E.g. if you already follow adapter patterns or hexagonal architecture, this is a nice place.

By logging to a proxy that can switch logging classes in runtime, you can configure this in Sinatra on boot, then switch it later in e.g. a before{} hook, your routes etc.

Commonly, Adapter patterns don't have a way to switch adapters in runtime, though, but that can be added. Most likely pattern to do so, is a proxy pattern:

E.g.

class Adapters
  def initialize(config_at_boot)
    @@map[:logger] = config_at_boot[:logger]
  end

  def self.set(name, value)
    @map[name] = value
  end
  def self.get(name)
    @map[name]
  end
end

class Adapters::Log
  extend Forwardable
  def_delegate(:logger, :log, :info, :debug, :warn, :error)

  private
  def logger
    Adapters.get(
  end
end
class Adapters::Log::SilentLogger; end
class Adapters::Log::TestLogger; end
class Adapters::Log::CommonLogger; end

# Use as:
Adapters.initialize(logger: Adapters::Log:::SilentLogger)
logger = Adapters::Log.new
logger.info("I will be logged using the SilentLogger")
Adapters.set(logger: Adapters::Log::TestLogger)
logger.warn("I will be logged using the TestLogger")

# web.rb
configure do
  Adapters.new(logger: Adapters::Log::TestLogger.new)
end

# Then, later, e.g in a route
post '/switch_logger' do
  Adapters.set(logger: Adapters::Log::CommonLogger)
end

# Or as a filter
before do
  Adapters.set(logger: params[:logger]) # This is really insecure, you need additional protections!
end

Benefits: Adapters and proxies are common patterns to keep your business logic separate from "communication with outside". Logging is such communication. Downside: You need to either already use these patterns, or to implement them clean. They will clutter your app with indirections. Esp. Logger in Sinatra, and Rack are proxies themselves already, so you will be adding more proxies; more complexity.

Another major downside is that, by keeping the logging adapters in your app, you will need to change logging adapters in each running version. Whereas a logger outside of your app can be a central service that accepts logs from any amount of running sinatra instances.

Personally, I mix the Adapter pattern with a dedicated logging service. But, YAGNI, only when the need arises to "do more with logs". This is because with Sinatra, I already use Hexagonal Architecture (where Sinatra acts the HTTP adapter, really) and with adapters, so putting the logging in such an adapter only keeps the application consistent.

Upvotes: 1

Related Questions