Rails Mixin: Log which line caused a query
- ruby
- rails
- mixins
Rails helpfully logs queries to ActiveRecord::Base.logger in development mode. This logging is an easy way to keep an
eye on unexpected costs hidden by Rails’ abstractions - like N + 1 queries - which might otherwise slip by unnoticed. One thing
I’ve found that makes this query logging even more useful is to log the line of code that caused the query.

I’ll walk through the genesis of this mixin in detail, so if you’re just interested in the mixin itself, jump to the end of this post to see the final code.
Finding something to modify
First, we’ll need to determine what to modify. We know what the logged lines look like, which gives us a reasonable starting point:
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 18 LIMIT 1`
. Most of the line is based on the query - but the “Load” text is likely to show up in a string literal. Sure enough,
“Load” only appears three times in string literals in ActiveRecord code, and only one of those looks relevant. In
querying.rb:
Following #select_all to #select in database_statements.rb, we find that it is just an empty placeholder method
for database adapters to implement:
def select(sql, name = nil, binds = []) end undef_method :select
Looking at a database adapter - mysql_adapter.rb in this case - we find an implementation of #select that passes
the name argument to #exec_query, which in turn passes it to #exec_without_stmt. Now we’re getting somewhere:
def exec_without_stmt(sql, name = 'SQL') # ... log(sql, name) do # ...
The #log method is defined in abstract_adapter.rb, and uses ActiveSupport::Notifications::Instrumenter to broadcast
an event named “sql.active_record”. Instrumentation allows for decoupling of the subscriber and broadcaster, so while we
could look for a string literal in a call like ActiveSupport::Notifications.subscribe('sql.active_record', ...) - that
doesn’t lead us anywhere promising in this case. Fortunately, “sql.active_record” shows up in a comment block in
ActiveSupport::LogSubscriber:
# After configured, whenever a "sql.active_record" notification is published, # it will properly dispatch the event (ActiveSupport::Notifications::Event) to # the sql method.
Additional comments in ActiveSupport::LogSubscriber direct us to ActiveRecord::LogSubscriber, where we find the code
responsible for logging those queries.
Determining how to modify
Looking at ActiveRecord::LogSubscriber, we see that the #sql method calls #debug to print the query. The #identity
method - used to log identity map access - also calls #debug. We’ll add our behavior to debug so it will affect
both methods.
The #debug method isn’t actually defined in ActiveRecord::LogsSubscriber. It’s defined in the base class -
ActiveSupport::LogSubscriber - along with a handful of similar helper methods:
%w(info debug warn error fatal unknown).each do |level|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{level}(*args, &block)
return unless logger
logger.#{level}(*args, &block)
end
METHOD
end
Since #debug isn’t defined in ActiveRecord::LogSubscriber itself, we have some options for modifying the behavior. We
could re-open the class to add our own #debug with extra behavior - and would still be able to call super
without worrying about hiding an existing method. Instead, we can take a more future-safe route and prepend a module that
defines a #debug method. By doing this, we allow for future updates to ActiveRecord::LogSubscriber that add an implementation
of #debug, without requiring changes to our mixed-in method:
module LogQuerySource
def debug(*args, &block)
super
# Add some new behavior here...
end
end
ActiveRecord::LogSubscriber.send :prepend, LogQuerySource
Adding the new behavior
Now we’ll figure out what to do in our modified #debug method. Jumping into the #sql method in a
debugger (byebug) just before the call to #debug, we can inspect the result of caller. Sure enough, our code that
caused the query shows up in the backtrace, but it’s buried under dozens of calls to ActiveRecord and ActiveSupport
helper methods.
We can clean up our caller pretty easily, the same way Rails clean up backtraces for error output:
backtrace = Rails.backtrace_cleaner.clean caller
Now we have a much cleaner backtrace, only containing lines with code in our project. In this particular case, we might have a mixin that shows up in the cleaned backtrace, but we don’t want to include that here, so we’ll clean a bit more:
backtrace = Rails.backtrace_cleaner.clean caller
relevant_caller_line = backtrace.detect do |caller_line|
!caller_line.include?('/initializers/')
end
relevant_caller_line will contain the last line of our code that caused the query - or nil if there is no relevant
line of code in the backtrace.
We want to call super, so the original method can log the query. Since the #debug method defined in
ActiveSupport::LogSubscriber has the line return unless logger - and the subsequent call to the logger returns
a truthy value - we can use that as a proxy for the logger being enabled in our method:
def debug(*args, &block) return unless super # ... end
Finally, if we’ve found a relevant caller line, and logging is enabled, we’ll go ahead and log something:
if relevant_caller_line
logger.debug(" ↳ #{ relevant_caller_line.sub("#{ Rails.root }/", '') }")
end
The mixin
In a config/initializers/active_record_log_subscriber.rb file, add:
module LogQuerySource
def debug(*args, &block)
return unless super
backtrace = Rails.backtrace_cleaner.clean caller
relevant_caller_line = backtrace.detect do |caller_line|
!caller_line.include?('/initializers/')
end
if relevant_caller_line
logger.debug(" ↳ #{ relevant_caller_line.sub("#{ Rails.root }/", '') }")
end
end
end
ActiveRecord::LogSubscriber.send :prepend, LogQuerySource
Now we’ll see which lines caused ActiveRecord queries, making inefficiencies much easier to track down.