diff --git a/instrumentation/base/lib/opentelemetry/instrumentation/base.rb b/instrumentation/base/lib/opentelemetry/instrumentation/base.rb index cfb124ce2..939ad69df 100644 --- a/instrumentation/base/lib/opentelemetry/instrumentation/base.rb +++ b/instrumentation/base/lib/opentelemetry/instrumentation/base.rb @@ -472,6 +472,16 @@ def metrics_env_var_name end end + def metrics_enabled_by_env_var? + var_name = name.dup + var_name.upcase! + var_name.gsub!('::', '_') + var_name.gsub!('OPENTELEMETRY_', 'OTEL_RUBY_') + var_name << '_METRICS_ENABLED' + + ENV.key?(var_name) && ENV[var_name] != 'false' + end + # Checks to see if the user has passed any environment variables that set options # for instrumentation. By convention, the environment variable will be the name # of the instrumentation, uppercased, with '::' replaced by underscores, diff --git a/instrumentation/concurrent_ruby/opentelemetry-instrumentation-concurrent_ruby.gemspec b/instrumentation/concurrent_ruby/opentelemetry-instrumentation-concurrent_ruby.gemspec index 3a09f1785..e4bb8a8a7 100644 --- a/instrumentation/concurrent_ruby/opentelemetry-instrumentation-concurrent_ruby.gemspec +++ b/instrumentation/concurrent_ruby/opentelemetry-instrumentation-concurrent_ruby.gemspec @@ -26,6 +26,7 @@ Gem::Specification.new do |spec| spec.required_ruby_version = '>= 3.0' spec.add_dependency 'opentelemetry-api', '~> 1.0' + spec.add_dependency 'opentelemetry-metrics-api', '~> 1.0' spec.add_dependency 'opentelemetry-instrumentation-base', '~> 0.22.1' spec.add_development_dependency 'appraisal', '~> 2.5' diff --git a/instrumentation/sidekiq/Appraisals b/instrumentation/sidekiq/Appraisals index 3b241272e..d5727c2f8 100644 --- a/instrumentation/sidekiq/Appraisals +++ b/instrumentation/sidekiq/Appraisals @@ -1,24 +1,39 @@ # frozen_string_literal: true -appraise 'sidekiq-7.0' do - gem 'sidekiq', '~> 7.0' -end - -appraise 'sidekiq-6.5' do - gem 'sidekiq', '>= 6.5', '< 7.0' -end +{ + 'sidekiq-7.0' => [['sidekiq', '~> 7.0']], + 'sidekiq-6.5' => [['sidekiq', '>= 6.5', '< 7.0']], + 'sidekiq-6.0' => [ + ['sidekiq', '>= 6.0', '< 6.5'], + ['redis', '< 4.8'] + ], + 'sidekiq-5.2' => [ + ['sidekiq', '~> 5.2'], + ['redis', '< 4.8'] + ], + 'sidekiq-4.2' => [ + ['sidekiq', '~> 4.2'], + ['redis', '< 4.8'] + ] +}.each do |gemfile_name, specs| + appraise gemfile_name do + specs.each do |spec| + gem *spec + remove_gem 'opentelemetry-metrics-api' + remove_gem 'opentelemetry-metrics-sdk' + end + end -appraise 'sidekiq-6.0' do - gem 'sidekiq', '>= 6.0', '< 6.5' - gem 'redis', '< 4.8' -end - -appraise 'sidekiq-5.2' do - gem 'sidekiq', '~> 5.2' - gem 'redis', '< 4.8' -end + appraise "#{gemfile_name}-metrics-api" do + specs.each do |spec| + gem *spec + remove_gem 'opentelemetry-metrics-sdk' + end + end -appraise 'sidekiq-4.2' do - gem 'sidekiq', '~> 4.2' - gem 'redis', '< 4.8' + appraise "#{gemfile_name}-metrics-sdk" do + specs.each do |spec| + gem *spec + end + end end diff --git a/instrumentation/sidekiq/Gemfile b/instrumentation/sidekiq/Gemfile index 84efc8a18..025c47110 100644 --- a/instrumentation/sidekiq/Gemfile +++ b/instrumentation/sidekiq/Gemfile @@ -8,8 +8,22 @@ source 'https://rubygems.org' gemspec +# FIXME: the metrics-api is behind the metrics-sdk gem for some reason; bundle from git for now +OTEL_RUBY_GEM = lambda do |short_name| + short_name = short_name.split(/-|_/) + long_name = ['opentelemetry', *short_name].join('-') + + gem long_name, + git: 'https://www.github.com/open-telemetry/opentelemetry-ruby', + glob: "#{short_name.join('_')}/*.gemspec", + ref: '035c32ad9791f6200733e087f2ee49e0a615879a' +end + +OTEL_RUBY_GEM['metrics-api'] + group :test do gem 'opentelemetry-instrumentation-base', path: '../base' gem 'opentelemetry-instrumentation-redis', path: '../redis' + OTEL_RUBY_GEM['metrics-sdk'] gem 'pry-byebug' end diff --git a/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/instrumentation.rb b/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/instrumentation.rb index 1f3fe2720..40f19dc48 100644 --- a/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/instrumentation.rb +++ b/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/instrumentation.rb @@ -107,14 +107,63 @@ class Instrumentation < OpenTelemetry::Instrumentation::Base option :trace_poller_wait, default: false, validate: :boolean option :trace_processor_process_one, default: false, validate: :boolean option :peer_service, default: nil, validate: :string + option :metrics, default: false, validate: :boolean + + # FIXME: descriptions? + + if defined?(OpenTelemetry::Metrics) + counter 'messaging.client.sent.messages' + histogram 'messaging.client.operation.duration', unit: 's' # FIXME: UCUM::S + counter 'messaging.client.consumed.messages' + histogram 'messaging.process.duration', unit: 's' + + # FIXME: not semconv + gauge 'messaging.queue.latency', unit: 's' + end + + # FIXME: upstream + def counter(name) + get_instrument(:counter, name) + end + + # FIXME: upstream + def histogram(name) + get_instrument(:histogram, name) + end + + # FIXME: upstream + def gauge(name) + get_instrument(:gauge, name) + end private + def get_instrument(kind, name) + return unless metrics_enabled? + + @instruments ||= {} + @instruments[[kind, name]] ||= create_configured_instrument(kind, name) + end + + def create_configured_instrument(kind, name) + config = @instrument_configs[[kind, name]] + + if config.nil? + Kernel.warn("unconfigured instrument requested: #{kind} of '#{name}'") + return + end + + # FIXME: some of these have different opts; + # should verify that they work before this point. + meter.public_send(:"create_#{kind}", name, **config) + end + def gem_version Gem::Version.new(::Sidekiq::VERSION) end def require_dependencies + require_relative 'middlewares/common' require_relative 'middlewares/client/tracer_middleware' require_relative 'middlewares/server/tracer_middleware' diff --git a/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/client/tracer_middleware.rb b/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/client/tracer_middleware.rb index 039390a8f..6cdd34231 100644 --- a/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/client/tracer_middleware.rb +++ b/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/client/tracer_middleware.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../common' + module OpenTelemetry module Instrumentation module Sidekiq @@ -12,6 +14,7 @@ module Client # TracerMiddleware propagates context and instruments Sidekiq client # by way of its middleware system class TracerMiddleware + include Common include ::Sidekiq::ClientMiddleware if defined?(::Sidekiq::ClientMiddleware) def call(_worker_class, job, _queue, _redis_pool) @@ -33,17 +36,50 @@ def call(_worker_class, job, _queue, _redis_pool) OpenTelemetry.propagation.inject(job) span.add_event('created_at', timestamp: job['created_at']) yield + end.tap do + # FIXME: is it possible/necessary to detect failures here? Does sidekiq bubble them up the middlewares? + count_sent_message(job) end end private - def instrumentation_config - Sidekiq::Instrumentation.instance.config + def count_sent_message(job) + with_meter do |_meter| + counter_attributes = metrics_attributes(job).merge( + { + 'messaging.operation.name' => 'create' + # server.address => # FIXME: required if available + # messaging.destination.partition.id => FIXME: recommended + # server.port => # FIXME: recommended + } + ) + + counter = messaging_client_sent_messages_counter + counter.add(1, attributes: counter_attributes) + end + end + + def messaging_client_sent_messages_counter + instrumentation.counter('messaging.client.sent.messages') end def tracer - Sidekiq::Instrumentation.instance.tracer + instrumentation.tracer + end + + def with_meter(&block) + instrumentation.with_meter(&block) + end + + def metrics_attributes(job) + { + 'messaging.system' => 'sidekiq', # FIXME: metrics semconv + 'messaging.destination.name' => job['queue'] # FIXME: metrics semconv + # server.address => # FIXME: required if available + # messaging.destination.partition.id => FIXME: recommended + # server.port => # FIXME: recommended + } end end end diff --git a/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/common.rb b/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/common.rb new file mode 100644 index 000000000..b842d2b50 --- /dev/null +++ b/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/common.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Instrumentation + module Sidekiq + module Middlewares + module Common + private + + def instrumentation + Sidekiq::Instrumentation.instance + end + + def instrumentation_config + Sidekiq::Instrumentation.instance.config + end + + # Bypasses _all_ enclosed logic unless metrics are enabled + def with_meter(&block) + instrumentation.with_meter(&block) + end + + # time an inner block and yield the duration to the given callback + def timed(callback) + return yield unless metrics_enabled? + + t0 = monotonic_now + + yield.tap do + callback.call(monotonic_now - t0) + end + end + + # FIXME: is this a util somewhere + def realtime_now + Process.clock_gettime(Process::CLOCK_REALTIME) + end + + def monotonic_now + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + + def tracer + instrumentation.tracer + end + + def metrics_enabled? + instrumentation.metrics_enabled? + end + end + end + end + end +end diff --git a/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/server/tracer_middleware.rb b/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/server/tracer_middleware.rb index 90da96ea3..f8d51ca70 100644 --- a/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/server/tracer_middleware.rb +++ b/instrumentation/sidekiq/lib/opentelemetry/instrumentation/sidekiq/middlewares/server/tracer_middleware.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../common' + module OpenTelemetry module Instrumentation module Sidekiq @@ -12,6 +14,7 @@ module Server # TracerMiddleware propagates context and instruments Sidekiq requests # by way of its middleware system class TracerMiddleware + include Common include ::Sidekiq::ServerMiddleware if defined?(::Sidekiq::ServerMiddleware) def call(_worker, msg, _queue) @@ -32,40 +35,91 @@ def call(_worker, msg, _queue) extracted_context = OpenTelemetry.propagation.extract(msg) OpenTelemetry::Context.with_current(extracted_context) do - if instrumentation_config[:propagation_style] == :child - tracer.in_span(span_name, attributes: attributes, kind: :consumer) do |span| - span.add_event('created_at', timestamp: msg['created_at']) - span.add_event('enqueued_at', timestamp: msg['enqueued_at']) - yield - end - else - links = [] - span_context = OpenTelemetry::Trace.current_span(extracted_context).context - links << OpenTelemetry::Trace::Link.new(span_context) if instrumentation_config[:propagation_style] == :link && span_context.valid? - span = tracer.start_root_span(span_name, attributes: attributes, links: links, kind: :consumer) - OpenTelemetry::Trace.with_span(span) do - span.add_event('created_at', timestamp: msg['created_at']) - span.add_event('enqueued_at', timestamp: msg['enqueued_at']) - yield - rescue Exception => e # rubocop:disable Lint/RescueException - span.record_exception(e) - span.status = OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{e.class}") - raise e - ensure - span.finish + track_queue_latency(msg) + + timed(track_process_time_callback(msg)) do + if instrumentation_config[:propagation_style] == :child + tracer.in_span(span_name, attributes: attributes, kind: :consumer) do |span| + span.add_event('created_at', timestamp: msg['created_at']) + span.add_event('enqueued_at', timestamp: msg['enqueued_at']) + yield + end + else + links = [] + span_context = OpenTelemetry::Trace.current_span(extracted_context).context + links << OpenTelemetry::Trace::Link.new(span_context) if instrumentation_config[:propagation_style] == :link && span_context.valid? + span = tracer.start_root_span(span_name, attributes: attributes, links: links, kind: :consumer) + OpenTelemetry::Trace.with_span(span) do + span.add_event('created_at', timestamp: msg['created_at']) + span.add_event('enqueued_at', timestamp: msg['enqueued_at']) + yield + rescue Exception => e # rubocop:disable Lint/RescueException + span.record_exception(e) + span.status = OpenTelemetry::Trace::Status.error("Unhandled exception of type: #{e.class}") + raise e + ensure + span.finish + end end end + + count_consumed_message(msg) end end private - def instrumentation_config - Sidekiq::Instrumentation.instance.config + def track_queue_latency(msg) + with_meter do + return unless (enqueued_at = msg['enqueued_at']) + return unless enqueued_at.is_a?(Numeric) + + latency = (realtime_now - enqueued_at).abs + + queue_latency_gauge&.record(latency, attributes: metrics_attributes(msg)) + end + end + + def track_process_time_callback(msg) + ->(duration) { track_process_time(msg, duration) } + end + + def track_process_time(msg, duration) + with_meter do + attributes = metrics_attributes(msg).merge( + { 'messaging.operation.name' => 'process' } + ) + messaging_process_duration_histogram&.record(duration, attributes: attributes) + end + end + + def messaging_process_duration_histogram + instrumentation.histogram('messaging.process.duration') + end + + def count_consumed_message(msg) + with_meter do + messaging_client_consumed_messages_counter.add(1, attributes: metrics_attributes(msg)) + end end - def tracer - Sidekiq::Instrumentation.instance.tracer + def messaging_client_consumed_messages_counter + instrumentation.counter('messaging.client.consumed.messages') + end + + def queue_latency_gauge + instrumentation.gauge('messaging.queue.latency') + end + + # FIXME: dedupe + def metrics_attributes(msg) + { + 'messaging.system' => 'sidekiq', # FIXME: metrics semconv + 'messaging.destination.name' => msg['queue'] # FIXME: metrics semconv + # server.address => # FIXME: required if available + # messaging.destination.partition.id => FIXME: recommended + # server.port => # FIXME: recommended + } end end end diff --git a/instrumentation/sidekiq/opentelemetry-instrumentation-sidekiq.gemspec b/instrumentation/sidekiq/opentelemetry-instrumentation-sidekiq.gemspec index 26aae6e4d..5a449ccd6 100644 --- a/instrumentation/sidekiq/opentelemetry-instrumentation-sidekiq.gemspec +++ b/instrumentation/sidekiq/opentelemetry-instrumentation-sidekiq.gemspec @@ -32,6 +32,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'appraisal', '~> 2.5' spec.add_development_dependency 'bundler', '~> 2.4' spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'minitest-reporters' spec.add_development_dependency 'opentelemetry-sdk', '~> 1.1' spec.add_development_dependency 'opentelemetry-test-helpers', '~> 0.3' spec.add_development_dependency 'rspec-mocks' diff --git a/instrumentation/sidekiq/test/opentelemetry/instrumentation/sidekiq/middlewares/client/tracer_middleware_test.rb b/instrumentation/sidekiq/test/opentelemetry/instrumentation/sidekiq/middlewares/client/tracer_middleware_test.rb index a2de3d05d..25a5e6b2f 100644 --- a/instrumentation/sidekiq/test/opentelemetry/instrumentation/sidekiq/middlewares/client/tracer_middleware_test.rb +++ b/instrumentation/sidekiq/test/opentelemetry/instrumentation/sidekiq/middlewares/client/tracer_middleware_test.rb @@ -14,10 +14,20 @@ let(:spans) { exporter.finished_spans } let(:enqueue_span) { spans.first } let(:config) { {} } + let(:metrics_exporter) { METRICS_EXPORTER } + + with_metrics_sdk do + let(:metric_snapshots) do + METRICS_EXPORTER.tap(&:pull) + .metric_snapshots.select { |snapshot| snapshot.data_points.any? } + .group_by(&:name) + end + end before do instrumentation.install(config) exporter.reset + reset_metrics_exporter end after do @@ -81,5 +91,34 @@ _(enqueue_span.attributes['peer.service']).must_equal 'MySidekiqService' end end + + with_metrics_sdk do + it 'yields no metrics if config is not set' do + _(instrumentation.metrics_enabled?).must_equal false + SimpleJob.perform_async + SimpleJob.drain + + _(metric_snapshots).must_be_empty + end + + describe 'with metrics enabled' do + let(:config) { { metrics: true } } + + it 'metrics processing' do + _(instrumentation.metrics_enabled?).must_equal true + SimpleJob.perform_async + SimpleJob.drain + + sent_messages = metric_snapshots['messaging.client.sent.messages'] + _(sent_messages.count).must_equal 1 + _(sent_messages.first.data_points.count).must_equal 1 + _(sent_messages.first.data_points.first.value).must_equal 1 + sent_messages_attributes = sent_messages.first.data_points.first.attributes + _(sent_messages_attributes['messaging.system']).must_equal 'sidekiq' + _(sent_messages_attributes['messaging.destination.name']).must_equal 'default' # FIXME: newer semconv specifies this key + _(sent_messages_attributes['messaging.operation.name']).must_equal 'create' + end + end + end end end diff --git a/instrumentation/sidekiq/test/opentelemetry/instrumentation/sidekiq/middlewares/server/tracer_middleware_test.rb b/instrumentation/sidekiq/test/opentelemetry/instrumentation/sidekiq/middlewares/server/tracer_middleware_test.rb index 145d3b743..c0d51b715 100644 --- a/instrumentation/sidekiq/test/opentelemetry/instrumentation/sidekiq/middlewares/server/tracer_middleware_test.rb +++ b/instrumentation/sidekiq/test/opentelemetry/instrumentation/sidekiq/middlewares/server/tracer_middleware_test.rb @@ -17,12 +17,23 @@ let(:root_span) { spans.find { |s| s.parent_span_id == OpenTelemetry::Trace::INVALID_SPAN_ID } } let(:config) { {} } + with_metrics_sdk do + let(:metric_snapshots) do + METRICS_EXPORTER.tap(&:pull) + .metric_snapshots.select { |snapshot| snapshot.data_points.any? } + .group_by(&:name) + end + end + before do instrumentation.install(config) exporter.reset + reset_metrics_exporter end - after { instrumentation.instance_variable_set(:@installed, false) } + after do + instrumentation.instance_variable_set(:@installed, false) + end describe 'enqueue spans' do it 'before performing any jobs' do @@ -49,6 +60,54 @@ _(job_span.events[1].name).must_equal('enqueued_at') end + with_metrics_sdk do + # FIXME: still seeing order-dependent failure here + it 'yields no metrics if config is not set' do + _(OpenTelemetry::Instrumentation::Sidekiq::Instrumentation.instance.metrics_enabled?).must_equal false + job_id = SimpleJob.perform_async + SimpleJob.drain + + _(exporter.finished_spans.size).must_equal 2 + _(metric_snapshots).must_be_empty + end + + describe 'with metrics enabled' do + let(:config) { { metrics: true } } + + it 'metrics processing' do + _(OpenTelemetry::Instrumentation::Sidekiq::Instrumentation.instance.metrics_enabled?).must_equal true + job_id = SimpleJob.perform_async + SimpleJob.drain + + queue_latency = metric_snapshots['messaging.queue.latency'] + _(queue_latency.count).must_equal 1 + _(queue_latency.first.data_points.count).must_equal 1 + queue_latency_attributes = queue_latency.first.data_points.first.attributes + _(queue_latency_attributes['messaging.system']).must_equal 'sidekiq' + _(queue_latency_attributes['messaging.destination.name']).must_equal 'default' # FIXME: newer semconv specifies this key + + process_duration = metric_snapshots['messaging.process.duration'] + _(process_duration.count).must_equal 1 + _(process_duration.first.data_points.count).must_equal 1 + process_duration_attributes = process_duration.first.data_points.first.attributes + _(process_duration_attributes['messaging.system']).must_equal 'sidekiq' + _(process_duration_attributes['messaging.operation.name']).must_equal 'process' + _(process_duration_attributes['messaging.destination.name']).must_equal 'default' + + process_duration_data_point = process_duration.first.data_points.first + _(process_duration_data_point.count).must_equal 1 + + consumed_messages = metric_snapshots['messaging.client.consumed.messages'] + _(consumed_messages.count).must_equal 1 + _(consumed_messages.first.data_points.count).must_equal 1 + consumed_messages_attributes = queue_latency.first.data_points.first.attributes + _(consumed_messages_attributes['messaging.system']).must_equal 'sidekiq' + _(consumed_messages_attributes['messaging.destination.name']).must_equal 'default' # FIXME: newer semconv specifies this key + _(consumed_messages.first.data_points.first.value).must_equal 1 + end + end + end + it 'traces when enqueued with Active Job' do SimpleJobWithActiveJob.perform_later(1, 2) Sidekiq::Worker.drain_all diff --git a/instrumentation/sidekiq/test/test_helper.rb b/instrumentation/sidekiq/test/test_helper.rb index df49e4125..154064701 100644 --- a/instrumentation/sidekiq/test/test_helper.rb +++ b/instrumentation/sidekiq/test/test_helper.rb @@ -10,9 +10,12 @@ require 'active_job' require 'minitest/autorun' +require 'minitest/reporters' require 'rspec/mocks/minitest_integration' require 'sidekiq/testing' +Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new + if Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('7.0.0') require 'helpers/mock_loader_for_7.0' elsif Gem::Version.new(Sidekiq::VERSION) >= Gem::Version.new('6.5.0') @@ -21,6 +24,14 @@ require 'helpers/mock_loader' end +# speed up tests that rely on empty queues +Sidekiq::BasicFetch::TIMEOUT = if Gem.loaded_specs['sidekiq'].version < Gem::Version.new('6.5.0') + # Redis 4.8 has trouble with float timeouts given as positional arguments + 1 + else + 0.1 + end + # OpenTelemetry SDK config for testing EXPORTER = OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new span_processor = OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(EXPORTER) @@ -30,6 +41,74 @@ c.add_span_processor span_processor end +module LoadedMetricsFeatures + OTEL_METRICS_API_LOADED = !Gem.loaded_specs['opentelemetry-metrics-api'].nil? + OTEL_METRICS_SDK_LOADED = !Gem.loaded_specs['opentelemetry-metrics-sdk'].nil? + + extend self + + def api_loaded? + OTEL_METRICS_API_LOADED + end + + def sdk_loaded? + OTEL_METRICS_SDK_LOADED + end +end + +# NOTE: this isn't currently used, but it may be useful to fully reset state between tests +def reset_meter_provider + return unless LoadedMetricsFeatures.sdk_loaded? + + resource = OpenTelemetry.meter_provider.resource + OpenTelemetry.meter_provider = OpenTelemetry::SDK::Metrics::MeterProvider.new(resource: resource) + OpenTelemetry.meter_provider.add_metric_reader(METRICS_EXPORTER) +end + +def reset_metrics_exporter + return unless LoadedMetricsFeatures.sdk_loaded? + + METRICS_EXPORTER.pull + METRICS_EXPORTER.reset +end + +if LoadedMetricsFeatures.sdk_loaded? + METRICS_EXPORTER = OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new + OpenTelemetry.meter_provider.add_metric_reader(METRICS_EXPORTER) +end + +module ConditionalEvaluation + def self.included(base) + base.extend(self) + end + + def self.prepended(base) + base.extend(self) + end + + def with_metrics_sdk + yield if LoadedMetricsFeatures.sdk_loaded? + end + + # FIXME: unclear if this is ever needed + def without_metrics_sdk + yield unless LoadedMetricsFeatures.sdk_loaded? + end + + def it(desc = 'anonymous', with_metrics_sdk: false, without_metrics_sdk: false, &block) + return super(desc, &block) unless with_metrics_sdk || without_metrics_sdk + + raise ArgumentError, 'without_metrics_sdk and with_metrics_sdk must be mutually exclusive' if without_metrics_sdk && with_metrics_sdk + + return if with_metrics_sdk && !LoadedMetricsFeatures.sdk_loaded? + return if without_metrics_sdk && LoadedMetricsFeatures.sdk_loaded? + + super(desc, &block) + end +end + +Minitest::Spec.prepend(ConditionalEvaluation) + # Sidekiq redis configuration ENV['TEST_REDIS_HOST'] ||= '127.0.0.1' ENV['TEST_REDIS_PORT'] ||= '16379'