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
  • source_location

WARNING: Calculating the source_location of a query can be slow, so you should consider its impact if using it in a production environment.

Also see config.active_record.verbose_query_logs.

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.

context includes the following keys:

  • controller - current Action Controller instance, if available
  • job - current Active Job instance, if available
  • connection - current database connection
  • sql - current SQL query

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",
  },
]

WARNING: SQL query can contain sensitive data and using :sql directly as a query log tag is unsafe because it will log the query unfiltered.

Example:

# unsafe
config.active_record.query_log_tags = [:sql]

# safe
config.active_record.query_log_tags = [sql_length: ->(context) { context[:sql].length } ]

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

The format can also be overridden per connection pool through the query_log_tags key of a database.yml entry. Connections checked out from that pool use the configured format, while setting query_log_tags to false instead opts the pool out of tagging entirely. Pools without any explicit configuration fall back to the global defaults:

production:
  primary:
    database: primary
  analytics:
    database: analytics
    query_log_tags:
      format: sqlcommenter
  replica:
    database: replica
    replica: true
    query_log_tags: false

Tag comments can be prepended to the query:

config.active_record.query_log_tags_prepend_comment = true

Whether the comment is prepended can also be overridden per connection pool with a .prepend_comment key under query_log_tags in a database.yml entry. Connections checked out from that pool use the configured value, while pools without any explicit configuration fall back to the global default:

production:
  primary:
    database: primary
  analytics:
    database: analytics
    query_log_tags:
      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)

This method is for internal use only.
[ GitHub ]

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

attr_accessor :prepend_comment, :cache_query_log_tags # :nodoc:

.prepend_comment (rw)

This method is for internal use only.
[ GitHub ]

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

attr_accessor :prepend_comment, :cache_query_log_tags # :nodoc:

.taggings (rw)

This method is for internal use only.
[ GitHub ]

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

attr_reader :tags, :taggings, :tags_formatter # :nodoc:

.taggings=(taggings) (rw)

This method is for internal use only.
[ GitHub ]

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

def taggings=(taggings) # :nodoc:
  @taggings = taggings.freeze
  @handlers = rebuild_handlers
end

.tags (rw)

This method is for internal use only.
[ GitHub ]

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

attr_reader :tags, :taggings, :tags_formatter # :nodoc:

.tags=(tags) (rw)

This method is for internal use only.
[ GitHub ]

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

def tags=(tags) # :nodoc:
  @tags = tags.freeze
  @handlers = rebuild_handlers
end

.tags_formatter (rw)

This method is for internal use only.
[ GitHub ]

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

attr_reader :tags, :taggings, :tags_formatter # :nodoc:

.tags_formatter=(format) (rw)

This method is for internal use only.
[ GitHub ]

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

def tags_formatter=(format) # :nodoc:
  @formatter = formatter_for(format)
  @tags_formatter = format
end

Class Method Details

.build_handler(name, handler = nil) (private)

[ GitHub ]

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

def build_handler(name, handler = nil)
  handler ||= @taggings[name]
  if handler.nil?
    GetKeyHandler.new(name)
  elsif handler.respond_to?(:call)
    if handler.arity == 0
      ZeroArityHandler.new(handler)
    else
      handler
    end
  else
    IdentityHandler.new(handler)
  end
end

.call(sql, connection)

This method is for internal use only.
[ GitHub ]

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

def call(sql, connection) # :nodoc:
  return sql if connection.pool.db_config.query_log_tags_config == false

  comment = self.comment(sql: sql, connection: connection)

  if comment.blank?
    sql
  elsif resolve_prepend_comment(connection)
    "#{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 199

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

.comment(extra_context) (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 265

def comment(extra_context)
  formatter = resolve_formatter(extra_context[:connection])

  if cache_query_log_tags
    cache = (self.cached_comments ||= {})
    cache.fetch(formatter) do
      cache[formatter] = uncached_comment(extra_context, formatter)
    end
  else
    uncached_comment(extra_context, formatter)
  end
end

.escape_sql_comment(content) (private)

[ GitHub ]

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

def escape_sql_comment(content)
  # Sanitize a string to appear within an 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_for(format) (private)

[ GitHub ]

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

def formatter_for(format)
  case format
  when :legacy
    LegacyFormatter
  when :sqlcommenter
    SQLCommenter
  else
    raise ArgumentError, "Formatter is unsupported: #{format}"
  end
end

.query_source_location

This method is for internal use only.
[ GitHub ]

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

def query_source_location # :nodoc:
  LogSubscriber.backtrace_cleaner.first_clean_frame
end

.rebuild_handlers (private)

[ GitHub ]

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

def rebuild_handlers
  handlers = []
  @tags.each do |i|
    if i.is_a?(Hash)
      i.each do |k, v|
        handlers << [k, build_handler(k, v)]
      end
    else
      handlers << [i, build_handler(i)]
    end
  end
  handlers.sort_by! { |(key, _)| key.to_s }
end

.resolve_formatter(connection) (private)

[ GitHub ]

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

def resolve_formatter(connection)
  if (format = connection.pool.db_config.query_log_tags_format)
    formatter_for(format)
  else
    @formatter
  end
end

.resolve_prepend_comment(connection) (private)

[ GitHub ]

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

def resolve_prepend_comment(connection)
  prepend = connection.pool.db_config.query_log_tags_prepend_comment
  prepend.nil? ? prepend_comment : prepend
end

.tag_content(extra_context, formatter) (private)

[ GitHub ]

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

def tag_content(extra_context, formatter)
  context = ActiveSupport::ExecutionContext.to_h
  context.reverse_merge!(extra_context)

  pairs = @handlers.filter_map do |(key, handler)|
    val = handler.call(context)
    formatter.format(key, val) unless val.nil?
  end
  formatter.join(pairs)
end

.uncached_comment(extra_context, formatter) (private)

[ GitHub ]

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

def uncached_comment(extra_context, formatter)
  content = tag_content(extra_context, formatter)

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