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
 |