Skip to content

Commit 5422751

Browse files
authored
Merge pull request rails#42240 from keeran/keeran/query-comments
Add Marginalia to Rails, via QueryLogs
2 parents 843c0a3 + 2408615 commit 5422751

File tree

9 files changed

+782
-0
lines changed

9 files changed

+782
-0
lines changed

actionpack/lib/action_controller/railtie.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,35 @@ class Railtie < Rails::Railtie # :nodoc:
8686
ActionController::Metal.descendants.each(&:action_methods) if config.eager_load
8787
end
8888
end
89+
90+
initializer "action_controller.query_log_tags" do |app|
91+
ActiveSupport.on_load(:action_controller_base) do
92+
singleton_class.attr_accessor :log_query_tags_around_actions
93+
self.log_query_tags_around_actions = true
94+
end
95+
96+
ActiveSupport.on_load(:active_record) do
97+
if app.config.active_record.query_log_tags_enabled && app.config.action_controller.log_query_tags_around_actions != false
98+
ActiveRecord::QueryLogs.taggings.merge! \
99+
controller: -> { context[:controller]&.controller_name },
100+
action: -> { context[:controller]&.action_name },
101+
namespaced_controller: -> { context[:controller]&.class&.name }
102+
103+
ActiveRecord::QueryLogs.tags << :controller << :action
104+
105+
context_extension = ->(controller) do
106+
around_action :expose_controller_to_query_logs
107+
108+
private
109+
def expose_controller_to_query_logs(&block)
110+
ActiveRecord::QueryLogs.set_context(controller: self, &block)
111+
end
112+
end
113+
114+
ActionController::Base.class_eval(&context_extension)
115+
ActionController::API.class_eval(&context_extension)
116+
end
117+
end
118+
end
89119
end
90120
end

activejob/lib/active_job/railtie.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,28 @@ class Railtie < Rails::Railtie # :nodoc:
4949
end
5050
end
5151
end
52+
53+
initializer "active_job.query_log_tags" do |app|
54+
ActiveSupport.on_load(:active_job) do
55+
singleton_class.attr_accessor :log_query_tags_around_perform
56+
self.log_query_tags_around_perform = true
57+
end
58+
59+
ActiveSupport.on_load(:active_record) do
60+
if app.config.active_record.query_log_tags_enabled && app.config.active_job.log_query_tags_around_perform != false
61+
ActiveRecord::QueryLogs.taggings[:job] = -> { context[:job]&.class&.name }
62+
ActiveRecord::QueryLogs.tags << :job
63+
64+
ActiveJob::Base.class_eval do
65+
around_perform :expose_job_to_query_logs
66+
67+
private
68+
def expose_job_to_query_logs(&block)
69+
ActiveRecord::QueryLogs.set_context(job: self, &block)
70+
end
71+
end
72+
end
73+
end
74+
end
5275
end
5376
end

activerecord/CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,47 @@
1+
* Add `ActiveRecord::QueryLogs`.
2+
3+
Configurable tags can be automatically added to all SQL queries generated by Active Record.
4+
5+
```ruby
6+
# config/application.rb
7+
module MyApp
8+
class Application < Rails::Application
9+
config.active_record.query_log_tags_enabled = true
10+
end
11+
end
12+
```
13+
14+
By default the application, controller and action details are added to the query tags:
15+
16+
```ruby
17+
class BooksController < ApplicationController
18+
def index
19+
@books = Book.all
20+
end
21+
end
22+
```
23+
24+
```ruby
25+
GET /books
26+
# SELECT * FROM books /*application:MyApp;controller:books;action:index*/
27+
```
28+
29+
Custom tags containing static values and Procs can be defined in the application configuration:
30+
31+
```ruby
32+
config.active_record.query_log_tags = [
33+
:application,
34+
:controller,
35+
:action,
36+
{
37+
custom_static: "foo",
38+
custom_dynamic: -> { Time.now }
39+
}
40+
]
41+
```
42+
43+
*Keeran Raj Hawoldar*, *Eileen M. Uchitelle*, *Kasper Timm Hansen*
44+
145
* Added support for multiple databases to `rails db:setup` and `rails db:reset`.
246

347
*Ryan Hall*

activerecord/lib/active_record.rb

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ module ActiveRecord
5858
autoload :Persistence
5959
autoload :QueryCache
6060
autoload :Querying
61+
autoload :QueryLogs
6162
autoload :ReadonlyAttributes
6263
autoload :RecordInvalid, "active_record/validations"
6364
autoload :Reflection
@@ -322,6 +323,51 @@ def self.global_executor_concurrency # :nodoc:
322323
singleton_class.attr_accessor :verify_foreign_keys_for_fixtures
323324
self.verify_foreign_keys_for_fixtures = false
324325

326+
##
327+
# :singleton-method:
328+
# Specify whether or not to enable adapter-level query comments.
329+
# To enable:
330+
#
331+
# config.active_record.query_log_tags_enabled = true
332+
#
333+
# When included in +ActionController+, controller context is automatically updated via an
334+
# +around_action+ filter. This behaviour can be disabled as follows:
335+
#
336+
# config.action_controller.log_query_tags_around_actions = false
337+
#
338+
# This behaviour can be disabled for +ActiveJob+ in a similar way:
339+
#
340+
# config.active_job.log_query_tags_around_perform = false
341+
singleton_class.attr_accessor :query_log_tags_enabled
342+
self.query_log_tags_enabled = false
343+
344+
##
345+
# :singleton-method:
346+
# An +Array+ specifying the key/value tags to be inserted in an SQL comment. Defaults to `[ :application ]`, a
347+
# predefined tag returning the application name.
348+
#
349+
# Custom values can be passed in as a +Hash+:
350+
#
351+
# config.active_record.query_log_tags = [ :application, { custom: 'value' } ]
352+
#
353+
# See +ActiveRecord::QueryLogs+ for more details
354+
# on predefined tags and defining new tag content.
355+
singleton_class.attr_accessor :query_log_tags
356+
self.query_log_tags = [ :application ]
357+
358+
##
359+
# :singleton-method:
360+
# Specify whether or not to enable caching of query log tags.
361+
# For applications that have a large number of queries, caching query log tags can
362+
# provide a performance benefit when the context does not change during the lifetime
363+
# of the request or job execution.
364+
#
365+
# To enable:
366+
#
367+
# config.active_record.cache_query_log_tags = true
368+
singleton_class.attr_accessor :cache_query_log_tags
369+
self.cache_query_log_tags = false
370+
325371
def self.eager_load!
326372
super
327373
ActiveRecord::Locking.eager_load!

activerecord/lib/active_record/database_configurations/hash_config.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ def host
5555
configuration_hash[:host]
5656
end
5757

58+
def socket # :nodoc:
59+
configuration_hash[:socket]
60+
end
61+
5862
def database
5963
configuration_hash[:database]
6064
end
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# frozen_string_literal: true
2+
3+
require "active_support/core_ext/module/attribute_accessors_per_thread"
4+
5+
module ActiveRecord
6+
# = Active Record Query Logs
7+
#
8+
# Automatically tag SQL queries with runtime information.
9+
#
10+
# Default tags available for use:
11+
#
12+
# * +application+
13+
# * +pid+
14+
# * +socket+
15+
# * +db_host+
16+
# * +database+
17+
#
18+
# _Action Controller and Active Job tags are also defined when used in Rails:_
19+
#
20+
# * +controller+
21+
# * +action+
22+
# * +job+
23+
#
24+
# The tags used in a query can be configured directly:
25+
#
26+
# ActiveRecord::QueryLogs.tags = [ :application, :controller, :action, :job ]
27+
#
28+
# or via Rails configuration:
29+
#
30+
# config.active_record.query_log_tags = [ :application, :controller, :action, :job ]
31+
#
32+
# To add new comment tags, add a hash to the tags array containing the keys and values you
33+
# want to add to the comment. Dynamic content can be created by setting a proc or lambda value in a hash,
34+
# and can reference any value stored in the +context+ object.
35+
#
36+
# Example:
37+
#
38+
# tags = [
39+
# :application,
40+
# { custom_tag: -> { context[:controller].controller_name } }
41+
# ]
42+
# ActiveRecord::QueryLogs.tags = tags
43+
#
44+
# The QueryLogs +context+ can be manipulated via +update_context+ & +set_context+ methods.
45+
#
46+
# Direct updates to a context value:
47+
#
48+
# ActiveRecord::QueryLogs.update_context(foo: Bar.new)
49+
#
50+
# Temporary updates limited to the execution of a block:
51+
#
52+
# ActiveRecord::QueryLogs.set_context(foo: Bar.new) do
53+
# posts = Post.all
54+
# end
55+
#
56+
# Tag comments can be prepended to the query:
57+
#
58+
# ActiveRecord::QueryLogs.prepend_comment = true
59+
#
60+
# For applications where the content will not change during the lifetime of
61+
# the request or job execution, the tags can be cached for reuse in every query:
62+
#
63+
# ActiveRecord::QueryLogs.cache_query_log_tags = true
64+
#
65+
# This option can be set during application configuration or in a Rails initializer:
66+
#
67+
# config.active_record.cache_query_log_tags = true
68+
module QueryLogs
69+
mattr_accessor :taggings, instance_accessor: false, default: {}
70+
mattr_accessor :tags, instance_accessor: false, default: [ :application ]
71+
mattr_accessor :prepend_comment, instance_accessor: false, default: false
72+
mattr_accessor :cache_query_log_tags, instance_accessor: false, default: false
73+
thread_mattr_accessor :cached_comment, instance_accessor: false
74+
75+
class << self
76+
# Updates the context used to construct tags in the SQL comment.
77+
# Resets the cached comment if <tt>cache_query_log_tags</tt> is +true+.
78+
def update_context(**options)
79+
context.merge!(**options.symbolize_keys)
80+
self.cached_comment = nil
81+
end
82+
83+
# Updates the context used to construct tags in the SQL comment during
84+
# execution of the provided block. Resets provided values to nil after
85+
# the block is executed.
86+
def set_context(**options)
87+
update_context(**options)
88+
yield if block_given?
89+
ensure
90+
update_context(**options.transform_values! { nil })
91+
end
92+
93+
# Temporarily tag any query executed within `&block`. Can be nested.
94+
def with_tag(tag, &block)
95+
inline_tags.push(tag)
96+
yield if block_given?
97+
ensure
98+
inline_tags.pop
99+
end
100+
101+
def add_query_log_tags_to_sql(sql) # :nodoc:
102+
comments.each do |comment|
103+
unless sql.include?(comment)
104+
sql = prepend_comment ? "#{comment} #{sql}" : "#{sql} #{comment}"
105+
end
106+
end
107+
sql
108+
end
109+
110+
private
111+
# Returns an array of comments which need to be added to the query, comprised
112+
# of configured and inline tags.
113+
def comments
114+
[ comment, inline_comment ].compact
115+
end
116+
117+
# Returns an SQL comment +String+ containing the query log tags.
118+
# Sets and returns a cached comment if <tt>cache_query_log_tags</tt> is +true+.
119+
def comment
120+
if cache_query_log_tags
121+
self.cached_comment ||= uncached_comment
122+
else
123+
uncached_comment
124+
end
125+
end
126+
127+
def uncached_comment
128+
content = tag_content
129+
if content.present?
130+
"/*#{escape_sql_comment(content)}*/"
131+
end
132+
end
133+
134+
# Returns a +String+ containing any inline comments from +with_tag+.
135+
def inline_comment
136+
return nil unless inline_tags.present?
137+
"/*#{escape_sql_comment(inline_tag_content)}*/"
138+
end
139+
140+
# Return the set of active inline tags from +with_tag+.
141+
def inline_tags
142+
context[:inline_tags] ||= []
143+
end
144+
145+
def context
146+
Thread.current[:active_record_query_log_tags_context] ||= {}
147+
end
148+
149+
def escape_sql_comment(content)
150+
content.to_s.gsub(%r{ (/ (?: | \g<1>) \*) \+? \s* | \s* (\* (?: | \g<2>) /) }x, "")
151+
end
152+
153+
def tag_content
154+
tags.flat_map { |i| [*i] }.filter_map do |tag|
155+
key, value_input = tag
156+
val = case value_input
157+
when nil then instance_exec(&taggings[key]) if taggings.has_key? key
158+
when Proc then instance_exec(&value_input)
159+
else value_input
160+
end
161+
"#{key}:#{val}" unless val.nil?
162+
end.join(",")
163+
end
164+
165+
def inline_tag_content
166+
inline_tags.join
167+
end
168+
end
169+
170+
module ExecutionMethods
171+
def execute(sql, *args, **kwargs)
172+
super(ActiveRecord::QueryLogs.add_query_log_tags_to_sql(sql), *args, **kwargs)
173+
end
174+
175+
def exec_query(sql, *args, **kwargs)
176+
super(ActiveRecord::QueryLogs.add_query_log_tags_to_sql(sql), *args, **kwargs)
177+
end
178+
end
179+
end
180+
end
181+
182+
ActiveSupport.on_load(:active_record) do
183+
ActiveRecord::QueryLogs.taggings.merge! \
184+
socket: -> { ActiveRecord::Base.connection_db_config.socket },
185+
db_host: -> { ActiveRecord::Base.connection_db_config.host },
186+
database: -> { ActiveRecord::Base.connection_db_config.database }
187+
end

0 commit comments

Comments
 (0)