Commit 0b4af617 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'ajk-improve-query-backtrace-logging' into 'master'

Ensure we prefix each line when logging the SQL

See merge request gitlab-org/gitlab!59186
parents 8d4291ed 4ad59b22
...@@ -3,37 +3,53 @@ ...@@ -3,37 +3,53 @@
module ActiveRecord module ActiveRecord
class QueryRecorder class QueryRecorder
attr_reader :log, :skip_cached, :cached, :data attr_reader :log, :skip_cached, :cached, :data
UNKNOWN = %w(unknown unknown).freeze
def initialize(skip_cached: true, query_recorder_debug: false, &block) UNKNOWN = %w[unknown unknown].freeze
@data = Hash.new { |h, k| h[k] = { count: 0, occurrences: [], backtrace: [] } }
def initialize(skip_cached: true, log_file: nil, query_recorder_debug: false, &block)
@data = Hash.new { |h, k| h[k] = { count: 0, occurrences: [], backtrace: [], durations: [] } }
@log = [] @log = []
@cached = [] @cached = []
@skip_cached = skip_cached @skip_cached = skip_cached
@query_recorder_debug = query_recorder_debug @query_recorder_debug = ENV['QUERY_RECORDER_DEBUG'] || query_recorder_debug
@log_file = log_file
# force replacement of bind parameters to give tests the ability to check for ids # force replacement of bind parameters to give tests the ability to check for ids
ActiveRecord::Base.connection.unprepared_statement do ActiveRecord::Base.connection.unprepared_statement do
ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
end end
end end
def show_backtrace(values) def show_backtrace(values, duration)
Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}") values[:sql].lines.each do |line|
print_to_log(:SQL, line)
end
print_to_log(:DURATION, duration)
Gitlab::BacktraceCleaner.clean_backtrace(caller).each do |line| Gitlab::BacktraceCleaner.clean_backtrace(caller).each do |line|
Rails.logger.debug("QueryRecorder backtrace: --> #{line}") print_to_log(:backtrace, line)
end
end
def print_to_log(label, line)
msg = "QueryRecorder #{label}: --> #{line}"
if @log_file
@log_file.puts(msg)
else
Rails.logger.debug(msg)
end end
end end
def get_sql_source(sql) def get_sql_source(sql)
matches = sql.match(/,line:(?<line>.*):in\s+`(?<method>.*)'\*\//) matches = sql.match(%r{,line:(?<line>.*):in\s+`(?<method>.*)'\*/})
matches ? [matches[:line], matches[:method]] : UNKNOWN matches ? [matches[:line], matches[:method]] : UNKNOWN
end end
def store_sql_by_source(values: {}, backtrace: nil) def store_sql_by_source(values: {}, duration: nil, backtrace: nil)
full_name = get_sql_source(values[:sql]).join(':') full_name = get_sql_source(values[:sql]).join(':')
@data[full_name][:count] += 1 @data[full_name][:count] += 1
@data[full_name][:occurrences] << values[:sql] @data[full_name][:occurrences] << values[:sql]
@data[full_name][:backtrace] << backtrace @data[full_name][:backtrace] << backtrace
@data[full_name][:durations] << duration
end end
def find_query(query_regexp, limit, first_only: false) def find_query(query_regexp, limit, first_only: false)
...@@ -55,14 +71,14 @@ module ActiveRecord ...@@ -55,14 +71,14 @@ module ActiveRecord
end end
def callback(name, start, finish, message_id, values) def callback(name, start, finish, message_id, values)
store_backtrace = ENV['QUERY_RECORDER_DEBUG'] || @query_recorder_debug duration = finish - start
backtrace = store_backtrace ? show_backtrace(values) : nil
if values[:cached] && skip_cached if values[:cached] && skip_cached
@cached << values[:sql] @cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA") elsif !values[:name]&.include?("SCHEMA")
backtrace = @query_recorder_debug ? show_backtrace(values, duration) : nil
@log << values[:sql] @log << values[:sql]
store_sql_by_source(values: values, backtrace: backtrace) store_sql_by_source(values: values, duration: duration, backtrace: backtrace)
end end
end end
......
...@@ -11,6 +11,72 @@ RSpec.describe ActiveRecord::QueryRecorder do ...@@ -11,6 +11,72 @@ RSpec.describe ActiveRecord::QueryRecorder do
end end
end end
describe 'printing to the log' do
let(:backtrace) { %r{QueryRecorder backtrace: --> (\w+/)*\w+\.rb:\d+:in `.*'} }
let(:duration_line) { %r{QueryRecorder DURATION: --> \d+\.\d+} }
def expect_section(query, lines)
query_lines = lines.take(query.size)
# the query comes first
expect(query_lines).to match(query)
# followed by the duration
expect(lines[query.size]).to match(duration_line)
# and then one or more lines of backtrace
backtrace_lines = lines.drop(query.size + 1).take_while { |line| line.match(backtrace) }
expect(backtrace_lines).not_to be_empty
# Advance to the next section
lines.drop(query.size + 1 + backtrace_lines.size)
end
it 'prints SQL, duration and backtrace, all prefixed with QueryRecorder', :aggregate_failures do
io = StringIO.new
control = ActiveRecord::QueryRecorder.new(log_file: io, query_recorder_debug: true) do
TestQueries.count
TestQueries.first
TestQueries.where(<<~FRAGMENT).to_a # tests multi-line SQL
version = 'foo'
OR
version = 'bar'
FRAGMENT
end
query_a = start_with(%q[QueryRecorder SQL: --> SELECT COUNT(*) FROM "schema_migrations"])
query_b = start_with(%q[QueryRecorder SQL: --> SELECT "schema_migrations".* FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC LIMIT 1])
query_c_a = eq(%q[QueryRecorder SQL: --> SELECT "schema_migrations".* FROM "schema_migrations" WHERE (version = 'foo'])
query_c_b = eq(%q(QueryRecorder SQL: --> OR))
query_c_c = eq(%q(QueryRecorder SQL: --> version = 'bar'))
query_c_d = start_with("QueryRecorder SQL: --> )")
expect(control.count).to eq(3)
lines = io.string.lines.map(&:chomp)
expect(lines).to all(start_with('QueryRecorder'))
lines = expect_section([query_a], lines)
lines = expect_section([query_b], lines)
lines = expect_section([query_c_a, query_c_b, query_c_c, query_c_d], lines)
expect(lines).to be_empty
end
end
it 'includes duration information' do
control = ActiveRecord::QueryRecorder.new do
TestQueries.count
TestQueries.first
end
expect(control.count).to eq(2)
expect(control.data.values.flat_map { _1[:durations] }).to match([be > 0, be > 0])
end
describe 'detecting the right number of calls and their origin' do describe 'detecting the right number of calls and their origin' do
it 'detects two separate queries' do it 'detects two separate queries' do
control = ActiveRecord::QueryRecorder.new query_recorder_debug: true do control = ActiveRecord::QueryRecorder.new query_recorder_debug: true do
...@@ -23,10 +89,10 @@ RSpec.describe ActiveRecord::QueryRecorder do ...@@ -23,10 +89,10 @@ RSpec.describe ActiveRecord::QueryRecorder do
.to eq(control.data.keys.size) .to eq(control.data.keys.size)
# Ensure exactly 2 COUNT queries were detected # Ensure exactly 2 COUNT queries were detected
expect(control.occurrences_by_line_method.last[1][:occurrences] expect(control.occurrences_by_line_method.last[1][:occurrences]
.find_all {|i| i.match(/SELECT COUNT/) }.count).to eq(2) .count { |str| str.start_with?('SELECT COUNT') }).to eq(2)
# Ensure exactly 1 LIMIT 1 (#first) # Ensure exactly 1 LIMIT 1 (#first)
expect(control.occurrences_by_line_method.first[1][:occurrences] expect(control.occurrences_by_line_method.first[1][:occurrences]
.find_all { |i| i.match(/ORDER BY.*#{TestQueries.table_name}.*LIMIT 1/) }.count).to eq(1) .count { |str| str.match(/ORDER BY.*#{TestQueries.table_name}.*LIMIT 1/) }).to eq(1)
# Ensure 3 DB calls overall were executed # Ensure 3 DB calls overall were executed
expect(control.log.size).to eq(3) expect(control.log.size).to eq(3)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment