summaryrefslogtreecommitdiff
path: root/lib/gitlab/metrics/instrumentation.rb
blob: 708ef79f3040c4051b05bcd591da37274074c457 (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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
module Gitlab
  module Metrics
    # Module for instrumenting methods.
    #
    # This module allows instrumenting of methods without having to actually
    # alter the target code (e.g. by including modules).
    #
    # Example usage:
    #
    #     Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login)
    module Instrumentation
      SERIES = 'method_calls'

      PROXY_IVAR = :@__gitlab_instrumentation_proxy

      def self.configure
        yield self
      end

      # Instruments a class method.
      #
      # mod  - The module to instrument as a Module/Class.
      # name - The name of the method to instrument.
      def self.instrument_method(mod, name)
        instrument(:class, mod, name)
      end

      # Instruments an instance method.
      #
      # mod  - The module to instrument as a Module/Class.
      # name - The name of the method to instrument.
      def self.instrument_instance_method(mod, name)
        instrument(:instance, mod, name)
      end

      # Recursively instruments all subclasses of the given root module.
      #
      # This can be used to for example instrument all ActiveRecord models (as
      # these all inherit from ActiveRecord::Base).
      #
      # This method can optionally take a block to pass to `instrument_methods`
      # and `instrument_instance_methods`.
      #
      # root - The root module for which to instrument subclasses. The root
      #        module itself is not instrumented.
      def self.instrument_class_hierarchy(root, &block)
        visit = root.subclasses

        until visit.empty?
          klass = visit.pop

          instrument_methods(klass, &block)
          instrument_instance_methods(klass, &block)

          klass.subclasses.each { |c| visit << c }
        end
      end

      # Instruments all public methods of a module.
      #
      # This method optionally takes a block that can be used to determine if a
      # method should be instrumented or not. The block is passed the receiving
      # module and an UnboundMethod. If the block returns a non truthy value the
      # method is not instrumented.
      #
      # mod - The module to instrument.
      def self.instrument_methods(mod)
        mod.public_methods(false).each do |name|
          method = mod.method(name)

          if method.owner == mod.singleton_class
            if !block_given? || block_given? && yield(mod, method)
              instrument_method(mod, name)
            end
          end
        end
      end

      # Instruments all public instance methods of a module.
      #
      # See `instrument_methods` for more information.
      #
      # mod - The module to instrument.
      def self.instrument_instance_methods(mod)
        mod.public_instance_methods(false).each do |name|
          method = mod.instance_method(name)

          if method.owner == mod
            if !block_given? || block_given? && yield(mod, method)
              instrument_instance_method(mod, name)
            end
          end
        end
      end

      # Returns true if a module is instrumented.
      #
      # mod - The module to check
      def self.instrumented?(mod)
        mod.instance_variable_defined?(PROXY_IVAR)
      end

      # Returns the proxy module (if any) of `mod`.
      def self.proxy_module(mod)
        mod.instance_variable_get(PROXY_IVAR)
      end

      # Instruments a method.
      #
      # type - The type (:class or :instance) of method to instrument.
      # mod  - The module containing the method.
      # name - The name of the method to instrument.
      def self.instrument(type, mod, name)
        return unless Metrics.enabled?

        name   = name.to_sym
        target = type == :instance ? mod : mod.singleton_class

        if type == :instance
          target = mod
          label  = "#{mod.name}##{name}"
          method = mod.instance_method(name)
        else
          target = mod.singleton_class
          label  = "#{mod.name}.#{name}"
          method = mod.method(name)
        end

        unless instrumented?(target)
          target.instance_variable_set(PROXY_IVAR, Module.new)
        end

        proxy_module = self.proxy_module(target)

        # Some code out there (e.g. the "state_machine" Gem) checks the arity of
        # a method to make sure it only passes arguments when the method expects
        # any. If we were to always overwrite a method to take an `*args`
        # signature this would break things. As a result we'll make sure the
        # generated method _only_ accepts regular arguments if the underlying
        # method also accepts them.
        if method.arity == 0
          args_signature = '&block'
        else
          args_signature = '*args, &block'
        end

        proxy_module.class_eval <<-EOF, __FILE__, __LINE__ + 1
          def #{name}(#{args_signature})
            trans = Gitlab::Metrics::Instrumentation.transaction

            if trans
              start    = Time.now
              retval   = super
              duration = (Time.now - start) * 1000.0

              if duration >= Gitlab::Metrics.method_call_threshold
                trans.increment(:method_duration, duration)

                trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
                                 { duration: duration },
                                 method: #{label.inspect})
              end

              retval
            else
              super
            end
          end
        EOF

        target.prepend(proxy_module)
      end

      # Small layer of indirection to make it easier to stub out the current
      # transaction.
      def self.transaction
        Transaction.current
      end
    end
  end
end