@@ -69,8 +69,9 @@ class << self
69
69
integer : -> ( v ) { v . is_a? ( Integer ) } ,
70
70
string : -> ( v ) { v . is_a? ( String ) }
71
71
} . freeze
72
+ SINGLETON_MUTEX = Thread ::Mutex . new
72
73
73
- private_constant :NAME_REGEX , :VALIDATORS
74
+ private_constant :NAME_REGEX , :VALIDATORS , :SINGLETON_MUTEX
74
75
75
76
private :new
76
77
@@ -163,20 +164,62 @@ def option(name, default:, validate:)
163
164
end
164
165
165
166
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
168
207
end
169
208
170
209
private
171
210
172
- attr_reader :install_blk , :present_blk , :compatible_blk , :options
211
+ attr_reader :install_blk , :present_blk , :compatible_blk , :options , :instrument_configs
173
212
174
213
def infer_name
175
214
@inferred_name ||= if ( md = name . match ( NAME_REGEX ) ) # rubocop:disable Naming/MemoizedInstanceVariableName
176
215
md [ 'namespace' ] || md [ 'classname' ]
177
216
end
178
217
end
179
218
219
+ def metrics_defined?
220
+ defined? ( OpenTelemetry ::Metrics )
221
+ end
222
+
180
223
def infer_version
181
224
return unless ( inferred_name = infer_name )
182
225
@@ -189,13 +232,13 @@ def infer_version
189
232
end
190
233
end
191
234
192
- attr_reader :name , :version , :config , :installed , :tracer
235
+ attr_reader :name , :version , :config , :installed , :tracer , :meter , :instrument_configs
193
236
194
237
alias installed? installed
195
238
196
239
# rubocop:disable Metrics/ParameterLists
197
240
def initialize ( name , version , install_blk , present_blk ,
198
- compatible_blk , options )
241
+ compatible_blk , options , instrument_configs )
199
242
@name = name
200
243
@version = version
201
244
@install_blk = install_blk
@@ -204,7 +247,9 @@ def initialize(name, version, install_blk, present_blk,
204
247
@config = { }
205
248
@installed = false
206
249
@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 || { }
208
253
end
209
254
# rubocop:enable Metrics/ParameterLists
210
255
@@ -217,10 +262,21 @@ def install(config = {})
217
262
return true if installed?
218
263
219
264
@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
+
220
273
return false unless installable? ( config )
221
274
222
- instance_exec ( @config , &@install_blk )
223
275
@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
+
224
280
@installed = true
225
281
end
226
282
@@ -261,8 +317,76 @@ def enabled?(config = nil)
261
317
true
262
318
end
263
319
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
+
264
348
private
265
349
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
+
266
390
# The config_options method is responsible for validating that the user supplied
267
391
# config hash is valid.
268
392
# Unknown configuration keys are not included in the final config hash.
@@ -317,13 +441,42 @@ def config_options(user_config)
317
441
# will be OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED. A value of 'false' will disable
318
442
# the instrumentation, all other values will enable it.
319
443
def enabled_by_env_var?
444
+ !disabled_by_env_var?
445
+ end
446
+
447
+ def disabled_by_env_var?
320
448
var_name = name . dup . tap do |n |
321
449
n . upcase!
322
450
n . gsub! ( '::' , '_' )
323
451
n . gsub! ( 'OPENTELEMETRY_' , 'OTEL_RUBY_' )
324
452
n << '_ENABLED'
325
453
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
327
480
end
328
481
329
482
# Checks to see if the user has passed any environment variables that set options
0 commit comments