diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument.rb index c440634c8..e7306d3d4 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument.rb @@ -14,6 +14,7 @@ module Instrument end require 'opentelemetry/sdk/metrics/instrument/synchronous_instrument' +require 'opentelemetry/sdk/metrics/instrument/asynchronous_instrument' require 'opentelemetry/sdk/metrics/instrument/counter' require 'opentelemetry/sdk/metrics/instrument/histogram' require 'opentelemetry/sdk/metrics/instrument/observable_counter' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/asynchronous_instrument.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/asynchronous_instrument.rb new file mode 100644 index 000000000..4a522ba2a --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/asynchronous_instrument.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Instrument + # {AsynchronousInstrument} contains the common functionality shared across + # the asynchronous instruments SDK instruments. + class AsynchronousInstrument + def initialize(name, unit, description, callback, instrumentation_scope, meter_provider) + @name = name + @unit = unit + @description = description + @instrumentation_scope = instrumentation_scope + @meter_provider = meter_provider + @metric_streams = [] + @callbacks = [] + @timeout = nil + @attributes = {} + + init_callback(callback) + meter_provider.register_asynchronous_instrument(self) + end + + # @api private + def register_with_new_metric_store(metric_store, aggregation: default_aggregation) + ms = OpenTelemetry::SDK::Metrics::State::AsynchronousMetricStream.new( + @name, + @description, + @unit, + instrument_kind, + @meter_provider, + @instrumentation_scope, + aggregation, + @callbacks, + @timeout, + @attributes + ) + @metric_streams << ms + metric_store.add_metric_stream(ms) + end + + # The API MUST support creation of asynchronous instruments by passing zero or more callback functions + # to be permanently registered to the newly created instrument. + def init_callback(callback) + if callback.instance_of?(Proc) + @callbacks << callback + elsif callback.instance_of?(Array) + callback.each { |cb| @callbacks << cb if cb.instance_of?(Proc) } + else + OpenTelemetry.logger.warn "Only accept single Proc or Array of Proc for initialization with callback (given callback #{callback.class}" + end + end + + # Where the API supports registration of callback functions after asynchronous instrumentation creation, + # the user MUST be able to undo registration of the specific callback after its registration by some means. + def register_callback(callback) + if callback.instance_of?(Proc) + @callbacks << callback + callback + else + OpenTelemetry.logger.warn "Only accept single Proc for registering callback (given callback #{callback.class}" + end + end + + def unregister(callback) + @callbacks.delete(callback) + end + + def timeout(timeout) + @timeout = timeout + end + + def add_attributes(attributes) + @attributes.merge!(attributes) if attributes.instance_of?(Hash) + end + + private + + # update the observed value (after calling observe) + # invoke callback will execute callback and export metric_data that is observed + def update(timeout, attributes) + @metric_streams.each { |ms| ms.invoke_callback(timeout, attributes) } + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_counter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_counter.rb index 4e5d49102..3ca7382b8 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_counter.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_counter.rb @@ -8,16 +8,31 @@ module OpenTelemetry module SDK module Metrics module Instrument - # {ObservableCounter} is the SDK implementation of {OpenTelemetry::Metrics::ObservableCounter}. - class ObservableCounter < OpenTelemetry::Metrics::Instrument::ObservableCounter - attr_reader :name, :unit, :description + # {ObservableCounter} is the SDK implementation of {OpenTelemetry::SDK::Metrics::Instrument::AsynchronousInstrument}. + # Asynchronous Counter is an asynchronous Instrument which reports monotonically increasing value(s) when the instrument is being observed. + class ObservableCounter < OpenTelemetry::SDK::Metrics::Instrument::AsynchronousInstrument + # Returns the instrument kind as a Symbol + # + # @return [Symbol] + def instrument_kind + :observable_counter + end + + # Observe the ObservableCounter with fixed timeout duartion. + # + # @param [int] timeout The timeout duration for callback to run, which MUST be a non-negative numeric value. + # @param [Hash{String => String, Numeric, Boolean, Array}] attributes + # Values must be non-nil and (array of) string, boolean or numeric type. + # Array values must not contain nil elements and all elements must be of + # the same basic type (string, numeric, boolean). + def observe(timeout: nil, attributes: {}) + update(timeout, attributes) + end + + private - def initialize(name, unit, description, callback, meter) - @name = name - @unit = unit - @description = description - @callback = callback - @meter = meter + def default_aggregation + OpenTelemetry::SDK::Metrics::Aggregation::Sum.new end end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_gauge.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_gauge.rb index 7a122184f..7049255dc 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_gauge.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_gauge.rb @@ -8,16 +8,31 @@ module OpenTelemetry module SDK module Metrics module Instrument - # {ObservableGauge} is the SDK implementation of {OpenTelemetry::Metrics::ObservableGauge}. - class ObservableGauge < OpenTelemetry::Metrics::Instrument::ObservableGauge - attr_reader :name, :unit, :description + # {ObservableGauge} is the SDK implementation of {OpenTelemetry::SDK::Metrics::Instrument::AsynchronousInstrument}. + # Asynchronous Gauge is an asynchronous Instrument which reports non-additive value(s) (e.g. the room temperature) + class ObservableGauge < OpenTelemetry::SDK::Metrics::Instrument::AsynchronousInstrument + # Returns the instrument kind as a Symbol + # + # @return [Symbol] + def instrument_kind + :observable_gauge + end + + # Observe the ObservableCounter with fixed timeout duartion. + # + # @param [int] timeout The timeout duration for callback to run, which MUST be a non-negative numeric value. + # @param [Hash{String => String, Numeric, Boolean, Array}] attributes + # Values must be non-nil and (array of) string, boolean or numeric type. + # Array values must not contain nil elements and all elements must be of + # the same basic type (string, numeric, boolean). + def observe(timeout: nil, attributes: {}) + update(timeout, attributes) + end + + private - def initialize(name, unit, description, callback, meter) - @name = name - @unit = unit - @description = description - @callback = callback - @meter = meter + def default_aggregation + OpenTelemetry::SDK::Metrics::Aggregation::Sum.new end end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_up_down_counter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_up_down_counter.rb index 8eb812c5f..57e3dfdbc 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_up_down_counter.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_up_down_counter.rb @@ -8,16 +8,32 @@ module OpenTelemetry module SDK module Metrics module Instrument - # {ObservableUpDownCounter} is the SDK implementation of {OpenTelemetry::Metrics::ObservableUpDownCounter}. - class ObservableUpDownCounter < OpenTelemetry::Metrics::Instrument::ObservableUpDownCounter - attr_reader :name, :unit, :description + # {ObservableUpDownCounter} is the SDK implementation of {OpenTelemetry::SDK::Metrics::Instrument::AsynchronousInstrument}. + # Asynchronous UpDownCounter is an asynchronous Instrument which reports additive value(s) (e.g. the process heap size) + class ObservableUpDownCounter < OpenTelemetry::SDK::Metrics::Instrument::AsynchronousInstrument + # Returns the instrument kind as a Symbol + # + # @return [Symbol] + def instrument_kind + :observable_up_down_counter + end + + # Observe the ObservableCounter with fixed timeout duartion. + # Everytime observe, the value should be sent to backend through exporter + # + # @param [int] timeout The timeout duration for callback to run, which MUST be a non-negative numeric value. + # @param [Hash{String => String, Numeric, Boolean, Array}] attributes + # Values must be non-nil and (array of) string, boolean or numeric type. + # Array values must not contain nil elements and all elements must be of + # the same basic type (string, numeric, boolean). + def observe(timeout: nil, attributes: {}) + update(timeout, attributes) + end + + private - def initialize(name, unit, description, callback, meter) - @name = name - @unit = unit - @description = description - @callback = callback - @meter = meter + def default_aggregation + OpenTelemetry::SDK::Metrics::Aggregation::Sum.new(aggregation_temporality: :delta) end end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb index 1307817ec..1b2d34a59 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb @@ -28,6 +28,30 @@ def initialize(name, version, meter_provider) @meter_provider = meter_provider end + # Multiple-instrument callbacks + # Callbacks registered after the time of instrument creation MAY be associated with multiple instruments. + # Related spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#multiple-instrument-callbacks + # Related spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#synchronous-instrument-api + # + # @param [Array] instruments A list (or tuple, etc.) of Instruments used in the callback function. + # @param [Proc] callback A callback function + # + # It is RECOMMENDED that the API authors use one of the following forms for the callback function: + # The list (or tuple, etc.) returned by the callback function contains (Instrument, Measurement) pairs. + # the Observable Result parameter receives an additional (Instrument, Measurement) pairs + # Here it chose the second form + def register_callback(instruments, callback) + instruments.each do |instrument| + instrument.register_callback(callback) + end + end + + def unregister(instruments, callback) + instruments.each do |instrument| + instrument.unregister(callback) + end + end + # @api private def add_metric_reader(metric_reader) @instrument_registry.each_value do |instrument| diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb index 4538a88db..243fb64e5 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb @@ -126,6 +126,7 @@ def register_synchronous_instrument(instrument) end end end + alias register_asynchronous_instrument register_synchronous_instrument # A View provides SDK users with the flexibility to customize the metrics that are output by the SDK. # diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/state.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/state.rb index 0512f5136..17846c067 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/state.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/state.rb @@ -20,3 +20,4 @@ module State require 'opentelemetry/sdk/metrics/state/metric_data' require 'opentelemetry/sdk/metrics/state/metric_store' require 'opentelemetry/sdk/metrics/state/metric_stream' +require 'opentelemetry/sdk/metrics/state/asynchronous_metric_stream' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/state/asynchronous_metric_stream.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/asynchronous_metric_stream.rb new file mode 100644 index 000000000..22641cd54 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/asynchronous_metric_stream.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module State + # @api private + # + # The MetricStream class provides SDK internal functionality that is not a part of the + # public API. + class AsynchronousMetricStream + attr_reader :name, :description, :unit, :instrument_kind, :instrumentation_scope, :data_points + + def initialize( + name, + description, + unit, + instrument_kind, + meter_provider, + instrumentation_scope, + aggregation, + callback, + timeout, + attributes + ) + @name = name + @description = description + @unit = unit + @instrument_kind = instrument_kind + @meter_provider = meter_provider + @instrumentation_scope = instrumentation_scope + @aggregation = aggregation + @callback = callback + @start_time = now_in_nano + @timeout = timeout + @attributes = attributes + @data_points = {} + + @mutex = Mutex.new + end + + # When collect, if there are asynchronous SDK Instruments involved, their callback functions will be triggered. + # Related spec: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#collect + # invoke_callback will update the data_points in aggregation + def collect(start_time, end_time) + invoke_callback(@timeout, @attributes) + + @mutex.synchronize do + MetricData.new( + @name, + @description, + @unit, + @instrument_kind, + @meter_provider.resource, + @instrumentation_scope, + @aggregation.collect(start_time, end_time, @data_points), + @aggregation.aggregation_temporality, + start_time, + end_time + ) + end + end + + def invoke_callback(timeout, attributes) + @mutex.synchronize do + Timeout.timeout(timeout || 30) do + @callback.each do |cb| + value = cb.call + @aggregation.update(value, attributes, @data_points) + end + end + end + end + + def to_s + instrument_info = String.new + instrument_info << "name=#{@name}" + instrument_info << " description=#{@description}" if @description + instrument_info << " unit=#{@unit}" if @unit + @data_points.map do |attributes, value| + metric_stream_string = String.new + metric_stream_string << instrument_info + metric_stream_string << " attributes=#{attributes}" if attributes + metric_stream_string << " #{value}" + metric_stream_string + end.join("\n") + end + + def now_in_nano + (Time.now.to_r * 1_000_000_000).to_i + end + end + end + end + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_counter_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_counter_test.rb new file mode 100644 index 000000000..ba1972084 --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_counter_test.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::Instrument::ObservableCounter do + let(:metric_exporter) { OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new } + let(:meter) { OpenTelemetry.meter_provider.meter('test') } + + before do + reset_metrics_sdk + OpenTelemetry::SDK.configure + OpenTelemetry.meter_provider.add_metric_reader(metric_exporter) + end + + it 'counts without observe' do + callback = proc { 10 } + meter.create_observable_counter('counter', unit: 'smidgen', description: 'a small amount of something', callback: callback) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + # puts "last_snapshot.inspect: #{last_snapshot.inspect}" + _(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(10) + _(last_snapshot[0].data_points[0].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + end + + it 'counts with set timeout and attributes' do + callback = proc { 10 } + observable_counter = meter.create_observable_counter('counter', unit: 'smidgen', description: 'a small amount of something', callback: callback) + observable_counter.add_attributes({ 'foo' => 'bar' }) + observable_counter.timeout(10) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + # puts "last_snapshot.inspect: #{last_snapshot.inspect}" + _(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(10) + _(last_snapshot[0].data_points[0].attributes).must_equal({ 'foo' => 'bar' }) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + end + + it 'counts with observe' do + callback = proc { 10 } + observable_counter = meter.create_observable_counter('counter', unit: 'smidgen', description: 'a small amount of something', callback: callback) + observable_counter.observe(timeout: 10, attributes: { 'foo' => 'bar' }) # observe will make another data points modification + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(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(10) + _(last_snapshot[0].data_points[0].attributes).must_equal('foo' => 'bar') + + _(last_snapshot[0].data_points[1].value).must_equal(10) + _(last_snapshot[0].data_points[1].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + end + + it 'counts with observe after initialization' do + callback_first = proc { 10 } + observable_counter = meter.create_observable_counter('counter', unit: 'smidgen', description: 'a small amount of something', callback: callback_first) + _(observable_counter.instance_variable_get(:@callbacks).size).must_equal 1 + + callback_second = proc { 20 } + observable_counter.register_callback(callback_second) + _(observable_counter.instance_variable_get(:@callbacks).size).must_equal 2 + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(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(30) # two callback aggregate value to 30 + _(last_snapshot[0].data_points[0].attributes).must_equal({}) + end + + it 'remove the callback after initialization result no metrics data' do + callback_first = proc { 10 } + observable_counter = meter.create_observable_counter('counter', unit: 'smidgen', description: 'a small amount of something', callback: callback_first) + _(observable_counter.instance_variable_get(:@callbacks).size).must_equal 1 + + observable_counter.unregister(callback_first) + _(observable_counter.instance_variable_get(:@callbacks).size).must_equal 0 + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(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.size).must_equal 0 + end + + it 'creation of instruments with more than one callabck' do + callback_first = proc { 10 } + callback_second = proc { 20 } + observable_counter = meter.create_observable_counter('counter', unit: 'smidgen', description: 'a small amount of something', callback: [callback_first, callback_second]) + _(observable_counter.instance_variable_get(:@callbacks).size).must_equal 2 + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(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(30) + end + + it 'creation of instruments with more than one invalid callabck should result no callback' do + callback_first = 'callback_first' + callback_second = 'callback_second' + observable_counter = meter.create_observable_counter('counter', unit: 'smidgen', description: 'a small amount of something', callback: [callback_first, callback_second]) + _(observable_counter.instance_variable_get(:@callbacks).size).must_equal 0 + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + _(last_snapshot[0].data_points.size).must_equal 0 + end + + it 'creation of instruments with invalid argument result no callback' do + callback_first = 'callback_first' + observable_counter = meter.create_observable_counter('counter', unit: 'smidgen', description: 'a small amount of something', callback: callback_first) + _(observable_counter.instance_variable_get(:@callbacks).size).must_equal 0 + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + _(last_snapshot[0].data_points.size).must_equal 0 + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_gauge_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_gauge_test.rb new file mode 100644 index 000000000..91fca2cac --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_gauge_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::Instrument::ObservableGauge do + let(:metric_exporter) { OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new } + let(:meter) { OpenTelemetry.meter_provider.meter('test') } + + before do + reset_metrics_sdk + OpenTelemetry::SDK.configure + OpenTelemetry.meter_provider.add_metric_reader(metric_exporter) + end + + it 'counts without observe' do + callback = proc { 10 } + meter.create_observable_gauge('gauge', unit: 'smidgen', description: 'a small amount of something', callback: callback) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(last_snapshot[0].name).must_equal('gauge') + _(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(10) + _(last_snapshot[0].data_points[0].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + end + + it 'counts with observe' do + callback = proc { 10 } + observable_gauge = meter.create_observable_gauge('gauge', unit: 'smidgen', description: 'a small amount of something', callback: callback) + observable_gauge.observe(timeout: 10, attributes: { 'foo' => 'bar' }) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(last_snapshot[0].name).must_equal('gauge') + _(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(10) + _(last_snapshot[0].data_points[0].attributes).must_equal('foo' => 'bar') + + _(last_snapshot[0].data_points[1].value).must_equal(10) + _(last_snapshot[0].data_points[1].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_up_down_counter_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_up_down_counter_test.rb new file mode 100644 index 000000000..7393dac62 --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/observable_up_down_counter_test.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::Instrument::ObservableUpDownCounter do + let(:metric_exporter) { OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new } + let(:meter) { OpenTelemetry.meter_provider.meter('test') } + + before do + reset_metrics_sdk + OpenTelemetry::SDK.configure + OpenTelemetry.meter_provider.add_metric_reader(metric_exporter) + end + + it 'counts without observe' do + callback = proc { 10 } + meter.create_observable_up_down_counter('updown_counter', unit: 'smidgen', description: 'a small amount of something', callback: callback) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(last_snapshot[0].name).must_equal('updown_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(10) + _(last_snapshot[0].data_points[0].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + end + + it 'counts with observe' do + callback = proc { 10 } + up_down_counter = meter.create_observable_up_down_counter('updown_counter', unit: 'smidgen', description: 'a small amount of something', callback: callback) + up_down_counter.observe(timeout: 10, attributes: { 'foo' => 'bar' }) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(last_snapshot[0].name).must_equal('updown_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(10) + _(last_snapshot[0].data_points[0].attributes).must_equal('foo' => 'bar') + + _(last_snapshot[0].data_points[1].value).must_equal(10) + _(last_snapshot[0].data_points[1].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb index e8415d8d3..b9a4025ad 100644 --- a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb @@ -34,31 +34,123 @@ describe '#create_observable_counter' do it 'creates a observable_counter instrument' do - # TODO: Implement observable instruments - skip - instrument = meter.create_observable_counter('a_observable_counter', unit: 'minutes', description: 'useful description', callback: nil) + instrument = meter.create_observable_counter('a_observable_counter', unit: 'minutes', description: 'useful description', callback: proc { 10 }) _(instrument).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::ObservableCounter end end describe '#create_observable_gauge' do it 'creates a observable_gauge instrument' do - # TODO: Implement observable instruments - skip - instrument = meter.create_observable_gauge('a_observable_gauge', unit: 'minutes', description: 'useful description', callback: nil) + instrument = meter.create_observable_gauge('a_observable_gauge', unit: 'minutes', description: 'useful description', callback: proc { 10 }) _(instrument).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::ObservableGauge end end describe '#create_observable_up_down_counter' do it 'creates a observable_up_down_counter instrument' do - # TODO: Implement observable instruments - skip - instrument = meter.create_observable_up_down_counter('a_observable_up_down_counter', unit: 'minutes', description: 'useful description', callback: nil) + instrument = meter.create_observable_up_down_counter('a_observable_up_down_counter', unit: 'minutes', description: 'useful description', callback: proc { 10 }) _(instrument).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::ObservableUpDownCounter end end + describe 'callback' do + describe '#register_callback' do + let(:metric_exporter) { OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new } + let(:meter) { OpenTelemetry.meter_provider.meter('test') } + + before do + reset_metrics_sdk + OpenTelemetry::SDK.configure + OpenTelemetry.meter_provider.add_metric_reader(metric_exporter) + end + + it 'create callback with multi asychronous instrument' do + callback_first = proc { 10 } + counter_first = meter.create_observable_counter('counter_first', unit: 'smidgen', description: '', callback: callback_first) + counter_second = meter.create_observable_counter('counter_second', unit: 'smidgen', description: '', callback: callback_first) + + callback_second = proc { 20 } + meter.register_callback([counter_first, counter_second], callback_second) + + _(counter_first.instance_variable_get(:@callbacks).size).must_equal 2 + _(counter_second.instance_variable_get(:@callbacks).size).must_equal 2 + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(last_snapshot[0].name).must_equal('counter_first') + _(last_snapshot[0].unit).must_equal('smidgen') + _(last_snapshot[0].description).must_equal('') + _(last_snapshot[0].instrumentation_scope.name).must_equal('test') + _(last_snapshot[0].data_points[0].value).must_equal(30) + _(last_snapshot[0].data_points[0].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + + _(last_snapshot[1].name).must_equal('counter_second') + _(last_snapshot[1].unit).must_equal('smidgen') + _(last_snapshot[1].description).must_equal('') + _(last_snapshot[1].instrumentation_scope.name).must_equal('test') + _(last_snapshot[1].data_points[0].value).must_equal(30) + _(last_snapshot[1].data_points[0].attributes).must_equal({}) + _(last_snapshot[1].aggregation_temporality).must_equal(:delta) + end + + it 'remove callback with multi asychronous instrument' do + callback_first = proc { 10 } + counter_first = meter.create_observable_counter('counter_first', unit: 'smidgen', description: '', callback: callback_first) + counter_second = meter.create_observable_counter('counter_second', unit: 'smidgen', description: '', callback: callback_first) + + callback_second = proc { 20 } + meter.register_callback([counter_first, counter_second], callback_second) + + _(counter_first.instance_variable_get(:@callbacks).size).must_equal 2 + _(counter_second.instance_variable_get(:@callbacks).size).must_equal 2 + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(last_snapshot[0].name).must_equal('counter_first') + _(last_snapshot[0].unit).must_equal('smidgen') + _(last_snapshot[0].description).must_equal('') + _(last_snapshot[0].instrumentation_scope.name).must_equal('test') + _(last_snapshot[0].data_points[0].value).must_equal(30) + _(last_snapshot[0].data_points[0].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + + _(last_snapshot[1].name).must_equal('counter_second') + _(last_snapshot[1].unit).must_equal('smidgen') + _(last_snapshot[1].description).must_equal('') + _(last_snapshot[1].instrumentation_scope.name).must_equal('test') + _(last_snapshot[1].data_points[0].value).must_equal(30) + _(last_snapshot[1].data_points[0].attributes).must_equal({}) + _(last_snapshot[1].aggregation_temporality).must_equal(:delta) + + # unregister the callback_second from instruments counter_first and counter_second + meter.unregister([counter_first, counter_second], callback_second) + + metric_exporter.reset + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots + + _(last_snapshot[0].name).must_equal('counter_first') + _(last_snapshot[0].unit).must_equal('smidgen') + _(last_snapshot[0].description).must_equal('') + _(last_snapshot[0].instrumentation_scope.name).must_equal('test') + _(last_snapshot[0].data_points[0].value).must_equal(10) + _(last_snapshot[0].data_points[0].attributes).must_equal({}) + _(last_snapshot[0].aggregation_temporality).must_equal(:delta) + + _(last_snapshot[1].name).must_equal('counter_second') + _(last_snapshot[1].unit).must_equal('smidgen') + _(last_snapshot[1].description).must_equal('') + _(last_snapshot[1].instrumentation_scope.name).must_equal('test') + _(last_snapshot[1].data_points[0].value).must_equal(10) + _(last_snapshot[1].data_points[0].attributes).must_equal({}) + _(last_snapshot[1].aggregation_temporality).must_equal(:delta) + end + end + end + describe 'creating an instrument' do INSTRUMENT_NAME_ERROR = OpenTelemetry::Metrics::Meter::InstrumentNameError INSTRUMENT_UNIT_ERROR = OpenTelemetry::Metrics::Meter::InstrumentUnitError