Skip to content

Commit 7def994

Browse files
committed
feat: Gradually add metrics capabilities to Instrumentation::Base
1 parent 543a5fa commit 7def994

File tree

5 files changed

+283
-8
lines changed

5 files changed

+283
-8
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

+154-8
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,57 @@ 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 asynchronous_counter
176+
histogram gauge asynchronous_gauge
177+
updown_counter asynchronous_updown_counter
178+
].each do |instrument_kind|
179+
define_method(instrument_kind) do |name, **opts|
180+
register_instrument(instrument_kind, name, **opts)
181+
end
182+
end
183+
184+
def register_instrument(kind, name, **opts)
185+
@instrument_configs ||= {}
186+
187+
key = [kind, name]
188+
if @instrument_configs.key?(key)
189+
warn("Duplicate instrument configured for #{self}: #{key.inspect}")
190+
else
191+
@instrument_configs[key] = opts
192+
end
193+
end
194+
else
195+
def counter(*, **); end
196+
def asynchronous_counter(*, **); end
197+
def histogram(*, **); end
198+
def gauge(*, **); end
199+
def asynchronous_gauge(*, **); end
200+
def updown_counter(*, **); end
201+
def asynchronous_updown_counter(*, **); end
168202
end
169203

170204
private
171205

172-
attr_reader :install_blk, :present_blk, :compatible_blk, :options
206+
attr_reader :install_blk, :present_blk, :compatible_blk, :options, :instrument_configs
173207

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

214+
def metrics_defined?
215+
defined?(OpenTelemetry::Metrics)
216+
end
217+
180218
def infer_version
181219
return unless (inferred_name = infer_name)
182220

@@ -189,13 +227,13 @@ def infer_version
189227
end
190228
end
191229

192-
attr_reader :name, :version, :config, :installed, :tracer
230+
attr_reader :name, :version, :config, :installed, :tracer, :meter, :instrument_configs
193231

194232
alias installed? installed
195233

196234
# rubocop:disable Metrics/ParameterLists
197235
def initialize(name, version, install_blk, present_blk,
198-
compatible_blk, options)
236+
compatible_blk, options, instrument_configs)
199237
@name = name
200238
@version = version
201239
@install_blk = install_blk
@@ -204,7 +242,9 @@ def initialize(name, version, install_blk, present_blk,
204242
@config = {}
205243
@installed = false
206244
@options = options
207-
@tracer = OpenTelemetry::Trace::Tracer.new
245+
@tracer = OpenTelemetry::Trace::Tracer.new # default no-op tracer
246+
@meter = OpenTelemetry::Metrics::Meter.new if defined?(OpenTelemetry::Metrics::Meter) # default no-op meter
247+
@instrument_configs = instrument_configs || {}
208248
end
209249
# rubocop:enable Metrics/ParameterLists
210250

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

219259
@config = config_options(config)
260+
261+
@metrics_enabled = compute_metrics_enabled
262+
263+
if metrics_defined?
264+
@metrics_instruments = {}
265+
@instrument_mutex = Mutex.new
266+
end
267+
220268
return false unless installable?(config)
221269

222270
instance_exec(@config, &@install_blk)
223271
@tracer = OpenTelemetry.tracer_provider.tracer(name, version)
272+
@meter = OpenTelemetry.meter_provider.meter(name, version: version) if metrics_enabled?
224273
@installed = true
225274
end
226275

@@ -261,8 +310,76 @@ def enabled?(config = nil)
261310
true
262311
end
263312

313+
# This is based on a variety of factors, and should be invalidated when @config changes.
314+
# It should be explicitly set in `initialize` for now.
315+
def metrics_enabled?
316+
!!@metrics_enabled
317+
end
318+
319+
# @api private
320+
# ONLY yields if the meter is enabled.
321+
def with_meter
322+
yield @meter if metrics_enabled?
323+
end
324+
325+
if defined?(OpenTelemetry::Metrics)
326+
%i[
327+
counter
328+
asynchronous_counter
329+
histogram
330+
gauge
331+
asynchronous_gauge
332+
updown_counter
333+
asynchronous_updown_counter
334+
].each do |kind|
335+
define_method(kind) do |name|
336+
get_metrics_instrument(kind, name)
337+
end
338+
end
339+
end
340+
264341
private
265342

343+
def metrics_defined?
344+
defined?(OpenTelemetry::Metrics)
345+
end
346+
347+
def get_metrics_instrument(kind, name)
348+
# FIXME: we should probably return *something*
349+
# if metrics is not enabled, but if the api is undefined,
350+
# it's unclear exactly what would be suitable.
351+
# For now, there are no public methods that call this
352+
# if metrics isn't defined.
353+
return unless metrics_defined?
354+
355+
@metrics_instruments.fetch([kind, name]) do |key|
356+
@instrument_mutex.synchronize do
357+
@metrics_instruments[key] ||= create_configured_instrument(kind, name)
358+
end
359+
end
360+
end
361+
362+
def create_configured_instrument(kind, name)
363+
config = @instrument_configs[[kind, name]]
364+
365+
# FIXME: what is appropriate here?
366+
if config.nil?
367+
Kernel.warn("unconfigured instrument requested: #{kind} of '#{name}'")
368+
return
369+
end
370+
371+
# FIXME: some of these have different opts;
372+
# should verify that they work before this point.
373+
meter.public_send(:"create_#{kind}", name, **config)
374+
end
375+
376+
def compute_metrics_enabled
377+
return false unless defined?(OpenTelemetry::Metrics)
378+
return false if metrics_disabled_by_env_var?
379+
380+
!!@config[:metrics] || metrics_enabled_by_env_var?
381+
end
382+
266383
# The config_options method is responsible for validating that the user supplied
267384
# config hash is valid.
268385
# Unknown configuration keys are not included in the final config hash.
@@ -317,13 +434,42 @@ def config_options(user_config)
317434
# will be OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED. A value of 'false' will disable
318435
# the instrumentation, all other values will enable it.
319436
def enabled_by_env_var?
437+
!disabled_by_env_var?
438+
end
439+
440+
def disabled_by_env_var?
320441
var_name = name.dup.tap do |n|
321442
n.upcase!
322443
n.gsub!('::', '_')
323444
n.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
324445
n << '_ENABLED'
325446
end
326-
ENV[var_name] != 'false'
447+
ENV[var_name] == 'false'
448+
end
449+
450+
# Checks if this instrumentation's metrics are enabled by env var.
451+
# This follows the conventions as outlined above, using `_METRICS_ENABLED` as a suffix.
452+
# Unlike INSTRUMENTATION_*_ENABLED variables, these are explicitly opt-in (i.e.
453+
# if the variable is unset, and `metrics: true` is not in the instrumentation's config,
454+
# the metrics will not be enabled)
455+
def metrics_enabled_by_env_var?
456+
ENV.key?(metrics_env_var_name) && ENV[metrics_env_var_name] != 'false'
457+
end
458+
459+
def metrics_disabled_by_env_var?
460+
ENV[metrics_env_var_name] == 'false'
461+
end
462+
463+
def metrics_env_var_name
464+
@metrics_env_var_name ||=
465+
begin
466+
var_name = name.dup
467+
var_name.upcase!
468+
var_name.gsub!('::', '_')
469+
var_name.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
470+
var_name << '_METRICS_ENABLED'
471+
var_name
472+
end
327473
end
328474

329475
# 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)