diff --git a/metrics_api/lib/opentelemetry/internal/proxy_instrument.rb b/metrics_api/lib/opentelemetry/internal/proxy_instrument.rb index 124b434f06..ca7a15c6f0 100644 --- a/metrics_api/lib/opentelemetry/internal/proxy_instrument.rb +++ b/metrics_api/lib/opentelemetry/internal/proxy_instrument.rb @@ -8,21 +8,23 @@ module OpenTelemetry module Internal # @api private class ProxyInstrument - def initialize(kind, name, unit, desc, callable) + def initialize(kind, name, unit, desc, callable, exemplar_filter, exemplar_reservoir) @kind = kind @name = name @unit = unit @desc = desc @callable = callable + @exemplar_filter = exemplar_filter + @exemplar_reservoir = exemplar_reservoir @delegate = nil end def upgrade_with(meter) @delegate = case @kind - when :counter, :histogram, :up_down_counter, :gauge - meter.send("create_#{@kind}", @name, unit: @unit, description: @desc) + when :counter, :histogram, :up_down_counter + meter.send("create_#{@kind}", @name, unit: @unit, description: @desc, exemplar_filter: @exemplar_filter, exemplar_reservoir: @exemplar_reservoir) when :observable_counter, :observable_gauge, :observable_up_down_counter - meter.send("create_#{@kind}", @name, unit: @unit, description: @desc, callback: @callback) + meter.send("create_#{@kind}", @name, unit: @unit, description: @desc, exemplar_filter: @exemplar_filter, exemplar_reservoir: @exemplar_reservoir, callback: @callback) end end diff --git a/metrics_api/lib/opentelemetry/internal/proxy_meter.rb b/metrics_api/lib/opentelemetry/internal/proxy_meter.rb index 9f44b4bb67..98572a8ae2 100644 --- a/metrics_api/lib/opentelemetry/internal/proxy_meter.rb +++ b/metrics_api/lib/opentelemetry/internal/proxy_meter.rb @@ -38,18 +38,18 @@ def delegate=(meter) private - def create_instrument(kind, name, unit, description, callback) + def create_instrument(kind, name, unit, description, callback, exemplar_filter, exemplar_reservoir) super do - next ProxyInstrument.new(kind, name, unit, description, callback) if @delegate.nil? + next ProxyInstrument.new(kind, name, unit, description, callback, exemplar_filter, exemplar_reservoir) if @delegate.nil? case kind - when :counter then @delegate.create_counter(name, unit: unit, description: description) - when :histogram then @delegate.create_histogram(name, unit: unit, description: description) - when :gauge then @delegate.create_gauge(name, unit: unit, description: description) - when :up_down_counter then @delegate.create_up_down_counter(name, unit: unit, description: description) - when :observable_counter then @delegate.create_observable_counter(name, unit: unit, description: description, callback: callback) - when :observable_gauge then @delegate.create_observable_gauge(name, unit: unit, description: description, callback: callback) - when :observable_up_down_counter then @delegate.create_observable_up_down_counter(name, unit: unit, description: description, callback: callback) + when :counter then @delegate.create_counter(name, unit: unit, description: description, exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir) + when :histogram then @delegate.create_histogram(name, unit: unit, description: description, exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir) + when :up_down_counter then @delegate.create_up_down_counter(name, unit: unit, description: description, exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir) + when :gauge then @delegate.create_gauge(name, unit: unit, description: description, exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir) + when :observable_counter then @delegate.create_observable_counter(name, unit: unit, description: description, exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir, callback: callback) + when :observable_gauge then @delegate.create_observable_gauge(name, unit: unit, description: description, exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir, callback: callback) + when :observable_up_down_counter then @delegate.create_observable_up_down_counter(name, unit: unit, description: description, exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir, callback: callback) end end end diff --git a/metrics_api/lib/opentelemetry/metrics/meter.rb b/metrics_api/lib/opentelemetry/metrics/meter.rb index 7b7b524cdf..32a55de3d5 100644 --- a/metrics_api/lib/opentelemetry/metrics/meter.rb +++ b/metrics_api/lib/opentelemetry/metrics/meter.rb @@ -43,8 +43,8 @@ def initialize # @param description [optional String] an optional free-form text provided by user. # # @return [nil] after creation of counter, it will be stored in instrument_registry - def create_counter(name, unit: nil, description: nil) - create_instrument(:counter, name, unit, description, nil) { COUNTER } + def create_counter(name, unit: nil, description: nil, exemplar_filter: nil, exemplar_reservoir: nil) + create_instrument(:counter, name, unit, description, nil, exemplar_filter, exemplar_reservoir) { COUNTER } end # Histogram is a synchronous Instrument which can be used to report arbitrary values that are likely @@ -62,8 +62,8 @@ def create_counter(name, unit: nil, description: nil) # @param description [optional String] an optional free-form text provided by user. # # @return [nil] after creation of histogram, it will be stored in instrument_registry - def create_histogram(name, unit: nil, description: nil) - create_instrument(:histogram, name, unit, description, nil) { HISTOGRAM } + def create_histogram(name, unit: nil, description: nil, exemplar_filter: nil, exemplar_reservoir: nil) + create_instrument(:histogram, name, unit, description, nil, exemplar_filter, exemplar_reservoir) { HISTOGRAM } end # Gauge is an synchronous Instrument which reports non-additive value(s) @@ -80,8 +80,8 @@ def create_histogram(name, unit: nil, description: nil) # @param description [optional String] an optional free-form text provided by user. # # @return [nil] after creation of gauge, it will be stored in instrument_registry - def create_gauge(name, unit: nil, description: nil) - create_instrument(:gauge, name, unit, description, nil) { GAUGE } + def create_gauge(name, unit: nil, description: nil, exemplar_filter: nil, exemplar_reservoir: nil) + create_instrument(:gauge, name, unit, description, nil, exemplar_filter, exemplar_reservoir) { GAUGE } end # UpDownCounter is a synchronous Instrument which supports increments and decrements. @@ -97,8 +97,8 @@ def create_gauge(name, unit: nil, description: nil) # @param description [optional String] an optional free-form text provided by user. # # @return [nil] after creation of up_down_counter, it will be stored in instrument_registry - def create_up_down_counter(name, unit: nil, description: nil) - create_instrument(:up_down_counter, name, unit, description, nil) { UP_DOWN_COUNTER } + def create_up_down_counter(name, unit: nil, description: nil, exemplar_filter: nil, exemplar_reservoir: nil) + create_instrument(:up_down_counter, name, unit, description, nil, exemplar_filter, exemplar_reservoir) { UP_DOWN_COUNTER } end # ObservableCounter is an asynchronous Instrument which reports monotonically @@ -119,8 +119,8 @@ def create_up_down_counter(name, unit: nil, description: nil) # @param description [optional String] an optional free-form text provided by user. # # @return [nil] after creation of observable_counter, it will be stored in instrument_registry - def create_observable_counter(name, callback:, unit: nil, description: nil) - create_instrument(:observable_counter, name, unit, description, callback) { OBSERVABLE_COUNTER } + def create_observable_counter(name, callback:, unit: nil, description: nil, exemplar_filter: nil, exemplar_reservoir: nil) + create_instrument(:observable_counter, name, unit, description, callback, exemplar_filter, exemplar_reservoir) { OBSERVABLE_COUNTER } end # ObservableGauge is an asynchronous Instrument which reports non-additive value(s) @@ -142,8 +142,8 @@ def create_observable_counter(name, callback:, unit: nil, description: nil) # @param description [optional String] an optional free-form text provided by user. # # @return [nil] after creation of observable_gauge, it will be stored in instrument_registry - def create_observable_gauge(name, callback:, unit: nil, description: nil) - create_instrument(:observable_gauge, name, unit, description, callback) { OBSERVABLE_GAUGE } + def create_observable_gauge(name, callback:, unit: nil, description: nil, exemplar_filter: nil, exemplar_reservoir: nil) + create_instrument(:observable_gauge, name, unit, description, callback, exemplar_filter, exemplar_reservoir) { OBSERVABLE_GAUGE } end # ObservableUpDownCounter is an asynchronous Instrument which reports additive value(s) @@ -165,13 +165,13 @@ def create_observable_gauge(name, callback:, unit: nil, description: nil) # @param description [optional String] an optional free-form text provided by user. # # @return [nil] after creation of observable_up_down_counter, it will be stored in instrument_registry - def create_observable_up_down_counter(name, callback:, unit: nil, description: nil) - create_instrument(:observable_up_down_counter, name, unit, description, callback) { OBSERVABLE_UP_DOWN_COUNTER } + def create_observable_up_down_counter(name, callback:, unit: nil, description: nil, exemplar_filter: nil, exemplar_reservoir: nil) + create_instrument(:observable_up_down_counter, name, unit, description, callback, exemplar_filter, exemplar_reservoir) { OBSERVABLE_UP_DOWN_COUNTER } end private - def create_instrument(kind, name, unit, description, callback) + def create_instrument(kind, name, unit, description, callback, exemplar_filter, exemplar_reservoir) @mutex.synchronize do OpenTelemetry.logger.warn("duplicate instrument registration occurred for instrument #{name}") if @instrument_registry.include? name diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics.rb index 35593185f9..c89ad39250 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics.rb @@ -13,6 +13,7 @@ module Metrics end end +require 'opentelemetry/sdk/metrics/exemplar' require 'opentelemetry/sdk/metrics/aggregation' require 'opentelemetry/sdk/metrics/configuration_patch' require 'opentelemetry/sdk/metrics/export' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb index d379e0a9a4..d3c6441e97 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb @@ -14,6 +14,10 @@ class ExplicitBucketHistogram DEFAULT_BOUNDARIES = [0, 5, 10, 25, 50, 75, 100, 250, 500, 1000].freeze private_constant :DEFAULT_BOUNDARIES + # if no reservior pass from instrument, then use this empty reservior to avoid no method found error + DEFAULT_RESERVOIR = Metrics::Exemplar::FixedSizeExemplarReservoir.new + private_constant :DEFAULT_RESERVOIR + attr_reader :aggregation_temporality # The default value for boundaries represents the following buckets: @@ -23,8 +27,10 @@ class ExplicitBucketHistogram def initialize( aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :delta), # TODO: the default should be :cumulative, see issue #1555 boundaries: DEFAULT_BOUNDARIES, - record_min_max: true + record_min_max: true, + exemplar_reservoir: DEFAULT_RESERVOIR ) + @exemplar_reservoir = exemplar_reservoir @aggregation_temporality = aggregation_temporality.to_sym @boundaries = boundaries && !boundaries.empty? ? boundaries.sort : nil @record_min_max = record_min_max @@ -67,7 +73,7 @@ def update(amount, attributes, data_points) 0, # :sum empty_bucket_counts, # :bucket_counts @boundaries, # :explicit_bounds - nil, # :exemplars + @exemplar_reservoir.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality), # :exemplars min, # :min max # :max ) diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/sum.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/sum.rb index c2771b38e3..dcd7d5b89e 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/sum.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/sum.rb @@ -3,7 +3,6 @@ # Copyright The OpenTelemetry Authors # # SPDX-License-Identifier: Apache-2.0 - module OpenTelemetry module SDK module Metrics @@ -11,10 +10,16 @@ module Aggregation # Contains the implementation of the Sum aggregation # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#sum-aggregation class Sum + # if no reservior pass from instrument, then use this empty reservior to avoid no method found error + DEFAULT_RESERVOIR = Metrics::Exemplar::FixedSizeExemplarReservoir.new + private_constant :DEFAULT_RESERVOIR + attr_reader :aggregation_temporality - def initialize(aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :delta)) + def initialize(aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :delta), + exemplar_reservoir: DEFAULT_RESERVOIR) # TODO: the default should be :cumulative, see issue #1555 + @exemplar_reservoir = exemplar_reservoir @aggregation_temporality = aggregation_temporality.to_sym end @@ -39,12 +44,14 @@ def collect(start_time, end_time, data_points) end def update(increment, attributes, data_points) + # NumberDataPoint should include exemplars ndp = data_points[attributes] || data_points[attributes] = NumberDataPoint.new( attributes, nil, nil, 0, - nil + # will this cause the reservoir overloaded with old exemplars? + @exemplar_reservoir.collect(attributes: attributes, aggregation_temporality: @aggregation_temporality) # exemplar ) ndp.value += increment diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar.rb new file mode 100644 index 0000000000..1abc8210c5 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + # The Exemplar module contains the OpenTelemetry metrics reference + # exemplar implementations. + module Exemplar + end + end + end +end + +require 'opentelemetry/sdk/metrics/exemplar/exemplar' +require 'opentelemetry/sdk/metrics/exemplar/exemplar_filter' +require 'opentelemetry/sdk/metrics/exemplar/exemplar_reservoir' +require 'opentelemetry/sdk/metrics/exemplar/always_off_exemplar_filter' +require 'opentelemetry/sdk/metrics/exemplar/always_on_exemplar_filter' +require 'opentelemetry/sdk/metrics/exemplar/trace_based_exemplar_filter' +require 'opentelemetry/sdk/metrics/exemplar/noop_exemplar_reservoir' +require 'opentelemetry/sdk/metrics/exemplar/fixed_size_exemplar_reservoir' +require 'opentelemetry/sdk/metrics/exemplar/histogram_exemplar_reservoir' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/always_off_exemplar_filter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/always_off_exemplar_filter.rb new file mode 100644 index 0000000000..8b5b8dcad1 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/always_off_exemplar_filter.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # AlwaysOffExemplarFilter makes no measurements eligible for being an Exemplar. + # Using this ExemplarFilter is as good as disabling Exemplar feature. + class AlwaysOffExemplarFilter < ExemplarFilter + def self.should_sample?(value, timestamp, attributes, context) + false + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/always_on_exemplar_filter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/always_on_exemplar_filter.rb new file mode 100644 index 0000000000..1b44a0fd44 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/always_on_exemplar_filter.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # AlwaysOnExemplarFilter + class AlwaysOnExemplarFilter < ExemplarFilter + def self.should_sample?(value, timestamp, attributes, context) + true + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar.rb new file mode 100644 index 0000000000..8434654fa7 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # Exemplar + class Exemplar + attr_reader :value, :time_unix_nano, :attributes, :span_id, :trace_id + + def initialize(value, time_unix_nano, attributes, span_id, trace_id) + @value = value + @time_unix_nano = time_unix_nano + @attributes = attributes + @span_id = span_id + @trace_id = trace_id + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar_filter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar_filter.rb new file mode 100644 index 0000000000..800eb8a136 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar_filter.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # ExemplarFilter + class ExemplarFilter + # Returns a {Boolean} value. + # + # @param [Integer] value Value of the measurement + # @param [Hash] attributes Complete set of Attributes of the measurement + # @param [Context] context Context of the measurement, which covers the Baggage and the current active Span. + # + # @return [Boolean] + def self.should_sample?(value, attributes, context); end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar_reservoir.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar_reservoir.rb new file mode 100644 index 0000000000..a6fe2d6e62 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/exemplar_reservoir.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # ExemplarReservoir + class ExemplarReservoir + def initialize + @exemplars = [] + end + + # Store the info into exemplars bucket + # + # @param [Integer] value Value of the measurement + # @param [Integer] timestamp Time of recording + # @param [Hash] attributes Complete set of Attributes of the measurement + # @param [Context] context SpanContext of the measurement, which covers the Baggage and the current active Span. + # + # @return [Nil] + def offer(value: nil, timestamp: nil, attributes: nil, context: nil) + span_context = current_span_context(context) + @exemplars << Exemplar.new(value, timestamp, attributes, span_context.hex_span_id, span_context.hex_trace_id) + nil + end + + # return list of Exemplars based on given attributes + # + # @param [Hash] attributes Value of the measurement + # @param [Boolean] aggregation_temporality Should remove the original exemplars or not, default delta + # + # @return [Array] exemplars Array of exemplars + def collect(attributes: nil, aggregation_temporality: :delta) + exemplars = [] + @exemplars.each { |exemplar| exemplars << exemplar if exemplar } # TODO: Addition operation on selecting exemplar + @exemplars.clear if aggregation_temporality == :delta + exemplars + end + + def current_span_context(context) + ::OpenTelemetry::Trace.current_span(context).context + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/fixed_size_exemplar_reservoir.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/fixed_size_exemplar_reservoir.rb new file mode 100644 index 0000000000..37fe86c392 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/fixed_size_exemplar_reservoir.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # FixedSizeExemplarReservoir + class FixedSizeExemplarReservoir < ExemplarReservoir + MAX_BUCKET_SIZE = 1 + + def initialize(max_size: nil) + super() + @max_size = max_size || MAX_BUCKET_SIZE + end + + # MUST use an uniformly-weighted sampling algorithm based on the number of samples the reservoir + def offer(value: nil, timestamp: nil, attributes: nil, context: nil) + span_context = current_span_context(context) + if @exemplars.size >= @max_size + rand_index = rand(0..@max_size - 1) + @exemplars[rand_index] = Exemplar.new(value, timestamp, attributes, span_context.hex_span_id, span_context.hex_trace_id) + nil + else + super(value: value, timestamp: timestamp, attributes: attributes, context: context) + end + end + + def collect(attributes: nil, aggregation_temporality: nil) + super(attributes: attributes, aggregation_temporality: aggregation_temporality) + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/histogram_exemplar_reservoir.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/histogram_exemplar_reservoir.rb new file mode 100644 index 0000000000..c1b3328812 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/histogram_exemplar_reservoir.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # same as AlignedHistogramBucketExemplarReservoir + class HistogramExemplarReservoir < ExemplarReservoir + DEFAULT_BOUNDARIES = [0, 5, 10, 25, 50, 75, 100, 250, 500, 1000].freeze + private_constant :DEFAULT_BOUNDARIES + + def initialize(boundaries: nil) + super() + @boundaries = boundaries || DEFAULT_BOUNDARIES + end + + # TODO: align with the requirements of alignedhistogrambucketexemplarreservoir for offering + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#alignedhistogrambucketexemplarreservoir + # Assumption: each boundary should have one exemplar measurement + def offer(value: nil, timestamp: nil, attributes: nil, context: nil) + bucket = find_histogram_bucket(value) + return unless bucket < @boundaries.size + + span_context = current_span_context(context) + @exemplars[bucket] = Exemplar.new(value, timestamp, attributes, span_context.hex_span_id, span_context.hex_trace_id) + end + + # return Exemplar + def collect(attributes: nil, aggregation_temporality: nil) + super(attributes: attributes, aggregation_temporality: aggregation_temporality) + end + + def find_histogram_bucket(value) + @boundaries.bsearch_index { |i| i >= value } || @boundaries.size + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/noop_exemplar_reservoir.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/noop_exemplar_reservoir.rb new file mode 100644 index 0000000000..2d33d5c67a --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/noop_exemplar_reservoir.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # NoopExemplarReservoir + class NoopExemplarReservoir < ExemplarReservoir + def initialize; end + + def offer(value: nil, timestamp: nil, attributes: nil, context: nil); end + + def collect(attributes: nil, aggregation_temporality: :delta) + [] + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/trace_based_exemplar_filter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/trace_based_exemplar_filter.rb new file mode 100644 index 0000000000..a3e71199dd --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/exemplar/trace_based_exemplar_filter.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Exemplar + # TraceBasedExemplarFilter + class TraceBasedExemplarFilter < ExemplarFilter + def self.should_sample?(value, timestamp, attributes, context) + ::OpenTelemetry::Trace.current_span(context).context.trace_flags.sampled? + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/counter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/counter.rb index 6af04b1937..b0c9144ea8 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/counter.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/counter.rb @@ -31,6 +31,7 @@ def add(increment, attributes: {}) if increment.negative? OpenTelemetry.logger.warn("#{@name} received a negative value") else + exemplar_offer(increment, attributes) update(increment, attributes) end nil @@ -42,7 +43,7 @@ def add(increment, attributes: {}) private def default_aggregation - OpenTelemetry::SDK::Metrics::Aggregation::Sum.new + OpenTelemetry::SDK::Metrics::Aggregation::Sum.new(exemplar_reservoir: @exemplar_reservoir) end end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/histogram.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/histogram.rb index 5c8e00f157..88aef60b6f 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/histogram.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/histogram.rb @@ -25,6 +25,7 @@ def instrument_kind # Array values must not contain nil elements and all elements must be of # the same basic type (string, numeric, boolean). def record(amount, attributes: {}) + exemplar_offer(amount, attributes) update(amount, attributes) nil rescue StandardError => e @@ -35,7 +36,9 @@ def record(amount, attributes: {}) private def default_aggregation - OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new + # TODO: at this point, histogram always take the default DEFAULT_BOUNDARIES. In future, the histogram should be able to + # define the custom boundaries so for exemplar_reservoir can also takes the bounaries as parameter + OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new(exemplar_reservoir: @exemplar_reservoir) end end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/synchronous_instrument.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/synchronous_instrument.rb index f5bf321f0a..89796f48e0 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/synchronous_instrument.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/synchronous_instrument.rb @@ -11,13 +11,17 @@ module Instrument # {SynchronousInstrument} contains the common functionality shared across # the synchronous instruments SDK instruments. class SynchronousInstrument - def initialize(name, unit, description, instrumentation_scope, meter_provider) + NOOP_EXEMPLAR_RESERVOIR = Exemplar::NoopExemplarReservoir.new + + def initialize(name, unit, description, instrumentation_scope, meter_provider, exemplar_filter, exemplar_reservoir) @name = name @unit = unit @description = description @instrumentation_scope = instrumentation_scope @meter_provider = meter_provider @metric_streams = [] + @exemplar_filter = exemplar_filter || meter_provider.exemplar_filter + @exemplar_reservoir = exemplar_reservoir || NOOP_EXEMPLAR_RESERVOIR meter_provider.register_synchronous_instrument(self) end @@ -42,6 +46,16 @@ def register_with_new_metric_store(metric_store, aggregation: default_aggregatio def update(value, attributes) @metric_streams.each { |ms| ms.update(value, attributes) } end + + # Adding the exemplar to reservoir + # + def exemplar_offer(value, attributes) + context = OpenTelemetry::Context.current + time = (Time.now.to_r * 1_000_000).to_i + return unless @exemplar_filter.should_sample?(value, time, attributes, context) + + @exemplar_reservoir.offer(value: value, timestamp: time, attributes: attributes, context: context) + end end end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/up_down_counter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/up_down_counter.rb index bba734bb0c..b2c40e264c 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/up_down_counter.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/up_down_counter.rb @@ -25,6 +25,7 @@ def instrument_kind # Array values must not contain nil elements and all elements must be of # the same basic type (string, numeric, boolean). def add(amount, attributes: {}) + exemplar_offer(amount, attributes) update(amount, attributes) nil rescue StandardError => e @@ -35,7 +36,7 @@ def add(amount, attributes: {}) private def default_aggregation - OpenTelemetry::SDK::Metrics::Aggregation::Sum.new + OpenTelemetry::SDK::Metrics::Aggregation::Sum.new(exemplar_reservoir: @exemplar_reservoir) end end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb index dee8fea8d2..3b390a08fe 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb @@ -35,7 +35,7 @@ def add_metric_reader(metric_reader) end end - def create_instrument(kind, name, unit, description, callback) + def create_instrument(kind, name, unit, description, callback, exemplar_filter, exemplar_reservoir) raise InstrumentNameError if name.nil? raise InstrumentNameError if name.empty? raise InstrumentNameError unless NAME_REGEX.match?(name) @@ -44,12 +44,12 @@ def create_instrument(kind, name, unit, description, callback) super do case kind - when :counter then OpenTelemetry::SDK::Metrics::Instrument::Counter.new(name, unit, description, @instrumentation_scope, @meter_provider) + when :counter then OpenTelemetry::SDK::Metrics::Instrument::Counter.new(name, unit, description, @instrumentation_scope, @meter_provider, exemplar_filter, exemplar_reservoir) when :observable_counter then OpenTelemetry::SDK::Metrics::Instrument::ObservableCounter.new(name, unit, description, callback, @instrumentation_scope, @meter_provider) - when :gauge then OpenTelemetry::SDK::Metrics::Instrument::Gauge.new(name, unit, description, @instrumentation_scope, @meter_provider) - when :histogram then OpenTelemetry::SDK::Metrics::Instrument::Histogram.new(name, unit, description, @instrumentation_scope, @meter_provider) + when :gauge then OpenTelemetry::SDK::Metrics::Instrument::Gauge.new(name, unit, description, @instrumentation_scope, @meter_provider, exemplar_filter, exemplar_reservoir) + when :histogram then OpenTelemetry::SDK::Metrics::Instrument::Histogram.new(name, unit, description, @instrumentation_scope, @meter_provider, exemplar_filter, exemplar_reservoir) when :observable_gauge then OpenTelemetry::SDK::Metrics::Instrument::ObservableGauge.new(name, unit, description, callback, @instrumentation_scope, @meter_provider) - when :up_down_counter then OpenTelemetry::SDK::Metrics::Instrument::UpDownCounter.new(name, unit, description, @instrumentation_scope, @meter_provider) + when :up_down_counter then OpenTelemetry::SDK::Metrics::Instrument::UpDownCounter.new(name, unit, description, @instrumentation_scope, @meter_provider, exemplar_filter, exemplar_reservoir) when :observable_up_down_counter then OpenTelemetry::SDK::Metrics::Instrument::ObservableUpDownCounter.new(name, unit, description, callback, @instrumentation_scope, @meter_provider) end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb index 4538a88db4..cef0a6e376 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb @@ -10,11 +10,12 @@ module SDK # implementation. module Metrics # {MeterProvider} is the SDK implementation of {OpenTelemetry::Metrics::MeterProvider}. + # rubocop:disable Metrics/ClassLength class MeterProvider < OpenTelemetry::Metrics::MeterProvider Key = Struct.new(:name, :version) private_constant(:Key) - attr_reader :resource, :metric_readers, :registered_views + attr_reader :resource, :metric_readers, :registered_views, :exemplar_filter def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create) @mutex = Mutex.new @@ -22,7 +23,9 @@ def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create) @stopped = false @metric_readers = [] @resource = resource + @exemplar_filter = Exemplar::AlwaysOffExemplarFilter @registered_views = [] + exemplar_filter_setup end # Returns a {Meter} instance. @@ -127,6 +130,37 @@ def register_synchronous_instrument(instrument) end end + # spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/sdk-environment-variables.md#exemplar + # this is one way to turn on the exemplar (exemplar should be turned off by default) + # + def exemplar_filter_setup + return unless ENV.key?('OTEL_METRICS_EXEMPLAR_FILTER') + + case ENV['OTEL_METRICS_EXEMPLAR_FILTER'] + when 'always_on' + @exemplar_filter = Exemplar::AlwaysOnExemplarFilter + when 'trace_based' + @exemplar_filter = Exemplar::TraceBasedExemplarFilter + when 'always_off' + @exemplar_filter = Exemplar::AlwaysOffExemplarFilter + else + OpenTelemetry.logger.warn("OTEL_METRICS_EXEMPLAR_FILTER #{ENV['OTEL_METRICS_EXEMPLAR_FILTER']} is not part of provided exemplar filter; Exemplar is off.") + end + end + + # Adds a new exemplar_filter to replace exist exemplar_filter + # Default to TraceBasedExemplarFilter + # + # @param exemplar_filter the new ExemplarFilter to be added. + def exemplar_filter_on(exemplar_filter: Exemplar::TraceBasedExemplarFilter) + @exemplar_filter = exemplar_filter + end + + # turn off exemplar_filter by setting the exemplar_fitler to AlwaysOffExemplarFilter + def exemplar_filter_off + @exemplar_filter = Exemplar::AlwaysOffExemplarFilter + end + # A View provides SDK users with the flexibility to customize the metrics that are output by the SDK. # # Example: @@ -153,6 +187,7 @@ def add_view(name, **options) nil end end + # rubocop:enable Metrics/ClassLength end end end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb index 88cd4bdaa0..dfd33b4edc 100644 --- a/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb @@ -37,7 +37,7 @@ describe 'metric readers' do it 'defaults to a periodic reader with an otlp exporter' do skip 'OTLP exporter not compatible with JRuby' if RUBY_ENGINE == 'jruby' - + ENV['OTEL_METRICS_EXPORTER'] = nil configurator.configure assert_equal 1, OpenTelemetry.meter_provider.metric_readers.size diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_filter_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_filter_test.rb new file mode 100644 index 0000000000..9aa8a35adc --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_filter_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::Exemplar::ExemplarFilter do + let(:context) do + ::OpenTelemetry::Trace.context_with_span( + ::OpenTelemetry::Trace.non_recording_span( + ::OpenTelemetry::Trace::SpanContext.new( + trace_id: Array("w\xCBl\xCCR-1\x06\x11M\xD6\xEC\xBBp\x03j").pack('H*'), + span_id: Array("1\xE1u\x12\x8E\xFC@\x18").pack('H*'), + trace_flags: ::OpenTelemetry::Trace::TraceFlags::DEFAULT + ) + ) + ) + end + let(:timestamp) { 123_456_789 } + let(:attributes) { { 'test': 'test' } } + + it 'always true for always on exemplar filter' do + result = OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOnExemplarFilter.should_sample?(1, timestamp, attributes, context) + _(result).must_equal true + end + + it 'always false for always off exemplar filter' do + result = OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOffExemplarFilter.should_sample?(1, timestamp, attributes, context) + _(result).must_equal false + end + + it 'filter off when trace context flag is 0' do + result = OpenTelemetry::SDK::Metrics::Exemplar::TraceBasedExemplarFilter.should_sample?(1, timestamp, attributes, context) + _(result).must_equal false + end + + it 'filter on when trace context flag is 1' do + context.instance_variable_get(:@entries).values[0].instance_variable_get(:@context).instance_variable_set(:@trace_flags, ::OpenTelemetry::Trace::TraceFlags::SAMPLED) + result = OpenTelemetry::SDK::Metrics::Exemplar::TraceBasedExemplarFilter.should_sample?(1, timestamp, attributes, context) + _(result).must_equal true + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_integration_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_integration_test.rb new file mode 100644 index 0000000000..c3f909a5c0 --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_integration_test.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK do + describe '#exempler_integration_test' do + before { reset_metrics_sdk } + + it 'emits metrics with list of exempler' do + ENV['OTEL_METRICS_EXPORTER'] = 'none' + + OpenTelemetry::SDK.configure + + metric_exporter = OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new + OpenTelemetry.meter_provider.add_metric_reader(metric_exporter) + + OpenTelemetry.meter_provider.exemplar_filter_on(exemplar_filter: OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOnExemplarFilter) + + exemplar_reservoir = OpenTelemetry::SDK::Metrics::Exemplar::ExemplarReservoir.new + + meter = OpenTelemetry.meter_provider.meter('test') + counter = meter.create_counter('counter', unit: 'smidgen', description: 'a small amount of something', exemplar_reservoir: exemplar_reservoir) + + counter.add(1) + counter.add(2, attributes: { 'a' => 'b' }) + counter.add(2, attributes: { 'a' => 'b' }) + counter.add(3, attributes: { 'b' => 'c' }) + counter.add(4, attributes: { 'd' => 'e' }) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(last_snapshot).wont_be_empty + _(last_snapshot[0].name).must_equal('counter') + _(last_snapshot[0].unit).must_equal('smidgen') + _(last_snapshot[0].description).must_equal('a small amount of something') + + _(last_snapshot[0].instrumentation_scope.name).must_equal('test') + + _(last_snapshot[0].data_points[0].value).must_equal(1) + _(last_snapshot[0].data_points[0].attributes).must_equal({}) + + _(last_snapshot[0].data_points[1].value).must_equal(4) + _(last_snapshot[0].data_points[1].attributes).must_equal('a' => 'b') + + _(last_snapshot[0].data_points[0].exemplars.size).must_equal 1 + _(last_snapshot[0].data_points[0].exemplars[0].class).must_equal OpenTelemetry::SDK::Metrics::Exemplar::Exemplar + _(last_snapshot[0].data_points[0].exemplars[0].value).must_equal 1 + _(last_snapshot[0].data_points[0].exemplars[0].span_id).must_equal '0000000000000000' + _(last_snapshot[0].data_points[0].exemplars[0].trace_id).must_equal '00000000000000000000000000000000' + end + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_reservoir_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_reservoir_test.rb new file mode 100644 index 0000000000..345b44aa96 --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/exemplar/exemplar_reservoir_test.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::Exemplar::ExemplarReservoir do + describe 'basic exemplar reservoir operation test' do + let(:context) do + ::OpenTelemetry::Trace.context_with_span( + ::OpenTelemetry::Trace.non_recording_span( + ::OpenTelemetry::Trace::SpanContext.new( + trace_id: Array("w\xCBl\xCCR-1\x06\x11M\xD6\xEC\xBBp\x03j").pack('H*'), + span_id: Array("1\xE1u\x12\x8E\xFC@\x18").pack('H*'), + trace_flags: ::OpenTelemetry::Trace::TraceFlags::DEFAULT + ) + ) + ) + end + let(:timestamp) { 123_456_789 } + let(:attributes) { { 'test': 'test' } } + + it 'basic test for exemplar reservoir' do + exemplar = OpenTelemetry::SDK::Metrics::Exemplar::ExemplarReservoir.new + exemplar.offer(value: 1, timestamp: timestamp, attributes: attributes, context: context) + exemplars = exemplar.collect + + _(exemplars.class).must_equal Array + _(exemplars[0].class).must_equal OpenTelemetry::SDK::Metrics::Exemplar::Exemplar + _(exemplars[0].value).must_equal 1 + _(exemplars[0].time_unix_nano).must_equal 123_456_789 + _(exemplars[0].attributes[:test]).must_equal 'test' + _(exemplars[0].span_id).must_equal '11e2ec08' + _(exemplars[0].trace_id).must_equal '0b5cbd16166cb933' + end + + it 'basic test for fixed size exemplar reservoir' do + exemplar = OpenTelemetry::SDK::Metrics::Exemplar::FixedSizeExemplarReservoir.new(max_size: 2) + exemplar.offer(value: 1, timestamp: timestamp, attributes: attributes, context: context) + exemplars = exemplar.collect + + _(exemplars.class).must_equal Array + _(exemplars[0].class).must_equal OpenTelemetry::SDK::Metrics::Exemplar::Exemplar + _(exemplars[0].value).must_equal 1 + _(exemplars[0].time_unix_nano).must_equal 123_456_789 + _(exemplars[0].attributes[:test]).must_equal 'test' + _(exemplars[0].span_id).must_equal '11e2ec08' + _(exemplars[0].trace_id).must_equal '0b5cbd16166cb933' + end + + it 'basic test for fixed size exemplar reservoir when more offers' do + exemplar = OpenTelemetry::SDK::Metrics::Exemplar::FixedSizeExemplarReservoir.new(max_size: 2) + exemplar.offer(value: 1, timestamp: timestamp, attributes: attributes, context: context) + exemplar.offer(value: 2, timestamp: timestamp, attributes: attributes, context: context) + exemplar.offer(value: 3, timestamp: timestamp, attributes: attributes, context: context) + + exemplars = exemplar.collect + _(exemplars.class).must_equal Array + _(exemplars[0].class).must_equal OpenTelemetry::SDK::Metrics::Exemplar::Exemplar + _(exemplars.size).must_equal 2 + end + + it 'basic test for histogram exemplar reservoir' do + exemplar = OpenTelemetry::SDK::Metrics::Exemplar::HistogramExemplarReservoir.new + exemplar.offer(value: 20, timestamp: timestamp, attributes: attributes, context: context) + exemplars = exemplar.collect + + _(exemplars.class).must_equal Array + _(exemplars[0].class).must_equal OpenTelemetry::SDK::Metrics::Exemplar::Exemplar + _(exemplars[0].value).must_equal 20 + _(exemplars[0].time_unix_nano).must_equal 123_456_789 + _(exemplars[0].attributes[:test]).must_equal 'test' + _(exemplars[0].span_id).must_equal '11e2ec08' + _(exemplars[0].trace_id).must_equal '0b5cbd16166cb933' + end + end + + describe 'complex exemplar reservoir integration test always on filter' do + let(:metric_exporter) { OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new } + let(:exemplar_filter) { OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOnExemplarFilter } + let(:exemplar_reservoir) { OpenTelemetry::SDK::Metrics::Exemplar::FixedSizeExemplarReservoir.new(max_size: 2) } + + it 'integrate fixed size exemplar reservior with simple counter' do + reset_metrics_sdk + meter = create_meter + histogram = meter.create_histogram('histogram_always_on_exemplar', unit: 'smidgen', description: 'description', + exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir) + histogram.record(1, attributes: { 'foo' => 'bar' }) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + _(last_snapshot[0].description).must_equal 'description' + _(last_snapshot[0].data_points[0].exemplars[0].value).must_equal 1 + _(last_snapshot[0].data_points[0].exemplars[0].attributes['foo']).must_equal 'bar' + _(last_snapshot[0].data_points[0].exemplars[0].span_id).must_equal '0000000000000000' + _(last_snapshot[0].data_points[0].exemplars[0].trace_id).must_equal '00000000000000000000000000000000' + end + end + + describe 'complex exemplar reservoir integration test always off filter' do + let(:metric_exporter) { OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new } + let(:exemplar_filter) { OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOffExemplarFilter } + let(:exemplar_reservoir) { OpenTelemetry::SDK::Metrics::Exemplar::FixedSizeExemplarReservoir.new(max_size: 2) } + + it 'integrate fixed size exemplar reservior with simple counter' do + reset_metrics_sdk + meter = create_meter + histogram = meter.create_histogram('histogram_always_off_exemplar', unit: 'smidgen', description: 'description', + exemplar_filter: exemplar_filter, exemplar_reservoir: exemplar_reservoir) + histogram.record(1, attributes: { 'foo' => 'bar' }) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + _(last_snapshot[0].description).must_equal 'description' + _(last_snapshot[0].data_points[0].exemplars.size).must_equal 0 + end + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb index 72f04e08c9..f158bf4831 100644 --- a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb @@ -138,6 +138,62 @@ end end + describe 'exempler' do + describe '#exemplar_filter_setup' do + after do + ENV.delete('OTEL_METRICS_EXEMPLAR_FILTER') + end + + it 'without OTEL_METRICS_EXEMPLAR_FILTER' do + OpenTelemetry.meter_provider.exemplar_filter_setup + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOffExemplarFilter + end + + it 'with OTEL_METRICS_EXEMPLAR_FILTER as always_on' do + ENV['OTEL_METRICS_EXEMPLAR_FILTER'] = 'always_on' + OpenTelemetry.meter_provider.exemplar_filter_setup + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOnExemplarFilter + end + + it 'with OTEL_METRICS_EXEMPLAR_FILTER as invalid option' do + ENV['OTEL_METRICS_EXEMPLAR_FILTER'] = 'better_on' + OpenTelemetry.meter_provider.exemplar_filter_setup + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOffExemplarFilter + end + end + + describe '#exemplar_filter_off' do + it 'will turn it off' do + ENV['OTEL_METRICS_EXEMPLAR_FILTER'] = 'always_on' + OpenTelemetry.meter_provider.exemplar_filter_setup + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOnExemplarFilter + + OpenTelemetry.meter_provider.exemplar_filter_off + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOffExemplarFilter + ENV.delete('OTEL_METRICS_EXEMPLAR_FILTER') + end + end + + describe '#exemplar_filter_on' do + it 'will turn it on with default exempler filter' do + OpenTelemetry.meter_provider.exemplar_filter_setup + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOffExemplarFilter + + OpenTelemetry.meter_provider.exemplar_filter_on + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::TraceBasedExemplarFilter + end + + it 'will turn it on with customized always on' do + OpenTelemetry.meter_provider.exemplar_filter_setup + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOffExemplarFilter + + OpenTelemetry.meter_provider.exemplar_filter_on(exemplar_filter: OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOnExemplarFilter) + _(OpenTelemetry.meter_provider.exemplar_filter).must_equal OpenTelemetry::SDK::Metrics::Exemplar::AlwaysOnExemplarFilter + end + end + end + + # TODO: OpenTelemetry.meter_provider.add_view describe '#add_view' do it 'adds a view with aggregation' do OpenTelemetry.meter_provider.add_view('test', aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::Drop.new) diff --git a/metrics_sdk/test/test_helper.rb b/metrics_sdk/test/test_helper.rb index 11076bde18..4d4073c7b2 100644 --- a/metrics_sdk/test/test_helper.rb +++ b/metrics_sdk/test/test_helper.rb @@ -34,5 +34,14 @@ def with_test_logger OpenTelemetry.logger = original_logger end +def create_meter + ENV['OTEL_TRACES_EXPORTER'] = 'console' + ENV['OTEL_METRICS_EXPORTER'] = 'none' + OpenTelemetry::SDK.configure + OpenTelemetry.meter_provider.add_metric_reader(metric_exporter) + OpenTelemetry.meter_provider.exemplar_filter_on(exemplar_filter: exemplar_filter) + OpenTelemetry.meter_provider.meter('SAMPLE_METER_NAME') +end + # Suppress warn-level logs about a missing OTLP exporter for traces ENV['OTEL_TRACES_EXPORTER'] = 'none'