diff --git a/metrics_api/lib/opentelemetry-metrics-api.rb b/metrics_api/lib/opentelemetry-metrics-api.rb index 4fc3d5b65..7363a7561 100644 --- a/metrics_api/lib/opentelemetry-metrics-api.rb +++ b/metrics_api/lib/opentelemetry-metrics-api.rb @@ -17,6 +17,8 @@ # # The OpenTelemetry module provides global accessors for telemetry objects. module OpenTelemetry + @meter_provider = Internal::ProxyMeterProvider.new + # Register the global meter provider. # # @param [MeterProvider] provider A meter provider to register as the diff --git a/metrics_api/lib/opentelemetry/internal/proxy_meter.rb b/metrics_api/lib/opentelemetry/internal/proxy_meter.rb index 8d1a94ced..74d497c68 100644 --- a/metrics_api/lib/opentelemetry/internal/proxy_meter.rb +++ b/metrics_api/lib/opentelemetry/internal/proxy_meter.rb @@ -29,7 +29,7 @@ def delegate=(meter) @mutex.synchronize do if @delegate.nil? @delegate = meter - @registry.each_value { |instrument| instrument.upgrade_with(meter) } + @instrument_registry.each_value { |instrument| instrument.upgrade_with(meter) } else OpenTelemetry.logger.warn 'Attempt to reset delegate in ProxyMeter ignored.' end diff --git a/metrics_api/lib/opentelemetry/metrics.rb b/metrics_api/lib/opentelemetry/metrics.rb index ec586d831..ac10d6062 100644 --- a/metrics_api/lib/opentelemetry/metrics.rb +++ b/metrics_api/lib/opentelemetry/metrics.rb @@ -11,5 +11,6 @@ module Metrics end require 'opentelemetry/metrics/instrument' +require 'opentelemetry/metrics/measurement' require 'opentelemetry/metrics/meter' require 'opentelemetry/metrics/meter_provider' diff --git a/metrics_api/lib/opentelemetry/metrics/measurement.rb b/metrics_api/lib/opentelemetry/metrics/measurement.rb new file mode 100644 index 000000000..e16e35c3f --- /dev/null +++ b/metrics_api/lib/opentelemetry/metrics/measurement.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Metrics + Measurement = Struct.new(:value, :attributes) + end +end diff --git a/metrics_api/lib/opentelemetry/metrics/meter.rb b/metrics_api/lib/opentelemetry/metrics/meter.rb index bce93fec6..78b4c2416 100644 --- a/metrics_api/lib/opentelemetry/metrics/meter.rb +++ b/metrics_api/lib/opentelemetry/metrics/meter.rb @@ -26,29 +26,29 @@ class Meter def initialize @mutex = Mutex.new - @registry = {} + @instrument_registry = {} end def create_counter(name, unit: nil, description: nil) create_instrument(:counter, name, unit, description, nil) { COUNTER } end - def create_observable_counter(name, unit: nil, description: nil, callback:) - create_instrument(:observable_counter, name, unit, description, callback) { OBSERVABLE_COUNTER } - end - def create_histogram(name, unit: nil, description: nil) create_instrument(:histogram, name, unit, description, nil) { HISTOGRAM } end - def create_observable_gauge(name, unit: nil, description: nil, callback:) - create_instrument(:observable_gauge, name, unit, description, callback) { OBSERVABLE_GAUGE } - end - def create_up_down_counter(name, unit: nil, description: nil) create_instrument(:up_down_counter, name, unit, description, nil) { UP_DOWN_COUNTER } end + def create_observable_counter(name, unit: nil, description: nil, callback:) + create_instrument(:observable_counter, name, unit, description, callback) { OBSERVABLE_COUNTER } + end + + def create_observable_gauge(name, unit: nil, description: nil, callback:) + create_instrument(:observable_gauge, name, unit, description, callback) { OBSERVABLE_GAUGE } + end + def create_observable_up_down_counter(name, unit: nil, description: nil, callback:) create_instrument(:observable_up_down_counter, name, unit, description, callback) { OBSERVABLE_UP_DOWN_COUNTER } end @@ -60,12 +60,12 @@ def create_instrument(kind, name, unit, description, callback) raise InstrumentNameError if name.empty? raise InstrumentNameError unless NAME_REGEX.match?(name) raise InstrumentUnitError if unit && (!unit.ascii_only? || unit.size > 63) - raise InstrumentDescriptionError if description && (description.size > 1023 || !utf8mb3_encoding?(description)) + raise InstrumentDescriptionError if description && (description.size > 1023 || !utf8mb3_encoding?(description.dup)) @mutex.synchronize do - raise DuplicateInstrumentError if @registry.include? name + OpenTelemetry.logger.warn("duplicate instrument registration occurred for instrument #{name}") if @instrument_registry.include? name - @registry[name] = yield + @instrument_registry[name] = yield end end diff --git a/metrics_api/opentelemetry-metrics-api.gemspec b/metrics_api/opentelemetry-metrics-api.gemspec index de8064485..708d665b1 100644 --- a/metrics_api/opentelemetry-metrics-api.gemspec +++ b/metrics_api/opentelemetry-metrics-api.gemspec @@ -31,6 +31,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'bundler', '>= 1.17' spec.add_development_dependency 'faraday', '~> 0.13' spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'opentelemetry-test-helpers' spec.add_development_dependency 'rake', '~> 12.0' spec.add_development_dependency 'rubocop', '~> 0.73.0' spec.add_development_dependency 'simplecov', '~> 0.17' diff --git a/metrics_api/test/opentelemetry/metrics/meter_test.rb b/metrics_api/test/opentelemetry/metrics/meter_test.rb index 1bfecea58..493c373b5 100644 --- a/metrics_api/test/opentelemetry/metrics/meter_test.rb +++ b/metrics_api/test/opentelemetry/metrics/meter_test.rb @@ -16,10 +16,12 @@ let(:meter) { meter_provider.meter('test-meter') } describe 'creating an instrument' do - it 'instrument name must be unique' do - meter.create_counter('a_counter') - _(-> { meter.create_counter('a_counter') }).must_raise(DUPLICATE_INSTRUMENT_ERROR) - _(-> { meter.create_histogram('a_counter') }).must_raise(DUPLICATE_INSTRUMENT_ERROR) + it 'duplicate instrument registration logs a warning' do + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + meter.create_counter('a_counter') + meter.create_counter('a_counter') + _(log_stream.string).must_match(/duplicate instrument registration occurred for instrument a_counter/) + end end it 'instrument name must not be nil' do diff --git a/metrics_api/test/test_helper.rb b/metrics_api/test/test_helper.rb index 753cb8610..9e9698666 100644 --- a/metrics_api/test/test_helper.rb +++ b/metrics_api/test/test_helper.rb @@ -8,6 +8,7 @@ # # SimpleCov.start # # SimpleCov.minimum_coverage 85 +require 'opentelemetry-test-helpers' require 'opentelemetry-metrics-api' require 'minitest/autorun' require 'pry' diff --git a/metrics_sdk/.rubocop.yml b/metrics_sdk/.rubocop.yml new file mode 100644 index 000000000..26f13b7c4 --- /dev/null +++ b/metrics_sdk/.rubocop.yml @@ -0,0 +1,22 @@ +AllCops: + TargetRubyVersion: "2.6.0" + +Lint/UnusedMethodArgument: + Enabled: false +Metrics/AbcSize: + Max: 30 +Metrics/LineLength: + Enabled: false +Metrics/MethodLength: + Max: 50 +Metrics/PerceivedComplexity: + Max: 30 +Metrics/CyclomaticComplexity: + Max: 20 +Metrics/ParameterLists: + Enabled: false +Naming/FileName: + Exclude: + - "lib/opentelemetry-metrics-sdk.rb" +Style/ModuleFunction: + Enabled: false diff --git a/metrics_sdk/.yardopts b/metrics_sdk/.yardopts new file mode 100644 index 000000000..8c871bc8d --- /dev/null +++ b/metrics_sdk/.yardopts @@ -0,0 +1,9 @@ +--no-private +--title=OpenTelemetry Metrics SDK +--markup=markdown +--main=README.md +./lib/opentelemetry/**/*.rb +./lib/opentelemetry.rb +- +README.md +CHANGELOG.md diff --git a/metrics_sdk/CHANGELOG.md b/metrics_sdk/CHANGELOG.md new file mode 100644 index 000000000..789f15a23 --- /dev/null +++ b/metrics_sdk/CHANGELOG.md @@ -0,0 +1 @@ +# Release History: opentelemetry-metrics-sdk diff --git a/metrics_sdk/Gemfile b/metrics_sdk/Gemfile new file mode 100644 index 000000000..39ca86ec6 --- /dev/null +++ b/metrics_sdk/Gemfile @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +gem 'opentelemetry-api', path: '../api' +gem 'opentelemetry-metrics-api', path: '../metrics_api' +gem 'opentelemetry-sdk', path: '../sdk' +gem 'opentelemetry-test-helpers', path: '../test_helpers' + +group :test, :development do + gem 'pry' + gem 'pry-byebug' unless RUBY_ENGINE == 'jruby' +end diff --git a/metrics_sdk/LICENSE b/metrics_sdk/LICENSE new file mode 100644 index 000000000..1ef7dad2c --- /dev/null +++ b/metrics_sdk/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/metrics_sdk/Rakefile b/metrics_sdk/Rakefile new file mode 100644 index 000000000..99808b5ac --- /dev/null +++ b/metrics_sdk/Rakefile @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' + +require 'rubocop/rake_task' +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +if RUBY_ENGINE == 'truffleruby' + task default: %i[test] +else + task default: %i[test rubocop yard] +end diff --git a/metrics_sdk/lib/opentelemetry-metrics-sdk.rb b/metrics_sdk/lib/opentelemetry-metrics-sdk.rb new file mode 100644 index 000000000..bda7992d3 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry-metrics-sdk.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk' +require 'opentelemetry-metrics-api' +require 'opentelemetry/sdk/metrics' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics.rb new file mode 100644 index 000000000..42807c708 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + # The Metrics module contains the OpenTelemetry metrics reference + # implementation. + module Metrics + end + end +end + +require 'opentelemetry/sdk/metrics/aggregation' +require 'opentelemetry/sdk/metrics/configuration_patch' +require 'opentelemetry/sdk/metrics/export' +require 'opentelemetry/sdk/metrics/instrument' +require 'opentelemetry/sdk/metrics/meter' +require 'opentelemetry/sdk/metrics/meter_provider' +require 'opentelemetry/sdk/metrics/state' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation.rb new file mode 100644 index 000000000..8d4f9ba23 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk/metrics/aggregation/histogram' + +module OpenTelemetry + module SDK + module Metrics + # The Aggregation module contains the OpenTelemetry metrics reference + # aggregation implementations. + module Aggregation + extend self + + SUM = ->(v1, v2) { v1 + v2 } + EXPLICIT_BUCKET_HISTOGRAM = ExplicitBucketHistogram.new + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/histogram.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/histogram.rb new file mode 100644 index 000000000..0ae5f5e32 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/histogram.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Aggregation + # Contains the implementation of the ExplicitBucketHistogram aggregation + # https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#explicit-bucket-histogram-aggregation + class ExplicitBucketHistogram + # The Default Value represents the following buckets: + # (-inf, 0], (0, 5.0], (5.0, 10.0], (10.0, 25.0], (25.0, 50.0], + # (50.0, 75.0], (75.0, 100.0], (100.0, 250.0], (250.0, 500.0], + # (500.0, 1000.0], (1000.0, +inf) + DEFAULT_BOUNDARIES = [0, 5, 10, 25, 50, 75, 100, 250, 500, 1000].freeze + def initialize(boundaries: DEFAULT_BOUNDARIES, record_min_max: true) + @boundaries = boundaries + @record_min_max = record_min_max + end + + # TODO: Implement ExplicitBucketHistogram + def call(_old, _new); end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb new file mode 100644 index 000000000..ac529e67f --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.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 + # The ConfiguratorPatch implements a hook to configure the metrics + # portion of the SDK. + module ConfiguratorPatch + private + + # The metrics_configuration_hook method is where we define the setup process + # for metrics SDK. + def metrics_configuration_hook + OpenTelemetry.meter_provider = Metrics::MeterProvider.new(resource: @resource) + end + end + end + end +end + +OpenTelemetry::SDK::Configurator.prepend(OpenTelemetry::SDK::Metrics::ConfiguratorPatch) diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/export.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/export.rb new file mode 100644 index 000000000..37716c801 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/export.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Export + ExportError = Class.new(OpenTelemetry::Error) + + # The operation finished successfully. + SUCCESS = 0 + + # The operation finished with an error. + FAILURE = 1 + + # The operation timed out. + TIMEOUT = 2 + end + end + end +end + +require 'opentelemetry/sdk/metrics/export/metric_reader' +require 'opentelemetry/sdk/metrics/export/in_memory_metric_pull_exporter' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/export/in_memory_metric_pull_exporter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/export/in_memory_metric_pull_exporter.rb new file mode 100644 index 000000000..9a8612c22 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/export/in_memory_metric_pull_exporter.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Export + # The InMemoryMetricPullExporter behaves as a Metric Reader and Exporter. + # To be used for testing purposes, not production. + class InMemoryMetricPullExporter < MetricReader + attr_reader :metric_snapshots + + def initialize + super + @metric_snapshots = [] + @mutex = Mutex.new + end + + def pull + export(collect) + end + + def export(metrics) + @mutex.synchronize do + return FAILURE if @stopped + + @metric_snapshots << metrics + end + SUCCESS + end + + def reset + @mutex.synchronize do + @metric_snapshots.clear + end + end + + def shutdown + SUCCESS + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/export/metric_reader.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/export/metric_reader.rb new file mode 100644 index 000000000..c261f4f10 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/export/metric_reader.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Export + # MetricReader provides a minimal example implementation. + # It is not required to subclass this class to provide an implementation + # of MetricReader, provided the interface is satisfied. + class MetricReader + attr_reader :metric_store + + def initialize + @metric_store = OpenTelemetry::SDK::Metrics::State::MetricStore.new + end + + def collect + @metric_store.collect + end + + def shutdown(timeout: nil) + Export::SUCCESS + end + + def force_flush(timeout: nil) + Export::SUCCESS + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument.rb new file mode 100644 index 000000000..c440634c8 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + # The Instrument module contains the OpenTelemetry instruments reference + # implementation. + module Instrument + end + end +end + +require 'opentelemetry/sdk/metrics/instrument/synchronous_instrument' +require 'opentelemetry/sdk/metrics/instrument/counter' +require 'opentelemetry/sdk/metrics/instrument/histogram' +require 'opentelemetry/sdk/metrics/instrument/observable_counter' +require 'opentelemetry/sdk/metrics/instrument/observable_gauge' +require 'opentelemetry/sdk/metrics/instrument/observable_up_down_counter' +require 'opentelemetry/sdk/metrics/instrument/up_down_counter' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/counter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/counter.rb new file mode 100644 index 000000000..0ad83d385 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/counter.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Instrument + # {Counter} is the SDK implementation of {OpenTelemetry::Metrics::Counter}. + class Counter < OpenTelemetry::SDK::Metrics::Instrument::SynchronousInstrument + DEFAULT_AGGREGATION = OpenTelemetry::SDK::Metrics::Aggregation::SUM + + # Returns the instrument kind as a Symbol + # + # @return [Symbol] + def instrument_kind + :counter + end + + # Increment the Counter by a fixed amount. + # + # @param [numeric] increment The increment amount, 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 add(increment, attributes: {}) + # TODO: When the metrics SDK stabilizes and is merged into the main SDK, + # we can leverage the SDK Internal validation classes to enforce this: + # https://github.com/open-telemetry/opentelemetry-ruby/blob/6bec625ef49004f364457c26263df421526b60d6/sdk/lib/opentelemetry/sdk/internal.rb#L47 + if increment.negative? + OpenTelemetry.logger.warn("#{@name} received a negative value") + else + update( + OpenTelemetry::Metrics::Measurement.new(increment, attributes), + DEFAULT_AGGREGATION + ) + end + nil + rescue StandardError => e + OpenTelemetry.handle_error(exception: e) + nil + end + end + end + 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 new file mode 100644 index 000000000..cb78f99ea --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/histogram.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Instrument + # {Histogram} is the SDK implementation of {OpenTelemetry::Metrics::Histogram}. + class Histogram < OpenTelemetry::SDK::Metrics::Instrument::SynchronousInstrument + DEFAULT_AGGREGATION = OpenTelemetry::SDK::Metrics::Aggregation::EXPLICIT_BUCKET_HISTOGRAM + + # Returns the instrument kind as a Symbol + # + # @return [Symbol] + def instrument_kind + :histogram + end + + # Updates the statistics with the specified amount. + # + # @param [numeric] amount The amount of the Measurement, 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 record(amount, attributes: nil) + update(OpenTelemetry::Metrics::Measurement.new(amount, attributes)) + nil + rescue StandardError => e + OpenTelemetry.handle_error(exception: e) + nil + 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 new file mode 100644 index 000000000..4e5d49102 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_counter.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 Instrument + # {ObservableCounter} is the SDK implementation of {OpenTelemetry::Metrics::ObservableCounter}. + class ObservableCounter < OpenTelemetry::Metrics::Instrument::ObservableCounter + attr_reader :name, :unit, :description + + def initialize(name, unit, description, callback, meter) + @name = name + @unit = unit + @description = description + @callback = callback + @meter = meter + end + end + end + 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 new file mode 100644 index 000000000..7a122184f --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_gauge.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 Instrument + # {ObservableGauge} is the SDK implementation of {OpenTelemetry::Metrics::ObservableGauge}. + class ObservableGauge < OpenTelemetry::Metrics::Instrument::ObservableGauge + attr_reader :name, :unit, :description + + def initialize(name, unit, description, callback, meter) + @name = name + @unit = unit + @description = description + @callback = callback + @meter = meter + end + end + end + 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 new file mode 100644 index 000000000..8eb812c5f --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/observable_up_down_counter.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 Instrument + # {ObservableUpDownCounter} is the SDK implementation of {OpenTelemetry::Metrics::ObservableUpDownCounter}. + class ObservableUpDownCounter < OpenTelemetry::Metrics::Instrument::ObservableUpDownCounter + attr_reader :name, :unit, :description + + def initialize(name, unit, description, callback, meter) + @name = name + @unit = unit + @description = description + @callback = callback + @meter = meter + end + end + end + 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 new file mode 100644 index 000000000..0080186fd --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/synchronous_instrument.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Instrument + # {SynchronousInstrument} contains the common functionality shared across + # the synchronous instruments SDK instruments. + class SynchronousInstrument + def initialize(name, unit, description, instrumentation_library, meter_provider) + @name = name + @unit = unit + @description = description + @instrumentation_library = instrumentation_library + @meter_provider = meter_provider + @metric_streams = [] + + meter_provider.metric_readers.each do |metric_reader| + register_with_new_metric_store(metric_reader.metric_store) + end + end + + # @api private + def register_with_new_metric_store(metric_store) + ms = OpenTelemetry::SDK::Metrics::State::MetricStream.new( + @name, + @description, + @unit, + instrument_kind, + @meter_provider, + @instrumentation_library + ) + @metric_streams << ms + metric_store.add_metric_stream(ms) + end + + private + + def update(measurement, aggregation) + @metric_streams.each do |ms| + ms.update(measurement, aggregation) + end + end + end + 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 new file mode 100644 index 000000000..5e512f415 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/instrument/up_down_counter.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + module Instrument + # {UpDownCounter} is the SDK implementation of {OpenTelemetry::Metrics::UpDownCounter}. + class UpDownCounter < OpenTelemetry::SDK::Metrics::Instrument::SynchronousInstrument + DEFAULT_AGGREGATION = OpenTelemetry::SDK::Metrics::Aggregation::SUM + + # Returns the instrument kind as a Symbol + # + # @return [Symbol] + def instrument_kind + :up_down_counter + end + + # Increment or decrement the UpDownCounter by a fixed amount. + # + # @param [Numeric] amount The amount to be added, can be positive, negative or zero. + # @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 add(amount, attributes: nil) + update( + OpenTelemetry::Metrics::Measurement.new(amount, attributes), + DEFAULT_AGGREGATION + ) + nil + rescue StandardError => e + OpenTelemetry.handle_error(exception: e) + nil + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb new file mode 100644 index 000000000..f4355f116 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + # The Metrics module contains the OpenTelemetry metrics reference + # implementation. + module Metrics + # {Meter} is the SDK implementation of {OpenTelemetry::Metrics::Meter}. + class Meter < OpenTelemetry::Metrics::Meter + # @api private + # + # Returns a new {Meter} instance. + # + # @param [String] name Instrumentation package name + # @param [String] version Instrumentation package version + # + # @return [Meter] + def initialize(name, version, meter_provider) + @mutex = Mutex.new + @instrument_registry = {} + @instrumentation_library = InstrumentationLibrary.new(name, version) + @meter_provider = meter_provider + end + + # @api private + def add_metric_reader(metric_reader) + @instrument_registry.each do |_n, instrument| + instrument.register_with_new_metric_store(metric_reader.metric_store) + end + end + + 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_library, @meter_provider) + when :observable_counter then OpenTelemetry::SDK::Metrics::Instrument::ObservableCounter.new(name, unit, description, callback, @instrumentation_library, @meter_provider) + when :histogram then OpenTelemetry::SDK::Metrics::Instrument::Histogram.new(name, unit, description, @instrumentation_library, @meter_provider) + when :observable_gauge then OpenTelemetry::SDK::Metrics::Instrument::ObservableGauge.new(name, unit, description, callback, @instrumentation_library, @meter_provider) + when :up_down_counter then OpenTelemetry::SDK::Metrics::Instrument::UpDownCounter.new(name, unit, description, @instrumentation_library, @meter_provider) + when :observable_up_down_counter then OpenTelemetry::SDK::Metrics::Instrument::ObservableUpDownCounter.new(name, unit, description, callback, @instrumentation_library, @meter_provider) + end + end + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb new file mode 100644 index 000000000..c3b3c8000 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb @@ -0,0 +1,130 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + # The Metrics module contains the OpenTelemetry metrics reference + # implementation. + module Metrics + # {MeterProvider} is the SDK implementation of {OpenTelemetry::Metrics::MeterProvider}. + class MeterProvider < OpenTelemetry::Metrics::MeterProvider + Key = Struct.new(:name, :version) + private_constant(:Key) + + attr_reader :resource, :metric_readers + + def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create) + @mutex = Mutex.new + @meter_registry = {} + @stopped = false + @metric_readers = [] + @resource = resource + end + + # Returns a {Meter} instance. + # + # @param [String] name Instrumentation package name + # @param [optional String] version Instrumentation package version + # + # @return [Meter] + def meter(name, version = nil) + version ||= '' + if @stopped + OpenTelemetry.logger.warn 'calling MeterProvider#meter after shutdown, a noop meter will be returned.' + OpenTelemetry::Metrics::Meter.new + else + @mutex.synchronize { @meter_registry[Key.new(name, version)] ||= Meter.new(name, version, self) } + end + end + + # Attempts to stop all the activity for this {MeterProvider}. + # + # Calls MetricReader#shutdown for all registered MetricReaders. + # + # After this is called all the newly created {Meter}s will be no-op. + # + # @param [optional Numeric] timeout An optional timeout in seconds. + # @return [Integer] Export::SUCCESS if no error occurred, Export::FAILURE if + # a non-specific failure occurred, Export::TIMEOUT if a timeout occurred. + def shutdown(timeout: nil) + @mutex.synchronize do + if @stopped + OpenTelemetry.logger.warn('calling MetricProvider#shutdown multiple times.') + Export::FAILURE + else + start_time = OpenTelemetry::Common::Utilities.timeout_timestamp + results = @metric_readers.map do |metric_reader| + remaining_timeout = OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start_time) + if remaining_timeout&.zero? + Export::TIMEOUT + else + metric_reader.shutdown(timeout: remaining_timeout) + end + end + + @stopped = true + results.max || Export::SUCCESS + end + end + end + + # This method provides a way for provider to notify the registered + # {MetricReader} instances, so they can do as much as they could to consume + # or send the metrics. Note: unlike Push Metric Exporter which can send data on + # its own schedule, Pull Metric Exporter can only send the data when it is + # being asked by the scraper, so ForceFlush would not make much sense. + # + # @param [optional Numeric] timeout An optional timeout in seconds. + # @return [Integer] Export::SUCCESS if no error occurred, Export::FAILURE if + # a non-specific failure occurred, Export::TIMEOUT if a timeout occurred. + def force_flush(timeout: nil) + @mutex.synchronize do + if @stopped + Export::SUCCESS + else + start_time = OpenTelemetry::Common::Utilities.timeout_timestamp + results = @metric_readers.map do |metric_reader| + remaining_timeout = OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start_time) + if remaining_timeout&.zero? + Export::TIMEOUT + else + metric_reader.force_flush(timeout: remaining_timeout) + end + end + + results.max || Export::SUCCESS + end + end + end + + # Adds a new MetricReader to this {MeterProvider}. + # + # @param metric_reader the new MetricReader to be added. + def add_metric_reader(metric_reader) + @mutex.synchronize do + if @stopped + OpenTelemetry.logger.warn('calling MetricProvider#add_metric_reader after shutdown.') + else + @metric_readers.push(metric_reader) + @meter_registry.each_value { |meter| meter.add_metric_reader(metric_reader) } + end + + nil + end + end + + # The type of the Instrument(s) (optional). + # The name of the Instrument(s). OpenTelemetry SDK authors MAY choose to support wildcard characters, with the question mark (?) matching exactly one character and the asterisk character (*) matching zero or more characters. + # The name of the Meter (optional). + # The version of the Meter (optional). + # The schema_url of the Meter (optional). + def add_view + # TODO: For each meter add this view to all applicable instruments + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/state.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/state.rb new file mode 100644 index 000000000..0512f5136 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/state.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + # @api private + # + # The State module provides SDK internal functionality that is not a part of the + # public API. + module State + end + end + end +end + +require 'opentelemetry/sdk/metrics/state/metric_data' +require 'opentelemetry/sdk/metrics/state/metric_store' +require 'opentelemetry/sdk/metrics/state/metric_stream' diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_data.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_data.rb new file mode 100644 index 000000000..bf982c5cd --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_data.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 State + # MetricData is a Struct containing {MetricStream} data for export. + MetricData = Struct.new(:name, # String + :description, # String + :unit, # String + :instrument_kind, # Symbol + :resource, # OpenTelemetry::SDK::Resources::Resource + :instrumentation_library, # OpenTelemetry::SDK::InstrumentationLibrary + :data_points, # Hash{Hash{String => String, Numeric, Boolean, Array} => Numeric} + :start_time_unix_nano, # Integer nanoseconds since Epoch + :time_unix_nano) # Integer nanoseconds since Epoch + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_store.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_store.rb new file mode 100644 index 000000000..de2ecb83b --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_store.rb @@ -0,0 +1,48 @@ +# 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 MetricStore module provides SDK internal functionality that is not a part of the + # public API. + class MetricStore + def initialize + @mutex = Mutex.new + @epoch_start_time = now_in_nano + @epoch_end_time = nil + @metric_streams = [] + end + + def collect + @mutex.synchronize do + @epoch_end_time = now_in_nano + snapshot = @metric_streams.map { |ms| ms.collect(@epoch_start_time, @epoch_end_time) } + @epoch_start_time = @epoch_end_time + snapshot + end + end + + def add_metric_stream(metric_stream) + @mutex.synchronize do + @metric_streams = @metric_streams.dup.push(metric_stream) + nil + end + end + + private + + 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/lib/opentelemetry/sdk/metrics/state/metric_stream.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_stream.rb new file mode 100644 index 000000000..60ed86e75 --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_stream.rb @@ -0,0 +1,80 @@ +# 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 MetricStream + attr_reader :name, :description, :unit, :instrument_kind, :instrumentation_library, :data_points + + def initialize( + name, + description, + unit, + instrument_kind, + meter_provider, + instrumentation_library + ) + @name = name + @description = description + @unit = unit + @instrument_kind = instrument_kind + @meter_provider = meter_provider + @instrumentation_library = instrumentation_library + + @data_points = {} + @mutex = Mutex.new + end + + def collect(start_time, end_time) + @mutex.synchronize do + MetricData.new( + @name, + @description, + @unit, + @instrument_kind, + @meter_provider.resource, + @instrumentation_library, + @data_points.dup, + start_time, + end_time + ) + end + end + + def update(measurement, aggregation) + @mutex.synchronize do + @data_points[measurement.attributes] = if @data_points[measurement.attributes] + aggregation.call(@data_points[measurement.attributes], measurement.value) + else + measurement.value + 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 + end + end + end + end +end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb new file mode 100644 index 000000000..a26a06fec --- /dev/null +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Metrics + # Current OpenTelemetry metrics sdk version + VERSION = '0.0.1' + end + end +end diff --git a/metrics_sdk/opentelemetry-metrics-sdk.gemspec b/metrics_sdk/opentelemetry-metrics-sdk.gemspec new file mode 100644 index 000000000..4674ef152 --- /dev/null +++ b/metrics_sdk/opentelemetry-metrics-sdk.gemspec @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'opentelemetry/sdk/metrics/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-metrics-sdk' + spec.version = OpenTelemetry::SDK::Metrics::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'A stats collection and distributed tracing framework' + spec.description = 'A stats collection and distributed tracing framework' + spec.homepage = 'https://github.com/open-telemetry/opentelemetry-ruby' + spec.license = 'Apache-2.0' + + spec.files = ::Dir.glob('lib/**/*.rb') + + ::Dir.glob('*.md') + + ['LICENSE', '.yardopts'] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 2.6.0' + + spec.add_dependency 'opentelemetry-api', '~> 1.0' + spec.add_dependency 'opentelemetry-metrics-api' + spec.add_dependency 'opentelemetry-sdk', '~> 1.0' + + spec.add_development_dependency 'benchmark-ipsa', '~> 0.2.0' + spec.add_development_dependency 'bundler', '>= 1.17' + spec.add_development_dependency 'faraday', '~> 0.13' + spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'opentelemetry-test-helpers' + spec.add_development_dependency 'rake', '~> 12.0' + spec.add_development_dependency 'rubocop', '~> 0.73.0' + spec.add_development_dependency 'simplecov', '~> 0.17' + spec.add_development_dependency 'yard', '~> 0.9' + spec.add_development_dependency 'yard-doctest', '~> 0.1.6' + + if spec.respond_to?(:metadata) + spec.metadata['changelog_uri'] = "https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-metrics-sdk/v#{OpenTelemetry::SDK::Metrics::VERSION}/file.CHANGELOG.html" + spec.metadata['source_code_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby/tree/main/api' + spec.metadata['bug_tracker_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby/issues' + spec.metadata['documentation_uri'] = "https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-metrics-sdk/v#{OpenTelemetry::SDK::Metrics::VERSION}" + end +end diff --git a/metrics_sdk/test/.rubocop.yml b/metrics_sdk/test/.rubocop.yml new file mode 100644 index 000000000..4c8c0d91e --- /dev/null +++ b/metrics_sdk/test/.rubocop.yml @@ -0,0 +1,6 @@ +inherit_from: ../.rubocop.yml + +Metrics/BlockLength: + Enabled: false +Metrics/LineLength: + Enabled: false diff --git a/metrics_sdk/test/integration/in_memory_metric_pull_exporter_test.rb b/metrics_sdk/test/integration/in_memory_metric_pull_exporter_test.rb new file mode 100644 index 000000000..a62443da1 --- /dev/null +++ b/metrics_sdk/test/integration/in_memory_metric_pull_exporter_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 do + describe '#configure' do + before { reset_metrics_sdk } + + it 'emits metrics' do + OpenTelemetry::SDK.configure + + metric_exporter = OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new + OpenTelemetry.meter_provider.add_metric_reader(metric_exporter) + + meter = OpenTelemetry.meter_provider.meter('test') + instrument = meter.create_counter('b_counter', unit: 'smidgen', description: 'a small amount of something') + + instrument.add(1) + instrument.add(2, attributes: { 'a' => 'b' }) + instrument.add(2, attributes: { 'a' => 'b' }) + instrument.add(3, attributes: { 'b' => 'c' }) + instrument.add(4, attributes: { 'd' => 'e' }) + + metric_exporter.pull + last_snapshot = metric_exporter.metric_snapshots.last + + _(last_snapshot).wont_be_empty + _(last_snapshot[0].name).must_equal('b_counter') + _(last_snapshot[0].unit).must_equal('smidgen') + _(last_snapshot[0].description).must_equal('a small amount of something') + _(last_snapshot[0].instrumentation_library.name).must_equal('test') + _(last_snapshot[0].data_points).must_equal( + {} => 1, + { 'a' => 'b' } => 4, + { 'b' => 'c' } => 3, + { 'd' => 'e' } => 4 + ) + end + end +end diff --git a/metrics_sdk/test/opentelemetry/metrics_sdk_test.rb b/metrics_sdk/test/opentelemetry/metrics_sdk_test.rb new file mode 100644 index 000000000..62a9ecf5c --- /dev/null +++ b/metrics_sdk/test/opentelemetry/metrics_sdk_test.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK do + describe '#configure' do + before { reset_metrics_sdk } + + it 'upgrades the API MeterProvider, Meters, and Instruments' do + meter_provider = OpenTelemetry.meter_provider + meter = meter_provider.meter('test') + instrument = meter.create_counter('a_counter') + + # Calls before the SDK is configured return Proxy implementations + _(meter_provider).must_be_instance_of OpenTelemetry::Internal::ProxyMeterProvider + _(meter).must_be_instance_of OpenTelemetry::Internal::ProxyMeter + _(instrument).must_be_instance_of OpenTelemetry::Internal::ProxyInstrument + + OpenTelemetry::SDK.configure + + # Proxy implementations now have their delegates set + _(meter_provider.instance_variable_get(:@delegate)).must_be_instance_of OpenTelemetry::SDK::Metrics::MeterProvider + _(meter.instance_variable_get(:@delegate)).must_be_instance_of OpenTelemetry::SDK::Metrics::Meter + _(instrument.instance_variable_get(:@delegate)).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::Counter + + # Calls after the SDK is configured now return the SDK implementations directly + _(OpenTelemetry.meter_provider).must_be_instance_of OpenTelemetry::SDK::Metrics::MeterProvider + _(OpenTelemetry.meter_provider.meter('test')).must_be_instance_of OpenTelemetry::SDK::Metrics::Meter + _(OpenTelemetry.meter_provider.meter('test').create_counter('b_counter')).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::Counter + end + + it 'sends the original configuration error to the error handler' do + received_exception = nil + received_message = nil + + OpenTelemetry.error_handler = lambda do |exception: nil, message: nil| + received_exception = exception + received_message = message + end + + OpenTelemetry::SDK.configure(&:do_something) + + _(received_exception).must_be_instance_of OpenTelemetry::SDK::ConfigurationError + _(received_message).must_match(/unexpected configuration error due to undefined method `do_something/) + end + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/counter_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/counter_test.rb new file mode 100644 index 000000000..8e9fb77c9 --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/counter_test.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::Instrument::Counter do +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 new file mode 100644 index 000000000..a234cdd69 --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::MeterProvider do + before do + reset_metrics_sdk + OpenTelemetry::SDK.configure + end + + describe '#meter' do + it 'requires a meter name' do + _(-> { OpenTelemetry.meter_provider.meter }).must_raise(ArgumentError) + end + + it 'creates a new meter' do + meter = OpenTelemetry.meter_provider.meter('test') + + _(meter).must_be_instance_of(OpenTelemetry::SDK::Metrics::Meter) + end + + it 'repeated calls does not recreate a meter of the same name' do + meter_a = OpenTelemetry.meter_provider.meter('test') + meter_b = OpenTelemetry.meter_provider.meter('test') + + _(meter_a).must_equal(meter_b) + end + end + + describe '#shutdown' do + it 'repeated calls to shutdown result in a failure' do + with_test_logger do |log_stream| + _(OpenTelemetry.meter_provider.shutdown).must_equal(OpenTelemetry::SDK::Metrics::Export::SUCCESS) + _(OpenTelemetry.meter_provider.shutdown).must_equal(OpenTelemetry::SDK::Metrics::Export::FAILURE) + _(log_stream.string).must_match(/calling MetricProvider#shutdown multiple times/) + end + end + + it 'returns a no-op meter after being shutdown' do + with_test_logger do |log_stream| + OpenTelemetry.meter_provider.shutdown + + _(OpenTelemetry.meter_provider.meter('test')).must_be_instance_of(OpenTelemetry::Metrics::Meter) + _(log_stream.string).must_match(/calling MeterProvider#meter after shutdown, a noop meter will be returned/) + end + end + + it 'returns a timeout response when it times out' do + mock_metric_reader = new_mock_reader + mock_metric_reader.expect(:nothing_gets_called_because_it_times_out_first, nil) + OpenTelemetry.meter_provider.add_metric_reader(mock_metric_reader) + + _(OpenTelemetry.meter_provider.shutdown(timeout: 0)).must_equal(OpenTelemetry::SDK::Metrics::Export::TIMEOUT) + end + + it 'invokes shutdown on all registered Metric Readers' do + mock_metric_reader1 = new_mock_reader + mock_metric_reader2 = new_mock_reader + mock_metric_reader1.expect(:shutdown, nil, [{ timeout: nil }]) + mock_metric_reader2.expect(:shutdown, nil, [{ timeout: nil }]) + + OpenTelemetry.meter_provider.add_metric_reader(mock_metric_reader1) + OpenTelemetry.meter_provider.add_metric_reader(mock_metric_reader2) + OpenTelemetry.meter_provider.shutdown + + mock_metric_reader1.verify + mock_metric_reader2.verify + end + end + + describe '#force_flush' do + it 'returns a timeout response when it times out' do + mock_metric_reader = new_mock_reader + mock_metric_reader.expect(:nothing_gets_called_because_it_times_out_first, nil) + OpenTelemetry.meter_provider.add_metric_reader(mock_metric_reader) + + _(OpenTelemetry.meter_provider.force_flush(timeout: 0)).must_equal(OpenTelemetry::SDK::Metrics::Export::TIMEOUT) + end + + it 'invokes force_flush on all registered Metric Readers' do + mock_metric_reader1 = new_mock_reader + mock_metric_reader2 = new_mock_reader + mock_metric_reader1.expect(:force_flush, nil, [{ timeout: nil }]) + mock_metric_reader2.expect(:force_flush, nil, [{ timeout: nil }]) + OpenTelemetry.meter_provider.add_metric_reader(mock_metric_reader1) + OpenTelemetry.meter_provider.add_metric_reader(mock_metric_reader2) + + OpenTelemetry.meter_provider.force_flush + + mock_metric_reader1.verify + mock_metric_reader2.verify + end + end + + describe '#add_metric_reader' do + it 'adds a metric reader' do + metric_reader = OpenTelemetry::SDK::Metrics::Export::MetricReader.new + + OpenTelemetry.meter_provider.add_metric_reader(metric_reader) + + _(OpenTelemetry.meter_provider.instance_variable_get(:@metric_readers)).must_equal([metric_reader]) + end + + it 'associates the metric store with instruments created before the metric reader' do + meter_a = OpenTelemetry.meter_provider.meter('a').create_counter('meter_a') + + metric_reader_a = OpenTelemetry::SDK::Metrics::Export::MetricReader.new + OpenTelemetry.meter_provider.add_metric_reader(metric_reader_a) + + metric_reader_b = OpenTelemetry::SDK::Metrics::Export::MetricReader.new + OpenTelemetry.meter_provider.add_metric_reader(metric_reader_b) + + _(meter_a.instance_variable_get(:@metric_streams).size).must_equal(2) + _(metric_reader_a.metric_store.instance_variable_get(:@metric_streams).size).must_equal(1) + _(metric_reader_b.metric_store.instance_variable_get(:@metric_streams).size).must_equal(1) + end + + it 'associates the metric store with instruments created after the metric reader' do + metric_reader_a = OpenTelemetry::SDK::Metrics::Export::MetricReader.new + OpenTelemetry.meter_provider.add_metric_reader(metric_reader_a) + + metric_reader_b = OpenTelemetry::SDK::Metrics::Export::MetricReader.new + OpenTelemetry.meter_provider.add_metric_reader(metric_reader_b) + + meter_a = OpenTelemetry.meter_provider.meter('a').create_counter('meter_a') + + _(meter_a.instance_variable_get(:@metric_streams).size).must_equal(2) + _(metric_reader_a.metric_store.instance_variable_get(:@metric_streams).size).must_equal(1) + _(metric_reader_b.metric_store.instance_variable_get(:@metric_streams).size).must_equal(1) + end + end + + # TODO: OpenTelemetry.meter_provider.add_view + describe '#add_view' do + end + + private + + def new_mock_reader + Minitest::Mock.new(OpenTelemetry::SDK::Metrics::Export::MetricReader.new) + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb new file mode 100644 index 000000000..6a6cba2fd --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::Meter do + before { OpenTelemetry::SDK.configure } + + let(:meter) { OpenTelemetry.meter_provider.meter('new_meter') } + + describe '#create_counter' do + it 'creates a counter instrument' do + instrument = meter.create_counter('a_counter', unit: 'minutes', description: 'useful description') + _(instrument).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::Counter + end + end + + describe '#create_histogram' do + it 'creates a histogram instrument' do + instrument = meter.create_histogram('a_histogram', unit: 'minutes', description: 'useful description') + _(instrument).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::Histogram + end + end + + describe '#create_up_down_counter' do + it 'creates a up_down_counter instrument' do + instrument = meter.create_up_down_counter('a_up_down_counter', unit: 'minutes', description: 'useful description') + _(instrument).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::UpDownCounter + end + end + + 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).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).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).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::ObservableUpDownCounter + end + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/state/metric_store_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/state/metric_store_test.rb new file mode 100644 index 000000000..01cd0fcf9 --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/state/metric_store_test.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::State::MetricStore do + describe '#collect' do + end + + describe '#add_metric_stream' do + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/state/metric_stream_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/state/metric_stream_test.rb new file mode 100644 index 000000000..8a0084101 --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/state/metric_stream_test.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Metrics::State::MetricStream do + describe '#update' do + end +end diff --git a/metrics_sdk/test/test_helper.rb b/metrics_sdk/test/test_helper.rb new file mode 100644 index 000000000..99aa4deee --- /dev/null +++ b/metrics_sdk/test/test_helper.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +# require 'simplecov' +# # SimpleCov.start +# # SimpleCov.minimum_coverage 85 + +require 'opentelemetry-metrics-sdk' +require 'opentelemetry-test-helpers' +require 'minitest/autorun' +require 'pry' + +# reset_metrics_sdk is a test helper used to clear +# SDK configuration state between calls +def reset_metrics_sdk + OpenTelemetry.instance_variable_set( + :@meter_provider, + OpenTelemetry::Internal::ProxyMeterProvider.new + ) + + OpenTelemetry.logger = Logger.new(File::NULL) + OpenTelemetry.error_handler = nil +end + +def with_test_logger + log_stream = StringIO.new + original_logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + yield log_stream +ensure + OpenTelemetry.logger = original_logger +end diff --git a/sdk/lib/opentelemetry/sdk/configurator.rb b/sdk/lib/opentelemetry/sdk/configurator.rb index 14b23d9c9..1aa918a4c 100644 --- a/sdk/lib/opentelemetry/sdk/configurator.rb +++ b/sdk/lib/opentelemetry/sdk/configurator.rb @@ -141,11 +141,14 @@ def configure configure_span_processors tracer_provider.id_generator = @id_generator OpenTelemetry.tracer_provider = tracer_provider + metrics_configuration_hook install_instrumentation end private + def metrics_configuration_hook; end + def tracer_provider @tracer_provider ||= Trace::TracerProvider.new(resource: @resource) end