@@ -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,57 @@ 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 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
168
202
end
169
203
170
204
private
171
205
172
- attr_reader :install_blk , :present_blk , :compatible_blk , :options
206
+ attr_reader :install_blk , :present_blk , :compatible_blk , :options , :instrument_configs
173
207
174
208
def infer_name
175
209
@inferred_name ||= if ( md = name . match ( NAME_REGEX ) ) # rubocop:disable Naming/MemoizedInstanceVariableName
176
210
md [ 'namespace' ] || md [ 'classname' ]
177
211
end
178
212
end
179
213
214
+ def metrics_defined?
215
+ defined? ( OpenTelemetry ::Metrics )
216
+ end
217
+
180
218
def infer_version
181
219
return unless ( inferred_name = infer_name )
182
220
@@ -189,13 +227,13 @@ def infer_version
189
227
end
190
228
end
191
229
192
- attr_reader :name , :version , :config , :installed , :tracer
230
+ attr_reader :name , :version , :config , :installed , :tracer , :meter , :instrument_configs
193
231
194
232
alias installed? installed
195
233
196
234
# rubocop:disable Metrics/ParameterLists
197
235
def initialize ( name , version , install_blk , present_blk ,
198
- compatible_blk , options )
236
+ compatible_blk , options , instrument_configs )
199
237
@name = name
200
238
@version = version
201
239
@install_blk = install_blk
@@ -204,7 +242,9 @@ def initialize(name, version, install_blk, present_blk,
204
242
@config = { }
205
243
@installed = false
206
244
@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 || { }
208
248
end
209
249
# rubocop:enable Metrics/ParameterLists
210
250
@@ -217,10 +257,19 @@ def install(config = {})
217
257
return true if installed?
218
258
219
259
@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
+
220
268
return false unless installable? ( config )
221
269
222
270
instance_exec ( @config , &@install_blk )
223
271
@tracer = OpenTelemetry . tracer_provider . tracer ( name , version )
272
+ @meter = OpenTelemetry . meter_provider . meter ( name , version : version ) if metrics_enabled?
224
273
@installed = true
225
274
end
226
275
@@ -261,8 +310,76 @@ def enabled?(config = nil)
261
310
true
262
311
end
263
312
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
+
264
341
private
265
342
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
+
266
383
# The config_options method is responsible for validating that the user supplied
267
384
# config hash is valid.
268
385
# Unknown configuration keys are not included in the final config hash.
@@ -317,13 +434,42 @@ def config_options(user_config)
317
434
# will be OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED. A value of 'false' will disable
318
435
# the instrumentation, all other values will enable it.
319
436
def enabled_by_env_var?
437
+ !disabled_by_env_var?
438
+ end
439
+
440
+ def disabled_by_env_var?
320
441
var_name = name . dup . tap do |n |
321
442
n . upcase!
322
443
n . gsub! ( '::' , '_' )
323
444
n . gsub! ( 'OPENTELEMETRY_' , 'OTEL_RUBY_' )
324
445
n << '_ENABLED'
325
446
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
327
473
end
328
474
329
475
# Checks to see if the user has passed any environment variables that set options
0 commit comments