123456789_123456789_123456789_123456789_123456789_

Module: ActiveRecord::QueryLogs

Overview

Automatically append comments to SQL queries with runtime information tags. This can be used to trace troublesome SQL statements back to the application code that generated these statements.

Query logs can be enabled via Rails configuration in config/application.rb or an initializer:

config.active_record.query_log_tags_enabled = true

By default the name of the application, the name and action of the controller, or the name of the job are logged. The default format is / SQLCommenter. The tags shown in a query comment can be configured via Rails configuration:

config.active_record.query_log_tags = [ :application, :controller, :action, :job ]

Active Record defines default tags available for use:

  • application

  • pid

  • socket

  • db_host

  • database

Action Controller adds default tags when loaded:

  • controller

  • action

  • namespaced_controller

Active Job adds default tags when loaded:

  • job

New comment tags can be defined by adding them in a ::Hash to the tags ::Array. Tags can have dynamic content by setting a Proc or lambda value in the ::Hash, and can reference any value stored by Rails in the context object. ::ActiveSupport::CurrentAttributes can be used to store application values. Tags with nil values are omitted from the query comment.

Escaping is performed on the string returned, however untrusted user input should not be used.

Example:

config.active_record.query_log_tags = [
  :namespaced_controller,
  :action,
  :job,
  {
    request_id: ->(context) { context[:controller]&.request&.request_id },
    job_id: ->(context) { context[:job]&.job_id },
    tenant_id: -> { Current.tenant&.id },
    static: "value",
  },
]

By default the name of the application, the name and action of the controller, or the name of the job are logged using the / SQLCommenter format. This can be changed via config.active_record.query_log_tags_format

Tag comments can be prepended to the query:

ActiveRecord::QueryLogs.prepend_comment = true

For applications where the content will not change during the lifetime of the request or job execution, the tags can be cached for reuse in every query:

config.active_record.cache_query_log_tags = true

Class Attribute Summary

Class Method Summary

Class Attribute Details

.cache_query_log_tags (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 77

mattr_accessor :cache_query_log_tags, instance_accessor: false, default: false

.prepend_comment (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 76

mattr_accessor :prepend_comment, instance_accessor: false, default: false

.taggings (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 74

mattr_accessor :taggings, instance_accessor: false, default: {}

.tags (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 75

mattr_accessor :tags, instance_accessor: false, default: [ :application ]

.tags_formatter (rw)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 78

mattr_accessor :tags_formatter, instance_accessor: false

Class Method Details

.call(sql, connection)

This method is for internal use only.
[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 82

def call(sql, connection) # :nodoc:
  comment = self.comment(connection)

  if comment.blank?
    sql
  elsif prepend_comment
    "#{comment} #{sql}"
  else
    "#{sql} #{comment}"
  end
end

.clear_cache

This method is for internal use only.
[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 94

def clear_cache # :nodoc:
  self.cached_comment = nil
end

.comment(connection) (private)

Returns an SQL comment ::String containing the query log tags. Sets and returns a cached comment if .cache_query_log_tags is true.

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 116

def comment(connection)
  if cache_query_log_tags
    self.cached_comment ||= uncached_comment(connection)
  else
    uncached_comment(connection)
  end
end

.escape_sql_comment(content) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 136

def escape_sql_comment(content)
  # Sanitize a string to appear within a SQL comment
  # For compatibility, this also surrounding "/*+", "/*", and "*/"
  # characters, possibly with single surrounding space.
  # Then follows that by replacing any internal "*/" or "/ *" with
  # "* /" or "/ *"
  comment = content.to_s.dup
  comment.gsub!(%r{\A\s*/\*\+?\s?|\s?\*/\s*\Z}, "")
  comment.gsub!("*/", "* /")
  comment.gsub!("/*", "/ *")
  comment
end

.formatter (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 124

def formatter
  self.tags_formatter || self.update_formatter(:legacy)
end

.tag_content(connection) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 149

def tag_content(connection)
  context = ActiveSupport::ExecutionContext.to_h
  context[:connection] ||= connection

  pairs = tags.flat_map { |i| [*i] }.filter_map do |tag|
    key, handler = tag
    handler ||= taggings[key]

    val = if handler.nil?
      context[key]
    elsif handler.respond_to?(:call)
      if handler.arity == 0
        handler.call
      else
        handler.call(context)
      end
    else
      handler
    end
    [key, val] unless val.nil?
  end
  self.formatter.format(pairs)
end

.uncached_comment(connection) (private)

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 128

def uncached_comment(connection)
  content = tag_content(connection)

  if content.present?
    "/*#{escape_sql_comment(content)}*/"
  end
end

.update_formatter(format)

Updates the formatter to be what the passed in format is.

[ GitHub ]

  
# File 'activerecord/lib/active_record/query_logs.rb', line 99

def update_formatter(format)
  self.tags_formatter =
    case format
    when :legacy
      LegacyFormatter.new
    when :sqlcommenter
      SQLCommenter.new
    else
      raise ArgumentError, "Formatter is unsupported: #{formatter}"
    end
end