Skip to content

Commit ef747e7

Browse files
committed
feat: Gradually add metrics capabilities to Instrumentation::Base
1 parent 4d60d62 commit ef747e7

File tree

7 files changed

+362
-11
lines changed

7 files changed

+362
-11
lines changed

instrumentation/base/Appraisals

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
appraise 'base' do
4+
remove_gem 'opentelemetry-metrics-api'
5+
remove_gem 'opentelemetry-metrics-sdk'
6+
end
7+
8+
appraise 'metrics-api' do
9+
remove_gem 'opentelemetry-metrics-sdk'
10+
end
11+
12+
appraise 'metrics-sdk' do # rubocop: disable Lint/EmptyBlock
13+
end

instrumentation/base/Gemfile

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

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

9+
gem 'opentelemetry-metrics-api', '~> 0.2'
10+
gem 'opentelemetry-metrics-sdk'
11+
912
gemspec

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

+25-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,8 +164,10 @@ 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)
170+
end
168171
end
169172

170173
private
@@ -189,13 +192,15 @@ def infer_version
189192
end
190193
end
191194

192-
attr_reader :name, :version, :config, :installed, :tracer
195+
attr_reader :name, :version, :config, :installed, :tracer, :meter
193196

194197
alias installed? installed
195198

199+
require_relative 'metrics'
200+
prepend(OpenTelemetry::Instrumentation::Metrics)
201+
196202
# rubocop:disable Metrics/ParameterLists
197-
def initialize(name, version, install_blk, present_blk,
198-
compatible_blk, options)
203+
def initialize(name, version, install_blk, present_blk, compatible_blk, options)
199204
@name = name
200205
@version = version
201206
@install_blk = install_blk
@@ -204,7 +209,8 @@ def initialize(name, version, install_blk, present_blk,
204209
@config = {}
205210
@installed = false
206211
@options = options
207-
@tracer = OpenTelemetry::Trace::Tracer.new
212+
@tracer = OpenTelemetry::Trace::Tracer.new # default no-op tracer
213+
@meter = OpenTelemetry::Metrics::Meter.new if defined?(OpenTelemetry::Metrics::Meter) # default no-op meter
208214
end
209215
# rubocop:enable Metrics/ParameterLists
210216

@@ -217,10 +223,12 @@ def install(config = {})
217223
return true if installed?
218224

219225
@config = config_options(config)
226+
220227
return false unless installable?(config)
221228

229+
prepare_install
222230
instance_exec(@config, &@install_blk)
223-
@tracer = OpenTelemetry.tracer_provider.tracer(name, version)
231+
224232
@installed = true
225233
end
226234

@@ -263,6 +271,10 @@ def enabled?(config = nil)
263271

264272
private
265273

274+
def prepare_install
275+
@tracer = OpenTelemetry.tracer_provider.tracer(name, version)
276+
end
277+
266278
# The config_options method is responsible for validating that the user supplied
267279
# config hash is valid.
268280
# Unknown configuration keys are not included in the final config hash.
@@ -317,13 +329,17 @@ def config_options(user_config)
317329
# will be OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED. A value of 'false' will disable
318330
# the instrumentation, all other values will enable it.
319331
def enabled_by_env_var?
332+
!disabled_by_env_var?
333+
end
334+
335+
def disabled_by_env_var?
320336
var_name = name.dup.tap do |n|
321337
n.upcase!
322338
n.gsub!('::', '_')
323339
n.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
324340
n << '_ENABLED'
325341
end
326-
ENV[var_name] != 'false'
342+
ENV[var_name] == 'false'
327343
end
328344

329345
# Checks to see if the user has passed any environment variables that set options
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# frozen_string_literal: true
2+
3+
# Copyright The OpenTelemetry Authors
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
module OpenTelemetry
8+
module Instrumentation
9+
# Extensions to Instrumentation::Base that handle metrics instruments.
10+
# The goal here is to allow metrics to be added gradually to instrumentation libraries,
11+
# without requiring that the metrics-sdk or metrics-api gems are present in the bundle
12+
# (if they are not, or if the metrics-api gem does not meet the minimum version requirement,
13+
# the no-op edition is installed.)
14+
module Metrics
15+
METER_TYPES = %i[
16+
counter
17+
observable_counter
18+
histogram
19+
gauge
20+
observable_gauge
21+
up_down_counter
22+
observable_up_down_counter
23+
].freeze
24+
25+
def self.prepended(base)
26+
base.prepend(Compatibility)
27+
base.extend(Compatibility)
28+
base.extend(Registration)
29+
30+
if base.metrics_compatible?
31+
base.prepend(Extensions)
32+
else
33+
base.prepend(NoopExtensions)
34+
end
35+
end
36+
37+
# Methods to check whether the metrics API is defined
38+
# and is a compatible version
39+
module Compatibility
40+
METRICS_API_MINIMUM_GEM_VERSION = Gem::Version.new('0.2.0')
41+
42+
def metrics_defined?
43+
defined?(OpenTelemetry::Metrics)
44+
end
45+
46+
def metrics_compatible?
47+
metrics_defined? && Gem.loaded_specs['opentelemetry-metrics-api'].version >= METRICS_API_MINIMUM_GEM_VERSION
48+
end
49+
50+
extend(self)
51+
end
52+
53+
# class-level methods to declare and register metrics instruments.
54+
# This can be extended even if metrics is not active or present.
55+
module Registration
56+
METER_TYPES.each do |instrument_kind|
57+
define_method(instrument_kind) do |name, **opts, &block|
58+
opts[:callback] ||= block if block
59+
register_instrument(instrument_kind, name, **opts)
60+
end
61+
end
62+
63+
def register_instrument(kind, name, **opts)
64+
key = [kind, name]
65+
if instrument_configs.key?(key)
66+
warn("Duplicate instrument configured for #{self}: #{key.inspect}")
67+
else
68+
instrument_configs[key] = opts
69+
end
70+
end
71+
72+
def instrument_configs
73+
@instrument_configs ||= {}
74+
end
75+
end
76+
77+
# No-op instance methods for metrics instruments.
78+
module NoopExtensions
79+
METER_TYPES.each do |kind|
80+
define_method(kind) { |*, **| } # rubocop: disable Lint/EmptyBlock
81+
end
82+
83+
def with_meter; end
84+
85+
def metrics_enabled?
86+
false
87+
end
88+
end
89+
90+
# Instance methods for metrics instruments.
91+
module Extensions
92+
%i[
93+
counter
94+
observable_counter
95+
histogram
96+
gauge
97+
observable_gauge
98+
up_down_counter
99+
observable_up_down_counter
100+
].each do |kind|
101+
define_method(kind) do |name|
102+
get_metrics_instrument(kind, name)
103+
end
104+
end
105+
106+
# This is based on a variety of factors, and should be invalidated when @config changes.
107+
# It should be explicitly set in `prepare_install` for now.
108+
def metrics_enabled?
109+
!!@metrics_enabled
110+
end
111+
112+
# @api private
113+
# ONLY yields if the meter is enabled.
114+
def with_meter
115+
yield @meter if metrics_enabled?
116+
end
117+
118+
private
119+
120+
def compute_metrics_enabled
121+
return false unless metrics_compatible?
122+
return false if metrics_disabled_by_env_var?
123+
124+
!!@config[:metrics] || metrics_enabled_by_env_var?
125+
end
126+
127+
# Checks if this instrumentation's metrics are enabled by env var.
128+
# This follows the conventions as outlined above, using `_METRICS_ENABLED` as a suffix.
129+
# Unlike INSTRUMENTATION_*_ENABLED variables, these are explicitly opt-in (i.e.
130+
# if the variable is unset, and `metrics: true` is not in the instrumentation's config,
131+
# the metrics will not be enabled)
132+
def metrics_enabled_by_env_var?
133+
ENV.key?(metrics_env_var_name) && ENV[metrics_env_var_name] != 'false'
134+
end
135+
136+
def metrics_disabled_by_env_var?
137+
ENV[metrics_env_var_name] == 'false'
138+
end
139+
140+
def metrics_env_var_name
141+
@metrics_env_var_name ||=
142+
begin
143+
var_name = name.dup
144+
var_name.upcase!
145+
var_name.gsub!('::', '_')
146+
var_name.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_')
147+
var_name << '_METRICS_ENABLED'
148+
var_name
149+
end
150+
end
151+
152+
def prepare_install
153+
@metrics_enabled = compute_metrics_enabled
154+
if metrics_defined?
155+
@metrics_instruments = {}
156+
@instrument_mutex = Mutex.new
157+
end
158+
159+
@meter = OpenTelemetry.meter_provider.meter(name, version: version) if metrics_enabled?
160+
161+
super
162+
end
163+
164+
def get_metrics_instrument(kind, name)
165+
# TODO: we should probably return *something*
166+
# if metrics is not enabled, but if the api is undefined,
167+
# it's unclear exactly what would be suitable.
168+
# For now, there are no public methods that call this
169+
# if metrics isn't defined.
170+
return unless metrics_defined?
171+
172+
@metrics_instruments.fetch([kind, name]) do |key|
173+
@instrument_mutex.synchronize do
174+
@metrics_instruments[key] ||= create_configured_instrument(kind, name)
175+
end
176+
end
177+
end
178+
179+
def create_configured_instrument(kind, name)
180+
config = self.class.instrument_configs[[kind, name]]
181+
182+
if config.nil?
183+
Kernel.warn("unconfigured instrument requested: #{kind} of '#{name}'")
184+
return
185+
end
186+
187+
meter.public_send(:"create_#{kind}", name, **config)
188+
end
189+
end
190+
end
191+
end
192+
end

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)