@@ -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
0 commit comments