gitlab-org--gitlab-foss/lib/gitlab/metrics/instrumentation.rb
Yorick Peterse 945c5b3fe6
Removed tracking of total method execution times
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
2016-05-12 15:15:45 +02:00

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