Skip to content

Instantly share code, notes, and snippets.

@mwlang
Created April 22, 2021 19:29
Show Gist options
  • Save mwlang/ef1693378fbea3e0e5349ca4c8965599 to your computer and use it in GitHub Desktop.
Save mwlang/ef1693378fbea3e0e5349ca4c8965599 to your computer and use it in GitHub Desktop.
Tracer exploration
# This Tracer module is an overly simplified demonstration of what is currently implemented
# in our MethodTracer module. It shows the alternative implementation that completely eliminates
# the string evaluated code.
#
# This implementation also shows how to handle keyword arguments across Ruby versions
# Whether to trace with and without scope is not presented here, but it's fairly straightforward
# to conditionally handle options passed accordingly.
module Tracer
module_function
# expose tracer methods at class and instance level
def self.included instrumented_class
instrumented_class.extend self
end
# Only invoke a callback if one was provided
def trace_callback proc
proc.call unless proc.nil?
end
# This is only place we're conditional on Ruby versions and the main difference
# is the additional **kwargs parameter.
#
# NOTE: personally, I think we should get away from conditional declarations like this and
# factor out to individual files that are then conditionally loaded.
# For example, if we didn't define the below, but had a ruby2.rb and ruby3.rb with the two
# variations, then we'd simply do something like this:
# require_relative RUBY_VERSION < "2.7.0" ? "ruby2" : "ruby3"
def wrap_with_tracer method_name, metric_name, options
if RUBY_VERSION < "2.7.0"
define_method method_name do |*args, &block|
with_tracing(method_name, metric_name, options, *args) { super(*args, &block) }
end
else
define_method method_name do |*args, **kwargs, &block|
with_tracing(method_name, metric_name, options, *args) { super(*args, **kwargs, &block) }
end
end
end
# Mostly taken as-is from the MethodTracer module, but quickly tweaked to deal with
# using prepend to attach tracing to methods. WARNING: almost certainly has bugs!
def derived_class_name
return self.name if respond_to?(:name) && !self.name.empty?
name = method(__method__).receiver.to_s
return "AnonymousModule" if name.start_with?("#<Module:")
# trying to get the "MyClass" portion of "#<Class:MyClass>"
if class_name = name[/^#<Class:(.+)>$/, 1]
if class_name.start_with?("0x")
"AnonymousClass"
elsif class_name.start_with?("#<Class:")
"AnonymousClass/Class"
else
"#{class_name}/Class"
end
else
name
end
end
def default_metric_name method_name, options
"Custom/#{derived_class_name}/#{method_name}"
end
# everything that needs to be accessed as part of our tracing logic needs to be
# passed in as method parameters
def with_tracing method_name, metric_name, options, *args
begin
trace_callback(options[:header])
start_time = Time.now
yield
ensure
duration = (Time.now - start_time).to_f * 1000
metric_name ||= default_metric_name(method_name, options)
resolved_metric_name = metric_name.is_a?(Proc) ? metric_name.call(method_name, *args) : metric_name
puts "#{resolved_metric_name} : #{duration.round(3)}"
trace_callback(options[:footer])
end
end
# Trace is our "API" to wrapping a tracer around a given customer's method. This approach
# is building an anonymous module that we then prepend to the module where the given method
# was originally defined.
def trace method_name, metric_name=nil, options={}
tracer = Module.new do
include Tracer
wrap_with_tracer method_name, metric_name, options
end
prepend tracer
end
end
# #################################################################
# The examples below here demonstrates the code above.
# #################################################################
class Foo
include Tracer
def speak a, b
puts "#{a} says #{b}"
end
trace :speak
trace def bark!
puts "woof!"
end
end
Foo.new.speak("John", "Hello!")
Foo.new.bark!
# This is a fairly complete demonstration of all the bells and whistles
class Bar
include Tracer
def speak a, b
puts "#{a} said #{b}"
end
trace :speak,
-> (method_name, *args) {"Custom/#{args[0]}/#{method_name}/#{Time.now.to_i}"},
header: -> { puts "*" * 40 },
footer: -> { puts "footer" }
end
Bar.new.speak("Sally", "Howdy!")
Bar.new.speak("Homer", "Yo")
module Tracer
module_function
def wrap_with_tracer method_name, metric_name, options
define_method method_name do |*args, &block|
with_tracing(method_name, metric_name, options) { super(*args, &block) }
end
end
end
module Tracer
module_function
def wrap_with_tracer method_name, metric_name, options
define_method method_name do |*args, **kwargs, &block|
with_tracing(method_name, metric_name, options) { super(*args, **kwargs, &block) }
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment