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