Skip to content

Commit

Permalink
feat: Gradually add metrics capabilities to Instrumentation::Base
Browse files Browse the repository at this point in the history
  • Loading branch information
zvkemp committed Jan 7, 2025
1 parent 543a5fa commit 7def994
Show file tree
Hide file tree
Showing 5 changed files with 283 additions and 8 deletions.
6 changes: 6 additions & 0 deletions instrumentation/base/Appraisals
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
appraise "base" do
remove_gem "opentelemetry-metrics-api"
end

appraise "metrics-api" do
end
2 changes: 2 additions & 0 deletions instrumentation/base/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@

source 'https://rubygems.org'

gem 'opentelemetry-metrics-api'

gemspec
162 changes: 154 additions & 8 deletions instrumentation/base/lib/opentelemetry/instrumentation/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ class << self
integer: ->(v) { v.is_a?(Integer) },
string: ->(v) { v.is_a?(String) }
}.freeze
SINGLETON_MUTEX = Thread::Mutex.new

private_constant :NAME_REGEX, :VALIDATORS
private_constant :NAME_REGEX, :VALIDATORS, :SINGLETON_MUTEX

private :new

Expand Down Expand Up @@ -163,20 +164,57 @@ def option(name, default:, validate:)
end

def instance
@instance ||= new(instrumentation_name, instrumentation_version, install_blk,
present_blk, compatible_blk, options)
@instance || SINGLETON_MUTEX.synchronize do
@instance ||= new(instrumentation_name, instrumentation_version, install_blk,
present_blk, compatible_blk, options, instrument_configs)
end
end

if defined?(OpenTelemetry::Metrics)
%i[
counter asynchronous_counter
histogram gauge asynchronous_gauge
updown_counter asynchronous_updown_counter
].each do |instrument_kind|
define_method(instrument_kind) do |name, **opts|
register_instrument(instrument_kind, name, **opts)
end
end

def register_instrument(kind, name, **opts)
@instrument_configs ||= {}

key = [kind, name]
if @instrument_configs.key?(key)
warn("Duplicate instrument configured for #{self}: #{key.inspect}")
else
@instrument_configs[key] = opts
end
end
else
def counter(*, **); end
def asynchronous_counter(*, **); end
def histogram(*, **); end
def gauge(*, **); end
def asynchronous_gauge(*, **); end
def updown_counter(*, **); end
def asynchronous_updown_counter(*, **); end
end

private

attr_reader :install_blk, :present_blk, :compatible_blk, :options
attr_reader :install_blk, :present_blk, :compatible_blk, :options, :instrument_configs

def infer_name
@inferred_name ||= if (md = name.match(NAME_REGEX)) # rubocop:disable Naming/MemoizedInstanceVariableName
md['namespace'] || md['classname']
end
end

def metrics_defined?
defined?(OpenTelemetry::Metrics)
end

def infer_version
return unless (inferred_name = infer_name)

Expand All @@ -189,13 +227,13 @@ def infer_version
end
end

attr_reader :name, :version, :config, :installed, :tracer
attr_reader :name, :version, :config, :installed, :tracer, :meter, :instrument_configs

alias installed? installed

# rubocop:disable Metrics/ParameterLists
def initialize(name, version, install_blk, present_blk,
compatible_blk, options)
compatible_blk, options, instrument_configs)
@name = name
@version = version
@install_blk = install_blk
Expand All @@ -204,7 +242,9 @@ def initialize(name, version, install_blk, present_blk,
@config = {}
@installed = false
@options = options
@tracer = OpenTelemetry::Trace::Tracer.new
@tracer = OpenTelemetry::Trace::Tracer.new # default no-op tracer
@meter = OpenTelemetry::Metrics::Meter.new if defined?(OpenTelemetry::Metrics::Meter) # default no-op meter
@instrument_configs = instrument_configs || {}
end
# rubocop:enable Metrics/ParameterLists

Expand All @@ -217,10 +257,19 @@ def install(config = {})
return true if installed?

@config = config_options(config)

@metrics_enabled = compute_metrics_enabled

if metrics_defined?
@metrics_instruments = {}
@instrument_mutex = Mutex.new
end

return false unless installable?(config)

instance_exec(@config, &@install_blk)
@tracer = OpenTelemetry.tracer_provider.tracer(name, version)
@meter = OpenTelemetry.meter_provider.meter(name, version: version) if metrics_enabled?
@installed = true
end

Expand Down Expand Up @@ -261,8 +310,76 @@ def enabled?(config = nil)
true
end

# This is based on a variety of factors, and should be invalidated when @config changes.
# It should be explicitly set in `initialize` for now.
def metrics_enabled?
!!@metrics_enabled
end

# @api private
# ONLY yields if the meter is enabled.
def with_meter
yield @meter if metrics_enabled?
end

if defined?(OpenTelemetry::Metrics)
%i[
counter
asynchronous_counter
histogram
gauge
asynchronous_gauge
updown_counter
asynchronous_updown_counter
].each do |kind|
define_method(kind) do |name|
get_metrics_instrument(kind, name)
end
end
end

private

def metrics_defined?
defined?(OpenTelemetry::Metrics)
end

def get_metrics_instrument(kind, name)
# FIXME: we should probably return *something*
# if metrics is not enabled, but if the api is undefined,
# it's unclear exactly what would be suitable.
# For now, there are no public methods that call this
# if metrics isn't defined.
return unless metrics_defined?

@metrics_instruments.fetch([kind, name]) do |key|
@instrument_mutex.synchronize do
@metrics_instruments[key] ||= create_configured_instrument(kind, name)
end
end
end

def create_configured_instrument(kind, name)
config = @instrument_configs[[kind, name]]

# FIXME: what is appropriate here?
if config.nil?
Kernel.warn("unconfigured instrument requested: #{kind} of '#{name}'")
return
end

# FIXME: some of these have different opts;
# should verify that they work before this point.
meter.public_send(:"create_#{kind}", name, **config)
end

def compute_metrics_enabled
return false unless defined?(OpenTelemetry::Metrics)
return false if metrics_disabled_by_env_var?

!!@config[:metrics] || metrics_enabled_by_env_var?
end

# The config_options method is responsible for validating that the user supplied
# config hash is valid.
# Unknown configuration keys are not included in the final config hash.
Expand Down Expand Up @@ -317,13 +434,42 @@ def config_options(user_config)
# will be OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED. A value of 'false' will disable
# the instrumentation, all other values will enable it.
def enabled_by_env_var?
!disabled_by_env_var?
end

def disabled_by_env_var?
var_name = name.dup.tap do |n|
n.upcase!
n.gsub!('::', '_')
n.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
n << '_ENABLED'
end
ENV[var_name] != 'false'
ENV[var_name] == 'false'
end

# Checks if this instrumentation's metrics are enabled by env var.
# This follows the conventions as outlined above, using `_METRICS_ENABLED` as a suffix.
# Unlike INSTRUMENTATION_*_ENABLED variables, these are explicitly opt-in (i.e.
# if the variable is unset, and `metrics: true` is not in the instrumentation's config,
# the metrics will not be enabled)
def metrics_enabled_by_env_var?
ENV.key?(metrics_env_var_name) && ENV[metrics_env_var_name] != 'false'
end

def metrics_disabled_by_env_var?
ENV[metrics_env_var_name] == 'false'
end

def metrics_env_var_name
@metrics_env_var_name ||=
begin
var_name = name.dup
var_name.upcase!
var_name.gsub!('::', '_')
var_name.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
var_name << '_METRICS_ENABLED'
var_name
end
end

# Checks to see if the user has passed any environment variables that set options
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'opentelemetry-common', '~> 0.21'
spec.add_dependency 'opentelemetry-registry', '~> 0.1'

spec.add_development_dependency 'appraisal', '~> 2.5'
spec.add_development_dependency 'bundler', '~> 2.4'
spec.add_development_dependency 'minitest', '~> 5.0'
spec.add_development_dependency 'opentelemetry-test-helpers', '~> 0.3'
Expand Down
Loading

0 comments on commit 7def994

Please sign in to comment.