mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-08-06 10:16:57 +00:00
154 lines
4.4 KiB
Ruby
154 lines
4.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Gitlab
|
|
module Database
|
|
# The purpose of this class is to implement a various query analyzers based on `pg_query`
|
|
# And process them all via `Gitlab::Database::QueryAnalyzers::*`
|
|
#
|
|
# Sometimes this might cause errors in specs.
|
|
# This is best to be disable with `describe '...', query_analyzers: false do`
|
|
class QueryAnalyzer
|
|
include ::Singleton
|
|
|
|
class Parsed
|
|
include Gitlab::Utils::StrongMemoize
|
|
|
|
attr_reader :raw, :connection, :event_name, :error
|
|
|
|
def initialize(raw, connection, event_name)
|
|
@raw = raw
|
|
@connection = connection
|
|
@event_name = normalize_event_name(event_name)
|
|
@error = nil
|
|
end
|
|
|
|
def sql
|
|
try_parse do
|
|
PgQuery.normalize(raw)
|
|
end
|
|
end
|
|
strong_memoize_attr :sql
|
|
|
|
def pg
|
|
try_parse do
|
|
PgQuery.parse(raw)
|
|
end
|
|
end
|
|
strong_memoize_attr :pg
|
|
|
|
private
|
|
|
|
def try_parse
|
|
return if error
|
|
|
|
yield
|
|
rescue PgQuery::ParseError => e
|
|
# Ignore PgQuery parse errors (due to depth limit or other reasons)
|
|
Gitlab::ErrorTracking.track_exception(e)
|
|
@error = e
|
|
|
|
nil
|
|
end
|
|
|
|
def normalize_event_name(event_name)
|
|
split_event_name = event_name.to_s.downcase.split(' ')
|
|
|
|
split_event_name.size > 1 ? split_event_name.from(1).join('_') : split_event_name.join('_')
|
|
end
|
|
end
|
|
|
|
attr_reader :all_analyzers
|
|
|
|
def initialize
|
|
@all_analyzers = []
|
|
end
|
|
|
|
# @info most common event names are:
|
|
# Model Load, Model Create, Model Update, Model Pluck, Model Destroy, Model Insert, Model Delete All
|
|
# Model Exists?, nil, TRANSACTION, SCHEMA
|
|
def hook!
|
|
@subscriber = ActiveSupport::Notifications.subscribe('sql.active_record') do |event|
|
|
# In some cases analyzer code might trigger another SQL call
|
|
# to avoid stack too deep this detects recursive call of subscriber
|
|
with_ignored_recursive_calls do
|
|
process_sql(event.payload[:sql], event.payload[:connection], event.payload[:name].to_s)
|
|
end
|
|
end
|
|
end
|
|
|
|
def within(analyzers = all_analyzers)
|
|
newly_enabled_analyzers = begin!(analyzers)
|
|
|
|
begin
|
|
yield
|
|
ensure
|
|
end!(newly_enabled_analyzers)
|
|
end
|
|
end
|
|
|
|
# Enable query analyzers (only the ones that were not yet enabled)
|
|
# Returns a list of newly enabled analyzers
|
|
def begin!(analyzers)
|
|
analyzers.select do |analyzer|
|
|
next if enabled_analyzers.include?(analyzer)
|
|
|
|
if analyzer.enabled?
|
|
analyzer.begin!
|
|
enabled_analyzers.append(analyzer)
|
|
|
|
true
|
|
end
|
|
rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
|
|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
|
|
|
false
|
|
end
|
|
end
|
|
|
|
# Disable enabled query analyzers (only the ones that were enabled previously)
|
|
def end!(analyzers)
|
|
analyzers.each do |analyzer|
|
|
next unless enabled_analyzers.delete(analyzer)
|
|
|
|
analyzer.end!
|
|
rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
|
|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def enabled_analyzers
|
|
Thread.current[:query_analyzer_enabled_analyzers] ||= []
|
|
end
|
|
|
|
def process_sql(sql, connection, event_name)
|
|
analyzers = enabled_analyzers
|
|
return unless analyzers&.any?
|
|
|
|
parsed = Parsed.new(sql, connection, event_name)
|
|
|
|
analyzers.each do |analyzer|
|
|
next if analyzer.suppressed? && !analyzer.requires_tracking?(parsed)
|
|
|
|
analyzer.analyze(parsed)
|
|
rescue StandardError, ::Gitlab::Database::QueryAnalyzers::Base::QueryAnalyzerError => e
|
|
# We catch all standard errors to prevent validation errors to introduce fatal errors in production
|
|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e)
|
|
end
|
|
end
|
|
|
|
def with_ignored_recursive_calls
|
|
return if Thread.current[:query_analyzer_recursive]
|
|
|
|
begin
|
|
Thread.current[:query_analyzer_recursive] = true
|
|
yield
|
|
ensure
|
|
Thread.current[:query_analyzer_recursive] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|