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