7b6785b3b1
By using Module#prepend we can define a Module containing all proxy methods. This removes the need for setting up crazy method alias chains and in turn prevents us from having to deal with all that madness (e.g. methods calling each other recursively). Fixes gitlab-org/gitlab-ce#15281
181 lines
5.6 KiB
Ruby
181 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.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
|