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
|