active_record.rb 4.78 KB
Newer Older
1 2
# frozen_string_literal: true

3 4 5 6 7 8 9
module Gitlab
  module Metrics
    module Subscribers
      # Class for tracking the total query duration of a transaction.
      class ActiveRecord < ActiveSupport::Subscriber
        attach_to :active_record

10
        IGNORABLE_SQL = %w{BEGIN COMMIT}.freeze
nmilojevic1's avatar
nmilojevic1 committed
11
        DB_COUNTERS = %i{db_count db_write_count db_cached_count}.freeze
12
        SQL_COMMANDS_WITH_COMMENTS_REGEX = /\A(\/\*.*\*\/\s)?((?!(.*[^\w'"](DELETE|UPDATE|INSERT INTO)[^\w'"])))(WITH.*)?(SELECT)((?!(FOR UPDATE|FOR SHARE)).)*$/i.freeze
13

14
        SQL_DURATION_BUCKET = [0.05, 0.1, 0.25].freeze
15
        TRANSACTION_DURATION_BUCKET = [0.1, 0.25, 1].freeze
16

17 18 19 20 21 22 23 24
        DB_LOAD_BALANCING_COUNTERS = %i{
          db_replica_count db_replica_cached_count db_replica_wal_count
          db_primary_count db_primary_cached_count db_primary_wal_count
        }.freeze
        DB_LOAD_BALANCING_DURATIONS = %i{db_primary_duration_s db_replica_duration_s}.freeze

        SQL_WAL_LOCATION_REGEX = /(pg_current_wal_insert_lsn\(\)::text|pg_last_wal_replay_lsn\(\)::text)/.freeze

Quang-Minh Nguyen's avatar
Quang-Minh Nguyen committed
25 26 27
        # This event is published from ActiveRecordBaseTransactionMetrics and
        # used to record a database transaction duration when calling
        # ActiveRecord::Base.transaction {} block.
28
        def transaction(event)
29
          observe(:gitlab_database_transaction_seconds, event) do
30
            buckets TRANSACTION_DURATION_BUCKET
31
          end
32 33
        end

34
        def sql(event)
35 36 37 38 39
          # Mark this thread as requiring a database connection. This is used
          # by the Gitlab::Metrics::Samplers::ThreadsSampler to count threads
          # using a connection.
          Thread.current[:uses_db_connection] = true

40
          payload = event.payload
41
          return if ignored_query?(payload)
42

43 44 45 46
          increment(:db_count)
          increment(:db_cached_count) if cached_query?(payload)
          increment(:db_write_count) unless select_sql_command?(payload)

47 48 49
          observe(:gitlab_sql_duration_seconds, event) do
            buckets SQL_DURATION_BUCKET
          end
50 51 52 53 54 55 56 57

          if ::Gitlab::Database::LoadBalancing.enable?
            db_role = ::Gitlab::Database::LoadBalancing.db_role_for_connection(payload[:connection])
            return if db_role.blank?

            increment_db_role_counters(db_role, payload)
            observe_db_role_duration(db_role, event)
          end
58 59
        end

nmilojevic1's avatar
nmilojevic1 committed
60
        def self.db_counter_payload
nmilojevic1's avatar
nmilojevic1 committed
61
          return {} unless Gitlab::SafeRequestStore.active?
nmilojevic1's avatar
nmilojevic1 committed
62

63 64 65 66 67 68 69 70 71 72 73 74 75
          {}.tap do |payload|
            DB_COUNTERS.each do |counter|
              payload[counter] = Gitlab::SafeRequestStore[counter].to_i
            end

            if ::Gitlab::SafeRequestStore.active? && ::Gitlab::Database::LoadBalancing.enable?
              DB_LOAD_BALANCING_COUNTERS.each do |counter|
                payload[counter] = ::Gitlab::SafeRequestStore[counter].to_i
              end
              DB_LOAD_BALANCING_DURATIONS.each do |duration|
                payload[duration] = ::Gitlab::SafeRequestStore[duration].to_f.round(3)
              end
            end
76
          end
nmilojevic1's avatar
nmilojevic1 committed
77 78
        end

79 80
        private

81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
        def wal_command?(payload)
          payload[:sql].match(SQL_WAL_LOCATION_REGEX)
        end

        def increment_db_role_counters(db_role, payload)
          increment("db_#{db_role}_count".to_sym)
          increment("db_#{db_role}_cached_count".to_sym) if cached_query?(payload)
          increment("db_#{db_role}_wal_count".to_sym) if !cached_query?(payload) && wal_command?(payload)
        end

        def observe_db_role_duration(db_role, event)
          observe("gitlab_sql_#{db_role}_duration_seconds".to_sym, event) do
            buckets ::Gitlab::Metrics::Subscribers::ActiveRecord::SQL_DURATION_BUCKET
          end

          duration = event.duration / 1000.0
          duration_key = "db_#{db_role}_duration_s".to_sym
          ::Gitlab::SafeRequestStore[duration_key] = (::Gitlab::SafeRequestStore[duration_key].presence || 0) + duration
        end

101 102 103 104 105 106 107 108
        def ignored_query?(payload)
          payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql])
        end

        def cached_query?(payload)
          payload.fetch(:cached, payload[:name] == 'CACHE')
        end

nmilojevic1's avatar
nmilojevic1 committed
109
        def select_sql_command?(payload)
110
          payload[:sql].match(SQL_COMMANDS_WITH_COMMENTS_REGEX)
nmilojevic1's avatar
nmilojevic1 committed
111 112
        end

nmilojevic1's avatar
nmilojevic1 committed
113
        def increment(counter)
114
          current_transaction&.increment("gitlab_transaction_#{counter}_total".to_sym, 1)
nmilojevic1's avatar
nmilojevic1 committed
115

116
          Gitlab::SafeRequestStore[counter] = Gitlab::SafeRequestStore[counter].to_i + 1
nmilojevic1's avatar
nmilojevic1 committed
117 118
        end

119 120
        def observe(histogram, event, &block)
          current_transaction&.observe(histogram, event.duration / 1000.0, &block)
121 122
        end

123 124
        def current_transaction
          ::Gitlab::Metrics::WebTransaction.current || ::Gitlab::Metrics::BackgroundTransaction.current
125
        end
126 127 128 129
      end
    end
  end
end