Disable RackLogger in favor of manually hooking into actions' "handle" for logging requests

Hello :wave:

I’ve been spending a few hours investigating but couldn’t yet find a clean way - is it possible to disable rack_monitor completely? My intention is to hook around the Hanami::Action#call method, so it’s easier for actions to add some payload to the logging.

A practical example would be adding e.g.: “user_id: …” to the log payload - and considering that user authentication might happen during the Hanami::Action#call lifecycle.

Also for context, here’s how we can do that using Rails:

class AuthenticatedController < ApplicationController
  before_action :authenticate!

  def append_info_to_payload(payload)
    super
    payload[:usr] = {
      id: current_user&.id,
      email: current_user&.email
    }
  end

  # implementation details...
  # def authenticate!
  # def current_user
end

class LogSubscriber < ActiveSupport::LogSubscriber
  def process_action(event)
    info { { message: "Processed request", user: event.payload[:usr] }.to_json }
  end
  subscribe_log_level :process_action, :info
end

# detach old log subscriber
ActiveSupport::Notifications.unsubscribe("process_action.action_controller")

# attach my new log subscriber
::LogSubscriber.attach_to :action_controller

NOTE: Not directly related to the topic but I think it’s also related - I’m also looking to swap Dry::Logger for SemanticLogger (which all of our other services are now using). As of now I’m doing that by:

# lib/rack_semantic_logger.rb
# patch Hanami RackLogger so it understands that SemanticLogger is compatible
Hanami::Web::RackLogger::UniversalLogger.singleton_class.prepend(
  Module.new do
    private def compatible_logger?(*)
      true
    end
  end
)
# Rack middleware to add some http-specific tags to all logs
class RackSemanticLogger
  def initialize(app, logger: SemanticLogger[Rack])
    @logger = logger
    @app = app
  end
  def call(env)
    request = Rack::Request.new(env)
    named_tags = {
      url: request.url,
      host: request.host,
      user_agent: request.user_agent,
      # etc...
    }

    @logger.tagged(http: named_tags) { @app.(env) }
  end
end

# config.ru
# NOTE: this is done in config.ru as it was the only way I found to
#   wrap rack_monitor logs in my `@logger.tagged { }` block
use RackSemanticLogger

# app configuration in config/app.rb
$stdout.sync = true
config.logger = SemanticLogger[Hanami]
SemanticLogger.default_level = settings.log_level
SemanticLogger.add_appender(io: $stdout, formatter: MyCustomFormatter.new)

I struggled with this too, for exactly the same reasons. I ended up writing a custom formatter for Dry::Logger that appends the current request’s context from RequestStore.

class JSONWithContext < Dry::Logger::Formatters::JSON
  def format(entry)
    RequestStore.store.each do |key, value|
      entry.payload[key] = value
    end
    super
  end
end

And then register it with config.logger.formatter = JSONWithContext in your app.rb.

I should point out that this has the side-effect of appending this context to all your logs emitted during each request, however if you are logging to e.g. Datadog this is actually desirable, as all your logs now have user_id etc as attributes.