Skip to content

Commit bccd9f5

Browse files
committed
feat: Gradually add metrics capabilities to Instrumentation::Base
1 parent 3bb36a6 commit bccd9f5

File tree

5 files changed

+291
-9
lines changed

5 files changed

+291
-9
lines changed

instrumentation/base/Appraisals

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
appraise "base" do
2+
remove_gem "opentelemetry-metrics-api"
3+
end
4+
5+
appraise "metrics-api" do
6+
end

instrumentation/base/Gemfile

+2
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@
66

77
source 'https://rubygems.org'
88

9+
gem 'opentelemetry-metrics-api'
10+
911
gemspec

instrumentation/base/lib/opentelemetry/instrumentation/base.rb

+162-9
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ class << self
6969
integer: ->(v) { v.is_a?(Integer) },
7070
string: ->(v) { v.is_a?(String) }
7171
}.freeze
72+
SINGLETON_MUTEX = Thread::Mutex.new
7273

73-
private_constant :NAME_REGEX, :VALIDATORS
74+
private_constant :NAME_REGEX, :VALIDATORS, :SINGLETON_MUTEX
7475

7576
private :new
7677

@@ -163,20 +164,62 @@ def option(name, default:, validate:)
163164
end
164165

165166
def instance
166-
@instance ||= new(instrumentation_name, instrumentation_version, install_blk,
167-
present_blk, compatible_blk, options)
167+
@instance || SINGLETON_MUTEX.synchronize do
168+
@instance ||= new(instrumentation_name, instrumentation_version, install_blk,
169+
present_blk, compatible_blk, options, instrument_configs)
170+
end
171+
end
172+
173+
if defined?(OpenTelemetry::Metrics)
174+
%i[
175+
counter
176+
observable_counter
177+
histogram
178+
gauge
179+
observable_gauge
180+
up_down_counter
181+
observable_up_down_counter
182+
].each do |instrument_kind|
183+
define_method(instrument_kind) do |name, **opts, &block|
184+
opts[:callback] ||= block
185+
register_instrument(instrument_kind, name, **opts)
186+
end
187+
end
188+
189+
def register_instrument(kind, name, **opts)
190+
@instrument_configs ||= {}
191+
192+
key = [kind, name]
193+
if @instrument_configs.key?(key)
194+
warn("Duplicate instrument configured for #{self}: #{key.inspect}")
195+
else
196+
@instrument_configs[key] = opts
197+
end
198+
end
199+
else
200+
def counter(*, **); end
201+
def observable_counter(*, **); end
202+
def histogram(*, **); end
203+
def gauge(*, **); end
204+
def observable_gauge(*, **); end
205+
def up_down_counter(*, **); end
206+
def observable_up_down_counter(*, **); end
168207
end
169208

170209
private
171210

172-
attr_reader :install_blk, :present_blk, :compatible_blk, :options
211+
attr_reader :install_blk, :present_blk, :compatible_blk, :options, :instrument_configs
173212

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

219+
def metrics_defined?
220+
defined?(OpenTelemetry::Metrics)
221+
end
222+
180223
def infer_version
181224
return unless (inferred_name = infer_name)
182225

@@ -189,13 +232,13 @@ def infer_version
189232
end
190233
end
191234

192-
attr_reader :name, :version, :config, :installed, :tracer
235+
attr_reader :name, :version, :config, :installed, :tracer, :meter, :instrument_configs
193236

194237
alias installed? installed
195238

196239
# rubocop:disable Metrics/ParameterLists
197240
def initialize(name, version, install_blk, present_blk,
198-
compatible_blk, options)
241+
compatible_blk, options, instrument_configs)
199242
@name = name
200243
@version = version
201244
@install_blk = install_blk
@@ -204,7 +247,9 @@ def initialize(name, version, install_blk, present_blk,
204247
@config = {}
205248
@installed = false
206249
@options = options
207-
@tracer = OpenTelemetry::Trace::Tracer.new
250+
@tracer = OpenTelemetry::Trace::Tracer.new # default no-op tracer
251+
@meter = OpenTelemetry::Metrics::Meter.new if defined?(OpenTelemetry::Metrics::Meter) # default no-op meter
252+
@instrument_configs = instrument_configs || {}
208253
end
209254
# rubocop:enable Metrics/ParameterLists
210255

@@ -217,10 +262,21 @@ def install(config = {})
217262
return true if installed?
218263

219264
@config = config_options(config)
265+
266+
@metrics_enabled = compute_metrics_enabled
267+
268+
if metrics_defined?
269+
@metrics_instruments = {}
270+
@instrument_mutex = Mutex.new
271+
end
272+
220273
return false unless installable?(config)
221274

222-
instance_exec(@config, &@install_blk)
223275
@tracer = OpenTelemetry.tracer_provider.tracer(name, version)
276+
@meter = OpenTelemetry.meter_provider.meter(name, version: version) if metrics_enabled?
277+
278+
instance_exec(@config, &@install_blk)
279+
224280
@installed = true
225281
end
226282

@@ -261,8 +317,76 @@ def enabled?(config = nil)
261317
true
262318
end
263319

320+
# This is based on a variety of factors, and should be invalidated when @config changes.
321+
# It should be explicitly set in `initialize` for now.
322+
def metrics_enabled?
323+
!!@metrics_enabled
324+
end
325+
326+
# @api private
327+
# ONLY yields if the meter is enabled.
328+
def with_meter
329+
yield @meter if metrics_enabled?
330+
end
331+
332+
if defined?(OpenTelemetry::Metrics)
333+
%i[
334+
counter
335+
observable_counter
336+
histogram
337+
gauge
338+
observable_gauge
339+
up_down_counter
340+
observable_up_down_counter
341+
].each do |kind|
342+
define_method(kind) do |name|
343+
get_metrics_instrument(kind, name)
344+
end
345+
end
346+
end
347+
264348
private
265349

350+
def metrics_defined?
351+
defined?(OpenTelemetry::Metrics)
352+
end
353+
354+
def get_metrics_instrument(kind, name)
355+
# FIXME: we should probably return *something*
356+
# if metrics is not enabled, but if the api is undefined,
357+
# it's unclear exactly what would be suitable.
358+
# For now, there are no public methods that call this
359+
# if metrics isn't defined.
360+
return unless metrics_defined?
361+
362+
@metrics_instruments.fetch([kind, name]) do |key|
363+
@instrument_mutex.synchronize do
364+
@metrics_instruments[key] ||= create_configured_instrument(kind, name)
365+
end
366+
end
367+
end
368+
369+
def create_configured_instrument(kind, name)
370+
config = @instrument_configs[[kind, name]]
371+
372+
# FIXME: what is appropriate here?
373+
if config.nil?
374+
Kernel.warn("unconfigured instrument requested: #{kind} of '#{name}'")
375+
return
376+
end
377+
378+
# FIXME: some of these have different opts;
379+
# should verify that they work before this point.
380+
meter.public_send(:"create_#{kind}", name, **config)
381+
end
382+
383+
def compute_metrics_enabled
384+
return false unless defined?(OpenTelemetry::Metrics)
385+
return false if metrics_disabled_by_env_var?
386+
387+
!!@config[:metrics] || metrics_enabled_by_env_var?
388+
end
389+
266390
# The config_options method is responsible for validating that the user supplied
267391
# config hash is valid.
268392
# Unknown configuration keys are not included in the final config hash.
@@ -317,13 +441,42 @@ def config_options(user_config)
317441
# will be OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED. A value of 'false' will disable
318442
# the instrumentation, all other values will enable it.
319443
def enabled_by_env_var?
444+
!disabled_by_env_var?
445+
end
446+
447+
def disabled_by_env_var?
320448
var_name = name.dup.tap do |n|
321449
n.upcase!
322450
n.gsub!('::', '_')
323451
n.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
324452
n << '_ENABLED'
325453
end
326-
ENV[var_name] != 'false'
454+
ENV[var_name] == 'false'
455+
end
456+
457+
# Checks if this instrumentation's metrics are enabled by env var.
458+
# This follows the conventions as outlined above, using `_METRICS_ENABLED` as a suffix.
459+
# Unlike INSTRUMENTATION_*_ENABLED variables, these are explicitly opt-in (i.e.
460+
# if the variable is unset, and `metrics: true` is not in the instrumentation's config,
461+
# the metrics will not be enabled)
462+
def metrics_enabled_by_env_var?
463+
ENV.key?(metrics_env_var_name) && ENV[metrics_env_var_name] != 'false'
464+
end
465+
466+
def metrics_disabled_by_env_var?
467+
ENV[metrics_env_var_name] == 'false'
468+
end
469+
470+
def metrics_env_var_name
471+
@metrics_env_var_name ||=
472+
begin
473+
var_name = name.dup
474+
var_name.upcase!
475+
var_name.gsub!('::', '_')
476+
var_name.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
477+
var_name << '_METRICS_ENABLED'
478+
var_name
479+
end
327480
end
328481

329482
# Checks to see if the user has passed any environment variables that set options

instrumentation/base/opentelemetry-instrumentation-base.gemspec

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
2929
spec.add_dependency 'opentelemetry-common', '~> 0.21'
3030
spec.add_dependency 'opentelemetry-registry', '~> 0.1'
3131

32+
spec.add_development_dependency 'appraisal', '~> 2.5'
3233
spec.add_development_dependency 'bundler', '~> 2.4'
3334
spec.add_development_dependency 'minitest', '~> 5.0'
3435
spec.add_development_dependency 'opentelemetry-test-helpers', '~> 0.3'

0 commit comments

Comments
 (0)