945c5b3fe6
Because method call timings are inclusive (that is, they include the time of any sub method calls) this would lead to the total method execution time often being far greater than the total transaction time. Because this is incredibly confusing it's best to simply _not_ track the total method execution time, after all it's not that useful to begin with. Fixes gitlab-org/gitlab-ce#17239
179 lines
5.6 KiB
Ruby
179 lines
5.6 KiB
Ruby
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.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
|