@@ -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,62 @@ 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
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
168207 end
169208
170209 private
171210
172- attr_reader :install_blk , :present_blk , :compatible_blk , :options
211+ attr_reader :install_blk , :present_blk , :compatible_blk , :options , :instrument_configs
173212
174213 def infer_name
175214 @inferred_name ||= if ( md = name . match ( NAME_REGEX ) ) # rubocop:disable Naming/MemoizedInstanceVariableName
176215 md [ 'namespace' ] || md [ 'classname' ]
177216 end
178217 end
179218
219+ def metrics_defined?
220+ defined? ( OpenTelemetry ::Metrics )
221+ end
222+
180223 def infer_version
181224 return unless ( inferred_name = infer_name )
182225
@@ -189,13 +232,13 @@ def infer_version
189232 end
190233 end
191234
192- attr_reader :name , :version , :config , :installed , :tracer
235+ attr_reader :name , :version , :config , :installed , :tracer , :meter , :instrument_configs
193236
194237 alias installed? installed
195238
196239 # rubocop:disable Metrics/ParameterLists
197240 def initialize ( name , version , install_blk , present_blk ,
198- compatible_blk , options )
241+ compatible_blk , options , instrument_configs )
199242 @name = name
200243 @version = version
201244 @install_blk = install_blk
@@ -204,7 +247,9 @@ def initialize(name, version, install_blk, present_blk,
204247 @config = { }
205248 @installed = false
206249 @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 || { }
208253 end
209254 # rubocop:enable Metrics/ParameterLists
210255
@@ -217,10 +262,21 @@ def install(config = {})
217262 return true if installed?
218263
219264 @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+
220273 return false unless installable? ( config )
221274
222- instance_exec ( @config , &@install_blk )
223275 @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+
224280 @installed = true
225281 end
226282
@@ -261,8 +317,76 @@ def enabled?(config = nil)
261317 true
262318 end
263319
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+
264348 private
265349
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+
266390 # The config_options method is responsible for validating that the user supplied
267391 # config hash is valid.
268392 # Unknown configuration keys are not included in the final config hash.
@@ -317,13 +441,42 @@ def config_options(user_config)
317441 # will be OTEL_RUBY_INSTRUMENTATION_SINATRA_ENABLED. A value of 'false' will disable
318442 # the instrumentation, all other values will enable it.
319443 def enabled_by_env_var?
444+ !disabled_by_env_var?
445+ end
446+
447+ def disabled_by_env_var?
320448 var_name = name . dup . tap do |n |
321449 n . upcase!
322450 n . gsub! ( '::' , '_' )
323451 n . gsub! ( 'OPENTELEMETRY_' , 'OTEL_RUBY_' )
324452 n << '_ENABLED'
325453 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
327480 end
328481
329482 # Checks to see if the user has passed any environment variables that set options
0 commit comments