Infield upgrades Ruby on Rails apps, and part of that work is adopting new features of Rails to replace something customers were previously relying on a gem for. One such migration is moving from the marginalia
gem to Rails’ built-in QueryLogs system.
The marginalia gem annotates SQL statements generated by ActiveRecord with comments about the source of the query. For instance you can add the originating controller, action, or source location of the query. Created in 2013 by the basecamp team, its functionality has since been incorporated into Rails core with the QueryLogs feature and marginalia is no longer compatible as of Rails 8 (we’ve also gotten reports from a few of our customers about missing query log comments under Rails 7.2, even though the gem is technically compatible).
The migration path is to remove the gem and move to the builtin QueryLogs feature. This means porting your marginalia configuration over to the new style. Most options have 1-1 replacements. Issues arise if you’ve patched the Marginalia::Comment
class to dynamically generate comment tags at query time or if you’re integrating marginalia
with other systems like Sidekiq. Here are a few cases we’ve encountered:
request_id
from a request headerTo reach into the controller request object while generating a query tag you’ll need a proc:
Rails.application.config.active_record.query_log_tags = [
request_id: ->(context) { context[:controller]&.request&.env&.dig('REQUEST_ID') },
]
A very helpful tag is the line in your source code that the query was generated from. Rails has this built-in with the source_location
tag, which replaces the line
option from marginalia. However, there is no 1-1 replacement for the old Marginalia::Comment.lines_to_ignore =
option. Instead you’ll need to create your own BacktraceSilencer and integrate that:
query_log_backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
query_log_backtrace_cleaner.add_filter { |line| line.delete_prefix("#{Rails.root}/") }
lines_to_ignore_regexp = /tmp/
query_log_backtrace_cleaner.add_silencer do |line|
lines_to_ignore_regexp.match?(line)
end
Then create a proc that grabs the top of the callstack, applying the cleaner. Note that this only works on Ruby versions 3.2 and above. For earlier rubies you’ll need to use caller_locations
(see how Rails does it here).first_clean_frame = -> do
Thread.each_caller_location do |location|
frame = query_log_backtrace_cleaner.clean_frame(location)
return frame if frame
end
nil
end
Then apply your proc as a custom tag in query_log_tags
.
It can be helpful to add the name of the sidekiq job that triggered a query as a query comment. You can do this with sidekiq middleware and the ActiveSupport::ExecutionContext
, which is available to QueryLogs:
module Sidekiq
class QueryLogContext
def call(_worker_class, job, _queue, &)
sidekiq_job_class = job['class'].to_s
ActiveSupport::ExecutionContext.set(job: sidekiq_job_class, &)
end
end
end
Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add Sidekiq::QueryLogContext
end
end
Rails.application.config.active_record.query_log_tags = [ :job ]
SQL query comments are not usually well-covered by automated tests. In fact, some of our customers go so far as to disable query log commenting in tests to speed them up. You’ll need to run queries in a variety of settings manually to compare the comments before/after the migration in order to ensure you’re still capturing the metadata you expect.
We’d recommend doing the migration away from marginalia while you’re still running Rails 7.2 (or even 7.1). That way when you go and do your eventual Rails 8 upgrade you’re not stuck migrating gems at the same time.
We’d love to hear from you if you ran into any issues with this migration or tackled it another way.