summaryrefslogtreecommitdiff
path: root/lib/gitlab/instrumentation_helper.rb
blob: 6b0f01757b786c7d2322571f2b330984a14fe317 (plain)
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
# frozen_string_literal: true

module Gitlab
  module InstrumentationHelper
    extend self

    DURATION_PRECISION = 6 # microseconds

    def keys
      @keys ||= [:gitaly_calls,
                 :gitaly_duration_s,
                 :rugged_calls,
                 :rugged_duration_s,
                 :elasticsearch_calls,
                 :elasticsearch_duration_s,
                 *::Gitlab::Instrumentation::Redis.known_payload_keys,
                 *::Gitlab::Metrics::Subscribers::ActiveRecord::DB_COUNTERS]
    end

    def add_instrumentation_data(payload)
      instrument_gitaly(payload)
      instrument_rugged(payload)
      instrument_redis(payload)
      instrument_elasticsearch(payload)
      instrument_throttle(payload)
      instrument_active_record(payload)
    end

    def instrument_gitaly(payload)
      gitaly_calls = Gitlab::GitalyClient.get_request_count

      return if gitaly_calls == 0

      payload[:gitaly_calls] = gitaly_calls
      payload[:gitaly_duration_s] = Gitlab::GitalyClient.query_time
    end

    def instrument_rugged(payload)
      rugged_calls = Gitlab::RuggedInstrumentation.query_count

      return if rugged_calls == 0

      payload[:rugged_calls] = rugged_calls
      payload[:rugged_duration_s] = Gitlab::RuggedInstrumentation.query_time
    end

    def instrument_redis(payload)
      payload.merge! ::Gitlab::Instrumentation::Redis.payload
    end

    def instrument_elasticsearch(payload)
      # Elasticsearch integration is only available in EE but instrumentation
      # only depends on the Gem which is also available in FOSS.
      elasticsearch_calls = Gitlab::Instrumentation::ElasticsearchTransport.get_request_count

      return if elasticsearch_calls == 0

      payload[:elasticsearch_calls] = elasticsearch_calls
      payload[:elasticsearch_duration_s] = Gitlab::Instrumentation::ElasticsearchTransport.query_time
    end

    def instrument_throttle(payload)
      safelist = Gitlab::Instrumentation::Throttle.safelist
      payload[:throttle_safelist] = safelist if safelist.present?
    end

    def instrument_active_record(payload)
      db_counters = ::Gitlab::Metrics::Subscribers::ActiveRecord.db_counter_payload

      payload.merge!(db_counters)
    end

    # Returns the queuing duration for a Sidekiq job in seconds, as a float, if the
    # `enqueued_at` field or `created_at` field is available.
    #
    # * If the job doesn't contain sufficient information, returns nil
    # * If the job has a start time in the future, returns 0
    # * If the job contains an invalid start time value, returns nil
    # @param [Hash] job a Sidekiq job, represented as a hash
    def self.queue_duration_for_job(job)
      # Old gitlab-shell messages don't provide enqueued_at/created_at attributes
      enqueued_at = job['enqueued_at'] || job['created_at']
      return unless enqueued_at

      enqueued_at_time = convert_to_time(enqueued_at)
      return unless enqueued_at_time

      # Its possible that if theres clock-skew between two nodes
      # this value may be less than zero. In that event, we record the value
      # as zero.
      [elapsed_by_absolute_time(enqueued_at_time), 0].max.round(DURATION_PRECISION)
    end

    # Calculates the time in seconds, as a float, from
    # the provided start time until now
    #
    # @param [Time] start
    def self.elapsed_by_absolute_time(start)
      (Time.now - start).to_f.round(6)
    end
    private_class_method :elapsed_by_absolute_time

    # Convert a representation of a time into a `Time` value
    #
    # @param time_value String, Float time representation, or nil
    def self.convert_to_time(time_value)
      return time_value if time_value.is_a?(Time)
      return Time.iso8601(time_value) if time_value.is_a?(String)
      return Time.at(time_value) if time_value.is_a?(Numeric) && time_value > 0
    rescue ArgumentError
      # Swallow invalid dates. Better to loose some observability
      # than bring all background processing down because of a date
      # formatting bug in a client
    end
    private_class_method :convert_to_time
  end
end