Third Party Instrumentation Proposal for OpenTelemetry Ruby
Overview
There is an RFC for auto-instrumentation and this document describes a possible approach that we could use for managing instrumentation for the OpenTelemetry Ruby Project.
Packaging
- Instrumentation should be packaged as gems
- Packages should list dependencies in their gemspec for
- OpenTelemetry API Version
- Framework version
- This will allow us to use Bundler to do the heavy lifting regarding versioning and compatibility of dependencies
Installation
- The tracer should be able to auto detect any instrumentation that is listed as a dependency in an application's Gemfile
- The time at which to install instrumentation in a Ruby application can be challenging to get right
- To make this straightforward for the reference implementation, the SDK should have an API method that a user can call to install instrumentation at the right time for their application
- Various SDK implementations can engage in whatever clever means they need to try to install instrumentation at the right time by calling this API method at the right time on behalf of the user
Implementation
The implementation I am proposing is very similar to and inspired by Rails::Railtie
.
OpenTelemetry Ruby will ship with a common base class, OpenTelemetry::Instrumentation
that
- registers instrumentation with tracer (via the
Class#inherited
callback)
- provides hooks for the instrumentation to be installed
- provides the ability to enable or disable instrumentation
- by having an optional, explicit hook that can be defined for complex use cases
- establishing a naming convention for environment variables users can specify to override whether or not instrumentation is enabled
Example Instrumentation Package
A package should list its dependencies in its gemspec
Gem::Specification.new do |spec|
spec.add_dependency 'activerecord', '>= 5.0'
spec.add_dependency 'opentelemetry-api', '~> 1.0'
end
A package will ship with a class that subclasses OpenTelemetry::Instrumentation
which will auto-register instrumentation, and provide hooks for installing and enabling it. The base class will automatically check for an environment variable derived off of the instrumentation_name
field for overriding an enabled decision. The convention I'm proposing for an environment variable name is OPENTELEMETRY_{insrumentation_name}_ENABLED
, which in this example would be OPENTELEMETRY_OTEL_ACTIVE_RECORD_ENABLED
.
class ActiveRecordInstrumentation < OpenTelemetry::Instrumentation
instrumentation_name :otel_active_record
enabled do
# optional and overridable by environment variable, but complex logic around
# whether this is enabled can go here
true
end
install do |tracer|
require 'active_record_instrumentation/subscriber'
subscriber = Subscriber.new tracer
ActiveSupport::Notifications.subscribe 'sql.active_record' do |*args|
subscriber.call(*args)
end
end
end
Instrumentation installation API
The SDK should provide an API method to install instrumentation that can be called at the right time during application startup. This method should evaluate and install all instrumentation that subclasses OpenTelemetry::Instrumentation
if it is enabled.
tracer = TracerImplementation.new
OpenTelemetry::Instrumentation.install tracer
OpenTelemetry::Instrumentation Implementation
Below is a rough-draft of what the OpenTelemetry::Instrumentation class would look like
module OpenTelemetry
class Instrumentation
class << self
private :new
#subclasses are registered via this hook
def inherited subclass
subclasses << subclass
end
def subclasses
@subclasses ||= []
end
def install_instrumentation tracer
subclasses.each do |subclass|
instrumentation = subclass.instance
if instrumentation.enabled? && !instrumentation.installed?
instrumentation.install tracer
end
end
end
#this method is dual purpose, its used by the DSL to setup an install block,
#but when passed a tracer, will install all registered instrumentation
def install tracer = nil, &blk
if tracer && blk
raise ArgumentError, "Pass a tracer or block, but not both"
end
if blk
instance.install_block = blk
elsif tracer
install_instrumentation tracer
end
end
def enabled &blk
instance.enabled_block = blk
end
def instrumentation_name name = nil
if name.nil?
@instrumentation_name
else
@instrumentation_name = name.to_sym
end
end
def instance
@instance ||= new
end
end
attr_accessor :install_block
attr_accessor :enabled_block
attr_reader :installed
alias_method :installed?, :installed
def initialize
@installed = false
end
def install tracer = ::OpenTelemetry.global_tracer
instance_exec tracer, &install_block
@installed = true
end
def enabled?
if enabled_block
enabled_by_env? && instance_eval(&enabled_block)
else
enabled_by_env?
end
end
private
def enabled_by_env?
key = "OPENTELEMETRY_#{self.class.instrumentation_name.to_s.upcase}_ENABLED"
if ENV.include? key
ENV[key] == 'true'
else
true
end
end
end
end
Discussion
I've been thinking about how we can package instrumentation for OpenTelemetry Ruby and wanted to put these thoughts in writing as a starting point for a discussion about it. I'm open to any and all suggestions with the end goal of us having the best possible instrumentation distribution story, even if it doesn't end up being this one.