diff --git a/.commit-me.json b/.commit-me.json
new file mode 100644
index 0000000000..80224890c4
--- /dev/null
+++ b/.commit-me.json
@@ -0,0 +1,4 @@
+{
+ "include-pull-requests": true,
+ "types": [ "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "release", "revert", "squash", "style", "test" ]
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1eadcc02c4..e5b4956795 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -87,6 +87,7 @@ jobs:
- opentelemetry-exporter-otlp-grpc
- opentelemetry-exporter-otlp-http
- opentelemetry-exporter-zipkin
+ - opentelemetry-exporter-otlp-logs
- opentelemetry-exporter-otlp-metrics
os:
- ubuntu-latest
@@ -134,8 +135,9 @@ jobs:
echo "skip=false" >> $GITHUB_OUTPUT
[[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp" ]] && echo "skip=true" >> $GITHUB_OUTPUT
[[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-common" ]] && echo "skip=true" >> $GITHUB_OUTPUT
- [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-http" ]] && echo "skip=true" >> $GITHUB_OUTPUT
[[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-grpc" ]] && echo "skip=true" >> $GITHUB_OUTPUT
+ [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-http" ]] && echo "skip=true" >> $GITHUB_OUTPUT
+ [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-logs" ]] && echo "skip=true" >> $GITHUB_OUTPUT
[[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-metrics" ]] && echo "skip=true" >> $GITHUB_OUTPUT
# This is essentially a bash script getting evaluated, so we need to return true or the whole job fails.
true
diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml
new file mode 100644
index 0000000000..26cd79d03a
--- /dev/null
+++ b/.github/workflows/conventional-commits.yml
@@ -0,0 +1,32 @@
+name: Conventional Commits Validation
+
+on:
+ workflow_dispatch:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - edited
+
+permissions:
+ contents: read
+ pull-requests: read
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }} # Ensure that only one instance of this workflow is running per Pull Request
+ cancel-in-progress: true # Cancel any previous runs of this workflow
+
+jobs:
+ validate-commits:
+ name: Conventional Commits Validation
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dev-build-deploy/commit-me@v1.5.0
+ env:
+ FORCE_COLOR: 3
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ include-commits: false
+ update-labels: false
+ config: ".commit-me.json"
diff --git a/README.md b/README.md
index 76c7b82084..68d9faf8ff 100644
--- a/README.md
+++ b/README.md
@@ -33,7 +33,6 @@ Approvers ([@open-telemetry/ruby-approvers](https://github.com/orgs/open-telemet
- [Andrew Hayworth](https://github.com/ahayworth), Shopify
- [Sam Handler](https://github.com/plantfansam), Shopify
- [Robb Kidd](https://github.com/robbkidd), Honeycomb
-- [Kayla Reopelle](https://github.com/kaylareopelle), New Relic
*Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).*
@@ -43,6 +42,7 @@ Maintainers ([@open-telemetry/ruby-maintainers](https://github.com/orgs/open-tel
- [Francis Bogsanyi](https://github.com/fbogsany), Shopify
- [Matthew Wear](https://github.com/mwear), Lightstep
- [Daniel Azuma](https://github.com/dazuma), Google
+- [Kayla Reopelle](https://github.com/kaylareopelle), New Relic
*Find more about the maintainer role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#maintainer).*
diff --git a/examples/metrics_sdk/README.md b/examples/metrics_sdk/README.md
index 3ba206d783..427a39fb24 100644
--- a/examples/metrics_sdk/README.md
+++ b/examples/metrics_sdk/README.md
@@ -36,8 +36,8 @@ receivers:
# Default endpoints: 0.0.0.0:4317 for gRPC and 0.0.0.0:4318 for HTTP
exporters:
- logging:
- loglevel: debug
+ debug:
+ verbosity: detailed
processors:
batch:
@@ -47,11 +47,11 @@ service:
traces:
receivers: [otlp]
processors: [batch]
- exporters: [logging]
+ exporters: [debug]
metrics:
receivers: [otlp]
processors: [batch]
- exporters: [logging]
+ exporters: [debug]
```
More information on how to setup the OTel collector can be found in the in [quick start docs](https://opentelemetry.io/docs/collector/quick-start/).
diff --git a/examples/otel-collector/.env b/examples/otel-collector/.env
index d9c19232cd..2a845851e9 100644
--- a/examples/otel-collector/.env
+++ b/examples/otel-collector/.env
@@ -1,2 +1,2 @@
-OTELCOL_IMG=otel/opentelemetry-collector-contrib:0.35.0
+OTELCOL_IMG=otel/opentelemetry-collector-contrib:0.109.0
OTELCOL_ARGS=
diff --git a/examples/otel-collector/otel-collector-config.yaml b/examples/otel-collector/otel-collector-config.yaml
index 997fc86aa6..9484b84b91 100644
--- a/examples/otel-collector/otel-collector-config.yaml
+++ b/examples/otel-collector/otel-collector-config.yaml
@@ -3,17 +3,19 @@ receivers:
otlp:
protocols:
http:
+ endpoint: 0.0.0.0:4318
exporters:
- logging:
+ debug:
zipkin:
endpoint: "http://zipkin-all-in-one:9411/api/v2/spans"
format: proto
- jaeger:
- endpoint: jaeger-all-in-one:14250
- insecure: true
+ otlp:
+ endpoint: jaeger-all-in-one:4317
+ tls:
+ insecure: true
processors:
batch:
@@ -23,8 +25,8 @@ service:
traces:
receivers: [otlp]
processors: [batch]
- exporters: [logging, zipkin, jaeger]
+ exporters: [debug, zipkin, otlp]
metrics:
receivers: [otlp]
processors: [batch]
- exporters: [logging]
+ exporters: [debug]
diff --git a/exporter/otlp-common/test/opentelemetry/exporter/otlp/common/common_test.rb b/exporter/otlp-common/test/opentelemetry/exporter/otlp/common/common_test.rb
index 1d079711da..ea0635ff12 100644
--- a/exporter/otlp-common/test/opentelemetry/exporter/otlp/common/common_test.rb
+++ b/exporter/otlp-common/test/opentelemetry/exporter/otlp/common/common_test.rb
@@ -41,9 +41,6 @@
end
it 'translates all the things' do
- # TODO: See issue #1507 to fix
- skip 'Intermittently fails' if RUBY_ENGINE == 'truffleruby'
-
OpenTelemetry.tracer_provider = OpenTelemetry::SDK::Trace::TracerProvider.new(resource: OpenTelemetry::SDK::Resources::Resource.telemetry_sdk)
tracer = OpenTelemetry.tracer_provider.tracer('tracer', 'v0.0.1')
other_tracer = OpenTelemetry.tracer_provider.tracer('other_tracer')
diff --git a/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb b/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb
index 11f2865953..1e3cf78ef5 100644
--- a/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb
+++ b/exporter/otlp-http/test/opentelemetry/exporter/otlp/http/trace_exporter_test.rb
@@ -468,9 +468,6 @@
end
it 'translates all the things' do
- # TODO: See issue #1507 to fix
- skip 'Intermittently fails' if RUBY_ENGINE == 'truffleruby'
-
stub_request(:post, 'http://localhost:4318/v1/traces').to_return(status: 200)
processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
tracer = OpenTelemetry.tracer_provider.tracer('tracer', 'v0.0.1')
@@ -635,7 +632,7 @@
)
assert_requested(:post, 'http://localhost:4318/v1/traces') do |req|
- req.body == Zlib.gzip(encoded_etsr)
+ Zlib.gunzip(req.body) == encoded_etsr
end
end
end
diff --git a/exporter/otlp-logs/.rubocop.yml b/exporter/otlp-logs/.rubocop.yml
new file mode 100644
index 0000000000..5c3cff545c
--- /dev/null
+++ b/exporter/otlp-logs/.rubocop.yml
@@ -0,0 +1,19 @@
+inherit_from:
+ - ../../contrib/rubocop.yml
+
+AllCops:
+ Exclude:
+ - "lib/opentelemetry/proto/**/*"
+ - "vendor/**/*"
+
+Naming/FileName:
+ Exclude:
+ - lib/opentelemetry-exporter-otlp-logs.rb
+Style/StringLiterals:
+ Exclude:
+ - gemfiles/**/*
+Style/FrozenStringLiteralComment:
+ Exclude:
+ - gemfiles/**/*
+Metrics/BlockLength:
+ Enabled: false
diff --git a/exporter/otlp-logs/.yardopts b/exporter/otlp-logs/.yardopts
new file mode 100644
index 0000000000..f8ba2c9cd6
--- /dev/null
+++ b/exporter/otlp-logs/.yardopts
@@ -0,0 +1,9 @@
+--no-private
+--title=OpenTelemetry OTLP Logs Exporter
+--markup=markdown
+--main=README.md
+./lib/opentelemetry/exporter/otlp-logs/**/*.rb
+./lib/opentelemetry/exporter/otlp.rb
+-
+README.md
+CHANGELOG.md
diff --git a/exporter/otlp-logs/Appraisals b/exporter/otlp-logs/Appraisals
new file mode 100644
index 0000000000..b59a40a8bb
--- /dev/null
+++ b/exporter/otlp-logs/Appraisals
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+appraise 'google-protobuf-3.18.0' do
+ gem 'google-protobuf', '~> 3.18.0'
+end
+
+appraise 'google-protobuf-3.25.0' do
+ gem 'google-protobuf', '~> 3.25.0'
+end
+
+appraise 'google-protobuf-4.29.0' do
+ gem 'google-protobuf', '~> 4.29.0'
+end
+
+appraise 'google-protobuf-latest' do
+ gem 'google-protobuf'
+end
diff --git a/exporter/otlp-logs/CHANGELOG.md b/exporter/otlp-logs/CHANGELOG.md
new file mode 100644
index 0000000000..d1805e9311
--- /dev/null
+++ b/exporter/otlp-logs/CHANGELOG.md
@@ -0,0 +1 @@
+# Release History: opentelemetry-exporter-otlp-logs
diff --git a/exporter/otlp-logs/Gemfile b/exporter/otlp-logs/Gemfile
new file mode 100644
index 0000000000..fa879e23fc
--- /dev/null
+++ b/exporter/otlp-logs/Gemfile
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+source 'https://rubygems.org'
+
+gemspec
+
+group :test, :development do
+ gem 'opentelemetry-api', path: '../../api'
+ gem 'opentelemetry-common', path: '../../common'
+ gem 'opentelemetry-logs-api', path: '../../logs_api'
+ gem 'opentelemetry-logs-sdk', path: '../../logs_sdk'
+ gem 'opentelemetry-registry', path: '../../registry'
+ gem 'opentelemetry-sdk', path: '../../sdk'
+ gem 'opentelemetry-semantic_conventions', path: '../../semantic_conventions'
+ gem 'opentelemetry-test-helpers', path: '../../test_helpers'
+end
+
+group :test do
+ gem 'minitest-stub-const'
+end
diff --git a/exporter/otlp-logs/LICENSE b/exporter/otlp-logs/LICENSE
new file mode 100644
index 0000000000..1ef7dad2c5
--- /dev/null
+++ b/exporter/otlp-logs/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/exporter/otlp-logs/README.md b/exporter/otlp-logs/README.md
new file mode 100644
index 0000000000..0dc0805bef
--- /dev/null
+++ b/exporter/otlp-logs/README.md
@@ -0,0 +1,146 @@
+# opentelemetry-exporter-otlp-logs
+
+The `opentelemetry-exporter-otlp-logs` gem provides an [OTLP](https://github.com/open-telemetry/opentelemetry-proto) logs exporter for OpenTelemetry for Ruby. Using `opentelemetry-exporter-otlp-logs`, an application can configure OpenTelemetry to export collected log record data to [the OpenTelemetry Collector][opentelemetry-collector-home].
+
+## What is OpenTelemetry?
+
+[OpenTelemetry][opentelemetry-home] is an open source observability framework, providing a general-purpose API, SDK, and related tools required for the instrumentation of cloud-native software, frameworks, and libraries.
+
+OpenTelemetry provides a single set of APIs, libraries, agents, and collector services to capture distributed traces, metrics and logs from your application. You can analyze them using Prometheus, Jaeger, and other observability tools.
+
+## How does this gem fit in?
+
+The `opentelemetry-exporter-otlp-logs` gem is a plugin that provides OTLP export. To export to the OpenTelemetry Collector, an application can include this gem along with `opentelemetry-logs-sdk` and configure the SDK to use the provided OTLP exporter with a log record processor.
+
+Generally, *libraries* that produce telemetry data should avoid depending directly on specific exporter, deferring that choice to the application developer.
+
+### Supported protocol version
+
+This gem supports the [v0.20.0 release][otel-proto-release] of OTLP.
+
+## How do I get started?
+
+Install the gem using:
+
+```console
+
+gem install opentelemetry-logs-sdk
+gem install opentelemetry-exporter-otlp
+
+```
+
+Or, if you use [bundler][bundler-home], include `opentelemetry-sdk` in your `Gemfile`.
+
+Then, configure the SDK to use the OTLP exporter as a log record processor, and use the OpenTelemetry interfaces to produce log records. The following is a basic example.
+
+```ruby
+require 'opentelemetry-logs-sdk'
+require 'opentelemetry/exporter/otlp_logs'
+
+# Create a LoggerProvider
+logger_provider = OpenTelemetry::SDK::Logs::LoggerProvider.new
+# Create a batching processor configured to export to the OTLP exporter
+processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(OpenTelemetry::Exporter::OTLP::LogsExporter.new)
+# Add the processor to the LoggerProvider
+logger_provider.add_log_record_processor(processor)
+# Access a Logger for your library from your LoggerProvider
+logger = logger_provider.logger(name: 'my_app_or_gem', version: '0.1.0')
+
+# Use your Logger to emit a log record
+logger.on_emit(
+ timestamp: Time.now,
+ severity_text: 'INFO',
+ body: 'Thuja plicata',
+ attributes: { 'cedar' => true },
+)
+
+logger_provider.shutdown
+```
+
+For additional examples, see the [examples on github][examples-github].
+
+## How can I configure the OTLP logs exporter?
+
+The collector exporter can be configured explicitly in code, or via environment variables. The configuration parameters, environment variables, and defaults are shown below.
+
+| Parameter | Environment variable | Default |
+| ------------------------- | -------------------------------------------- | ----------------------------------- |
+| `endpoint:` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `"http://localhost:4318/v1/logs"` |
+| `certificate_file:` | `OTEL_EXPORTER_OTLP_CERTIFICATE` | |
+| `client_certificate_file` | `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` | |
+| `client_key_file` | `OTEL_EXPORTER_OTLP_CLIENT_KEY` | |
+| `headers:` | `OTEL_EXPORTER_OTLP_HEADERS` | |
+| `compression:` | `OTEL_EXPORTER_OTLP_COMPRESSION` | `"gzip"` |
+| `timeout:` | `OTEL_EXPORTER_OTLP_TIMEOUT` | `10` |
+| `ssl_verify_mode:` | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER` or | `OpenSSL::SSL:VERIFY_PEER` |
+| | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE` | |
+
+`ssl_verify_mode:` parameter values should be flags for server certificate verification: `OpenSSL::SSL:VERIFY_PEER` and `OpenSSL::SSL:VERIFY_NONE` are acceptable. These values can also be set using the appropriately named environment variables as shown where `VERIFY_PEER` will take precedence over `VERIFY_NONE`. Please see [the Net::HTTP docs](https://ruby-doc.org/stdlib-2.7.6/libdoc/net/http/rdoc/Net/HTTP.html#verify_mode) for more information about these flags.
+
+## How can I get involved?
+
+The `opentelemetry-exporter-otlp-logs` gem source is [on github][repo-github], along with related gems including `opentelemetry-logs-sdk`.
+
+The OpenTelemetry Ruby gems are maintained by the OpenTelemetry-Ruby special interest group (SIG). You can get involved by joining us in [GitHub Discussions][discussions-url] or attending our weekly meeting. See the [meeting calendar][community-meetings] for dates and times. For more information on this and other language SIGs, see the OpenTelemetry [community page][ruby-sig].
+
+## License
+
+The `opentelemetry-exporter-otlp-logs` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information.
+
+## Working with Proto Definitions
+
+The OTel community maintains a [repository with protobuf definitions][otel-proto-github] that language and collector implementors use to generate code.
+
+Maintainers are expected to keep up to date with the latest version of protos. This guide will provide you with step-by-step instructions on updating the OTLP Logs Exporter gem with the latest definitions.
+
+### System Requirements
+
+- [`git` 2.41+][git-install]
+- [`protoc` 22.5][protoc-install]
+- [Ruby 3+][ruby-downloads]
+
+> :warning: `protoc 23.x` *changes the Ruby code generator to emit a serialized proto instead of a DSL.* . Please ensure you use `protoc` version `22.x` in order to ensure we remain compatible with versions of protobuf prior to `google-protobuf` gem `3.18`.
+
+### Upgrade Proto Definitions
+
+**Update the target otel-proto version in the `Rakefile` that matches a release `tag` in the proto repo, e.g.**
+
+```ruby
+ # Rakefile
+
+ # https://github.com/open-telemetry/opentelemetry-proto/tree/v0.20.0
+ PROTO_VERSION = `v0.20.0`
+```
+
+**Generate the Ruby source files using `rake`:**
+
+```console
+
+$> bundle exec rake protobuf:generate
+
+```
+
+**Run tests and fix any errors:**
+
+```console
+
+$> bundle exec rake test
+
+```
+
+**Commit the chnages and open a PR!**
+
+[opentelemetry-collector-home]: https://opentelemetry.io/docs/collector/about/
+[opentelemetry-home]: https://opentelemetry.io
+[bundler-home]: https://bundler.io
+[repo-github]: https://github.com/open-telemetry/opentelemetry-ruby
+[license-github]: https://github.com/open-telemetry/opentelemetry-ruby/blob/main/LICENSE
+[examples-github]: https://github.com/open-telemetry/opentelemetry-ruby/tree/main/examples
+[ruby-sig]: https://github.com/open-telemetry/community#ruby-sig
+[community-meetings]: https://github.com/open-telemetry/community#community-meetings
+[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions
+[git-install]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
+[protoc-install]: https://github.com/protocolbuffers/protobuf/releases/tag/v22.5
+[ruby-downloads]: https://www.ruby-lang.org/en/downloads/
+[otel-proto-github]: https://github.com/open-telemetry/opentelemetry-proto
+[otel-proto-release]: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.20.0
diff --git a/exporter/otlp-logs/Rakefile b/exporter/otlp-logs/Rakefile
new file mode 100644
index 0000000000..ce1edbd6e9
--- /dev/null
+++ b/exporter/otlp-logs/Rakefile
@@ -0,0 +1,49 @@
+# 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.libs << '../../api/lib'
+ t.libs << '../../sdk/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
+
+# https://github.com/open-telemetry/opentelemetry-proto/tree/v0.20.0
+PROTO_VERSION = 'v0.20.0'
+
+namespace :protobuf do
+ task :clean do
+ FileUtils.rm_rf('lib/opentelemetry/proto')
+ FileUtils.rm_rf('opentelemetry-proto')
+ end
+
+ desc "Generate Ruby Source files from OTel Proto Version #{PROTO_VERSION}"
+ task generate: [:clean] do
+ system("git clone -b #{PROTO_VERSION} https://github.com/open-telemetry/opentelemetry-proto", exception: true)
+ Dir['opentelemetry-proto/opentelemetry/proto/**/*.proto'].each do |file|
+ system("protoc --ruby_out=lib/ --proto_path=opentelemetry-proto #{file.gsub('opentelemetry-proto/', '')}", exception: true)
+ end
+ FileUtils.rm_rf('opentelemetry-proto')
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry-exporter-otlp-logs.rb b/exporter/otlp-logs/lib/opentelemetry-exporter-otlp-logs.rb
new file mode 100644
index 0000000000..1af5afb085
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry-exporter-otlp-logs.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'opentelemetry/exporter/otlp_logs'
diff --git a/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs_exporter.rb b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs_exporter.rb
new file mode 100644
index 0000000000..3031809596
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs_exporter.rb
@@ -0,0 +1,377 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'opentelemetry/common'
+require 'opentelemetry/sdk'
+require 'opentelemetry-logs-api' # the sdk isn't loading the api, but not sure why
+require 'opentelemetry/sdk/logs'
+require 'net/http'
+require 'zlib'
+
+require 'google/rpc/status_pb'
+
+require 'opentelemetry/proto/common/v1/common_pb'
+require 'opentelemetry/proto/resource/v1/resource_pb'
+require 'opentelemetry/proto/logs/v1/logs_pb'
+require 'opentelemetry/proto/collector/logs/v1/logs_service_pb'
+
+module OpenTelemetry
+ module Exporter
+ module OTLP
+ # An OpenTelemetry log exporter that sends log records over HTTP as Protobuf encoded OTLP ExportLogsServiceRequests.
+ class LogsExporter # rubocop:disable Metrics/ClassLength
+ SUCCESS = OpenTelemetry::SDK::Logs::Export::SUCCESS
+ FAILURE = OpenTelemetry::SDK::Logs::Export::FAILURE
+ private_constant(:SUCCESS, :FAILURE)
+
+ # Default timeouts in seconds.
+ KEEP_ALIVE_TIMEOUT = 30
+ RETRY_COUNT = 5
+ private_constant(:KEEP_ALIVE_TIMEOUT, :RETRY_COUNT)
+
+ ERROR_MESSAGE_INVALID_HEADERS = 'headers must be a String with comma-separated URL Encoded UTF-8 k=v pairs or a Hash'
+ private_constant(:ERROR_MESSAGE_INVALID_HEADERS)
+
+ DEFAULT_USER_AGENT = "OTel-OTLP-Exporter-Ruby/#{OpenTelemetry::Exporter::OTLP::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})".freeze
+
+ def self.ssl_verify_mode
+ if ENV['OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER'] == 'true'
+ OpenSSL::SSL::VERIFY_PEER
+ elsif ENV['OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE'] == 'true'
+ OpenSSL::SSL::VERIFY_NONE
+ else
+ OpenSSL::SSL::VERIFY_PEER
+ end
+ end
+
+ def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'http://localhost:4318/v1/logs'),
+ certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'),
+ client_certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE'),
+ client_key_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY', 'OTEL_EXPORTER_OTLP_CLIENT_KEY'),
+ ssl_verify_mode: LogsExporter.ssl_verify_mode,
+ headers: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', default: {}),
+ compression: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION', default: 'gzip'),
+ timeout: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10))
+ raise ArgumentError, "invalid url for OTLP::LogsExporter #{endpoint}" unless OpenTelemetry::Common::Utilities.valid_url?(endpoint)
+ raise ArgumentError, "unsupported compression key #{compression}" unless compression.nil? || %w[gzip none].include?(compression)
+
+ @uri = if endpoint == ENV['OTEL_EXPORTER_OTLP_ENDPOINT']
+ URI.join(endpoint, 'v1/logs')
+ else
+ URI(endpoint)
+ end
+
+ @http = http_connection(@uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file)
+
+ @path = @uri.path
+ @headers = prepare_headers(headers)
+ @timeout = timeout.to_f
+ @compression = compression
+ @shutdown = false
+ end
+
+ # Called to export sampled {OpenTelemetry::SDK::Logs::LogRecordData} structs.
+ #
+ # @param [Enumerable] log_record_data the
+ # list of recorded {OpenTelemetry::SDK::Logs::LogRecordData} structs to be
+ # exported.
+ # @param [optional Numeric] timeout An optional timeout in seconds.
+ # @return [Integer] the result of the export.
+ def export(log_record_data, timeout: nil)
+ OpenTelemetry.logger.error('Logs Exporter tried to export, but it has already shut down') if @shutdown
+ return FAILURE if @shutdown
+
+ send_bytes(encode(log_record_data), timeout: timeout)
+ end
+
+ # Called when {OpenTelemetry::SDK::Logs::LoggerProvider#force_flush} is called, if
+ # this exporter is registered to a {OpenTelemetry::SDK::Logs::LoggerProvider}
+ # object.
+ #
+ # @param [optional Numeric] timeout An optional timeout in seconds.
+ def force_flush(timeout: nil)
+ SUCCESS
+ end
+
+ # Called when {OpenTelemetry::SDK::Logs::LoggerProvider#shutdown} is called, if
+ # this exporter is registered to a {OpenTelemetry::SDK::Logs::LoggerProvider}
+ # object.
+ #
+ # @param [optional Numeric] timeout An optional timeout in seconds.
+ def shutdown(timeout: nil)
+ @shutdown = true
+ @http.finish if @http.started?
+ SUCCESS
+ end
+
+ private
+
+ def handle_http_error(response)
+ OpenTelemetry.handle_error(message: "OTLP logs exporter received #{response.class.name}, http.code=#{response.code}, for uri: '#{@path}'")
+ end
+
+ def http_connection(uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file)
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = uri.scheme == 'https'
+ http.verify_mode = ssl_verify_mode
+ http.ca_file = certificate_file unless certificate_file.nil?
+ http.cert = OpenSSL::X509::Certificate.new(File.read(client_certificate_file)) unless client_certificate_file.nil?
+ http.key = OpenSSL::PKey::RSA.new(File.read(client_key_file)) unless client_key_file.nil?
+ http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
+ http
+ end
+
+ # The around_request is a private method that provides an extension
+ # point for the exporters network calls. The default behaviour
+ # is to not record these operations.
+ #
+ # An example use case would be to prepend a patch, or extend this class
+ # and override this method's behaviour to explicitly record the HTTP request.
+ # This would allow you to create log records for your export pipeline.
+ def around_request
+ OpenTelemetry::Common::Utilities.untraced { yield } # rubocop:disable Style/ExplicitBlockArgument
+ end
+
+ def send_bytes(bytes, timeout:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
+ return FAILURE if bytes.nil?
+
+ request = Net::HTTP::Post.new(@path)
+ if @compression == 'gzip'
+ request.add_field('Content-Encoding', 'gzip')
+ body = Zlib.gzip(bytes)
+ else
+ body = bytes
+ end
+
+ request.body = body
+ request.add_field('Content-Type', 'application/x-protobuf')
+ @headers.each { |key, value| request.add_field(key, value) }
+
+ retry_count = 0
+ timeout ||= @timeout
+ start_time = OpenTelemetry::Common::Utilities.timeout_timestamp
+
+ around_request do
+ remaining_timeout = OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start_time)
+ return FAILURE if remaining_timeout.zero?
+
+ @http.open_timeout = remaining_timeout
+ @http.read_timeout = remaining_timeout
+ @http.write_timeout = remaining_timeout
+ @http.start unless @http.started?
+ response = @http.request(request)
+
+ case response
+ when Net::HTTPOK
+ response.body # Read and discard body
+ SUCCESS
+ when Net::HTTPServiceUnavailable, Net::HTTPTooManyRequests
+ response.body # Read and discard body
+ handle_http_error(response)
+ redo if backoff?(retry_after: response['Retry-After'], retry_count: retry_count += 1)
+ FAILURE
+ when Net::HTTPRequestTimeOut, Net::HTTPGatewayTimeOut, Net::HTTPBadGateway
+ response.body # Read and discard body
+ handle_http_error(response)
+ redo if backoff?(retry_count: retry_count += 1)
+ FAILURE
+ when Net::HTTPNotFound
+ handle_http_error(response)
+ FAILURE
+ when Net::HTTPBadRequest, Net::HTTPClientError, Net::HTTPServerError
+ log_status(response.body)
+ FAILURE
+ when Net::HTTPRedirection
+ @http.finish
+ handle_redirect(response['location'])
+ redo if backoff?(retry_after: 0, retry_count: retry_count += 1)
+ else
+ @http.finish
+ handle_http_error(response)
+ FAILURE
+ end
+ rescue Net::OpenTimeout, Net::ReadTimeout => e
+ OpenTelemetry.handle_error(exception: e)
+ retry if backoff?(retry_count: retry_count += 1)
+ return FAILURE
+ rescue OpenSSL::SSL::SSLError => e
+ OpenTelemetry.handle_error(exception: e)
+ retry if backoff?(retry_count: retry_count += 1)
+ return FAILURE
+ rescue SocketError => e
+ OpenTelemetry.handle_error(exception: e)
+ retry if backoff?(retry_count: retry_count += 1)
+ return FAILURE
+ rescue SystemCallError => e
+ retry if backoff?(retry_count: retry_count += 1)
+ OpenTelemetry.handle_error(exception: e)
+ return FAILURE
+ rescue EOFError => e
+ OpenTelemetry.handle_error(exception: e)
+ retry if backoff?(retry_count: retry_count += 1)
+ return FAILURE
+ rescue Zlib::DataError => e
+ OpenTelemetry.handle_error(exception: e)
+ retry if backoff?(retry_count: retry_count += 1)
+ return FAILURE
+ rescue StandardError => e
+ OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#send_bytes')
+ return FAILURE
+ end
+ ensure
+ # Reset timeouts to defaults for the next call.
+ @http.open_timeout = @timeout
+ @http.read_timeout = @timeout
+ @http.write_timeout = @timeout
+ end
+
+ def handle_redirect(location)
+ # TODO: figure out destination and reinitialize @http and @path
+ end
+
+ def log_status(body)
+ status = Google::Rpc::Status.decode(body)
+ details = status.details.map do |detail|
+ klass_or_nil = ::Google::Protobuf::DescriptorPool.generated_pool.lookup(detail.type_name).msgclass
+ detail.unpack(klass_or_nil) if klass_or_nil
+ end.compact
+ OpenTelemetry.handle_error(message: "OTLP logs exporter received rpc.Status{message=#{status.message}, details=#{details}}")
+ rescue StandardError => e
+ OpenTelemetry.handle_error(exception: e, message: 'unexpected error decoding rpc.Status in OTLP::Exporter#log_status')
+ end
+
+ def backoff?(retry_count:, retry_after: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
+ return false if retry_count > RETRY_COUNT
+
+ sleep_interval = nil
+ unless retry_after.nil?
+ sleep_interval =
+ begin
+ Integer(retry_after)
+ rescue ArgumentError
+ nil
+ end
+ sleep_interval ||=
+ begin
+ Time.httpdate(retry_after) - Time.now
+ rescue # rubocop:disable Style/RescueStandardError
+ nil
+ end
+ sleep_interval = nil unless sleep_interval&.positive?
+ end
+ sleep_interval ||= rand(2**retry_count)
+
+ sleep(sleep_interval)
+ true
+ end
+
+ def encode(log_record_data) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
+ Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.encode(
+ Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.new(
+ resource_logs: log_record_data
+ .group_by(&:resource)
+ .map do |resource, log_record_datas|
+ Opentelemetry::Proto::Logs::V1::ResourceLogs.new(
+ resource: Opentelemetry::Proto::Resource::V1::Resource.new(
+ attributes: resource.attribute_enumerator.map { |key, value| as_otlp_key_value(key, value) }
+ ),
+ scope_logs: log_record_datas
+ .group_by(&:instrumentation_scope)
+ .map do |il, lrd|
+ Opentelemetry::Proto::Logs::V1::ScopeLogs.new(
+ scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new(
+ name: il.name,
+ version: il.version
+ ),
+ log_records: lrd.map { |lr| as_otlp_log_record(lr) }
+ )
+ end
+ )
+ end
+ )
+ )
+ rescue StandardError => e
+ OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#encode')
+ nil
+ end
+
+ def as_otlp_log_record(log_record_data)
+ Opentelemetry::Proto::Logs::V1::LogRecord.new(
+ time_unix_nano: log_record_data.timestamp,
+ observed_time_unix_nano: log_record_data.observed_timestamp,
+ severity_number: log_record_data.severity_number,
+ severity_text: log_record_data.severity_text,
+ body: as_otlp_any_value(log_record_data.body),
+ attributes: log_record_data.attributes&.map { |k, v| as_otlp_key_value(k, v) },
+ dropped_attributes_count: log_record_data.total_recorded_attributes - log_record_data.attributes&.size.to_i,
+ flags: log_record_data.trace_flags.instance_variable_get(:@flags),
+ trace_id: log_record_data.trace_id,
+ span_id: log_record_data.span_id
+ )
+ end
+
+ def as_otlp_key_value(key, value)
+ Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value(value))
+ rescue Encoding::UndefinedConversionError => e
+ encoded_value = value.encode('UTF-8', invalid: :replace, undef: :replace, replace: '�')
+ OpenTelemetry.handle_error(exception: e, message: "encoding error for key #{key} and value #{encoded_value}")
+ Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value('Encoding Error'))
+ end
+
+ def as_otlp_any_value(value)
+ result = Opentelemetry::Proto::Common::V1::AnyValue.new
+ case value
+ when String
+ result.string_value = value
+ when Integer
+ result.int_value = value
+ when Float
+ result.double_value = value
+ when true, false
+ result.bool_value = value
+ when Array
+ values = value.map { |element| as_otlp_any_value(element) }
+ result.array_value = Opentelemetry::Proto::Common::V1::ArrayValue.new(values: values)
+ end
+ result
+ end
+
+ def prepare_headers(config_headers)
+ headers = case config_headers
+ when String then parse_headers(config_headers)
+ when Hash then config_headers.dup
+ else
+ raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS
+ end
+
+ headers['User-Agent'] = "#{headers.fetch('User-Agent', '')} #{DEFAULT_USER_AGENT}".strip
+
+ headers
+ end
+
+ def parse_headers(raw)
+ entries = raw.split(',')
+ raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if entries.empty?
+
+ entries.each_with_object({}) do |entry, headers|
+ k, v = entry.split('=', 2).map(&CGI.method(:unescape))
+ begin
+ k = k.to_s.strip
+ v = v.to_s.strip
+ rescue Encoding::CompatibilityError
+ raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS
+ rescue ArgumentError => e
+ raise e, ERROR_MESSAGE_INVALID_HEADERS
+ end
+ raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if k.empty? || v.empty?
+
+ headers[k] = v
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/version.rb b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/version.rb
new file mode 100644
index 0000000000..d30a779f91
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/version.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+module OpenTelemetry
+ module Exporter
+ module OTLP
+ ## Current OpenTelemetry OTLP logs exporter version
+ VERSION = '0.1.0'
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/exporter/otlp_logs.rb b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp_logs.rb
new file mode 100644
index 0000000000..b1c041c5ed
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp_logs.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'opentelemetry/exporter/otlp/version'
+require 'opentelemetry/exporter/otlp/logs_exporter'
+
+# OpenTelemetry is an open source observability framework, providing a
+# general-purpose API, SDK, and related tools required for the instrumentation
+# of cloud-native software, frameworks, and libraries.
+#
+# The OpenTelemetry module provides global accessors for telemetry objects.
+# See the documentation for the `opentelemetry-api` gem for details.
+module OpenTelemetry
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb
new file mode 100644
index 0000000000..cdefc01c2a
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/collector/logs/v1/logs_service.proto
+
+require 'google/protobuf'
+
+require 'opentelemetry/proto/logs/v1/logs_pb'
+
+descriptor_data = "\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"u\n\x19\x45xportLogsServiceResponse\x12X\n\x0fpartial_success\x18\x01 \x01(\x0b\x32?.opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess\"O\n\x18\x45xportLogsPartialSuccess\x12\x1c\n\x14rejected_log_records\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42\x98\x01\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1\xaa\x02%OpenTelemetry.Proto.Collector.Logs.V1b\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Opentelemetry
+ module Proto
+ module Collector
+ module Logs
+ module V1
+ ExportLogsServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest').msgclass
+ ExportLogsServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse').msgclass
+ ExportLogsPartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess').msgclass
+ end
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb
new file mode 100644
index 0000000000..593d752e71
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/collector/metrics/v1/metrics_service.proto
+
+require 'google/protobuf'
+
+require 'opentelemetry/proto/metrics/v1/metrics_pb'
+
+descriptor_data = "\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"~\n\x1c\x45xportMetricsServiceResponse\x12^\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess\"R\n\x1b\x45xportMetricsPartialSuccess\x12\x1c\n\x14rejected_data_points\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\xa4\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\xaa\x02(OpenTelemetry.Proto.Collector.Metrics.V1b\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Opentelemetry
+ module Proto
+ module Collector
+ module Metrics
+ module V1
+ ExportMetricsServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest').msgclass
+ ExportMetricsServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse').msgclass
+ ExportMetricsPartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess').msgclass
+ end
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb
new file mode 100644
index 0000000000..ae8226063c
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/collector/trace/v1/trace_service.proto
+
+require 'google/protobuf'
+
+require 'opentelemetry/proto/trace/v1/trace_pb'
+
+descriptor_data = "\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"x\n\x1a\x45xportTraceServiceResponse\x12Z\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x41.opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess\"J\n\x19\x45xportTracePartialSuccess\x12\x16\n\x0erejected_spans\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42\x9c\x01\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1\xaa\x02&OpenTelemetry.Proto.Collector.Trace.V1b\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Opentelemetry
+ module Proto
+ module Collector
+ module Trace
+ module V1
+ ExportTraceServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest').msgclass
+ ExportTraceServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse').msgclass
+ ExportTracePartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess').msgclass
+ end
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb
new file mode 100644
index 0000000000..919181359c
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/common/v1/common.proto
+
+require 'google/protobuf'
+
+descriptor_data = "\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"\x94\x01\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\rB{\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1\xaa\x02\x1dOpenTelemetry.Proto.Common.V1b\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Opentelemetry
+ module Proto
+ module Common
+ module V1
+ AnyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.AnyValue').msgclass
+ ArrayValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.ArrayValue').msgclass
+ KeyValueList = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.KeyValueList').msgclass
+ KeyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.KeyValue').msgclass
+ InstrumentationScope = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.InstrumentationScope').msgclass
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb
new file mode 100644
index 0000000000..2e59dad95a
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/logs/v1/logs.proto
+
+require 'google/protobuf'
+
+require 'opentelemetry/proto/common/v1/common_pb'
+require 'opentelemetry/proto/resource/v1/resource_pb'
+
+descriptor_data = "\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xa3\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xef\x02\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0cJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*Y\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAGS_DO_NOT_USE\x10\x00\x12&\n!LOG_RECORD_FLAGS_TRACE_FLAGS_MASK\x10\xff\x01\x42s\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1\xaa\x02\x1bOpenTelemetry.Proto.Logs.V1b\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Opentelemetry
+ module Proto
+ module Logs
+ module V1
+ LogsData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.LogsData').msgclass
+ ResourceLogs = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.ResourceLogs').msgclass
+ ScopeLogs = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.ScopeLogs').msgclass
+ LogRecord = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.LogRecord').msgclass
+ SeverityNumber = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.SeverityNumber').enummodule
+ LogRecordFlags = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.LogRecordFlags').enummodule
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb
new file mode 100644
index 0000000000..8ce9521271
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/metrics/v1/metrics.proto
+
+require 'google/protobuf'
+
+require 'opentelemetry/proto/common/v1/common_pb'
+require 'opentelemetry/proto/resource/v1/resource_pb'
+
+descriptor_data = "\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\xaf\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xda\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x02\x88\x01\x01\x12\x16\n\x0ezero_threshold\x18\x0e \x01(\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*^\n\x0e\x44\x61taPointFlags\x12\x1f\n\x1b\x44\x41TA_POINT_FLAGS_DO_NOT_USE\x10\x00\x12+\n\'DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK\x10\x01\x42\x7f\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1\xaa\x02\x1eOpenTelemetry.Proto.Metrics.V1b\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Opentelemetry
+ module Proto
+ module Metrics
+ module V1
+ MetricsData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.MetricsData').msgclass
+ ResourceMetrics = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ResourceMetrics').msgclass
+ ScopeMetrics = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ScopeMetrics').msgclass
+ Metric = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Metric').msgclass
+ Gauge = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Gauge').msgclass
+ Sum = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Sum').msgclass
+ Histogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Histogram').msgclass
+ ExponentialHistogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ExponentialHistogram').msgclass
+ Summary = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Summary').msgclass
+ NumberDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.NumberDataPoint').msgclass
+ HistogramDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.HistogramDataPoint').msgclass
+ ExponentialHistogramDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint').msgclass
+ ExponentialHistogramDataPoint::Buckets = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets').msgclass
+ SummaryDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.SummaryDataPoint').msgclass
+ SummaryDataPoint::ValueAtQuantile = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile').msgclass
+ Exemplar = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Exemplar').msgclass
+ AggregationTemporality = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.AggregationTemporality').enummodule
+ DataPointFlags = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.DataPointFlags').enummodule
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb
new file mode 100644
index 0000000000..adfb9add9f
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/resource/v1/resource.proto
+
+require 'google/protobuf'
+
+require 'opentelemetry/proto/common/v1/common_pb'
+
+descriptor_data = "\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rB\x83\x01\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1\xaa\x02\x1fOpenTelemetry.Proto.Resource.V1b\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Opentelemetry
+ module Proto
+ module Resource
+ module V1
+ Resource = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.resource.v1.Resource').msgclass
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb
new file mode 100644
index 0000000000..43b719989c
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/trace/v1/trace.proto
+
+require 'google/protobuf'
+
+require 'opentelemetry/proto/common/v1/common_pb'
+require 'opentelemetry/proto/resource/v1/resource_pb'
+
+descriptor_data = "\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\xa7\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42w\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1\xaa\x02\x1cOpenTelemetry.Proto.Trace.V1b\x06proto3"
+
+pool = Google::Protobuf::DescriptorPool.generated_pool
+pool.add_serialized_file(descriptor_data)
+
+module Opentelemetry
+ module Proto
+ module Trace
+ module V1
+ TracesData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.TracesData').msgclass
+ ResourceSpans = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.ResourceSpans').msgclass
+ ScopeSpans = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.ScopeSpans').msgclass
+ Span = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Span').msgclass
+ Span::Event = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Span.Event').msgclass
+ Span::Link = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Span.Link').msgclass
+ Span::SpanKind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Span.SpanKind').enummodule
+ Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Status').msgclass
+ Status::StatusCode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Status.StatusCode').enummodule
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/opentelemetry-exporter-otlp-logs.gemspec b/exporter/otlp-logs/opentelemetry-exporter-otlp-logs.gemspec
new file mode 100644
index 0000000000..a7ae777557
--- /dev/null
+++ b/exporter/otlp-logs/opentelemetry-exporter-otlp-logs.gemspec
@@ -0,0 +1,56 @@
+# 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/exporter/otlp/version'
+
+Gem::Specification.new do |spec|
+ spec.name = 'opentelemetry-exporter-otlp-logs'
+ spec.version = OpenTelemetry::Exporter::OTLP::VERSION
+ spec.authors = ['OpenTelemetry Authors']
+ spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io']
+
+ spec.summary = 'Experimental OTLP Logs exporter for the OpenTelemetry framework'
+ spec.description = 'Experimental OTLP Logs exporter for the OpenTelemetry 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 = '>= 3.0'
+
+ spec.add_dependency 'googleapis-common-protos-types', '~> 1.3'
+ spec.add_dependency 'google-protobuf', '>= 3.18'
+ spec.add_dependency 'opentelemetry-api', '~> 1.1'
+ spec.add_dependency 'opentelemetry-common', '~> 0.20'
+ spec.add_dependency 'opentelemetry-logs-api', '~> 0.1'
+ spec.add_dependency 'opentelemetry-logs-sdk', '~> 0.1'
+ spec.add_dependency 'opentelemetry-sdk'
+ spec.add_dependency 'opentelemetry-semantic_conventions'
+
+ spec.add_development_dependency 'appraisal', '~> 2.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 'pry-byebug' unless RUBY_ENGINE == 'jruby'
+ spec.add_development_dependency 'rake', '~> 12.0'
+ spec.add_development_dependency 'rubocop', '~> 1.3'
+ spec.add_development_dependency 'simplecov', '~> 0.17'
+ spec.add_development_dependency 'webmock', '~> 3.7.6'
+ 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-exporter-otlp/v#{OpenTelemetry::Exporter::OTLP::VERSION}/file.CHANGELOG.html"
+ spec.metadata['source_code_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby/tree/main/exporter/otlp'
+ 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-exporter-otlp/v#{OpenTelemetry::Exporter::OTLP::VERSION}"
+ end
+end
diff --git a/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb
new file mode 100644
index 0000000000..0b5b1745b1
--- /dev/null
+++ b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb
@@ -0,0 +1,748 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+require 'test_helper'
+require 'google/protobuf/wrappers_pb'
+require 'google/protobuf/well_known_types'
+
+describe OpenTelemetry::Exporter::OTLP::LogsExporter do
+ SUCCESS = OpenTelemetry::SDK::Logs::Export::SUCCESS
+ FAILURE = OpenTelemetry::SDK::Logs::Export::FAILURE
+ VERSION = OpenTelemetry::Exporter::OTLP::VERSION
+ DEFAULT_USER_AGENT = OpenTelemetry::Exporter::OTLP::LogsExporter::DEFAULT_USER_AGENT
+ CLIENT_CERT_A_PATH = File.dirname(__FILE__) + '/mtls-client-a.pem'
+ CLIENT_CERT_A = OpenSSL::X509::Certificate.new(File.read(CLIENT_CERT_A_PATH))
+ CLIENT_KEY_A = OpenSSL::PKey::RSA.new(File.read(CLIENT_CERT_A_PATH))
+ CLIENT_CERT_B_PATH = File.dirname(__FILE__) + '/mtls-client-b.pem'
+ CLIENT_CERT_B = OpenSSL::X509::Certificate.new(File.read(CLIENT_CERT_B_PATH))
+ CLIENT_KEY_B = OpenSSL::PKey::RSA.new(File.read(CLIENT_CERT_B_PATH))
+
+ describe '#initialize' do
+ it 'initializes with defaults' do
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ _(exp).wont_be_nil
+ _(exp.instance_variable_get(:@headers)).must_equal('User-Agent' => DEFAULT_USER_AGENT)
+ _(exp.instance_variable_get(:@timeout)).must_equal 10.0
+ _(exp.instance_variable_get(:@path)).must_equal '/v1/logs'
+ _(exp.instance_variable_get(:@compression)).must_equal 'gzip'
+ http = exp.instance_variable_get(:@http)
+ _(http.ca_file).must_be_nil
+ _(http.cert).must_be_nil
+ _(http.key).must_be_nil
+ _(http.use_ssl?).must_equal false
+ _(http.address).must_equal 'localhost'
+ _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_PEER
+ _(http.port).must_equal 4318
+ end
+
+ it 'provides a useful, spec-compliant default user agent header' do
+ # spec compliance: OTLP Exporter name and version
+ _(DEFAULT_USER_AGENT).must_match("OTel-OTLP-Exporter-Ruby/#{VERSION}")
+ # bonus: incredibly useful troubleshooting information
+ _(DEFAULT_USER_AGENT).must_match("Ruby/#{RUBY_VERSION}")
+ _(DEFAULT_USER_AGENT).must_match(RUBY_PLATFORM)
+ _(DEFAULT_USER_AGENT).must_match("#{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION}")
+ end
+
+ it 'refuses invalid endpoint' do
+ assert_raises ArgumentError do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'not a url')
+ end
+ end
+
+ it 'uses endpoints path if provided' do
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'https://localhost/custom/path')
+ _(exp.instance_variable_get(:@path)).must_equal '/custom/path'
+ end
+
+ it 'only allows gzip compression or none' do
+ assert_raises ArgumentError do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new(compression: 'flate')
+ end
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(compression: nil)
+ _(exp.instance_variable_get(:@compression)).must_be_nil
+
+ %w[gzip none].each do |compression|
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(compression: compression)
+ _(exp.instance_variable_get(:@compression)).must_equal(compression)
+ end
+
+ [
+ { envar: 'OTEL_EXPORTER_OTLP_COMPRESSION', value: 'gzip' },
+ { envar: 'OTEL_EXPORTER_OTLP_COMPRESSION', value: 'none' },
+ { envar: 'OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', value: 'gzip' },
+ { envar: 'OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', value: 'none' }
+ ].each do |example|
+ OpenTelemetry::TestHelpers.with_env(example[:envar] => example[:value]) do
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ _(exp.instance_variable_get(:@compression)).must_equal(example[:value])
+ end
+ end
+ end
+
+ it 'sets parameters from the environment' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234',
+ 'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar/cacert',
+ 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE' => CLIENT_CERT_A_PATH,
+ 'OTEL_EXPORTER_OTLP_CLIENT_KEY' => CLIENT_CERT_A_PATH,
+ 'OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d',
+ 'OTEL_EXPORTER_OTLP_COMPRESSION' => 'gzip',
+ 'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true',
+ 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'User-Agent' => DEFAULT_USER_AGENT)
+ _(exp.instance_variable_get(:@timeout)).must_equal 11.0
+ _(exp.instance_variable_get(:@path)).must_equal '/v1/logs'
+ _(exp.instance_variable_get(:@compression)).must_equal 'gzip'
+ http = exp.instance_variable_get(:@http)
+ _(http.ca_file).must_equal '/foo/bar/cacert'
+ # Quality check fails in JRuby
+ _(http.cert).must_equal CLIENT_CERT_A unless RUBY_ENGINE == 'jruby'
+ _(http.key.params).must_equal CLIENT_KEY_A.params
+ _(http.use_ssl?).must_equal true
+ _(http.address).must_equal 'localhost'
+ _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE
+ _(http.port).must_equal 1234
+ end
+
+ it 'prefers explicit parameters rather than the environment' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234',
+ 'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar',
+ 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE' => CLIENT_CERT_A_PATH,
+ 'OTEL_EXPORTER_OTLP_CLIENT_KEY' => CLIENT_CERT_A_PATH,
+ 'OTEL_EXPORTER_OTLP_HEADERS' => 'a:b,c:d',
+ 'OTEL_EXPORTER_OTLP_COMPRESSION' => 'flate',
+ 'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER' => 'true',
+ 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'http://localhost:4321',
+ certificate_file: '/baz',
+ client_certificate_file: CLIENT_CERT_B_PATH,
+ client_key_file: CLIENT_CERT_B_PATH,
+ headers: { 'x' => 'y' },
+ compression: 'gzip',
+ ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE,
+ timeout: 12)
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('x' => 'y', 'User-Agent' => DEFAULT_USER_AGENT)
+ _(exp.instance_variable_get(:@timeout)).must_equal 12.0
+ _(exp.instance_variable_get(:@path)).must_equal ''
+ _(exp.instance_variable_get(:@compression)).must_equal 'gzip'
+ http = exp.instance_variable_get(:@http)
+ _(http.ca_file).must_equal '/baz'
+ # equality check fails in JRuby
+ _(http.cert).must_equal CLIENT_CERT_B unless RUBY_ENGINE == 'jruby'
+ _(http.key.params).must_equal CLIENT_KEY_B.params
+ _(http.use_ssl?).must_equal false
+ _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE
+ _(http.address).must_equal 'localhost'
+ _(http.port).must_equal 4321
+ end
+
+ it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT has a trailing slash' do
+ exp = OpenTelemetry::TestHelpers.with_env(
+ 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/'
+ ) do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@path)).must_equal '/v1/logs'
+ end
+
+ it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT does not have a trailing slash' do
+ exp = OpenTelemetry::TestHelpers.with_env(
+ 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234'
+ ) do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@path)).must_equal '/v1/logs'
+ end
+
+ it 'restricts explicit headers to a String or Hash' do
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: { 'token' => 'über' })
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
+
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: 'token=%C3%BCber')
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
+
+ error = _ do
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: Object.new)
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über')
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+ end
+
+ it 'ignores later mutations of a headers Hash parameter' do
+ a_hash_to_mutate_later = { 'token' => 'über' }
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: a_hash_to_mutate_later)
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
+
+ a_hash_to_mutate_later['token'] = 'unter'
+ a_hash_to_mutate_later['oops'] = 'i forgot to add this, too'
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
+ end
+
+ describe 'Headers Environment Variable' do
+ it 'allows any number of the equal sign (=) characters in the value' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d==,e=f') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd==', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT)
+
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'a=b,c=d==,e=f') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd==', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT)
+ end
+
+ it 'trims any leading or trailing whitespaces in keys and values' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a = b ,c=d , e=f') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT)
+
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'a = b ,c=d , e=f') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT)
+ end
+
+ it 'decodes values as URL encoded UTF-8 strings' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'token=%C3%BCber') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
+
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => '%C3%BCber=token') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('über' => 'token', 'User-Agent' => DEFAULT_USER_AGENT)
+
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'token=%C3%BCber') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
+
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => '%C3%BCber=token') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('über' => 'token', 'User-Agent' => DEFAULT_USER_AGENT)
+ end
+
+ it 'appends the default user agent to one provided in config' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'User-Agent=%C3%BCber/3.2.1') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('User-Agent' => "über/3.2.1 #{DEFAULT_USER_AGENT}")
+ end
+
+ it 'prefers LOGS specific variable' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d==,e=f', 'OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'token=%C3%BCber') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT)
+ end
+
+ it 'fails fast when header values are missing' do
+ error = _ do
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a = ') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+
+ error = _ do
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'a = ') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+ end
+
+ it 'fails fast when header or values are not found' do
+ error = _ do
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => ',') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+
+ error = _ do
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => ',') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+ end
+
+ it 'fails fast when header values contain invalid escape characters' do
+ error = _ do
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'c=hi%F3') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+
+ error = _ do
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'c=hi%F3') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+ end
+
+ it 'fails fast when headers are invalid' do
+ error = _ do
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'this is not a header') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+
+ error = _ do
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'this is not a header') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ end.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+ end
+ end
+ end
+
+ describe 'ssl_verify_mode:' do
+ it 'can be set to VERIFY_NONE by an envvar' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ http = exp.instance_variable_get(:@http)
+ _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE
+ end
+
+ it 'can be set to VERIFY_PEER by an envvar' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER' => 'true') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ http = exp.instance_variable_get(:@http)
+ _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_PEER
+ end
+
+ it 'VERIFY_PEER will override VERIFY_NONE' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true',
+ 'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER' => 'true') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ http = exp.instance_variable_get(:@http)
+ _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_PEER
+ end
+ end
+
+ describe '#export' do
+ let(:exporter) { OpenTelemetry::Exporter::OTLP::LogsExporter.new }
+ # TODO: replace with a before block to set a global logger provider through OpenTelemetry.logger_provider when the API code is merged
+ let(:logger_provider) { OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: OpenTelemetry::SDK::Resources::Resource.telemetry_sdk) }
+
+ it 'integrates with collector' do
+ skip unless ENV['TRACING_INTEGRATION_TEST']
+ WebMock.disable_net_connect!(allow: 'localhost')
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ exporter = OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'http://localhost:4318', compression: 'gzip')
+ result = exporter.export([log_record_data])
+ _(result).must_equal(SUCCESS)
+ end
+
+ it 'retries on timeout' do
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_timeout.then.to_return(status: 200)
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ result = exporter.export([log_record_data])
+ _(result).must_equal(SUCCESS)
+ end
+
+ it 'returns FAILURE on timeout' do
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200)
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ result = exporter.export([log_record_data], timeout: 0)
+ _(result).must_equal(FAILURE)
+ end
+
+ it 'returns FAILURE on unexpected exceptions' do
+ log_stream = StringIO.new
+ logger = OpenTelemetry.logger
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_raise('something unexpected')
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ result = exporter.export([log_record_data], timeout: 1)
+
+ _(log_stream.string).must_match(
+ /ERROR -- : OpenTelemetry error: unexpected error in OTLP::Exporter#send_bytes - something unexpected/
+ )
+
+ _(result).must_equal(FAILURE)
+ ensure
+ OpenTelemetry.logger = logger
+ end
+
+ it 'handles encoding failures' do
+ log_stream = StringIO.new
+ logger = OpenTelemetry.logger
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200)
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+
+ Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.stub(:encode, ->(_) { raise 'a little hell' }) do
+ _(exporter.export([log_record_data], timeout: 1)).must_equal(FAILURE)
+ end
+
+ _(log_stream.string).must_match(
+ /ERROR -- : OpenTelemetry error: unexpected error in OTLP::Exporter#encode - a little hell/
+ )
+ ensure
+ OpenTelemetry.logger = logger
+ end
+
+ { 'Net::HTTPServiceUnavailable' => 503,
+ 'Net::HTTPTooManyRequests' => 429,
+ 'Net::HTTPRequestTimeout' => 408,
+ 'Net::HTTPGatewayTimeout' => 504,
+ 'Net::HTTPBadGateway' => 502,
+ 'Net::HTTPNotFound' => 404 }.each do |klass, code|
+ it "logs an error and returns FAILURE with #{code}s" do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.stub_const(:RETRY_COUNT, 0) do
+ log_stream = StringIO.new
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: code)
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ _(exporter.export([log_record_data])).must_equal(FAILURE)
+ _(log_stream.string).must_match(
+ %r{ERROR -- : OpenTelemetry error: OTLP logs exporter received #{klass}, http.code=#{code}, for uri: '/v1/logs'}
+ )
+ end
+ end
+ end
+
+ [
+ Net::OpenTimeout,
+ Net::ReadTimeout,
+ OpenSSL::SSL::SSLError,
+ SocketError,
+ EOFError,
+ Zlib::DataError
+ ].each do |error|
+ it "logs error and returns FAILURE when #{error} is raised" do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.stub_const(:RETRY_COUNT, 0) do
+ log_stream = StringIO.new
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_raise(error.send(:new))
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ _(exporter.export([log_record_data])).must_equal(FAILURE)
+ _(log_stream.string).must_match(
+ /ERROR -- : OpenTelemetry error: #{error}/
+ )
+ end
+ end
+ end
+
+ it 'works with a SystemCallError' do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.stub_const(:RETRY_COUNT, 0) do
+ log_stream = StringIO.new
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_raise(SystemCallError.new('Failed to open TCP connection', 61))
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ _(exporter.export([log_record_data])).must_equal(FAILURE)
+ _(log_stream.string).must_match(
+ /ERROR -- : OpenTelemetry error:.*Failed to open TCP connection/
+ )
+ end
+ end
+
+ it 'returns FAILURE on timeout after retrying' do
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_timeout.then.to_raise('this should not be reached')
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+
+ @retry_count = 0
+ backoff_stubbed_call = lambda do |**_args|
+ sleep(0.10)
+ @retry_count += 1
+ true
+ end
+
+ exporter.stub(:backoff?, backoff_stubbed_call) do
+ _(exporter.export([log_record_data], timeout: 0.1)).must_equal(FAILURE)
+ end
+ ensure
+ @retry_count = 0
+ end
+
+ it 'returns FAILURE when shutdown' do
+ exporter.shutdown
+ result = exporter.export(nil)
+ _(result).must_equal(FAILURE)
+ end
+
+ it 'returns FAILURE when encryption to receiver endpoint fails' do
+ log_stream = StringIO.new
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+
+ exporter = OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'https://localhost:4318/v1/logs')
+ stub_request(:post, 'https://localhost:4318/v1/logs').to_raise(OpenSSL::SSL::SSLError.new('enigma wedged'))
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ exporter.stub(:backoff?, ->(**_) { false }) do
+ _(exporter.export([log_record_data])).must_equal(FAILURE)
+
+ _(log_stream.string).must_match(
+ /ERROR -- : OpenTelemetry error: enigma wedged/
+ )
+ end
+ end
+
+ it 'exports a log_record_data' do
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200)
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ result = exporter.export([log_record_data])
+ _(result).must_equal(SUCCESS)
+ end
+
+ it 'handles encoding errors with poise and grace' do
+ log_stream = StringIO.new
+ logger = OpenTelemetry.logger
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200)
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data(total_recorded_attributes: 1, attributes: { 'a' => "\xC2".dup.force_encoding(::Encoding::ASCII_8BIT) })
+
+ result = exporter.export([log_record_data])
+
+ _(log_stream.string).must_match(
+ /ERROR -- : OpenTelemetry error: encoding error for key a and value �/
+ )
+
+ _(result).must_equal(SUCCESS)
+ ensure
+ OpenTelemetry.logger = logger
+ end
+
+ it 'logs rpc.Status on bad request' do
+ log_stream = StringIO.new
+ logger = OpenTelemetry.logger
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+
+ details = [::Google::Protobuf::Any.pack(::Google::Protobuf::StringValue.new(value: 'you are a bad request'))]
+ status = ::Google::Rpc::Status.encode(::Google::Rpc::Status.new(code: 1, message: 'bad request', details: details))
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 400, body: status, headers: { 'Content-Type' => 'application/x-protobuf' })
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+
+ result = exporter.export([log_record_data])
+
+ _(log_stream.string).must_match(
+ /ERROR -- : OpenTelemetry error: OTLP logs exporter received rpc.Status{message=bad request, details=\[.*you are a bad request.*\]}/
+ )
+
+ _(result).must_equal(FAILURE)
+ ensure
+ OpenTelemetry.logger = logger
+ end
+
+ it 'logs a specific message when there is a 404' do
+ log_stream = StringIO.new
+ logger = OpenTelemetry.logger
+ OpenTelemetry.logger = ::Logger.new(log_stream)
+
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 404, body: "Not Found\n")
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+
+ result = exporter.export([log_record_data])
+
+ _(log_stream.string).must_match(
+ %r{ERROR -- : OpenTelemetry error: OTLP logs exporter received Net::HTTPNotFound, http.code=404, for uri: '/v1/logs'\n}
+ )
+
+ _(result).must_equal(FAILURE)
+ ensure
+ OpenTelemetry.logger = logger
+ end
+
+ it 'handles Zlib gzip compression errors' do
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_raise(Zlib::DataError.new('data error'))
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ exporter.stub(:backoff?, ->(**_) { false }) do
+ _(exporter.export([log_record_data])).must_equal(FAILURE)
+ end
+ end
+
+ it 'exports a log record from a logger' do
+ stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200)
+ processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter, max_queue_size: 1, max_export_batch_size: 1)
+ logger_provider.add_log_record_processor(processor)
+ logger_provider.logger(name: 'test').on_emit(body: 'test')
+ logger_provider.shutdown
+ assert_requested(stub_post)
+ end
+
+ it 'compresses with gzip if enabled' do
+ exporter = OpenTelemetry::Exporter::OTLP::LogsExporter.new(compression: 'gzip')
+ stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return do |request|
+ Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(Zlib.gunzip(request.body))
+ { status: 200 }
+ end
+
+ log_record_data = OpenTelemetry::TestHelpers.create_log_record_data
+ result = exporter.export([log_record_data])
+
+ _(result).must_equal(SUCCESS)
+ assert_requested(stub_post)
+ end
+
+ it 'batches per resource' do
+ etsr = nil
+ stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return do |request|
+ proto = Zlib.gunzip(request.body)
+ etsr = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(proto)
+ { status: 200 }
+ end
+
+ log_record_data1 = OpenTelemetry::TestHelpers.create_log_record_data(resource: OpenTelemetry::SDK::Resources::Resource.create('k1' => 'v1'))
+ log_record_data2 = OpenTelemetry::TestHelpers.create_log_record_data(resource: OpenTelemetry::SDK::Resources::Resource.create('k2' => 'v2'))
+
+ result = exporter.export([log_record_data1, log_record_data2])
+
+ _(result).must_equal(SUCCESS)
+ assert_requested(stub_post)
+ _(etsr.resource_logs.length).must_equal(2)
+ end
+
+ it 'translates all the things' do
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200)
+ processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter)
+ logger = logger_provider.logger(name: 'logger', version: 'v0.0.1')
+ other_logger = logger_provider.logger(name: 'other_logger', version: 'v0.1.0')
+
+ lr1 = {
+ timestamp: Time.now,
+ observed_timestamp: Time.now + 1,
+ severity_text: 'DEBUG',
+ severity_number: 5,
+ body: 'log_1',
+ attributes: { 'b' => true },
+ trace_id: OpenTelemetry::Trace.generate_trace_id,
+ span_id: OpenTelemetry::Trace.generate_span_id,
+ trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT,
+ context: OpenTelemetry::Context.current
+ }
+
+ lr2 = {
+ timestamp: Time.now + 2,
+ observed_timestamp: Time.now + 3,
+ severity_text: 'WARN',
+ severity_number: 13,
+ body: 'log_1',
+ attributes: { 'a' => false },
+ trace_id: OpenTelemetry::Trace.generate_trace_id,
+ span_id: OpenTelemetry::Trace.generate_span_id,
+ trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT,
+ context: OpenTelemetry::Context.current
+ }
+
+ lr3 = {
+ timestamp: Time.now + 4,
+ observed_timestamp: Time.now + 5,
+ severity_text: 'ERROR',
+ severity_number: 17,
+ body: 'log_1',
+ attributes: { 'c' => 12_345 },
+ trace_id: OpenTelemetry::Trace.generate_trace_id,
+ span_id: OpenTelemetry::Trace.generate_span_id,
+ trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT,
+ context: OpenTelemetry::Context.current
+ }
+
+ logger_provider.add_log_record_processor(processor)
+ logger.on_emit(**lr1)
+ logger.on_emit(**lr2)
+ other_logger.on_emit(**lr3)
+ logger_provider.shutdown
+ encoded_etsr = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.encode(
+ Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.new(
+ resource_logs: [
+ Opentelemetry::Proto::Logs::V1::ResourceLogs.new(
+ resource: Opentelemetry::Proto::Resource::V1::Resource.new(
+ attributes: [
+ Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.name', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'opentelemetry')),
+ Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.language', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'ruby')),
+ Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.version', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: OpenTelemetry::SDK::VERSION))
+ ]
+ ),
+ scope_logs: [
+ Opentelemetry::Proto::Logs::V1::ScopeLogs.new(
+ scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new(
+ name: 'logger',
+ version: 'v0.0.1'
+ ),
+ log_records: [
+ Opentelemetry::Proto::Logs::V1::LogRecord.new(
+ time_unix_nano: (lr1[:timestamp].to_r * 1_000_000_000).to_i,
+ observed_time_unix_nano: (lr1[:observed_timestamp].to_r * 1_000_000_000).to_i,
+ severity_number: 5,
+ severity_text: lr1[:severity_text],
+ body: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: lr1[:body]),
+ attributes: [
+ Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'b', value: Opentelemetry::Proto::Common::V1::AnyValue.new(bool_value: true))
+ ],
+ dropped_attributes_count: 0,
+ flags: lr1[:trace_flags].instance_variable_get(:@flags),
+ trace_id: lr1[:trace_id],
+ span_id: lr1[:span_id]
+ ),
+ Opentelemetry::Proto::Logs::V1::LogRecord.new(
+ time_unix_nano: (lr2[:timestamp].to_r * 1_000_000_000).to_i,
+ observed_time_unix_nano: (lr2[:observed_timestamp].to_r * 1_000_000_000).to_i,
+ severity_number: 13,
+ severity_text: lr2[:severity_text],
+ body: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: lr2[:body]),
+ attributes: [
+ Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'a', value: Opentelemetry::Proto::Common::V1::AnyValue.new(bool_value: false))
+ ],
+ dropped_attributes_count: 0,
+ flags: lr2[:trace_flags].instance_variable_get(:@flags),
+ trace_id: lr2[:trace_id],
+ span_id: lr2[:span_id]
+ )
+ ]
+ ),
+ Opentelemetry::Proto::Logs::V1::ScopeLogs.new(
+ scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new(
+ name: 'other_logger',
+ version: 'v0.1.0'
+ ),
+ log_records: [
+ Opentelemetry::Proto::Logs::V1::LogRecord.new(
+ time_unix_nano: (lr3[:timestamp].to_r * 1_000_000_000).to_i,
+ observed_time_unix_nano: (lr3[:observed_timestamp].to_r * 1_000_000_000).to_i,
+ severity_number: 17,
+ severity_text: lr3[:severity_text],
+ body: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: lr3[:body]),
+ attributes: [
+ Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'c', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 12_345))
+ ],
+ dropped_attributes_count: 0,
+ flags: lr3[:trace_flags].instance_variable_get(:@flags),
+ trace_id: lr3[:trace_id],
+ span_id: lr3[:span_id]
+ )
+ ]
+ )
+ ]
+ )
+ ]
+ )
+ )
+
+ assert_requested(:post, 'http://localhost:4318/v1/logs') do |req|
+ req.body == Zlib.gzip(encoded_etsr)
+ end
+ end
+ end
+end
diff --git a/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-a.pem b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-a.pem
new file mode 100644
index 0000000000..ac4e9c42f0
--- /dev/null
+++ b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-a.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+MIIDmzCCAoOgAwIBAgIUGfmv/4kRRFbg319TIHkcwDMC3pUwDQYJKoZIhvcNAQEL
+BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNYS5leGFtcGxlLnRs
+ZDAeFw0yNDAzMDYyMjM4MzNaFw0yNDAzMDcyMjM4MzNaMF0xCzAJBgNVBAYTAkFV
+MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz
+IFB0eSBMdGQxFjAUBgNVBAMMDWEuZXhhbXBsZS50bGQwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCaJG6fvdjFs9cnbF8i3wzO3VPUFH4hbAg5pV6rs81s
+LuJnnlG3WX1sxYQGASqLKIzPiz3g4nKFaBfXvBYXo7M/AuJ2tEspedIcvdTqwx2k
+owaaX7Y9lSx+h1OhovrviCqrqX5t9cIZWSeTU1bETcoTEd9/5usVe3XeaqmY8mAP
+OA0dBKeotGIOxtTEP23CxW1AJwWPLZC1go8ycvsTQfmeif+g+6BOcKeZxkayhCvo
+Ous+dt2dXa3x3yROe8ffZ5lAkPBLHfEUOSk/zpQnlkGzVrbXP4LCxLDQ8PlD3qEm
+HbCK+c29mNeTjeoye5EVeQDO0ATiNh1/vSlMPpOY93IdAgMBAAGjUzBRMB0GA1Ud
+DgQWBBQ5kq4dpjcQ8JSiyYrPswAoeltJJTAfBgNVHSMEGDAWgBQ5kq4dpjcQ8JSi
+yYrPswAoeltJJTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBt
+E0JyqwqL63ZrlxiBnnYrk0LoCuXjss7B9p3+6F5v0qKxxC+fpAVoh3CTRkDJRerz
+ORXECXJ4drBetvje/dTX+5pNSLOOyQfOCaSohO4S82xNLSpFd6LcjYsfN8he482w
+E1wuLoi9abktDVmX+sNVeiUUeuDMyqm51NRAlmzDhxTOPvqljdFZXRQO3X00qiQV
+YJs7e1xql0R7DbrwOG5J2lenCwfj51ngmIpGxAaU3eLMAqLT6AZHhZxiATzCtCL9
+2cAKOrW/O8dRVhuWbCjFCIJIRIPrThbMaaw6p4mMkED7dnbOZofXLDgM+pxT5/eU
+8DnQZlgXAkIlZY+9r3oG
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaJG6fvdjFs9cn
+bF8i3wzO3VPUFH4hbAg5pV6rs81sLuJnnlG3WX1sxYQGASqLKIzPiz3g4nKFaBfX
+vBYXo7M/AuJ2tEspedIcvdTqwx2kowaaX7Y9lSx+h1OhovrviCqrqX5t9cIZWSeT
+U1bETcoTEd9/5usVe3XeaqmY8mAPOA0dBKeotGIOxtTEP23CxW1AJwWPLZC1go8y
+cvsTQfmeif+g+6BOcKeZxkayhCvoOus+dt2dXa3x3yROe8ffZ5lAkPBLHfEUOSk/
+zpQnlkGzVrbXP4LCxLDQ8PlD3qEmHbCK+c29mNeTjeoye5EVeQDO0ATiNh1/vSlM
+PpOY93IdAgMBAAECggEAGE1LJaM8VHs0njIPU8Of1NN/Pm4WrrVGHLSOVvrLldVU
+e6qxznrs8+O2G24+o176yFP3Jwf8rzzImYo9X2+/OF1/j+CAAyOCNWbWdUba2xSa
+22bgqBfnQnGahV7ZOj+ZHqRj2vlGp1FvlGIsyVlMVTJZruQcxy/GVxEw+PypmWxp
+u1MOjYEZWjvqJuxTjjnYDcQezfy9Qu0JNOF4+cVVKydewGApmcdDGThcDltxWizM
+n144vgcfR97g2jDKs0GxSAuCbfYo2xoetei5iEVKmlXI/OuUIS78LSud9oBOoYPF
++5MVYOISTNcE+/GEp/BNTq6E8Kk4A1dhaYMvC2qXUQKBgQDKAtFfVP4DusMQd4PD
+oixZ50GENohG4uGCDVBT/RJXUDueil5G4h9iSGxSt0nVOe3nBv5sHMgAARX3S+Pl
+717tbzDgLXqfEqhCj9Kow7BtOussfSQ8hwxBazh9GEva48/Bx+OXFLcpPAcmc9WU
+oQzCCb1yeZ7gsK6yNiLJTQGYCQKBgQDDVokcPHseqFJMOdd9Hr4Ci9gshbZj4htb
+EXiUf50PP0Qco1VJ0MnPW9hateEdRYGVjLeCKHi1XHi8ARQQRJXVI4BI9yiIt8VO
+snnFiEYJ/wgq4lyO8eWeNUaimFvhKHDBcz5tKwtmS4CGf7eAHdTB6R0izLtxkXcs
+6+ZiO/bGdQKBgHdXVNPKBUq0wdpvkMM5gpQWP6lZAgdGr8zCCsujfXthpecSfYHI
+wpuwh3YSXCcA0yAiDJpYInuGKLDw/5DuahlBEBHQLFnfjtHL37rd6NOO9DJTN94e
+NkpLipK0kNOetDUZ3sV5cn+EvACme+4TetMDKA2B9i9tkbcsrj5YJPHpAoGAc5Gh
+MTl/RlYjysF2AqrLlEoUrdK2ZEYEFU8y3fjafYjazW69KR0EKVCXoqN0+pKC5m4I
+rFMxh2ucau7gZfeOBjoozgKc0raXX8YsUXgcqBFhTa37QP9Q8NdoYQ5vXblFbM64
+InKTHgSRmAG8GWqM0+UNvecPB1QfBE7VUU1U5XkCgYA34SlJVa1es7hifeAtb2XC
+jVsHeEwcVnVq4S8aNo51taBJqPR9QIg4bssj4QmaMJntyQl94CE7NM11OssQ4rez
+lY+BZGSmkuEFybqJ5CwHsKk+Cjdm4agqqU/uupOxFPxEzcD2YDgFto7RMPDP/Daf
+iH9tE2qrnzQvE43+caLAuQ==
+-----END PRIVATE KEY-----
diff --git a/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-b.pem b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-b.pem
new file mode 100644
index 0000000000..0371b0be72
--- /dev/null
+++ b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-b.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+MIIDmzCCAoOgAwIBAgIUALrFiUtkMZj2wNSxHvtR+KBoBfowDQYJKoZIhvcNAQEL
+BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNYi5leGFtcGxlLnRs
+ZDAeFw0yNDAzMDYyMjM5NDdaFw0yNDAzMDcyMjM5NDdaMF0xCzAJBgNVBAYTAkFV
+MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz
+IFB0eSBMdGQxFjAUBgNVBAMMDWIuZXhhbXBsZS50bGQwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQC78NVeBRf/WK39UM1BpiQROA8mV8YXB8ugGB7AlOC2
+uLmVdWknMoS155afr4u0DICyNGDkxhWxyqG9UEsBOBmlNZSg85ewjdSMSBRh6Lxk
+uWP9mVS0f1nRVMMBYNbkEPIJ5T2IRCwHP6/gpNKO9prH06atLCs6HP8y0cLKCWNJ
+WixJJpT5goRldEBKTIUtOfM8Sa7ktoYeEvmGmjXjgP9pcdlmC3pjfzTk4+HH3cKL
+RGG4dlhSTrOjVNBL30GWQjiNCM2fAHugUcrcGsmXhbBzmkBa8Rs6mI0ZJQIa/bWv
+6KfWJ1eDF+VIVyhaQPeEkLgatP5NyuaqafvBrTMT1/GHAgMBAAGjUzBRMB0GA1Ud
+DgQWBBQPlAhA673kZnZ9rvRwDejlc2kcjDAfBgNVHSMEGDAWgBQPlAhA673kZnZ9
+rvRwDejlc2kcjDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBx
+OOFo6UxqdtFrN7BZPV00e2tlz3on21JlTxlk92fL0lHL86XJPMr1znBg0OkD1riw
+OYJQ47wLZAgYIhs8UTrcZIZd1xwbJ4fjSDRFHOnI2BHJ/5pR9/NFsrOgeOorLbZf
+x6aa+mQt1qYltTsH8gA1PP5syUcHmlSVLk5NWreMaHEEU8THVsyhxlv8yE+Zuh5y
+JX1KpH4Eo3ekM4RwGuqjbtMgumD8gQf4lHysMEyemQOnebKxViz0bfLfEOMYIXBf
+DaavjgfinXAQnOhItlHXisuAIDxSajnyR0kvTDdRbZBruRpUeKBcENQO95D4b5uA
+BNnkf1CmWZoTwwqvDdNZ
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC78NVeBRf/WK39
+UM1BpiQROA8mV8YXB8ugGB7AlOC2uLmVdWknMoS155afr4u0DICyNGDkxhWxyqG9
+UEsBOBmlNZSg85ewjdSMSBRh6LxkuWP9mVS0f1nRVMMBYNbkEPIJ5T2IRCwHP6/g
+pNKO9prH06atLCs6HP8y0cLKCWNJWixJJpT5goRldEBKTIUtOfM8Sa7ktoYeEvmG
+mjXjgP9pcdlmC3pjfzTk4+HH3cKLRGG4dlhSTrOjVNBL30GWQjiNCM2fAHugUcrc
+GsmXhbBzmkBa8Rs6mI0ZJQIa/bWv6KfWJ1eDF+VIVyhaQPeEkLgatP5NyuaqafvB
+rTMT1/GHAgMBAAECggEACftFJGnaLReaoK+KlzKptmMR0D7X0x/43UHVWvO36Oca
+rtNo3Z9ok6NUc/AGLW5/Ofe02wb3iVUdDfRBEjQr4y2wp8kIL2T4+ueryB9s518+
+mO4XJOxL6uWOOHkvheFYhCoGq0FjopGWrQmpR9UqbJ59uwjI1Zygo878LMV0M9Bc
+5ABaF06yRVyZRH5lAC8zihBUsleE5jUM4FULHRUZFUVJx8ivFtJnLqLuywMOlkq4
+yY4Vf0M77dwVVY5enbnTbKEI25Csf47Gp9i5JS2jXY9ORPNjpYb8LHvp8NZTNczd
+mGxQsWd6pWohnGkZdbJqQoQxMfCNYev5ejf7bJ1AoQKBgQDkpFtsIA0ExmcefIsE
+ChuEpFUXko8wjNeCWFOjJx+Sed9RaRaQiSYOKmWwDjaP7dlJwUC0RIRy65gOGGnf
+PUHOd7nEBtyDIjpSsjWWBjfsUEAqrhze0qpXSPmBzhNlOdsN01VzTgTNiTpea7hi
+izgYjd1kLvJ5p+CV5NIxNhp+qQKBgQDSbbyKf6vPDNIZb9V/ef10Co31ahDVwlFa
+P3FrZv+9eDMJQfXtkRTlu5Auo+TDaFFSMjrb5rTJcEuzBwHyyodqPvPsYC33/DVQ
+jGYVqjQuG5q473DNebtinn5JR40ZfHiJlpx2Ms5xdbPIhN1efTRXmYIov0AIaKuR
+on9LE8X8rwKBgFsyIzTxY7/v0tmaG2i1D1zMnxQT5QEcbCkVSebdh/5IlgZGwDVO
+PtuPlZevU5v85ppAdqpwWdPsnG2i1zevmzvbDUFe6z1yvYiWhEEeodej+rQLVoCZ
+zk+aT8qyg5HwjarqDD89czT380wN8zF7DhjdHN0EzLoxd6bR6fSu+8phAoGAV4v0
+PyLy1gedeZu/lXOpcRfbC9l++5AGzKdMhsSpbaiOgzGAIcCUkye/ysfBK1NBUhM3
+zblkSdKAjBFETEDaqedbEGLLfTre644eArF3WB9/9aUYp0QYI+WQ4Of12j6g341b
+twlYPngbvjcY6nDoz/E757v55gW2K7cRgqjNXF0CgYEAxNfclcdKbUtGAsttZdY3
++dcdBtqcLvpYlMsZPQaxNppyKBI5svtK715FsVbmLhINqiNo1aKIA5M3E2P88Fa2
+nqVrKsBOn3gCe+GFlFeWwNAfRlfmP1ZUHDp07zvNtRm4ZR/3hdAze4DbyWv58LfL
+WSjqCjjBeurblkRv2QTXu2k=
+-----END PRIVATE KEY-----
diff --git a/exporter/otlp-logs/test/test_helper.rb b/exporter/otlp-logs/test/test_helper.rb
new file mode 100644
index 0000000000..2ab9f61361
--- /dev/null
+++ b/exporter/otlp-logs/test/test_helper.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+if RUBY_ENGINE == 'ruby'
+ require 'simplecov'
+ SimpleCov.start do
+ enable_coverage :branch
+ add_filter '/test/'
+ end
+
+ SimpleCov.minimum_coverage 85
+end
+
+require 'opentelemetry-test-helpers'
+require 'opentelemetry/exporter/otlp_logs'
+require 'minitest/autorun'
+require 'webmock/minitest'
+require 'minitest/stub_const'
+
+OpenTelemetry.logger = Logger.new(File::NULL)
diff --git a/exporter/otlp-metrics/README.md b/exporter/otlp-metrics/README.md
index 4d8986546f..ef91e019b7 100644
--- a/exporter/otlp-metrics/README.md
+++ b/exporter/otlp-metrics/README.md
@@ -101,12 +101,13 @@ The collector exporter can be configured explicitly in code, or via environment
| Parameter | Environment variable | Default |
| ------------------- | -------------------------------------------- | ----------------------------------- |
| `endpoint:` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `"http://localhost:4318/v1/metrics"` |
-| `certificate_file:`| `OTEL_EXPORTER_OTLP_CERTIFICATE` | |
+| `certificate_file: `| `OTEL_EXPORTER_OTLP_CERTIFICATE` | |
+| `client_certificate_file: `| `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` | |
+| `client_key_file:` | `OTEL_EXPORTER_OTLP_CLIENT_KEY` | |
| `headers:` | `OTEL_EXPORTER_OTLP_HEADERS` | |
| `compression:` | `OTEL_EXPORTER_OTLP_COMPRESSION` | `"gzip"` |
| `timeout:` | `OTEL_EXPORTER_OTLP_TIMEOUT` | `10` |
-| `ssl_verify_mode:` | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER` or | `OpenSSL::SSL:VERIFY_PEER` |
-| | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE` | |
+| `ssl_verify_mode:` | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER` or `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE` | `OpenSSL::SSL:VERIFY_PEER` |
`ssl_verify_mode:` parameter values should be flags for server certificate verification: `OpenSSL::SSL:VERIFY_PEER` and `OpenSSL::SSL:VERIFY_NONE` are acceptable. These values can also be set using the appropriately named environment variables as shown where `VERIFY_PEER` will take precedence over `VERIFY_NONE`. Please see [the Net::HTTP docs](https://ruby-doc.org/stdlib-2.7.6/libdoc/net/http/rdoc/Net/HTTP.html#verify_mode) for more information about these flags.
diff --git a/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/metrics_exporter.rb b/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/metrics_exporter.rb
index 44302e0e92..a6150fefb4 100644
--- a/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/metrics_exporter.rb
+++ b/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/metrics_exporter.rb
@@ -47,6 +47,8 @@ def self.ssl_verify_mode
def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_METRICS_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'http://localhost:4318/v1/metrics'),
certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_METRICS_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'),
+ client_certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_METRICS_CLIENT_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE'),
+ client_key_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_METRICS_CLIENT_KEY', 'OTEL_EXPORTER_OTLP_CLIENT_KEY'),
ssl_verify_mode: MetricsExporter.ssl_verify_mode,
headers: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_METRICS_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', default: {}),
compression: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_METRICS_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION', default: 'gzip'),
@@ -63,7 +65,7 @@ def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPOR
URI(endpoint)
end
- @http = http_connection(@uri, ssl_verify_mode, certificate_file)
+ @http = http_connection(@uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file)
@path = @uri.path
@headers = prepare_headers(headers)
diff --git a/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/util.rb b/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/util.rb
index 37f1896da0..363fb91504 100644
--- a/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/util.rb
+++ b/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/util.rb
@@ -9,17 +9,19 @@ module Exporter
module OTLP
module Metrics
# Util module provide essential functionality for exporter
- module Util
+ module Util # rubocop:disable Metrics/ModuleLength
KEEP_ALIVE_TIMEOUT = 30
RETRY_COUNT = 5
ERROR_MESSAGE_INVALID_HEADERS = 'headers must be a String with comma-separated URL Encoded UTF-8 k=v pairs or a Hash'
DEFAULT_USER_AGENT = "OTel-OTLP-MetricsExporter-Ruby/#{OpenTelemetry::Exporter::OTLP::Metrics::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})".freeze
- def http_connection(uri, ssl_verify_mode, certificate_file)
+ def http_connection(uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.verify_mode = ssl_verify_mode
http.ca_file = certificate_file unless certificate_file.nil?
+ http.cert = OpenSSL::X509::Certificate.new(File.read(client_certificate_file)) unless client_certificate_file.nil?
+ http.key = OpenSSL::PKey::RSA.new(File.read(client_key_file)) unless client_key_file.nil?
http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
http
end
diff --git a/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/metrics_exporter_test.rb b/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/metrics_exporter_test.rb
index 5add4cbc55..93176ce33a 100644
--- a/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/metrics_exporter_test.rb
+++ b/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/metrics_exporter_test.rb
@@ -13,6 +13,12 @@
METRICS_FAILURE = OpenTelemetry::SDK::Metrics::Export::FAILURE
METRICS_VERSION = OpenTelemetry::Exporter::OTLP::Metrics::VERSION
METRICS_DEFAULT_USER_AGENT = OpenTelemetry::Exporter::OTLP::Metrics::Util::DEFAULT_USER_AGENT
+ METRICS_CLIENT_CERT_A_PATH = File.dirname(__FILE__) + '/mtls-client-a.pem'
+ METRICS_CLIENT_CERT_A = OpenSSL::X509::Certificate.new(File.read(METRICS_CLIENT_CERT_A_PATH))
+ METRICS_CLIENT_KEY_A = OpenSSL::PKey::RSA.new(File.read(METRICS_CLIENT_CERT_A_PATH))
+ METRICS_CLIENT_CERT_B_PATH = File.dirname(__FILE__) + '/mtls-client-b.pem'
+ METRICS_CLIENT_CERT_B = OpenSSL::X509::Certificate.new(File.read(METRICS_CLIENT_CERT_B_PATH))
+ METRICS_CLIENT_KEY_B = OpenSSL::PKey::RSA.new(File.read(METRICS_CLIENT_CERT_B_PATH))
describe '#initialize' do
it 'initializes with defaults' do
@@ -24,6 +30,8 @@
_(exp.instance_variable_get(:@compression)).must_equal 'gzip'
http = exp.instance_variable_get(:@http)
_(http.ca_file).must_be_nil
+ _(http.cert).must_be_nil
+ _(http.key).must_be_nil
_(http.use_ssl?).must_equal false
_(http.address).must_equal 'localhost'
_(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_PEER
@@ -76,6 +84,8 @@
it 'sets parameters from the environment' do
exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234',
'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar',
+ 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE' => METRICS_CLIENT_CERT_A_PATH,
+ 'OTEL_EXPORTER_OTLP_CLIENT_KEY' => METRICS_CLIENT_CERT_A_PATH,
'OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d',
'OTEL_EXPORTER_OTLP_COMPRESSION' => 'gzip',
'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true',
@@ -88,6 +98,8 @@
_(exp.instance_variable_get(:@compression)).must_equal 'gzip'
http = exp.instance_variable_get(:@http)
_(http.ca_file).must_equal '/foo/bar'
+ _(http.cert).must_equal METRICS_CLIENT_CERT_A
+ _(http.key.params).must_equal METRICS_CLIENT_KEY_A.params
_(http.use_ssl?).must_equal true
_(http.address).must_equal 'localhost'
_(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE
@@ -97,12 +109,16 @@
it 'prefers explicit parameters rather than the environment' do
exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234',
'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar',
+ 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE' => METRICS_CLIENT_CERT_A_PATH,
+ 'OTEL_EXPORTER_OTLP_CLIENT_KEY' => METRICS_CLIENT_CERT_A_PATH,
'OTEL_EXPORTER_OTLP_HEADERS' => 'a:b,c:d',
'OTEL_EXPORTER_OTLP_COMPRESSION' => 'flate',
'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER' => 'true',
'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do
OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new(endpoint: 'http://localhost:4321',
certificate_file: '/baz',
+ client_certificate_file: METRICS_CLIENT_CERT_B_PATH,
+ client_key_file: METRICS_CLIENT_CERT_B_PATH,
headers: { 'x' => 'y' },
compression: 'gzip',
ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE,
@@ -114,6 +130,8 @@
_(exp.instance_variable_get(:@compression)).must_equal 'gzip'
http = exp.instance_variable_get(:@http)
_(http.ca_file).must_equal '/baz'
+ _(http.cert).must_equal METRICS_CLIENT_CERT_B
+ _(http.key.params).must_equal METRICS_CLIENT_KEY_B.params
_(http.use_ssl?).must_equal false
_(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE
_(http.address).must_equal 'localhost'
@@ -538,8 +556,6 @@
end
it 'translates all the things' do
- skip 'Intermittently fails' if RUBY_ENGINE == 'truffleruby'
-
stub_request(:post, 'http://localhost:4318/v1/metrics').to_return(status: 200)
meter_provider.add_metric_reader(exporter)
meter = meter_provider.meter('test')
@@ -621,7 +637,7 @@
)
assert_requested(:post, 'http://localhost:4318/v1/metrics') do |req|
- req.body == Zlib.gzip(encoded_etsr) # is asserting that the body of the HTTP request is equal to the result of gzipping the encoded_etsr.
+ Zlib.gunzip(req.body) == encoded_etsr
end
end
end
diff --git a/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/mtls-client-a.pem b/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/mtls-client-a.pem
new file mode 100644
index 0000000000..b1ea23ebd9
--- /dev/null
+++ b/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/mtls-client-a.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+MIIDmzCCAoOgAwIBAgIUGfmv/4kRRFbg319TIHkcwDMC3pUwDQYJKoZIhvcNAQEL
+BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNYS5leGFtcGxlLnRs
+ZDAeFw0yNDAzMDYyMjM4MzNaFw0yNDAzMDcyMjM4MzNaMF0xCzAJBgNVBAYTAkFV
+MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz
+IFB0eSBMdGQxFjAUBgNVBAMMDWEuZXhhbXBsZS50bGQwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQCaJG6fvdjFs9cnbF8i3wzO3VPUFH4hbAg5pV6rs81s
+LuJnnlG3WX1sxYQGASqLKIzPiz3g4nKFaBfXvBYXo7M/AuJ2tEspedIcvdTqwx2k
+owaaX7Y9lSx+h1OhovrviCqrqX5t9cIZWSeTU1bETcoTEd9/5usVe3XeaqmY8mAP
+OA0dBKeotGIOxtTEP23CxW1AJwWPLZC1go8ycvsTQfmeif+g+6BOcKeZxkayhCvo
+Ous+dt2dXa3x3yROe8ffZ5lAkPBLHfEUOSk/zpQnlkGzVrbXP4LCxLDQ8PlD3qEm
+HbCK+c29mNeTjeoye5EVeQDO0ATiNh1/vSlMPpOY93IdAgMBAAGjUzBRMB0GA1Ud
+DgQWBBQ5kq4dpjcQ8JSiyYrPswAoeltJJTAfBgNVHSMEGDAWgBQ5kq4dpjcQ8JSi
+yYrPswAoeltJJTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBt
+E0JyqwqL63ZrlxiBnnYrk0LoCuXjss7B9p3+6F5v0qKxxC+fpAVoh3CTRkDJRerz
+ORXECXJ4drBetvje/dTX+5pNSLOOyQfOCaSohO4S82xNLSpFd6LcjYsfN8he482w
+E1wuLoi9abktDVmX+sNVeiUUeuDMyqm51NRAlmzDhxTOPvqljdFZXRQO3X00qiQV
+YJs7e1xql0R7DbrwOG5J2lenCwfj51ngmIpGxAaU3eLMAqLT6AZHhZxiATzCtCL9
+2cAKOrW/O8dRVhuWbCjFCIJIRIPrThbMaaw6p4mMkED7dnbOZofXLDgM+pxT5/eU
+8DnQZlgXAkIlZY+9r3oG
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaJG6fvdjFs9cn
+bF8i3wzO3VPUFH4hbAg5pV6rs81sLuJnnlG3WX1sxYQGASqLKIzPiz3g4nKFaBfX
+vBYXo7M/AuJ2tEspedIcvdTqwx2kowaaX7Y9lSx+h1OhovrviCqrqX5t9cIZWSeT
+U1bETcoTEd9/5usVe3XeaqmY8mAPOA0dBKeotGIOxtTEP23CxW1AJwWPLZC1go8y
+cvsTQfmeif+g+6BOcKeZxkayhCvoOus+dt2dXa3x3yROe8ffZ5lAkPBLHfEUOSk/
+zpQnlkGzVrbXP4LCxLDQ8PlD3qEmHbCK+c29mNeTjeoye5EVeQDO0ATiNh1/vSlM
+PpOY93IdAgMBAAECggEAGE1LJaM8VHs0njIPU8Of1NN/Pm4WrrVGHLSOVvrLldVU
+e6qxznrs8+O2G24+o176yFP3Jwf8rzzImYo9X2+/OF1/j+CAAyOCNWbWdUba2xSa
+22bgqBfnQnGahV7ZOj+ZHqRj2vlGp1FvlGIsyVlMVTJZruQcxy/GVxEw+PypmWxp
+u1MOjYEZWjvqJuxTjjnYDcQezfy9Qu0JNOF4+cVVKydewGApmcdDGThcDltxWizM
+n144vgcfR97g2jDKs0GxSAuCbfYo2xoetei5iEVKmlXI/OuUIS78LSud9oBOoYPF
++5MVYOISTNcE+/GEp/BNTq6E8Kk4A1dhaYMvC2qXUQKBgQDKAtFfVP4DusMQd4PD
+oixZ50GENohG4uGCDVBT/RJXUDueil5G4h9iSGxSt0nVOe3nBv5sHMgAARX3S+Pl
+717tbzDgLXqfEqhCj9Kow7BtOussfSQ8hwxBazh9GEva48/Bx+OXFLcpPAcmc9WU
+oQzCCb1yeZ7gsK6yNiLJTQGYCQKBgQDDVokcPHseqFJMOdd9Hr4Ci9gshbZj4htb
+EXiUf50PP0Qco1VJ0MnPW9hateEdRYGVjLeCKHi1XHi8ARQQRJXVI4BI9yiIt8VO
+snnFiEYJ/wgq4lyO8eWeNUaimFvhKHDBcz5tKwtmS4CGf7eAHdTB6R0izLtxkXcs
+6+ZiO/bGdQKBgHdXVNPKBUq0wdpvkMM5gpQWP6lZAgdGr8zCCsujfXthpecSfYHI
+wpuwh3YSXCcA0yAiDJpYInuGKLDw/5DuahlBEBHQLFnfjtHL37rd6NOO9DJTN94e
+NkpLipK0kNOetDUZ3sV5cn+EvACme+4TetMDKA2B9i9tkbcsrj5YJPHpAoGAc5Gh
+MTl/RlYjysF2AqrLlEoUrdK2ZEYEFU8y3fjafYjazW69KR0EKVCXoqN0+pKC5m4I
+rFMxh2ucau7gZfeOBjoozgKc0raXX8YsUXgcqBFhTa37QP9Q8NdoYQ5vXblFbM64
+InKTHgSRmAG8GWqM0+UNvecPB1QfBE7VUU1U5XkCgYA34SlJVa1es7hifeAtb2XC
+jVsHeEwcVnVq4S8aNo51taBJqPR9QIg4bssj4QmaMJntyQl94CE7NM11OssQ4rez
+lY+BZGSmkuEFybqJ5CwHsKk+Cjdm4agqqU/uupOxFPxEzcD2YDgFto7RMPDP/Daf
+iH9tE2qrnzQvE43+caLAuQ==
+-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/mtls-client-b.pem b/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/mtls-client-b.pem
new file mode 100644
index 0000000000..8701951523
--- /dev/null
+++ b/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/mtls-client-b.pem
@@ -0,0 +1,50 @@
+-----BEGIN CERTIFICATE-----
+MIIDmzCCAoOgAwIBAgIUALrFiUtkMZj2wNSxHvtR+KBoBfowDQYJKoZIhvcNAQEL
+BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
+GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNYi5leGFtcGxlLnRs
+ZDAeFw0yNDAzMDYyMjM5NDdaFw0yNDAzMDcyMjM5NDdaMF0xCzAJBgNVBAYTAkFV
+MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz
+IFB0eSBMdGQxFjAUBgNVBAMMDWIuZXhhbXBsZS50bGQwggEiMA0GCSqGSIb3DQEB
+AQUAA4IBDwAwggEKAoIBAQC78NVeBRf/WK39UM1BpiQROA8mV8YXB8ugGB7AlOC2
+uLmVdWknMoS155afr4u0DICyNGDkxhWxyqG9UEsBOBmlNZSg85ewjdSMSBRh6Lxk
+uWP9mVS0f1nRVMMBYNbkEPIJ5T2IRCwHP6/gpNKO9prH06atLCs6HP8y0cLKCWNJ
+WixJJpT5goRldEBKTIUtOfM8Sa7ktoYeEvmGmjXjgP9pcdlmC3pjfzTk4+HH3cKL
+RGG4dlhSTrOjVNBL30GWQjiNCM2fAHugUcrcGsmXhbBzmkBa8Rs6mI0ZJQIa/bWv
+6KfWJ1eDF+VIVyhaQPeEkLgatP5NyuaqafvBrTMT1/GHAgMBAAGjUzBRMB0GA1Ud
+DgQWBBQPlAhA673kZnZ9rvRwDejlc2kcjDAfBgNVHSMEGDAWgBQPlAhA673kZnZ9
+rvRwDejlc2kcjDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBx
+OOFo6UxqdtFrN7BZPV00e2tlz3on21JlTxlk92fL0lHL86XJPMr1znBg0OkD1riw
+OYJQ47wLZAgYIhs8UTrcZIZd1xwbJ4fjSDRFHOnI2BHJ/5pR9/NFsrOgeOorLbZf
+x6aa+mQt1qYltTsH8gA1PP5syUcHmlSVLk5NWreMaHEEU8THVsyhxlv8yE+Zuh5y
+JX1KpH4Eo3ekM4RwGuqjbtMgumD8gQf4lHysMEyemQOnebKxViz0bfLfEOMYIXBf
+DaavjgfinXAQnOhItlHXisuAIDxSajnyR0kvTDdRbZBruRpUeKBcENQO95D4b5uA
+BNnkf1CmWZoTwwqvDdNZ
+-----END CERTIFICATE-----
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC78NVeBRf/WK39
+UM1BpiQROA8mV8YXB8ugGB7AlOC2uLmVdWknMoS155afr4u0DICyNGDkxhWxyqG9
+UEsBOBmlNZSg85ewjdSMSBRh6LxkuWP9mVS0f1nRVMMBYNbkEPIJ5T2IRCwHP6/g
+pNKO9prH06atLCs6HP8y0cLKCWNJWixJJpT5goRldEBKTIUtOfM8Sa7ktoYeEvmG
+mjXjgP9pcdlmC3pjfzTk4+HH3cKLRGG4dlhSTrOjVNBL30GWQjiNCM2fAHugUcrc
+GsmXhbBzmkBa8Rs6mI0ZJQIa/bWv6KfWJ1eDF+VIVyhaQPeEkLgatP5NyuaqafvB
+rTMT1/GHAgMBAAECggEACftFJGnaLReaoK+KlzKptmMR0D7X0x/43UHVWvO36Oca
+rtNo3Z9ok6NUc/AGLW5/Ofe02wb3iVUdDfRBEjQr4y2wp8kIL2T4+ueryB9s518+
+mO4XJOxL6uWOOHkvheFYhCoGq0FjopGWrQmpR9UqbJ59uwjI1Zygo878LMV0M9Bc
+5ABaF06yRVyZRH5lAC8zihBUsleE5jUM4FULHRUZFUVJx8ivFtJnLqLuywMOlkq4
+yY4Vf0M77dwVVY5enbnTbKEI25Csf47Gp9i5JS2jXY9ORPNjpYb8LHvp8NZTNczd
+mGxQsWd6pWohnGkZdbJqQoQxMfCNYev5ejf7bJ1AoQKBgQDkpFtsIA0ExmcefIsE
+ChuEpFUXko8wjNeCWFOjJx+Sed9RaRaQiSYOKmWwDjaP7dlJwUC0RIRy65gOGGnf
+PUHOd7nEBtyDIjpSsjWWBjfsUEAqrhze0qpXSPmBzhNlOdsN01VzTgTNiTpea7hi
+izgYjd1kLvJ5p+CV5NIxNhp+qQKBgQDSbbyKf6vPDNIZb9V/ef10Co31ahDVwlFa
+P3FrZv+9eDMJQfXtkRTlu5Auo+TDaFFSMjrb5rTJcEuzBwHyyodqPvPsYC33/DVQ
+jGYVqjQuG5q473DNebtinn5JR40ZfHiJlpx2Ms5xdbPIhN1efTRXmYIov0AIaKuR
+on9LE8X8rwKBgFsyIzTxY7/v0tmaG2i1D1zMnxQT5QEcbCkVSebdh/5IlgZGwDVO
+PtuPlZevU5v85ppAdqpwWdPsnG2i1zevmzvbDUFe6z1yvYiWhEEeodej+rQLVoCZ
+zk+aT8qyg5HwjarqDD89czT380wN8zF7DhjdHN0EzLoxd6bR6fSu+8phAoGAV4v0
+PyLy1gedeZu/lXOpcRfbC9l++5AGzKdMhsSpbaiOgzGAIcCUkye/ysfBK1NBUhM3
+zblkSdKAjBFETEDaqedbEGLLfTre644eArF3WB9/9aUYp0QYI+WQ4Of12j6g341b
+twlYPngbvjcY6nDoz/E757v55gW2K7cRgqjNXF0CgYEAxNfclcdKbUtGAsttZdY3
++dcdBtqcLvpYlMsZPQaxNppyKBI5svtK715FsVbmLhINqiNo1aKIA5M3E2P88Fa2
+nqVrKsBOn3gCe+GFlFeWwNAfRlfmP1ZUHDp07zvNtRm4ZR/3hdAze4DbyWv58LfL
+WSjqCjjBeurblkRv2QTXu2k=
+-----END PRIVATE KEY-----
\ No newline at end of file
diff --git a/exporter/otlp-metrics/test/test_helper.rb b/exporter/otlp-metrics/test/test_helper.rb
index 461b180653..9fc4466458 100644
--- a/exporter/otlp-metrics/test/test_helper.rb
+++ b/exporter/otlp-metrics/test/test_helper.rb
@@ -17,7 +17,7 @@
OpenTelemetry.logger = Logger.new(File::NULL)
module MockSum
- def collect(start_time, end_time)
+ def collect(start_time, end_time, data_points)
start_time = 1_699_593_427_329_946_585 # rubocop:disable Lint/ShadowedArgument
end_time = 1_699_593_427_329_946_586 # rubocop:disable Lint/ShadowedArgument
super
diff --git a/exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb b/exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb
index f37fc3a8d3..2303f2e5d7 100644
--- a/exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb
+++ b/exporter/otlp/test/opentelemetry/exporter/otlp/exporter_test.rb
@@ -611,9 +611,6 @@
end
it 'translates all the things' do
- # TODO: See issue #1507 to fix
- skip 'Intermittently fails' if RUBY_ENGINE == 'truffleruby'
-
stub_request(:post, 'http://localhost:4318/v1/traces').to_return(status: 200)
processor = OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
tracer = OpenTelemetry.tracer_provider.tracer('tracer', 'v0.0.1')
@@ -778,7 +775,7 @@
)
assert_requested(:post, 'http://localhost:4318/v1/traces') do |req|
- req.body == Zlib.gzip(encoded_etsr)
+ Zlib.gunzip(req.body) == encoded_etsr
end
end
end
diff --git a/logs_api/lib/opentelemetry-logs-api.rb b/logs_api/lib/opentelemetry-logs-api.rb
index 47ba36905e..03b09d7fee 100644
--- a/logs_api/lib/opentelemetry-logs-api.rb
+++ b/logs_api/lib/opentelemetry-logs-api.rb
@@ -5,5 +5,37 @@
# SPDX-License-Identifier: Apache-2.0
require 'opentelemetry'
-require_relative 'opentelemetry/logs'
-require_relative 'opentelemetry/logs/version'
+require 'opentelemetry/logs'
+require 'opentelemetry/logs/version'
+require 'opentelemetry/internal/proxy_logger_provider'
+require 'opentelemetry/internal/proxy_logger'
+
+# OpenTelemetry is an open source observability framework, providing a
+# general-purpose API, SDK, and related tools required for the instrumentation
+# of cloud-native software, frameworks, and libraries.
+#
+# The OpenTelemetry module in the Logs API gem provides global accessors
+# for logs-related objects.
+module OpenTelemetry
+ @logger_provider = Internal::ProxyLoggerProvider.new
+
+ # Register the global logger provider.
+ #
+ # @param [LoggerProvider] provider A logger provider to register as the
+ # global instance.
+ def logger_provider=(provider)
+ @mutex.synchronize do
+ if @logger_provider.instance_of? Internal::ProxyLoggerProvider
+ logger.debug("Upgrading default proxy logger provider to #{provider.class}")
+ @logger_provider.delegate = provider
+ end
+ @logger_provider = provider
+ end
+ end
+
+ # @return [Object, Logs::LoggerProvider] registered logger provider or a
+ # default no-op implementation of the logger provider.
+ def logger_provider
+ @mutex.synchronize { @logger_provider }
+ end
+end
diff --git a/logs_api/lib/opentelemetry/internal/proxy_logger.rb b/logs_api/lib/opentelemetry/internal/proxy_logger.rb
new file mode 100644
index 0000000000..a392c3dbc9
--- /dev/null
+++ b/logs_api/lib/opentelemetry/internal/proxy_logger.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+module OpenTelemetry
+ module Internal
+ # @api private
+ #
+ # {ProxyLogger} is an implementation of {OpenTelemetry::Logs::Logger}. It is returned from
+ # the ProxyLoggerProvider until a delegate logger provider is installed. After the delegate
+ # logger provider is installed, the ProxyLogger will delegate to the corresponding "real"
+ # logger.
+ class ProxyLogger < Logs::Logger
+ attr_writer :delegate
+
+ # Returns a new {ProxyLogger} instance.
+ #
+ # @return [ProxyLogger]
+ def initialize
+ @delegate = nil
+ end
+
+ def on_emit(
+ timestamp: nil,
+ observed_timestamp: nil,
+ severity_number: nil,
+ severity_text: nil,
+ body: nil,
+ trace_id: nil,
+ span_id: nil,
+ trace_flags: nil,
+ attributes: nil,
+ context: nil
+ )
+ unless @delegate.nil?
+ return @delegate.on_emit(
+ timestamp: nil,
+ observed_timestamp: nil,
+ severity_number: nil,
+ severity_text: nil,
+ body: nil,
+ trace_id: nil,
+ span_id: nil,
+ trace_flags: nil,
+ attributes: nil,
+ context: nil
+ )
+ end
+
+ super
+ end
+ end
+ end
+end
diff --git a/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb b/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb
new file mode 100644
index 0000000000..0cbedc9d62
--- /dev/null
+++ b/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+module OpenTelemetry
+ module Internal
+ # @api private
+ #
+ # {ProxyLoggerProvider} is an implementation of {OpenTelemetry::Logs::LoggerProvider}.
+ # It is the default global logger provider returned by OpenTelemetry.logger_provider.
+ # It delegates to a "real" LoggerProvider after the global logger provider is registered.
+ # It returns {ProxyLogger} instances until the delegate is installed.
+ class ProxyLoggerProvider < Logs::LoggerProvider
+ Key = Struct.new(:name, :version)
+ private_constant(:Key)
+ # Returns a new {ProxyLoggerProvider} instance.
+ #
+ # @return [ProxyLoggerProvider]
+ def initialize
+ super
+
+ @mutex = Mutex.new
+ @registry = {}
+ @delegate = nil
+ end
+
+ # Set the delegate logger provider. If this is called more than once, a warning will
+ # be logged and superfluous calls will be ignored.
+ #
+ # @param [LoggerProvider] provider The logger provider to delegate to
+ def delegate=(provider)
+ unless @delegate.nil?
+ OpenTelemetry.logger.warn 'Attempt to reset delegate in ProxyLoggerProvider ignored.'
+ return
+ end
+
+ @mutex.synchronize do
+ @delegate = provider
+ @registry.each { |key, logger| logger.delegate = provider.logger(key.name, key.version) }
+ end
+ end
+
+ # Returns a {Logger} instance.
+ #
+ # @param [optional String] name Instrumentation package name
+ # @param [optional String] version Instrumentation package version
+ #
+ # @return [Logger]
+ def logger(name = nil, version = nil)
+ @mutex.synchronize do
+ return @delegate.logger(name, version) unless @delegate.nil?
+
+ @registry[Key.new(name, version)] ||= ProxyLogger.new
+ end
+ end
+ end
+ end
+end
diff --git a/logs_api/lib/opentelemetry/logs.rb b/logs_api/lib/opentelemetry/logs.rb
index 0669bb21d3..ade58537cf 100644
--- a/logs_api/lib/opentelemetry/logs.rb
+++ b/logs_api/lib/opentelemetry/logs.rb
@@ -4,11 +4,6 @@
#
# SPDX-License-Identifier: Apache-2.0
-require_relative 'logs/log_record'
-require_relative 'logs/logger'
-require_relative 'logs/logger_provider'
-require_relative 'logs/severity_number'
-
module OpenTelemetry
# The Logs API records a timestamped record with metadata.
# In OpenTelemetry, any data that is not part of a distributed trace or a
@@ -20,3 +15,8 @@ module OpenTelemetry
module Logs
end
end
+
+require 'opentelemetry/logs/log_record'
+require 'opentelemetry/logs/logger'
+require 'opentelemetry/logs/logger_provider'
+require 'opentelemetry/logs/severity_number'
diff --git a/logs_api/test/opentelemetry_logs_api_test.rb b/logs_api/test/opentelemetry_logs_api_test.rb
new file mode 100644
index 0000000000..9034a2b307
--- /dev/null
+++ b/logs_api/test/opentelemetry_logs_api_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'test_helper'
+
+describe OpenTelemetry do
+ class CustomLogRecord < OpenTelemetry::Logs::LogRecord
+ end
+
+ class CustomLogger < OpenTelemetry::Logs::Logger
+ def on_emit(*)
+ CustomLogRecord.new
+ end
+ end
+
+ class CustomLoggerProvider < OpenTelemetry::Logs::LoggerProvider
+ def logger(name = nil, version = nil)
+ CustomLogger.new
+ end
+ end
+
+ describe '.logger_provider' do
+ after do
+ # Ensure we don't leak custom logger factories and loggers to other tests
+ OpenTelemetry.logger_provider = OpenTelemetry::Internal::ProxyLoggerProvider.new
+ end
+
+ it 'returns a Logs::LoggerProvider by default' do
+ logger_provider = OpenTelemetry.logger_provider
+ _(logger_provider).must_be_kind_of(OpenTelemetry::Logs::LoggerProvider)
+ end
+
+ it 'returns the same instance when accessed multiple times' do
+ _(OpenTelemetry.logger_provider).must_equal(OpenTelemetry.logger_provider)
+ end
+
+ it 'returns user-specified logger provider' do
+ custom_logger_provider = CustomLoggerProvider.new
+ OpenTelemetry.logger_provider = custom_logger_provider
+ _(OpenTelemetry.logger_provider).must_equal(custom_logger_provider)
+ end
+ end
+
+ describe '.logger_provider=' do
+ after do
+ # Ensure we don't leak custom logger factories and loggers to other tests
+ OpenTelemetry.logger_provider = OpenTelemetry::Internal::ProxyLoggerProvider.new
+ end
+
+ it 'has a default proxy logger' do
+ refute_nil OpenTelemetry.logger_provider.logger
+ end
+
+ it 'upgrades default loggers to *real* loggers' do
+ # proxy loggers do not emit any log records, nor does the API logger
+ # the on_emit method is empty
+ default_logger = OpenTelemetry.logger_provider.logger
+ _(default_logger.on_emit(body: 'test')).must_be_instance_of(NilClass)
+ OpenTelemetry.logger_provider = CustomLoggerProvider.new
+ _(default_logger.on_emit(body: 'test')).must_be_instance_of(CustomLogRecord)
+ end
+
+ it 'upgrades the default logger provider to a *real* logger provider' do
+ default_logger_provider = OpenTelemetry.logger_provider
+ OpenTelemetry.logger_provider = CustomLoggerProvider.new
+ _(default_logger_provider.logger).must_be_instance_of(CustomLogger)
+ end
+ end
+end
diff --git a/logs_sdk/lib/opentelemetry-logs-sdk.rb b/logs_sdk/lib/opentelemetry-logs-sdk.rb
index 32c03df1f7..63c7502285 100644
--- a/logs_sdk/lib/opentelemetry-logs-sdk.rb
+++ b/logs_sdk/lib/opentelemetry-logs-sdk.rb
@@ -5,4 +5,5 @@
# SPDX-License-Identifier: Apache-2.0
require 'opentelemetry/sdk'
+require 'opentelemetry-logs-api'
require 'opentelemetry/sdk/logs'
diff --git a/logs_sdk/lib/opentelemetry/sdk/logs.rb b/logs_sdk/lib/opentelemetry/sdk/logs.rb
index fbd978c68c..b310d6aa5a 100644
--- a/logs_sdk/lib/opentelemetry/sdk/logs.rb
+++ b/logs_sdk/lib/opentelemetry/sdk/logs.rb
@@ -5,12 +5,14 @@
# SPDX-License-Identifier: Apache-2.0
require_relative 'logs/version'
+require_relative 'logs/configuration_patch'
require_relative 'logs/logger'
require_relative 'logs/logger_provider'
require_relative 'logs/log_record'
require_relative 'logs/log_record_data'
require_relative 'logs/log_record_processor'
require_relative 'logs/export'
+require_relative 'logs/log_record_limits'
module OpenTelemetry
module SDK
diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/configuration_patch.rb b/logs_sdk/lib/opentelemetry/sdk/logs/configuration_patch.rb
new file mode 100644
index 0000000000..6ca09e8aa7
--- /dev/null
+++ b/logs_sdk/lib/opentelemetry/sdk/logs/configuration_patch.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'opentelemetry/sdk/configurator'
+
+module OpenTelemetry
+ module SDK
+ module Logs
+ # The ConfiguratorPatch implements a hook to configure the logs portion
+ # of the SDK.
+ module ConfiguratorPatch
+ def add_log_record_processor(log_record_processor)
+ @log_record_processors << log_record_processor
+ end
+
+ private
+
+ def initialize
+ super
+ @log_record_processors = []
+ end
+
+ # The logs_configuration_hook method is where we define the setup
+ # process for logs SDK.
+ def logs_configuration_hook
+ OpenTelemetry.logger_provider = Logs::LoggerProvider.new(resource: @resource)
+ configure_log_record_processors
+ end
+
+ def configure_log_record_processors
+ processors = @log_record_processors.empty? ? wrapped_log_exporters_from_env.compact : @log_record_processors
+ processors.each { |p| OpenTelemetry.logger_provider.add_log_record_processor(p) }
+ end
+
+ def wrapped_log_exporters_from_env
+ # TODO: set default to OTLP to match traces, default is console until other exporters merged
+ exporters = ENV.fetch('OTEL_LOGS_EXPORTER', 'console')
+
+ exporters.split(',').map do |exporter|
+ case exporter.strip
+ when 'none' then nil
+ when 'console' then Logs::Export::SimpleLogRecordProcessor.new(Logs::Export::ConsoleLogRecordExporter.new)
+ when 'otlp'
+ otlp_protocol = ENV['OTEL_EXPORTER_OTLP_LOGS_PROTOCOL'] || ENV['OTEL_EXPORTER_OTLP_PROTOCOL'] || 'http/protobuf'
+
+ if otlp_protocol != 'http/protobuf'
+ OpenTelemetry.logger.warn "The #{otlp_protocol} transport protocol is not supported by the OTLP exporter, log_records will not be exported."
+ nil
+ else
+ begin
+ Logs::Export::BatchLogRecordProcessor.new(OpenTelemetry::Exporter::OTLP::LogsExporter.new)
+ rescue NameError
+ OpenTelemetry.logger.warn 'The otlp logs exporter cannot be configured - please add opentelemetry-exporter-otlp-logs to your Gemfile. Logs will not be exported'
+ nil
+ end
+ end
+ else
+ OpenTelemetry.logger.warn "The #{exporter} exporter is unknown and cannot be configured, log records will not be exported"
+ nil
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+OpenTelemetry::SDK::Configurator.prepend(OpenTelemetry::SDK::Logs::ConfiguratorPatch)
diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb b/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb
index 10d1dcb7ab..dc7cb83373 100644
--- a/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb
+++ b/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb
@@ -9,6 +9,10 @@ module SDK
module Logs
# Implementation of OpenTelemetry::Logs::LogRecord that records log events.
class LogRecord < OpenTelemetry::Logs::LogRecord
+ EMPTY_ATTRIBUTES = {}.freeze
+
+ private_constant :EMPTY_ATTRIBUTES
+
attr_accessor :timestamp,
:observed_timestamp,
:severity_text,
@@ -49,6 +53,8 @@ class LogRecord < OpenTelemetry::Logs::LogRecord
# source of the log, desrived from the LoggerProvider.
# @param [optional OpenTelemetry::SDK::InstrumentationScope] instrumentation_scope
# The instrumentation scope, derived from the emitting Logger
+ # @param [optional] OpenTelemetry::SDK::LogRecordLimits] log_record_limits
+ # Attribute limits
#
#
# @return [LogRecord]
@@ -63,7 +69,8 @@ def initialize(
span_id: nil,
trace_flags: nil,
resource: nil,
- instrumentation_scope: nil
+ instrumentation_scope: nil,
+ log_record_limits: nil
)
@timestamp = timestamp
@observed_timestamp = observed_timestamp || timestamp || Time.now
@@ -76,7 +83,10 @@ def initialize(
@trace_flags = trace_flags
@resource = resource
@instrumentation_scope = instrumentation_scope
+ @log_record_limits = log_record_limits || LogRecordLimits::DEFAULT
@total_recorded_attributes = @attributes&.size || 0
+
+ trim_attributes(@attributes)
end
def to_log_record_data
@@ -103,6 +113,51 @@ def to_integer_nanoseconds(timestamp)
(timestamp.to_r * 10**9).to_i
end
+
+ def trim_attributes(attributes)
+ return if attributes.nil?
+
+ # truncate total attributes
+ truncate_attributes(attributes, @log_record_limits.attribute_count_limit)
+
+ # truncate attribute values
+ truncate_attribute_values(attributes, @log_record_limits.attribute_length_limit)
+
+ # validate attributes
+ validate_attributes(attributes)
+
+ nil
+ end
+
+ def truncate_attributes(attributes, attribute_limit)
+ excess = attributes.size - attribute_limit
+ excess.times { attributes.shift } if excess.positive?
+ end
+
+ def validate_attributes(attrs)
+ # Similar to Internal.valid_attributes?, but with different messages
+ # Future refactor opportunity: https://github.com/open-telemetry/opentelemetry-ruby/issues/1739
+ attrs.keep_if do |k, v|
+ if !Internal.valid_key?(k)
+ OpenTelemetry.handle_error(message: "invalid log record attribute key type #{k.class} on record: '#{body}'")
+ return false
+ elsif !Internal.valid_value?(v)
+ OpenTelemetry.handle_error(message: "invalid log record attribute value type #{v.class} for key '#{k}' on record: '#{body}'")
+ return false
+ end
+
+ true
+ end
+ end
+
+ def truncate_attribute_values(attributes, attribute_length_limit)
+ return EMPTY_ATTRIBUTES if attributes.nil?
+ return attributes if attribute_length_limit.nil?
+
+ attributes.transform_values! { |value| OpenTelemetry::Common::Utilities.truncate_attribute_value(value, attribute_length_limit) }
+
+ attributes
+ end
end
end
end
diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/log_record_limits.rb b/logs_sdk/lib/opentelemetry/sdk/logs/log_record_limits.rb
new file mode 100644
index 0000000000..6440b7a4aa
--- /dev/null
+++ b/logs_sdk/lib/opentelemetry/sdk/logs/log_record_limits.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+module OpenTelemetry
+ module SDK
+ module Logs
+ # Class that holds log record attribute limit parameters.
+ class LogRecordLimits
+ # The global default max number of attributes per {LogRecord}.
+ attr_reader :attribute_count_limit
+
+ # The global default max length of attribute value per {LogRecord}.
+ attr_reader :attribute_length_limit
+
+ # Returns a {LogRecordLimits} with the desired values.
+ #
+ # @return [LogRecordLimits] with the desired values.
+ # @raise [ArgumentError] if any of the max numbers are not positive.
+ def initialize(attribute_count_limit: Integer(OpenTelemetry::Common::Utilities.config_opt(
+ 'OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT',
+ 'OTEL_ATTRIBUTE_COUNT_LIMIT',
+ default: 128
+ )),
+ attribute_length_limit: OpenTelemetry::Common::Utilities.config_opt(
+ 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT',
+ 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT'
+ ))
+ raise ArgumentError, 'attribute_count_limit must be positive' unless attribute_count_limit.positive?
+ raise ArgumentError, 'attribute_length_limit must not be less than 32' unless attribute_length_limit.nil? || Integer(attribute_length_limit) >= 32
+
+ @attribute_count_limit = attribute_count_limit
+ @attribute_length_limit = attribute_length_limit.nil? ? nil : Integer(attribute_length_limit)
+ end
+
+ # The default {LogRecordLimits}.
+ DEFAULT = new
+ end
+ end
+ end
+end
diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb b/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb
index 99e7df10e5..a06494772d 100644
--- a/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb
+++ b/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb
@@ -9,6 +9,9 @@ module SDK
module Logs
# The SDK implementation of OpenTelemetry::Logs::LoggerProvider.
class LoggerProvider < OpenTelemetry::Logs::LoggerProvider
+ Key = Struct.new(:name, :version)
+ private_constant(:Key)
+
UNEXPECTED_ERROR_MESSAGE = 'unexpected error in ' \
'OpenTelemetry::SDK::Logs::LoggerProvider#%s'
@@ -18,13 +21,18 @@ class LoggerProvider < OpenTelemetry::Logs::LoggerProvider
#
# @param [optional Resource] resource The resource to associate with
# new LogRecords created by {Logger}s created by this LoggerProvider.
+ # @param [optional LogRecordLimits] log_record_limits The limits for
+ # attributes count and attribute length for LogRecords.
#
# @return [OpenTelemetry::SDK::Logs::LoggerProvider]
- def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create)
+ def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create, log_record_limits: LogRecordLimits::DEFAULT)
@log_record_processors = []
+ @log_record_limits = log_record_limits
@mutex = Mutex.new
@resource = resource
@stopped = false
+ @registry = {}
+ @registry_mutex = Mutex.new
end
# Returns an {OpenTelemetry::SDK::Logs::Logger} instance.
@@ -41,7 +49,9 @@ def logger(name:, version: nil)
"invalid name. Name provided: #{name.inspect}")
end
- Logger.new(name, version, self)
+ @registry_mutex.synchronize do
+ @registry[Key.new(name, version)] ||= Logger.new(name, version, self)
+ end
end
# Adds a new log record processor to this LoggerProvider's
@@ -132,6 +142,8 @@ def on_emit(timestamp: nil,
instrumentation_scope: nil,
context: nil)
+ return if @stopped
+
log_record = LogRecord.new(timestamp: timestamp,
observed_timestamp: observed_timestamp,
severity_text: severity_text,
@@ -142,7 +154,8 @@ def on_emit(timestamp: nil,
span_id: span_id,
trace_flags: trace_flags,
resource: @resource,
- instrumentation_scope: instrumentation_scope)
+ instrumentation_scope: instrumentation_scope,
+ log_record_limits: @log_record_limits)
@log_record_processors.each { |processor| processor.on_emit(log_record, context) }
end
diff --git a/logs_sdk/test/opentelemetry/sdk/logs/export/batch_log_record_processor_test.rb b/logs_sdk/test/opentelemetry/sdk/logs/export/batch_log_record_processor_test.rb
index 64e81477a3..cff2b718d2 100644
--- a/logs_sdk/test/opentelemetry/sdk/logs/export/batch_log_record_processor_test.rb
+++ b/logs_sdk/test/opentelemetry/sdk/logs/export/batch_log_record_processor_test.rb
@@ -186,21 +186,24 @@ def to_log_record_data
end
it 'removes the older log records from the batch if full' do
- processor = BatchLogRecordProcessor.new(TestExporter.new, max_queue_size: 1, max_export_batch_size: 1)
+ # Windows intermittently fails if we don't set this
+ OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_BLRP_START_THREAD_ON_BOOT' => 'false') do
+ processor = BatchLogRecordProcessor.new(TestExporter.new, max_queue_size: 1, max_export_batch_size: 1)
- older_log_record = TestLogRecord.new
- newer_log_record = TestLogRecord.new
- newest_log_record = TestLogRecord.new
+ # Don't actually try to export, we're looking at the log records array
+ processor.stub(:work, nil) do
+ older_log_record = TestLogRecord.new
+ newest_log_record = TestLogRecord.new
- processor.on_emit(older_log_record, mock_context)
- processor.on_emit(newer_log_record, mock_context)
- processor.on_emit(newest_log_record, mock_context)
+ processor.on_emit(older_log_record, mock_context)
+ processor.on_emit(newest_log_record, mock_context)
- records = processor.instance_variable_get(:@log_records)
+ records = processor.instance_variable_get(:@log_records)
- assert_includes(records, newest_log_record)
- refute_includes(records, newer_log_record)
- refute_includes(records, older_log_record)
+ assert_includes(records, newest_log_record)
+ refute_includes(records, older_log_record)
+ end
+ end
end
it 'logs a warning if a log record was emitted after the buffer is full' do
@@ -469,18 +472,21 @@ def shutdown(timeout: nil)
let(:processor) { BatchLogRecordProcessor.new(exporter) }
it 'reports export failures' do
- mock_logger = Minitest::Mock.new
- mock_logger.expect(:error, nil, [/Unable to export/])
- mock_logger.expect(:error, nil, [/Result code: 1/])
- mock_logger.expect(:error, nil, [/unexpected error in .*\#export_batch/])
-
- OpenTelemetry.stub(:logger, mock_logger) do
- log_records = [TestLogRecord.new, TestLogRecord.new, TestLogRecord.new, TestLogRecord.new]
- log_records.each { |log_record| processor.on_emit(log_record, mock_context) }
- processor.shutdown
- end
+ # skip the work method's behavior, we rely on shutdown to get us to the failures
+ processor.stub(:work, nil) do
+ mock_logger = Minitest::Mock.new
+ mock_logger.expect(:error, nil, [/Unable to export/])
+ mock_logger.expect(:error, nil, [/Result code: 1/])
+ mock_logger.expect(:error, nil, [/unexpected error in .*\#export_batch/])
+
+ OpenTelemetry.stub(:logger, mock_logger) do
+ log_records = [TestLogRecord.new, TestLogRecord.new, TestLogRecord.new, TestLogRecord.new]
+ log_records.each { |log_record| processor.on_emit(log_record, mock_context) }
+ processor.shutdown
+ end
- mock_logger.verify
+ mock_logger.verify
+ end
end
end
diff --git a/logs_sdk/test/opentelemetry/sdk/logs/log_record_limits_test.rb b/logs_sdk/test/opentelemetry/sdk/logs/log_record_limits_test.rb
new file mode 100644
index 0000000000..04c6238276
--- /dev/null
+++ b/logs_sdk/test/opentelemetry/sdk/logs/log_record_limits_test.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'test_helper'
+
+describe OpenTelemetry::SDK::Logs::LogRecordLimits do
+ let(:log_record_limits) { OpenTelemetry::SDK::Logs::LogRecordLimits.new }
+
+ describe '#initialize' do
+ it 'provides defaults' do
+ _(log_record_limits.attribute_count_limit).must_equal 128
+ _(log_record_limits.attribute_length_limit).must_be_nil
+ end
+
+ it 'prioritizes specific environment varibles for attribute value length limits' do
+ OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '35',
+ 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '33') do
+ _(log_record_limits.attribute_length_limit).must_equal 33
+ end
+ end
+
+ it 'uses general attribute value length limits in the absence of more specific ones' do
+ OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '35') do
+ _(log_record_limits.attribute_length_limit).must_equal 35
+ end
+ end
+
+ it 'reflects environment variables' do
+ OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1',
+ 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32') do
+ _(log_record_limits.attribute_count_limit).must_equal 1
+ _(log_record_limits.attribute_length_limit).must_equal 32
+ end
+ end
+
+ it 'reflects explicit overrides' do
+ OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1',
+ 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '4') do
+ log_record_limits = OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_count_limit: 10,
+ attribute_length_limit: 32)
+ _(log_record_limits.attribute_count_limit).must_equal 10
+ _(log_record_limits.attribute_length_limit).must_equal 32
+ end
+ end
+
+ it 'reflects generic attribute env vars' do
+ OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_COUNT_LIMIT' => '1',
+ 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32') do
+ _(log_record_limits.attribute_count_limit).must_equal 1
+ _(log_record_limits.attribute_length_limit).must_equal 32
+ end
+ end
+
+ it 'prefers model-specific attribute env vars over generic attribute env vars' do
+ OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1',
+ 'OTEL_ATTRIBUTE_COUNT_LIMIT' => '2',
+ 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32',
+ 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '33') do
+ _(log_record_limits.attribute_count_limit).must_equal 1
+ _(log_record_limits.attribute_length_limit).must_equal 32
+ end
+ end
+
+ it 'raises if attribute_count_limit is not positive' do
+ assert_raises ArgumentError do
+ OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_count_limit: -1)
+ end
+ end
+
+ it 'raises if attribute_length_limit is less than 32' do
+ assert_raises ArgumentError do
+ OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_length_limit: 31)
+ end
+ end
+ end
+end
diff --git a/logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb b/logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb
index ca38350e1f..71a2557e9d 100644
--- a/logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb
+++ b/logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb
@@ -92,5 +92,85 @@
assert_equal(args[:instrumentation_scope], log_record_data.instrumentation_scope)
end
end
+
+ describe 'attribute limits' do
+ it 'uses the limits set by the logger provider via the logger' do
+ # Spy on the console output
+ captured_stdout = StringIO.new
+ original_stdout = $stdout
+ $stdout = captured_stdout
+
+ # Create the LoggerProvider with the console exporter and an attribute limit of 1
+ limits = Logs::LogRecordLimits.new(attribute_count_limit: 1)
+ logger_provider = Logs::LoggerProvider.new(log_record_limits: limits)
+ console_exporter = Logs::Export::SimpleLogRecordProcessor.new(Logs::Export::ConsoleLogRecordExporter.new)
+ logger_provider.add_log_record_processor(console_exporter)
+
+ # Create a logger that uses the given LoggerProvider
+ logger = Logs::Logger.new('', '', logger_provider)
+
+ # Emit a log from that logger, with attribute count exceeding the limit
+ logger.on_emit(attributes: { 'a' => 'a', 'b' => 'b' })
+
+ # Look at the captured output to see if the attributes have been truncated
+ assert_match(/attributes={"b"=>"b"}/, captured_stdout.string)
+ refute_match(/"a"=>"a"/, captured_stdout.string)
+
+ # Return STDOUT to its normal output
+ $stdout = original_stdout
+ end
+
+ it 'emits an error message if attribute key is invalid' do
+ OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
+ logger.on_emit(attributes: { a: 'a' })
+ assert_match(/invalid log record attribute key type Symbol/, log_stream.string)
+ end
+ end
+
+ it 'emits an error message if the attribute value is invalid' do
+ OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
+ logger.on_emit(attributes: { 'a' => Class.new })
+ assert_match(/invalid log record attribute value type Class/, log_stream.string)
+ end
+ end
+
+ it 'uses the default limits if none provided' do
+ log_record = Logs::LogRecord.new
+ default = Logs::LogRecordLimits::DEFAULT
+
+ assert_equal(default.attribute_count_limit, log_record.instance_variable_get(:@log_record_limits).attribute_count_limit)
+ # default length is nil
+ assert_nil(log_record.instance_variable_get(:@log_record_limits).attribute_length_limit)
+ end
+
+ it 'trims the oldest attributes' do
+ limits = Logs::LogRecordLimits.new(attribute_count_limit: 1)
+ attributes = { 'old' => 'old', 'new' => 'new' }
+ log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: attributes)
+
+ assert_equal({ 'new' => 'new' }, log_record.attributes)
+ end
+ end
+
+ describe 'attribute value limit' do
+ it 'truncates the values that are too long' do
+ length_limit = 32
+ too_long = 'a' * (length_limit + 1)
+ just_right = 'a' * (length_limit - 3) # truncation removes 3 chars for the '...'
+ limits = Logs::LogRecordLimits.new(attribute_length_limit: length_limit)
+ log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: { 'key' => too_long })
+
+ assert_equal({ 'key' => "#{just_right}..." }, log_record.attributes)
+ end
+
+ it 'does not alter values within the range' do
+ length_limit = 32
+ within_range = 'a' * length_limit
+ limits = Logs::LogRecordLimits.new(attribute_length_limit: length_limit)
+ log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: { 'key' => within_range })
+
+ assert_equal({ 'key' => within_range }, log_record.attributes)
+ end
+ end
end
end
diff --git a/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb b/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb
index 023a9a67c9..992a4e8170 100644
--- a/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb
+++ b/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb
@@ -24,6 +24,15 @@
end
end
+ describe '#initialize' do
+ it 'activates a default LogRecordLimits' do
+ assert_equal(
+ OpenTelemetry::SDK::Logs::LogRecordLimits::DEFAULT,
+ logger_provider.instance_variable_get(:@log_record_limits)
+ )
+ end
+ end
+
describe '#add_log_record_processor' do
it "adds the processor to the logger provider's processors" do
assert_equal(0, logger_provider.instance_variable_get(:@log_record_processors).length)
@@ -73,15 +82,15 @@
# :version is nil by default, but explicitly setting it here
# to make the test easier to read
logger = logger_provider.logger(name: 'name', version: nil)
- assert_equal(logger.instance_variable_get(:@instrumentation_scope).version, '')
+ assert_equal('', logger.instance_variable_get(:@instrumentation_scope).version)
end
it 'creates a new logger with the passed-in name and version' do
name = 'name'
version = 'version'
logger = logger_provider.logger(name: name, version: version)
- assert_equal(logger.instance_variable_get(:@instrumentation_scope).name, name)
- assert_equal(logger.instance_variable_get(:@instrumentation_scope).version, version)
+ assert_equal(name, logger.instance_variable_get(:@instrumentation_scope).name)
+ assert_equal(version, logger.instance_variable_get(:@instrumentation_scope).version)
end
end
@@ -214,5 +223,32 @@ def mock_context.value(key); OpenTelemetry::Trace::Span::INVALID; end # rubocop:
mock_log_record_processor.verify
end
end
+
+ it 'does not emit if the provider is stopped' do
+ logger_provider.instance_variable_set(:@stopped, true)
+ mock_log_record = Minitest::Mock.new
+
+ OpenTelemetry::SDK::Logs::LogRecord.stub :new, mock_log_record do
+ logger_provider.add_log_record_processor(mock_log_record_processor)
+ mock_log_record_processor.expect(:on_emit, nil, [mock_log_record, mock_context])
+
+ logger_provider.on_emit(**args)
+ # The verify should fail because on_emit should never call new on LogRecord
+ assert_raises(MockExpectationError) { mock_log_record_processor.verify }
+ end
+ end
+ end
+
+ describe 'instrument registry' do
+ # On the first call, create a logger with an empty string for name and
+ # version and add to the registry. The second call returns that logger
+ # from the registry instead of creating a new instance.
+ it 'reuses the same logger if called twice when name and version are nil' do
+ logger = logger_provider.logger(name: nil, version: nil)
+ logger2 = logger_provider.logger(name: nil, version: nil)
+
+ assert_instance_of(Logs::Logger, logger)
+ assert_same(logger, logger2)
+ end
end
end
diff --git a/metrics_api/CHANGELOG.md b/metrics_api/CHANGELOG.md
index bdadf3885e..afbcca5eac 100644
--- a/metrics_api/CHANGELOG.md
+++ b/metrics_api/CHANGELOG.md
@@ -1,5 +1,9 @@
# Release History: opentelemetry-metrics-api
+### v0.1.1 / 2024-10-22
+
+* FIXED: Refactor instrument validation
+
### v0.1.0 / 2024-07-31
Initial release.
diff --git a/metrics_api/lib/opentelemetry/metrics/meter.rb b/metrics_api/lib/opentelemetry/metrics/meter.rb
index 1335437c02..f44b586110 100644
--- a/metrics_api/lib/opentelemetry/metrics/meter.rb
+++ b/metrics_api/lib/opentelemetry/metrics/meter.rb
@@ -15,8 +15,6 @@ class Meter
UP_DOWN_COUNTER = Instrument::UpDownCounter.new
OBSERVABLE_UP_DOWN_COUNTER = Instrument::ObservableUpDownCounter.new
- NAME_REGEX = /\A[a-zA-Z][-.\w]{0,62}\z/
-
private_constant(:COUNTER, :OBSERVABLE_COUNTER, :HISTOGRAM, :OBSERVABLE_GAUGE, :UP_DOWN_COUNTER, :OBSERVABLE_UP_DOWN_COUNTER)
DuplicateInstrumentError = Class.new(OpenTelemetry::Error)
@@ -56,23 +54,12 @@ def create_observable_up_down_counter(name, callback:, unit: nil, description: n
private
def create_instrument(kind, name, unit, description, callback)
- raise InstrumentNameError if name.nil?
- 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.dup))
-
@mutex.synchronize do
OpenTelemetry.logger.warn("duplicate instrument registration occurred for instrument #{name}") if @instrument_registry.include? name
@instrument_registry[name] = yield
end
end
-
- def utf8mb3_encoding?(string)
- string.force_encoding('UTF-8').valid_encoding? &&
- string.each_char { |c| return false if c.bytesize >= 4 }
- end
end
end
end
diff --git a/metrics_api/lib/opentelemetry/metrics/version.rb b/metrics_api/lib/opentelemetry/metrics/version.rb
index a1238c7212..ab557fc819 100644
--- a/metrics_api/lib/opentelemetry/metrics/version.rb
+++ b/metrics_api/lib/opentelemetry/metrics/version.rb
@@ -7,6 +7,6 @@
module OpenTelemetry
module Metrics
## Current OpenTelemetry metrics version
- VERSION = '0.1.0'
+ VERSION = '0.1.1'
end
end
diff --git a/metrics_api/test/opentelemetry/metrics/meter_test.rb b/metrics_api/test/opentelemetry/metrics/meter_test.rb
index 493c373b5b..ec9b53e6e5 100644
--- a/metrics_api/test/opentelemetry/metrics/meter_test.rb
+++ b/metrics_api/test/opentelemetry/metrics/meter_test.rb
@@ -7,11 +7,6 @@
require 'test_helper'
describe OpenTelemetry::Metrics::Meter do
- INSTRUMENT_NAME_ERROR = OpenTelemetry::Metrics::Meter::InstrumentNameError
- INSTRUMENT_UNIT_ERROR = OpenTelemetry::Metrics::Meter::InstrumentUnitError
- INSTRUMENT_DESCRIPTION_ERROR = OpenTelemetry::Metrics::Meter::InstrumentDescriptionError
- DUPLICATE_INSTRUMENT_ERROR = OpenTelemetry::Metrics::Meter::DuplicateInstrumentError
-
let(:meter_provider) { OpenTelemetry::Metrics::MeterProvider.new }
let(:meter) { meter_provider.meter('test-meter') }
@@ -24,50 +19,34 @@
end
end
- it 'instrument name must not be nil' do
- _(-> { meter.create_counter(nil) }).must_raise(INSTRUMENT_NAME_ERROR)
- end
-
- it 'instument name must not be an empty string' do
- _(-> { meter.create_counter('') }).must_raise(INSTRUMENT_NAME_ERROR)
- end
-
- it 'instrument name must have an alphabetic first character' do
- _(meter.create_counter('one_counter'))
- _(-> { meter.create_counter('1_counter') }).must_raise(INSTRUMENT_NAME_ERROR)
- end
-
- it 'instrument name must not exceed 63 character limit' do
- long_name = 'a' * 63
- meter.create_counter(long_name)
- _(-> { meter.create_counter(long_name + 'a') }).must_raise(INSTRUMENT_NAME_ERROR)
+ it 'test create_counter' do
+ counter = meter.create_counter('test')
+ _(counter.class).must_equal(OpenTelemetry::Metrics::Instrument::Counter)
end
- it 'instrument name must belong to alphanumeric characters, _, ., and -' do
- meter.create_counter('a_-..-_a')
- _(-> { meter.create_counter('a@') }).must_raise(INSTRUMENT_NAME_ERROR)
- _(-> { meter.create_counter('a!') }).must_raise(INSTRUMENT_NAME_ERROR)
+ it 'test create_histogram' do
+ counter = meter.create_histogram('test')
+ _(counter.class).must_equal(OpenTelemetry::Metrics::Instrument::Histogram)
end
- it 'instrument unit must be ASCII' do
- _(-> { meter.create_counter('a_counter', unit: 'á') }).must_raise(INSTRUMENT_UNIT_ERROR)
+ it 'test create_up_down_counter' do
+ counter = meter.create_up_down_counter('test')
+ _(counter.class).must_equal(OpenTelemetry::Metrics::Instrument::UpDownCounter)
end
- it 'instrument unit must not exceed 63 characters' do
- long_unit = 'a' * 63
- meter.create_counter('a_counter', unit: long_unit)
- _(-> { meter.create_counter('b_counter', unit: long_unit + 'a') }).must_raise(INSTRUMENT_UNIT_ERROR)
+ it 'test create_observable_counter' do
+ counter = meter.create_observable_counter('test', callback: -> {})
+ _(counter.class).must_equal(OpenTelemetry::Metrics::Instrument::ObservableCounter)
end
- it 'instrument description must be utf8mb3' do
- _(-> { meter.create_counter('a_counter', description: '💩'.dup) }).must_raise(INSTRUMENT_DESCRIPTION_ERROR)
- _(-> { meter.create_counter('b_counter', description: "\xc2".dup) }).must_raise(INSTRUMENT_DESCRIPTION_ERROR)
+ it 'test create_observable_gauge' do
+ counter = meter.create_observable_gauge('test', callback: -> {})
+ _(counter.class).must_equal(OpenTelemetry::Metrics::Instrument::ObservableGauge)
end
- it 'instrument description must not exceed 1023 characters' do
- long_description = 'a' * 1023
- meter.create_counter('a_counter', description: long_description)
- _(-> { meter.create_counter('b_counter', description: long_description + 'a') }).must_raise(INSTRUMENT_DESCRIPTION_ERROR)
+ it 'test create_observable_up_down_counter' do
+ counter = meter.create_observable_up_down_counter('test', callback: -> {})
+ _(counter.class).must_equal(OpenTelemetry::Metrics::Instrument::ObservableUpDownCounter)
end
end
end
diff --git a/metrics_sdk/CHANGELOG.md b/metrics_sdk/CHANGELOG.md
index 1328411e29..d8be534a1b 100644
--- a/metrics_sdk/CHANGELOG.md
+++ b/metrics_sdk/CHANGELOG.md
@@ -1,5 +1,11 @@
# Release History: opentelemetry-metrics-sdk
+### v0.3.0 / 2024-10-22
+
+* ADDED: Add basic metrics view
+* FIXED: Coerce aggregation_temporality to symbol
+* FIXED: Add warning if invalid meter name given
+
### v0.2.0 / 2024-08-27
* ADDED: Add basic periodic exporting metric_reader
diff --git a/metrics_sdk/Gemfile b/metrics_sdk/Gemfile
index a777c7f064..c4b8797f42 100644
--- a/metrics_sdk/Gemfile
+++ b/metrics_sdk/Gemfile
@@ -16,6 +16,7 @@ gem 'opentelemetry-sdk', path: '../sdk'
gem 'opentelemetry-test-helpers', path: '../test_helpers'
group :test, :development do
+ gem 'opentelemetry-exporter-otlp-metrics', path: '../exporter/otlp-metrics' unless RUBY_ENGINE == 'jruby'
gem 'pry'
gem 'pry-byebug' unless RUBY_ENGINE == 'jruby'
end
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics.rb
index 42807c708b..35593185f9 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics.rb
@@ -20,3 +20,4 @@ module Metrics
require 'opentelemetry/sdk/metrics/meter'
require 'opentelemetry/sdk/metrics/meter_provider'
require 'opentelemetry/sdk/metrics/state'
+require 'opentelemetry/sdk/metrics/view'
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation.rb
index f36aef0152..62f820eb93 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation.rb
@@ -19,3 +19,5 @@ module Aggregation
require 'opentelemetry/sdk/metrics/aggregation/histogram_data_point'
require 'opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram'
require 'opentelemetry/sdk/metrics/aggregation/sum'
+require 'opentelemetry/sdk/metrics/aggregation/last_value'
+require 'opentelemetry/sdk/metrics/aggregation/drop'
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/drop.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/drop.rb
new file mode 100644
index 0000000000..f638c649a5
--- /dev/null
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/drop.rb
@@ -0,0 +1,37 @@
+# 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 Drop aggregation
+ class Drop
+ attr_reader :aggregation_temporality
+
+ def initialize(aggregation_temporality: :delta)
+ @aggregation_temporality = aggregation_temporality
+ end
+
+ def collect(start_time, end_time, data_points)
+ data_points.values.map!(&:dup)
+ end
+
+ def update(increment, attributes, data_points)
+ data_points[attributes] = NumberDataPoint.new(
+ {},
+ 0,
+ 0,
+ 0,
+ 0
+ )
+ nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb
index 5af469b61d..0d357e1d49 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb
@@ -25,25 +25,25 @@ def initialize(
boundaries: DEFAULT_BOUNDARIES,
record_min_max: true
)
- @data_points = {}
- @aggregation_temporality = aggregation_temporality
+
+ @aggregation_temporality = aggregation_temporality.to_sym
@boundaries = boundaries && !boundaries.empty? ? boundaries.sort : nil
@record_min_max = record_min_max
end
- def collect(start_time, end_time)
+ def collect(start_time, end_time, data_points)
if @aggregation_temporality == :delta
# Set timestamps and 'move' data point values to result.
- hdps = @data_points.values.map! do |hdp|
+ hdps = data_points.values.map! do |hdp|
hdp.start_time_unix_nano = start_time
hdp.time_unix_nano = end_time
hdp
end
- @data_points.clear
+ data_points.clear
hdps
else
# Update timestamps and take a snapshot.
- @data_points.values.map! do |hdp|
+ data_points.values.map! do |hdp|
hdp.start_time_unix_nano ||= start_time # Start time of a data point is from the first observation.
hdp.time_unix_nano = end_time
hdp = hdp.dup
@@ -53,14 +53,14 @@ def collect(start_time, end_time)
end
end
- def update(amount, attributes)
- hdp = @data_points.fetch(attributes) do
+ def update(amount, attributes, data_points)
+ hdp = data_points.fetch(attributes) do
if @record_min_max
min = Float::INFINITY
max = -Float::INFINITY
end
- @data_points[attributes] = HistogramDataPoint.new(
+ data_points[attributes] = HistogramDataPoint.new(
attributes,
nil, # :start_time_unix_nano
nil, # :time_unix_nano
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/last_value.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/last_value.rb
new file mode 100644
index 0000000000..b2cffb74e2
--- /dev/null
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/last_value.rb
@@ -0,0 +1,53 @@
+# 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 LastValue aggregation
+ class LastValue
+ attr_reader :aggregation_temporality
+
+ def initialize(aggregation_temporality: :delta)
+ @aggregation_temporality = aggregation_temporality
+ end
+
+ def collect(start_time, end_time, data_points)
+ if @aggregation_temporality == :delta
+ # Set timestamps and 'move' data point values to result.
+ ndps = data_points.values.map! do |ndp|
+ ndp.start_time_unix_nano = start_time
+ ndp.time_unix_nano = end_time
+ ndp
+ end
+ data_points.clear
+ ndps
+ else
+ # Update timestamps and take a snapshot.
+ data_points.values.map! do |ndp|
+ ndp.start_time_unix_nano ||= start_time # Start time of a data point is from the first observation.
+ ndp.time_unix_nano = end_time
+ ndp.dup
+ end
+ end
+ end
+
+ def update(increment, attributes, data_points)
+ data_points[attributes] = NumberDataPoint.new(
+ attributes,
+ nil,
+ nil,
+ increment,
+ nil
+ )
+ nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/sum.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/sum.rb
index 16cbccc430..c2771b38e3 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/sum.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/sum.rb
@@ -15,23 +15,22 @@ class Sum
def initialize(aggregation_temporality: ENV.fetch('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE', :delta))
# TODO: the default should be :cumulative, see issue #1555
- @aggregation_temporality = aggregation_temporality
- @data_points = {}
+ @aggregation_temporality = aggregation_temporality.to_sym
end
- def collect(start_time, end_time)
+ def collect(start_time, end_time, data_points)
if @aggregation_temporality == :delta
# Set timestamps and 'move' data point values to result.
- ndps = @data_points.values.map! do |ndp|
+ ndps = data_points.values.map! do |ndp|
ndp.start_time_unix_nano = start_time
ndp.time_unix_nano = end_time
ndp
end
- @data_points.clear
+ data_points.clear
ndps
else
# Update timestamps and take a snapshot.
- @data_points.values.map! do |ndp|
+ data_points.values.map! do |ndp|
ndp.start_time_unix_nano ||= start_time # Start time of a data point is from the first observation.
ndp.time_unix_nano = end_time
ndp.dup
@@ -39,8 +38,8 @@ def collect(start_time, end_time)
end
end
- def update(increment, attributes)
- ndp = @data_points[attributes] || @data_points[attributes] = NumberDataPoint.new(
+ def update(increment, attributes, data_points)
+ ndp = data_points[attributes] || data_points[attributes] = NumberDataPoint.new(
attributes,
nil,
nil,
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb
index ac529e67f4..f9ee2c07fa 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb
@@ -10,12 +10,47 @@ module Metrics
# The ConfiguratorPatch implements a hook to configure the metrics
# portion of the SDK.
module ConfiguratorPatch
+ def add_metric_reader(metric_reader)
+ @metric_readers << metric_reader
+ end
+
private
- # The metrics_configuration_hook method is where we define the setup process
- # for metrics SDK.
+ def initialize
+ super
+ @metric_readers = []
+ end
+
+ # The metrics_configuration_hook method is where we define the setup process for the metrics SDK.
def metrics_configuration_hook
OpenTelemetry.meter_provider = Metrics::MeterProvider.new(resource: @resource)
+ configure_metric_readers
+ end
+
+ def configure_metric_readers
+ readers = @metric_readers.empty? ? wrapped_metric_exporters_from_env.compact : @metric_readers
+ readers.each { |r| OpenTelemetry.meter_provider.add_metric_reader(r) }
+ end
+
+ def wrapped_metric_exporters_from_env
+ exporters = ENV.fetch('OTEL_METRICS_EXPORTER', 'otlp')
+ exporters.split(',').map do |exporter|
+ case exporter.strip
+ when 'none' then nil
+ when 'console' then OpenTelemetry.meter_provider.add_metric_reader(Metrics::Export::PeriodicMetricReader.new(exporter: Metrics::Export::ConsoleMetricPullExporter.new))
+ when 'in-memory' then OpenTelemetry.meter_provider.add_metric_reader(Metrics::Export::InMemoryMetricPullExporter.new)
+ when 'otlp'
+ begin
+ OpenTelemetry.meter_provider.add_metric_reader(Metrics::Export::PeriodicMetricReader.new(exporter: OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new))
+ rescue NameError
+ OpenTelemetry.logger.warn 'The otlp metrics exporter cannot be configured - please add opentelemetry-exporter-otlp-metrics to your Gemfile, metrics will not be exported'
+ nil
+ end
+ else
+ OpenTelemetry.logger.warn "The #{exporter} exporter is unknown and cannot be configured, metrics will not be exported"
+ nil
+ end
+ end
end
end
end
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
index 8291ed9fe7..d0d8ccb902 100644
--- 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
@@ -25,7 +25,7 @@ def pull
def export(metrics, timeout: nil)
@mutex.synchronize do
- @metric_snapshots << metrics
+ @metric_snapshots.concat(Array(metrics))
end
SUCCESS
end
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb
index 8db3ea992c..1307817ec6 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter.rb
@@ -11,6 +11,8 @@ module SDK
module Metrics
# {Meter} is the SDK implementation of {OpenTelemetry::Metrics::Meter}.
class Meter < OpenTelemetry::Metrics::Meter
+ NAME_REGEX = /\A[a-zA-Z][-.\w]{0,62}\z/
+
# @api private
#
# Returns a new {Meter} instance.
@@ -34,6 +36,12 @@ def add_metric_reader(metric_reader)
end
def create_instrument(kind, name, unit, description, callback)
+ raise InstrumentNameError if name.nil?
+ 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.dup))
+
super do
case kind
when :counter then OpenTelemetry::SDK::Metrics::Instrument::Counter.new(name, unit, description, @instrumentation_scope, @meter_provider)
@@ -45,6 +53,11 @@ def create_instrument(kind, name, unit, description, callback)
end
end
end
+
+ def utf8mb3_encoding?(string)
+ string.force_encoding('UTF-8').valid_encoding? &&
+ string.each_char { |c| return false if c.bytesize >= 4 }
+ 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
index 205ff5db0d..4538a88db4 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/meter_provider.rb
@@ -14,7 +14,7 @@ class MeterProvider < OpenTelemetry::Metrics::MeterProvider
Key = Struct.new(:name, :version)
private_constant(:Key)
- attr_reader :resource, :metric_readers
+ attr_reader :resource, :metric_readers, :registered_views
def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create)
@mutex = Mutex.new
@@ -22,6 +22,7 @@ def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create)
@stopped = false
@metric_readers = []
@resource = resource
+ @registered_views = []
end
# Returns a {Meter} instance.
@@ -36,6 +37,7 @@ def meter(name, version: nil)
OpenTelemetry.logger.warn 'calling MeterProvider#meter after shutdown, a noop meter will be returned.'
OpenTelemetry::Metrics::Meter.new
else
+ OpenTelemetry.logger.warn "Invalid meter name provided: #{name.nil? ? 'nil' : 'empty'} value" if name.to_s.empty?
@mutex.synchronize { @meter_registry[Key.new(name, version)] ||= Meter.new(name, version, self) }
end
end
@@ -125,13 +127,30 @@ def register_synchronous_instrument(instrument)
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
+ # A View provides SDK users with the flexibility to customize the metrics that are output by the SDK.
+ #
+ # Example:
+ #
+ # OpenTelemetry.meter_provider.add_view('test', :aggregation => Aggregation::Drop.new,
+ # :type => :counter, :unit => 'smidgen',
+ # :meter_name => 'test', :meter_version => '1.0')
+ #
+ #
+ # @param [String] name Name of the view.
+ # @param [optional Hash] options For more precise matching, {View} and {MetricsStream}
+ # options may include:
+ # aggregation: An instance of an aggregation class, e.g. {ExplicitBucketHistogram}, {Sum}, {LastValue}
+ # type: A Symbol representing the instrument kind, e.g. :observable_gauge, :counter
+ # unit: A String matching an instrumentation unit, e.g. 'smidgen'
+ # meter_name: A String matching a meter name, e.g. meter_provider.meter('sample_meter_name', version: '1.2.0'), would be 'sample_meter_name'
+ # meter_version: A String matching a meter version, e.g. meter_provider.meter('sample_meter_name', version: '1.2.0'), would be '1.2.0'
+ #
+ # @return [nil] returns nil
+ #
+ def add_view(name, **options)
+ # TODO: add schema_url as part of options
+ @registered_views << View::RegisteredView.new(name, **options)
+ nil
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
index de2ecb83b6..b89eb09161 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_store.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_store.rb
@@ -23,7 +23,8 @@ def initialize
def collect
@mutex.synchronize do
@epoch_end_time = now_in_nano
- snapshot = @metric_streams.map { |ms| ms.collect(@epoch_start_time, @epoch_end_time) }
+ # snapshot = @metric_streams.map { |ms| ms.collect(@epoch_start_time, @epoch_end_time) }
+ snapshot = @metric_streams.flat_map { |ms| ms.collect(@epoch_start_time, @epoch_end_time) }
@epoch_start_time = @epoch_end_time
snapshot
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
index 7f98115dce..05033f522c 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_stream.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/state/metric_stream.rb
@@ -30,30 +30,61 @@ def initialize(
@instrument_kind = instrument_kind
@meter_provider = meter_provider
@instrumentation_scope = instrumentation_scope
- @aggregation = aggregation
+ @default_aggregation = aggregation
+ @data_points = {}
+ @registered_views = []
+ find_registered_view
@mutex = Mutex.new
end
def collect(start_time, end_time)
@mutex.synchronize do
- MetricData.new(
- @name,
- @description,
- @unit,
- @instrument_kind,
- @meter_provider.resource,
- @instrumentation_scope,
- @aggregation.collect(start_time, end_time),
- @aggregation.aggregation_temporality,
- start_time,
- end_time
- )
+ metric_data = []
+ if @registered_views.empty?
+ metric_data << aggregate_metric_data(start_time, end_time)
+ else
+ @registered_views.each { |view| metric_data << aggregate_metric_data(start_time, end_time, aggregation: view.aggregation) }
+ end
+
+ metric_data
end
end
def update(value, attributes)
- @mutex.synchronize { @aggregation.update(value, attributes) }
+ if @registered_views.empty?
+ @mutex.synchronize { @default_aggregation.update(value, attributes, @data_points) }
+ else
+ @registered_views.each do |view|
+ @mutex.synchronize do
+ attributes ||= {}
+ attributes.merge!(view.attribute_keys)
+ view.aggregation.update(value, attributes, @data_points) if view.valid_aggregation?
+ end
+ end
+ end
+ end
+
+ def aggregate_metric_data(start_time, end_time, aggregation: nil)
+ aggregator = aggregation || @default_aggregation
+ MetricData.new(
+ @name,
+ @description,
+ @unit,
+ @instrument_kind,
+ @meter_provider.resource,
+ @instrumentation_scope,
+ aggregator.collect(start_time, end_time, @data_points),
+ aggregator.aggregation_temporality,
+ start_time,
+ end_time
+ )
+ end
+
+ def find_registered_view
+ return if @meter_provider.nil?
+
+ @meter_provider.registered_views.each { |view| @registered_views << view if view.match_instrument?(self) }
end
def to_s
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb
index 4016895fb7..17e0d95702 100644
--- a/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb
@@ -8,7 +8,7 @@ module OpenTelemetry
module SDK
module Metrics
# Current OpenTelemetry metrics sdk version
- VERSION = '0.2.0'
+ VERSION = '0.3.0'
end
end
end
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/view.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/view.rb
new file mode 100644
index 0000000000..9606f383b1
--- /dev/null
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/view.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+module OpenTelemetry
+ module SDK
+ module Metrics
+ # A View provides SDK users with the flexibility to customize the metrics that are output by the SDK.
+ module View
+ end
+ end
+ end
+end
+
+require 'opentelemetry/sdk/metrics/view/registered_view'
diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/view/registered_view.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/view/registered_view.rb
new file mode 100644
index 0000000000..881f0f261e
--- /dev/null
+++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/view/registered_view.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+module OpenTelemetry
+ module SDK
+ module Metrics
+ module View
+ # RegisteredView is an internal class used to match Views with a given {MetricStream}
+ class RegisteredView
+ attr_reader :name, :aggregation, :attribute_keys, :regex
+
+ def initialize(name, **options)
+ @name = name
+ @options = options
+ @aggregation = options[:aggregation]
+ @attribute_keys = options[:attribute_keys] || {}
+
+ generate_regex_pattern(name)
+ end
+
+ def match_instrument?(metric_stream)
+ return false if @name && !name_match(metric_stream.name)
+ return false if @options[:type] && @options[:type] != metric_stream.instrument_kind
+ return false if @options[:unit] && @options[:unit] != metric_stream.unit
+ return false if @options[:meter_name] && @options[:meter_name] != metric_stream.instrumentation_scope.name
+ return false if @options[:meter_version] && @options[:meter_version] != metric_stream.instrumentation_scope.version
+
+ true
+ end
+
+ def name_match(stream_name)
+ !!@regex&.match(stream_name)
+ end
+
+ def valid_aggregation?
+ @aggregation.class.name.rpartition('::')[0] == 'OpenTelemetry::SDK::Metrics::Aggregation'
+ end
+
+ private
+
+ def generate_regex_pattern(view_name)
+ regex_pattern = Regexp.escape(view_name)
+
+ regex_pattern.gsub!('\*', '.*')
+ regex_pattern.gsub!('\?', '.')
+
+ @regex = Regexp.new("^#{regex_pattern}$")
+ rescue StandardError
+ @regex = nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/metrics_sdk/opentelemetry-metrics-sdk.gemspec b/metrics_sdk/opentelemetry-metrics-sdk.gemspec
index f7459963e2..1919305ac5 100644
--- a/metrics_sdk/opentelemetry-metrics-sdk.gemspec
+++ b/metrics_sdk/opentelemetry-metrics-sdk.gemspec
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
spec.required_ruby_version = '>= 3.0'
spec.add_dependency 'opentelemetry-api', '~> 1.1'
- spec.add_dependency 'opentelemetry-metrics-api'
+ spec.add_dependency 'opentelemetry-metrics-api', '~> 0.1.1'
spec.add_dependency 'opentelemetry-sdk', '~> 1.2'
spec.add_development_dependency 'benchmark-ipsa', '~> 0.2.0'
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
index bed4bdf935..3e060e79dc 100644
--- a/metrics_sdk/test/integration/in_memory_metric_pull_exporter_test.rb
+++ b/metrics_sdk/test/integration/in_memory_metric_pull_exporter_test.rb
@@ -26,7 +26,7 @@
counter.add(4, attributes: { 'd' => 'e' })
metric_exporter.pull
- last_snapshot = metric_exporter.metric_snapshots.last
+ last_snapshot = metric_exporter.metric_snapshots
_(last_snapshot).wont_be_empty
_(last_snapshot[0].name).must_equal('counter')
diff --git a/metrics_sdk/test/integration/periodic_metric_reader_test.rb b/metrics_sdk/test/integration/periodic_metric_reader_test.rb
index 7bf00a08fc..5abdfbbf8d 100644
--- a/metrics_sdk/test/integration/periodic_metric_reader_test.rb
+++ b/metrics_sdk/test/integration/periodic_metric_reader_test.rb
@@ -34,7 +34,7 @@
_(snapshot.size).must_equal(2)
- first_snapshot = snapshot[0]
+ first_snapshot = snapshot
_(first_snapshot[0].name).must_equal('counter')
_(first_snapshot[0].unit).must_equal('smidgen')
_(first_snapshot[0].description).must_equal('a small amount of something')
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/drop_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/drop_test.rb
new file mode 100644
index 0000000000..8c873107d9
--- /dev/null
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/drop_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'test_helper'
+
+describe OpenTelemetry::SDK::Metrics::Aggregation::LastValue do
+ let(:data_points) { {} }
+ let(:drop_aggregation) { OpenTelemetry::SDK::Metrics::Aggregation::Drop.new(aggregation_temporality: aggregation_temporality) }
+ let(:aggregation_temporality) { :delta }
+
+ # Time in nano
+ let(:start_time) { (Time.now.to_r * 1_000_000_000).to_i }
+ let(:end_time) { ((Time.now + 60).to_r * 1_000_000_000).to_i }
+
+ it 'sets the timestamps' do
+ drop_aggregation.update(0, {}, data_points)
+ ndp = drop_aggregation.collect(start_time, end_time, data_points)[0]
+ _(ndp.start_time_unix_nano).must_equal(0)
+ _(ndp.time_unix_nano).must_equal(0)
+ end
+
+ it 'aggregates and collects should collect no value for all collection' do
+ drop_aggregation.update(1, {}, data_points)
+ drop_aggregation.update(2, {}, data_points)
+
+ drop_aggregation.update(2, { 'foo' => 'bar' }, data_points)
+ drop_aggregation.update(2, { 'foo' => 'bar' }, data_points)
+
+ ndps = drop_aggregation.collect(start_time, end_time, data_points)
+
+ _(ndps.size).must_equal(2)
+ _(ndps[0].value).must_equal(0)
+ _(ndps[0].attributes).must_equal({})
+
+ _(ndps[1].value).must_equal(0)
+ _(ndps[1].attributes).must_equal({})
+ end
+end
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram_test.rb
index 06c0e7b799..4666ce37e6 100644
--- a/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram_test.rb
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram_test.rb
@@ -7,6 +7,7 @@
require 'test_helper'
describe OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram do
+ let(:data_points) { {} }
let(:ebh) do
OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new(
aggregation_temporality: aggregation_temporality,
@@ -21,21 +22,42 @@
let(:start_time) { (Time.now.to_r * 1_000_000_000).to_i }
let(:end_time) { ((Time.now + 60).to_r * 1_000_000_000).to_i }
+ describe '#initialize' do
+ it 'defaults to the delta aggregation temporality' do
+ exp = OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new
+ _(exp.instance_variable_get(:@aggregation_temporality)).must_equal :delta
+ end
+
+ it 'sets parameters from the environment and converts them to symbols' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE' => 'potato') do
+ OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new
+ end
+ _(exp.instance_variable_get(:@aggregation_temporality)).must_equal :potato
+ end
+
+ it 'prefers explicit parameters rather than the environment and converts them to symbols' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE' => 'potato') do
+ OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new(aggregation_temporality: 'pickles')
+ end
+ _(exp.instance_variable_get(:@aggregation_temporality)).must_equal :pickles
+ end
+ end
+
describe '#collect' do
it 'returns all the data points' do
- ebh.update(0, {})
- ebh.update(1, {})
- ebh.update(5, {})
- ebh.update(6, {})
- ebh.update(10, {})
-
- ebh.update(-10, 'foo' => 'bar')
- ebh.update(1, 'foo' => 'bar')
- ebh.update(22, 'foo' => 'bar')
- ebh.update(55, 'foo' => 'bar')
- ebh.update(80, 'foo' => 'bar')
-
- hdps = ebh.collect(start_time, end_time)
+ ebh.update(0, {}, data_points)
+ ebh.update(1, {}, data_points)
+ ebh.update(5, {}, data_points)
+ ebh.update(6, {}, data_points)
+ ebh.update(10, {}, data_points)
+
+ ebh.update(-10, { 'foo' => 'bar' }, data_points)
+ ebh.update(1, { 'foo' => 'bar' }, data_points)
+ ebh.update(22, { 'foo' => 'bar' }, data_points)
+ ebh.update(55, { 'foo' => 'bar' }, data_points)
+ ebh.update(80, { 'foo' => 'bar' }, data_points)
+
+ hdps = ebh.collect(start_time, end_time, data_points)
_(hdps.size).must_equal(2)
_(hdps[0].attributes).must_equal({})
_(hdps[0].count).must_equal(5)
@@ -55,34 +77,34 @@
end
it 'sets the timestamps' do
- ebh.update(0, {})
- hdp = ebh.collect(start_time, end_time)[0]
+ ebh.update(0, {}, data_points)
+ hdp = ebh.collect(start_time, end_time, data_points)[0]
_(hdp.start_time_unix_nano).must_equal(start_time)
_(hdp.time_unix_nano).must_equal(end_time)
end
it 'calculates the count' do
- ebh.update(0, {})
- ebh.update(0, {})
- ebh.update(0, {})
- ebh.update(0, {})
- hdp = ebh.collect(start_time, end_time)[0]
+ ebh.update(0, {}, data_points)
+ ebh.update(0, {}, data_points)
+ ebh.update(0, {}, data_points)
+ ebh.update(0, {}, data_points)
+ hdp = ebh.collect(start_time, end_time, data_points)[0]
_(hdp.count).must_equal(4)
end
it 'does not aggregate between collects with default delta aggregation' do
- ebh.update(0, {})
- ebh.update(1, {})
- ebh.update(5, {})
- ebh.update(6, {})
- ebh.update(10, {})
- hdps = ebh.collect(start_time, end_time)
-
- ebh.update(0, {})
- ebh.update(1, {})
- ebh.update(5, {})
- ebh.update(6, {})
- ebh.update(10, {})
+ ebh.update(0, {}, data_points)
+ ebh.update(1, {}, data_points)
+ ebh.update(5, {}, data_points)
+ ebh.update(6, {}, data_points)
+ ebh.update(10, {}, data_points)
+ hdps = ebh.collect(start_time, end_time, data_points)
+
+ ebh.update(0, {}, data_points)
+ ebh.update(1, {}, data_points)
+ ebh.update(5, {}, data_points)
+ ebh.update(6, {}, data_points)
+ ebh.update(10, {}, data_points)
# Assert that the recent update does not
# impact the already collected metrics
_(hdps[0].count).must_equal(5)
@@ -91,7 +113,7 @@
_(hdps[0].max).must_equal(10)
_(hdps[0].bucket_counts).must_equal([1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0])
- hdps = ebh.collect(start_time, end_time)
+ hdps = ebh.collect(start_time, end_time, data_points)
# Assert that we are not accumulating values
# between calls to collect
_(hdps[0].count).must_equal(5)
@@ -105,18 +127,18 @@
let(:aggregation_temporality) { :not_delta }
it 'allows metrics to accumulate' do
- ebh.update(0, {})
- ebh.update(1, {})
- ebh.update(5, {})
- ebh.update(6, {})
- ebh.update(10, {})
- hdps = ebh.collect(start_time, end_time)
-
- ebh.update(0, {})
- ebh.update(1, {})
- ebh.update(5, {})
- ebh.update(6, {})
- ebh.update(10, {})
+ ebh.update(0, {}, data_points)
+ ebh.update(1, {}, data_points)
+ ebh.update(5, {}, data_points)
+ ebh.update(6, {}, data_points)
+ ebh.update(10, {}, data_points)
+ hdps = ebh.collect(start_time, end_time, data_points)
+
+ ebh.update(0, {}, data_points)
+ ebh.update(1, {}, data_points)
+ ebh.update(5, {}, data_points)
+ ebh.update(6, {}, data_points)
+ ebh.update(10, {}, data_points)
# Assert that the recent update does not
# impact the already collected metrics
_(hdps[0].count).must_equal(5)
@@ -125,7 +147,7 @@
_(hdps[0].max).must_equal(10)
_(hdps[0].bucket_counts).must_equal([1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0])
- hdps1 = ebh.collect(start_time, end_time)
+ hdps1 = ebh.collect(start_time, end_time, data_points)
# Assert that we are accumulating values
# and not just capturing the delta since
# the previous collect call
@@ -148,38 +170,38 @@
describe '#update' do
it 'accumulates across the default boundaries' do
- ebh.update(0, {})
+ ebh.update(0, {}, data_points)
- ebh.update(1, {})
- ebh.update(5, {})
+ ebh.update(1, {}, data_points)
+ ebh.update(5, {}, data_points)
- ebh.update(6, {})
- ebh.update(10, {})
+ ebh.update(6, {}, data_points)
+ ebh.update(10, {}, data_points)
- ebh.update(11, {})
- ebh.update(25, {})
+ ebh.update(11, {}, data_points)
+ ebh.update(25, {}, data_points)
- ebh.update(26, {})
- ebh.update(50, {})
+ ebh.update(26, {}, data_points)
+ ebh.update(50, {}, data_points)
- ebh.update(51, {})
- ebh.update(75, {})
+ ebh.update(51, {}, data_points)
+ ebh.update(75, {}, data_points)
- ebh.update(76, {})
- ebh.update(100, {})
+ ebh.update(76, {}, data_points)
+ ebh.update(100, {}, data_points)
- ebh.update(101, {})
- ebh.update(250, {})
+ ebh.update(101, {}, data_points)
+ ebh.update(250, {}, data_points)
- ebh.update(251, {})
- ebh.update(500, {})
+ ebh.update(251, {}, data_points)
+ ebh.update(500, {}, data_points)
- ebh.update(501, {})
- ebh.update(1000, {})
+ ebh.update(501, {}, data_points)
+ ebh.update(1000, {}, data_points)
- ebh.update(1001, {})
+ ebh.update(1001, {}, data_points)
- hdp = ebh.collect(start_time, end_time)[0]
+ hdp = ebh.collect(start_time, end_time, data_points)[0]
_(hdp.bucket_counts).must_equal([1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1])
_(hdp.sum).must_equal(4040)
_(hdp.min).must_equal(0)
@@ -190,8 +212,8 @@
let(:boundaries) { [4, 2, 1] }
it 'sorts it' do
- ebh.update(0, {})
- _(ebh.collect(start_time, end_time)[0].explicit_bounds).must_equal([1, 2, 4])
+ ebh.update(0, {}, data_points)
+ _(ebh.collect(start_time, end_time, data_points)[0].explicit_bounds).must_equal([1, 2, 4])
end
end
@@ -199,8 +221,8 @@
let(:record_min_max) { false }
it 'does not record min max values' do
- ebh.update(-1, {})
- hdp = ebh.collect(start_time, end_time)[0]
+ ebh.update(-1, {}, data_points)
+ hdp = ebh.collect(start_time, end_time, data_points)[0]
_(hdp.min).must_be_nil
_(hdp.min).must_be_nil
end
@@ -210,14 +232,14 @@
let(:boundaries) { [0, 2, 4] }
it 'aggregates' do
- ebh.update(-1, {})
- ebh.update(0, {})
- ebh.update(1, {})
- ebh.update(2, {})
- ebh.update(3, {})
- ebh.update(4, {})
- ebh.update(5, {})
- hdp = ebh.collect(start_time, end_time)[0]
+ ebh.update(-1, {}, data_points)
+ ebh.update(0, {}, data_points)
+ ebh.update(1, {}, data_points)
+ ebh.update(2, {}, data_points)
+ ebh.update(3, {}, data_points)
+ ebh.update(4, {}, data_points)
+ ebh.update(5, {}, data_points)
+ hdp = ebh.collect(start_time, end_time, data_points)[0]
_(hdp.bucket_counts).must_equal([2, 2, 2, 1])
end
@@ -227,9 +249,9 @@
let(:boundaries) { [0] }
it 'aggregates' do
- ebh.update(-1, {})
- ebh.update(1, {})
- hdp = ebh.collect(start_time, end_time)[0]
+ ebh.update(-1, {}, data_points)
+ ebh.update(1, {}, data_points)
+ hdp = ebh.collect(start_time, end_time, data_points)[0]
_(hdp.bucket_counts).must_equal([1, 1])
end
@@ -239,9 +261,9 @@
let(:boundaries) { [] }
it 'aggregates but does not record bucket counts' do
- ebh.update(-1, {})
- ebh.update(3, {})
- hdp = ebh.collect(start_time, end_time)[0]
+ ebh.update(-1, {}, data_points)
+ ebh.update(3, {}, data_points)
+ hdp = ebh.collect(start_time, end_time, data_points)[0]
_(hdp.bucket_counts).must_be_nil
_(hdp.explicit_bounds).must_be_nil
@@ -256,9 +278,9 @@
let(:boundaries) { nil }
it 'aggregates but does not record bucket counts' do
- ebh.update(-1, {})
- ebh.update(3, {})
- hdp = ebh.collect(start_time, end_time)[0]
+ ebh.update(-1, {}, data_points)
+ ebh.update(3, {}, data_points)
+ hdp = ebh.collect(start_time, end_time, data_points)[0]
_(hdp.bucket_counts).must_be_nil
_(hdp.explicit_bounds).must_be_nil
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/last_value_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/last_value_test.rb
new file mode 100644
index 0000000000..8714b698d5
--- /dev/null
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/last_value_test.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'test_helper'
+
+describe OpenTelemetry::SDK::Metrics::Aggregation::LastValue do
+ let(:data_points) { {} }
+ let(:last_value_aggregation) { OpenTelemetry::SDK::Metrics::Aggregation::LastValue.new(aggregation_temporality: aggregation_temporality) }
+ let(:aggregation_temporality) { :delta }
+
+ # Time in nano
+ let(:start_time) { (Time.now.to_r * 1_000_000_000).to_i }
+ let(:end_time) { ((Time.now + 60).to_r * 1_000_000_000).to_i }
+
+ it 'sets the timestamps' do
+ last_value_aggregation.update(0, {}, data_points)
+ ndp = last_value_aggregation.collect(start_time, end_time, data_points)[0]
+ _(ndp.start_time_unix_nano).must_equal(start_time)
+ _(ndp.time_unix_nano).must_equal(end_time)
+ end
+
+ it 'aggregates and collects should collect the last value' do
+ last_value_aggregation.update(1, {}, data_points)
+ last_value_aggregation.update(2, {}, data_points)
+
+ last_value_aggregation.update(2, { 'foo' => 'bar' }, data_points)
+ last_value_aggregation.update(2, { 'foo' => 'bar' }, data_points)
+
+ ndps = last_value_aggregation.collect(start_time, end_time, data_points)
+ _(ndps[0].value).must_equal(2)
+ _(ndps[0].attributes).must_equal({}, data_points)
+
+ _(ndps[1].value).must_equal(2)
+ _(ndps[1].attributes).must_equal('foo' => 'bar')
+ end
+end
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/sum_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/sum_test.rb
index 3c0a3931e3..66e7667ecc 100644
--- a/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/sum_test.rb
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/aggregation/sum_test.rb
@@ -7,6 +7,7 @@
require 'test_helper'
describe OpenTelemetry::SDK::Metrics::Aggregation::Sum do
+ let(:data_points) { {} }
let(:sum_aggregation) { OpenTelemetry::SDK::Metrics::Aggregation::Sum.new(aggregation_temporality: aggregation_temporality) }
let(:aggregation_temporality) { :delta }
@@ -14,39 +15,60 @@
let(:start_time) { (Time.now.to_r * 1_000_000_000).to_i }
let(:end_time) { ((Time.now + 60).to_r * 1_000_000_000).to_i }
+ describe '#initialize' do
+ it 'defaults to the delta aggregation temporality' do
+ exp = OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new
+ _(exp.instance_variable_get(:@aggregation_temporality)).must_equal :delta
+ end
+
+ it 'sets parameters from the environment and converts them to symbols' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE' => 'potato') do
+ OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new
+ end
+ _(exp.instance_variable_get(:@aggregation_temporality)).must_equal :potato
+ end
+
+ it 'prefers explicit parameters rather than the environment and converts them to symbols' do
+ exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE' => 'potato') do
+ OpenTelemetry::SDK::Metrics::Aggregation::ExplicitBucketHistogram.new(aggregation_temporality: 'pickles')
+ end
+ _(exp.instance_variable_get(:@aggregation_temporality)).must_equal :pickles
+ end
+ end
+
it 'sets the timestamps' do
- sum_aggregation.update(0, {})
- ndp = sum_aggregation.collect(start_time, end_time)[0]
+ sum_aggregation.update(0, {}, data_points)
+ ndp = sum_aggregation.collect(start_time, end_time, data_points)[0]
_(ndp.start_time_unix_nano).must_equal(start_time)
_(ndp.time_unix_nano).must_equal(end_time)
end
it 'aggregates and collects' do
- sum_aggregation.update(1, {})
- sum_aggregation.update(2, {})
+ sum_aggregation.update(1, {}, data_points)
+ sum_aggregation.update(2, {}, data_points)
- sum_aggregation.update(2, 'foo' => 'bar')
- sum_aggregation.update(2, 'foo' => 'bar')
+ sum_aggregation.update(2, { 'foo' => 'bar' }, data_points)
+ sum_aggregation.update(2, { 'foo' => 'bar' }, data_points)
- ndps = sum_aggregation.collect(start_time, end_time)
+ ndps = sum_aggregation.collect(start_time, end_time, data_points)
_(ndps[0].value).must_equal(3)
- _(ndps[0].attributes).must_equal({})
+ _(ndps[0].attributes).must_equal({}, data_points)
_(ndps[1].value).must_equal(4)
_(ndps[1].attributes).must_equal('foo' => 'bar')
end
it 'does not aggregate between collects' do
- sum_aggregation.update(1, {})
- sum_aggregation.update(2, {})
- ndps = sum_aggregation.collect(start_time, end_time)
+ sum_aggregation.update(1, {}, data_points)
+ sum_aggregation.update(2, {}, data_points)
+ ndps = sum_aggregation.collect(start_time, end_time, data_points)
- sum_aggregation.update(1, {})
+ sum_aggregation.update(1, {}, data_points)
# Assert that the recent update does not
# impact the already collected metrics
_(ndps[0].value).must_equal(3)
- ndps = sum_aggregation.collect(start_time, end_time)
+ ndps = sum_aggregation.collect(start_time, end_time, data_points)
# Assert that we are not accumulating values
# between calls to collect
_(ndps[0].value).must_equal(1)
@@ -56,16 +78,16 @@
let(:aggregation_temporality) { :not_delta }
it 'allows metrics to accumulate' do
- sum_aggregation.update(1, {})
- sum_aggregation.update(2, {})
- ndps = sum_aggregation.collect(start_time, end_time)
+ sum_aggregation.update(1, {}, data_points)
+ sum_aggregation.update(2, {}, data_points)
+ ndps = sum_aggregation.collect(start_time, end_time, data_points)
- sum_aggregation.update(1, {})
+ sum_aggregation.update(1, {}, data_points)
# Assert that the recent update does not
# impact the already collected metrics
_(ndps[0].value).must_equal(3)
- ndps = sum_aggregation.collect(start_time, end_time)
+ ndps = sum_aggregation.collect(start_time, end_time, data_points)
# Assert that we are accumulating values
# and not just capturing the delta since
# the previous collect call
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb
new file mode 100644
index 0000000000..88cd4bdaa0
--- /dev/null
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'test_helper'
+require 'opentelemetry-exporter-otlp-metrics' unless RUBY_ENGINE == 'jruby'
+
+describe OpenTelemetry::SDK::Metrics::ConfiguratorPatch do
+ let(:configurator) { OpenTelemetry::SDK::Configurator.new }
+ let(:default_resource_attributes) do
+ {
+ 'telemetry.sdk.name' => 'opentelemetry',
+ 'telemetry.sdk.language' => 'ruby',
+ 'telemetry.sdk.version' => OpenTelemetry::SDK::VERSION,
+ 'process.pid' => Process.pid,
+ 'process.command' => $PROGRAM_NAME,
+ 'process.runtime.name' => RUBY_ENGINE,
+ 'process.runtime.version' => RUBY_VERSION,
+ 'process.runtime.description' => RUBY_DESCRIPTION,
+ 'service.name' => 'unknown_service'
+ }
+ end
+
+ describe '#configure' do
+ describe 'meter_provider' do
+ it 'is an instance of SDK::Metrics::MeterProvider' do
+ configurator.configure
+
+ _(OpenTelemetry.meter_provider).must_be_instance_of(
+ OpenTelemetry::SDK::Metrics::MeterProvider
+ )
+ end
+ end
+
+ describe 'metric readers' do
+ it 'defaults to a periodic reader with an otlp exporter' do
+ skip 'OTLP exporter not compatible with JRuby' if RUBY_ENGINE == 'jruby'
+
+ configurator.configure
+
+ assert_equal 1, OpenTelemetry.meter_provider.metric_readers.size
+ reader = OpenTelemetry.meter_provider.metric_readers[0]
+
+ assert_instance_of OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader, reader
+ assert_instance_of OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter, reader.instance_variable_get(:@exporter)
+ end
+
+ it 'can be set by environment variable' do
+ OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'console') do
+ configurator.configure
+ end
+
+ assert_equal 1, OpenTelemetry.meter_provider.metric_readers.size
+
+ reader = OpenTelemetry.meter_provider.metric_readers[0]
+
+ assert_instance_of OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader, reader
+ assert_instance_of OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter, reader.instance_variable_get(:@exporter)
+ end
+
+ it 'supports "none" as an environment variable' do
+ OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
+ OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'none') do
+ configurator.configure
+ end
+
+ assert_empty OpenTelemetry.meter_provider.metric_readers
+
+ refute_match(/The none exporter is unknown and cannot be configured/, log_stream.string)
+ end
+ end
+
+ it 'supports multiple exporters passed by environment variable' do
+ OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'console,in-memory') do
+ configurator.configure
+ end
+
+ assert_equal 2, OpenTelemetry.meter_provider.metric_readers.size
+
+ reader1 = OpenTelemetry.meter_provider.metric_readers[0]
+ reader2 = OpenTelemetry.meter_provider.metric_readers[1]
+
+ assert_instance_of OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader, reader1
+ assert_instance_of OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter, reader1.instance_variable_get(:@exporter)
+
+ assert_instance_of OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter, reader2
+ end
+
+ it 'defaults to noop with invalid env var' do
+ OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
+ OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'unladen_swallow') do
+ configurator.configure
+ end
+
+ assert_empty OpenTelemetry.meter_provider.metric_readers
+ assert_match(/The unladen_swallow exporter is unknown and cannot be configured/, log_stream.string)
+ end
+ end
+
+ it 'rescues NameErrors when otlp set to env var and the library is not installed' do
+ if RUBY_ENGINE == 'jruby'
+ OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
+ OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'otlp') do
+ configurator.configure
+ end
+
+ assert_empty OpenTelemetry.meter_provider.metric_readers
+ assert_match(/The otlp metrics exporter cannot be configured - please add opentelemetry-exporter-otlp-metrics to your Gemfile, metrics will not be exported/, log_stream.string)
+ end
+ else
+ OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
+ OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.stub(:new, -> { raise NameError }) do
+ OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'otlp') do
+ configurator.configure
+ end
+
+ assert_empty OpenTelemetry.meter_provider.metric_readers
+ assert_match(/The otlp metrics exporter cannot be configured - please add opentelemetry-exporter-otlp-metrics to your Gemfile, metrics will not be exported/, log_stream.string)
+ end
+ end
+ end
+ end
+ 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
index c00e2eba7b..ff5c3edfe2 100644
--- a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/counter_test.rb
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/counter_test.rb
@@ -20,7 +20,7 @@
it 'counts' do
counter.add(1, attributes: { 'foo' => 'bar' })
metric_exporter.pull
- last_snapshot = metric_exporter.metric_snapshots.last
+ last_snapshot = metric_exporter.metric_snapshots
_(last_snapshot[0].name).must_equal('counter')
_(last_snapshot[0].unit).must_equal('smidgen')
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/histogram_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/histogram_test.rb
index bddc7f5568..771ffaef83 100644
--- a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/histogram_test.rb
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/histogram_test.rb
@@ -21,7 +21,7 @@
histogram.record(5, attributes: { 'foo' => 'bar' })
histogram.record(6, attributes: { 'foo' => 'bar' })
metric_exporter.pull
- last_snapshot = metric_exporter.metric_snapshots.last
+ last_snapshot = metric_exporter.metric_snapshots
_(last_snapshot[0].name).must_equal('histogram')
_(last_snapshot[0].unit).must_equal('smidgen')
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/up_down_counter_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/up_down_counter_test.rb
index 456c2c5052..687ad27a89 100644
--- a/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/up_down_counter_test.rb
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/instrument/up_down_counter_test.rb
@@ -21,7 +21,7 @@
up_down_counter.add(1, attributes: { 'foo' => 'bar' })
up_down_counter.add(-2, attributes: { 'foo' => 'bar' })
metric_exporter.pull
- last_snapshot = metric_exporter.metric_snapshots.last
+ last_snapshot = metric_exporter.metric_snapshots
_(last_snapshot[0].name).must_equal('up_down_counter')
_(last_snapshot[0].unit).must_equal('smidgen')
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb
index f781d613b3..72f04e08c9 100644
--- a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb
@@ -9,7 +9,11 @@
describe OpenTelemetry::SDK::Metrics::MeterProvider do
before do
reset_metrics_sdk
- OpenTelemetry::SDK.configure
+ # The MeterProvider tests deal with mock MetricReaders that are manually added
+ # Run the tests without the configuration patch interfering with the setup
+ OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'none') do
+ OpenTelemetry::SDK.configure
+ end
end
describe '#meter' do
@@ -134,8 +138,26 @@
end
end
- # TODO: OpenTelemetry.meter_provider.add_view
describe '#add_view' do
+ it 'adds a view with aggregation' do
+ OpenTelemetry.meter_provider.add_view('test', aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::Drop.new)
+
+ registered_views = OpenTelemetry.meter_provider.instance_variable_get(:@registered_views)
+
+ _(registered_views.size).must_equal 1
+ _(registered_views[0].class).must_equal ::OpenTelemetry::SDK::Metrics::View::RegisteredView
+ _(registered_views[0].name).must_equal 'test'
+ _(registered_views[0].aggregation.class).must_equal ::OpenTelemetry::SDK::Metrics::Aggregation::Drop
+ end
+
+ it 'add a view without aggregation but aggregation as nil' do
+ OpenTelemetry.meter_provider.add_view('test')
+
+ registered_views = OpenTelemetry.meter_provider.instance_variable_get(:@registered_views)
+
+ _(registered_views.size).must_equal 1
+ _(registered_views[0].aggregation).must_be_nil
+ end
end
private
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb
index 6a6cba2fdc..e8415d8d35 100644
--- a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_test.rb
@@ -58,4 +58,65 @@
_(instrument).must_be_instance_of OpenTelemetry::SDK::Metrics::Instrument::ObservableUpDownCounter
end
end
+
+ describe 'creating an instrument' do
+ INSTRUMENT_NAME_ERROR = OpenTelemetry::Metrics::Meter::InstrumentNameError
+ INSTRUMENT_UNIT_ERROR = OpenTelemetry::Metrics::Meter::InstrumentUnitError
+ INSTRUMENT_DESCRIPTION_ERROR = OpenTelemetry::Metrics::Meter::InstrumentDescriptionError
+ DUPLICATE_INSTRUMENT_ERROR = OpenTelemetry::Metrics::Meter::DuplicateInstrumentError
+
+ 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
+ _(-> { meter.create_counter(nil) }).must_raise(INSTRUMENT_NAME_ERROR)
+ end
+
+ it 'instument name must not be an empty string' do
+ _(-> { meter.create_counter('') }).must_raise(INSTRUMENT_NAME_ERROR)
+ end
+
+ it 'instrument name must have an alphabetic first character' do
+ _(meter.create_counter('one_counter'))
+ _(-> { meter.create_counter('1_counter') }).must_raise(INSTRUMENT_NAME_ERROR)
+ end
+
+ it 'instrument name must not exceed 63 character limit' do
+ long_name = 'a' * 63
+ meter.create_counter(long_name)
+ _(-> { meter.create_counter(long_name + 'a') }).must_raise(INSTRUMENT_NAME_ERROR)
+ end
+
+ it 'instrument name must belong to alphanumeric characters, _, ., and -' do
+ meter.create_counter('a_-..-_a')
+ _(-> { meter.create_counter('a@') }).must_raise(INSTRUMENT_NAME_ERROR)
+ _(-> { meter.create_counter('a!') }).must_raise(INSTRUMENT_NAME_ERROR)
+ end
+
+ it 'instrument unit must be ASCII' do
+ _(-> { meter.create_counter('a_counter', unit: 'á') }).must_raise(INSTRUMENT_UNIT_ERROR)
+ end
+
+ it 'instrument unit must not exceed 63 characters' do
+ long_unit = 'a' * 63
+ meter.create_counter('a_counter', unit: long_unit)
+ _(-> { meter.create_counter('b_counter', unit: long_unit + 'a') }).must_raise(INSTRUMENT_UNIT_ERROR)
+ end
+
+ it 'instrument description must be utf8mb3' do
+ _(-> { meter.create_counter('a_counter', description: '💩'.dup) }).must_raise(INSTRUMENT_DESCRIPTION_ERROR)
+ _(-> { meter.create_counter('b_counter', description: "\xc2".dup) }).must_raise(INSTRUMENT_DESCRIPTION_ERROR)
+ end
+
+ it 'instrument description must not exceed 1023 characters' do
+ long_description = 'a' * 1023
+ meter.create_counter('a_counter', description: long_description)
+ _(-> { meter.create_counter('b_counter', description: long_description + 'a') }).must_raise(INSTRUMENT_DESCRIPTION_ERROR)
+ end
+ end
end
diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/view/registered_view_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/view/registered_view_test.rb
new file mode 100644
index 0000000000..4638485777
--- /dev/null
+++ b/metrics_sdk/test/opentelemetry/sdk/metrics/view/registered_view_test.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'test_helper'
+
+describe OpenTelemetry::SDK::Metrics::View::RegisteredView do
+ describe '#registered_view' do
+ before { reset_metrics_sdk }
+
+ it 'emits metrics with no data_points if view is drop' 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')
+ OpenTelemetry.meter_provider.add_view('counter', aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::Drop.new)
+
+ counter = meter.create_counter('counter', unit: 'smidgen', description: 'a small amount of something')
+
+ counter.add(1)
+ counter.add(2, attributes: { 'a' => 'b' })
+
+ metric_exporter.pull
+ last_snapshot = metric_exporter.metric_snapshots
+
+ _(last_snapshot).wont_be_empty
+ _(last_snapshot[0].name).must_equal('counter')
+ _(last_snapshot[0].unit).must_equal('smidgen')
+ _(last_snapshot[0].description).must_equal('a small amount of something')
+
+ _(last_snapshot[0].instrumentation_scope.name).must_equal('test')
+
+ _(last_snapshot[0].data_points[0].value).must_equal 0
+ _(last_snapshot[0].data_points[0].start_time_unix_nano).must_equal 0
+ _(last_snapshot[0].data_points[0].time_unix_nano).must_equal 0
+
+ _(last_snapshot[0].data_points[1].value).must_equal 0
+ _(last_snapshot[0].data_points[1].start_time_unix_nano).must_equal 0
+ _(last_snapshot[0].data_points[1].time_unix_nano).must_equal 0
+ end
+
+ it 'emits metrics with only last value in data_points if view is last_value' 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')
+ OpenTelemetry.meter_provider.add_view('counter', aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::LastValue.new)
+
+ counter = meter.create_counter('counter', unit: 'smidgen', description: 'a small amount of something')
+
+ counter.add(1)
+ counter.add(2)
+ counter.add(3)
+ counter.add(4)
+
+ metric_exporter.pull
+ last_snapshot = metric_exporter.metric_snapshots
+
+ _(last_snapshot[0].data_points).wont_be_empty
+ _(last_snapshot[0].data_points[0].value).must_equal 4
+ end
+
+ it 'emits metrics with sum of value in data_points if view is last_value but not matching to instrument' 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')
+ OpenTelemetry.meter_provider.add_view('retnuoc', aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::LastValue.new)
+
+ counter = meter.create_counter('counter', unit: 'smidgen', description: 'a small amount of something')
+
+ counter.add(1)
+ counter.add(2)
+ counter.add(3)
+ counter.add(4)
+
+ metric_exporter.pull
+ last_snapshot = metric_exporter.metric_snapshots
+
+ _(last_snapshot[0].data_points).wont_be_empty
+ _(last_snapshot[0].data_points[0].value).must_equal 10
+ end
+ end
+
+ describe '#registered_view select instrument' do
+ let(:registered_view) { OpenTelemetry::SDK::Metrics::View::RegisteredView.new(nil, aggregation: ::OpenTelemetry::SDK::Metrics::Aggregation::LastValue.new) }
+ let(:instrumentation_scope) do
+ OpenTelemetry::SDK::InstrumentationScope.new('test_scope', '1.0.1')
+ end
+
+ let(:metric_stream) do
+ OpenTelemetry::SDK::Metrics::State::MetricStream.new('test', 'description', 'smidgen', :counter, nil, instrumentation_scope, nil)
+ end
+
+ it 'registered view with matching name' do
+ registered_view.instance_variable_set(:@name, 'test')
+ registered_view.send(:generate_regex_pattern, 'test')
+ _(registered_view.match_instrument?(metric_stream)).must_equal true
+ end
+
+ it 'registered view with matching type' do
+ registered_view.instance_variable_set(:@options, { type: :counter })
+ _(registered_view.match_instrument?(metric_stream)).must_equal true
+ end
+
+ it 'registered view with matching version' do
+ registered_view.instance_variable_set(:@options, { meter_version: '1.0.1' })
+ _(registered_view.match_instrument?(metric_stream)).must_equal true
+ end
+
+ it 'registered view with matching meter_name' do
+ registered_view.instance_variable_set(:@options, { meter_name: 'test_scope' })
+ _(registered_view.match_instrument?(metric_stream)).must_equal true
+ end
+
+ it 'do not registered view with unmatching name and matching type' do
+ registered_view.instance_variable_set(:@options, { type: :counter })
+ registered_view.instance_variable_set(:@name, 'tset')
+ _(registered_view.match_instrument?(metric_stream)).must_equal false
+ end
+
+ describe '#name_match' do
+ it 'name_match_for_wild_card' do
+ registered_view.instance_variable_set(:@name, 'log*2024?.txt')
+ registered_view.send(:generate_regex_pattern, 'log*2024?.txt')
+ _(registered_view.name_match('logfile20242.txt')).must_equal true
+ _(registered_view.name_match('log2024a.txt')).must_equal true
+ _(registered_view.name_match('log_test_2024.txt')).must_equal false
+ end
+
+ it 'name_match_for_*' do
+ registered_view.instance_variable_set(:@name, '*')
+ registered_view.send(:generate_regex_pattern, '*')
+ _(registered_view.name_match('*')).must_equal true
+ _(registered_view.name_match('aaaaaaaaa')).must_equal true
+ _(registered_view.name_match('!@#$%^&')).must_equal true
+ end
+ end
+ end
+end
diff --git a/metrics_sdk/test/test_helper.rb b/metrics_sdk/test/test_helper.rb
index 99aa4deee0..11076bde18 100644
--- a/metrics_sdk/test/test_helper.rb
+++ b/metrics_sdk/test/test_helper.rb
@@ -33,3 +33,6 @@ def with_test_logger
ensure
OpenTelemetry.logger = original_logger
end
+
+# Suppress warn-level logs about a missing OTLP exporter for traces
+ENV['OTEL_TRACES_EXPORTER'] = 'none'
diff --git a/sdk/lib/opentelemetry/sdk/configurator.rb b/sdk/lib/opentelemetry/sdk/configurator.rb
index ae1bbaf07b..94a5020ebf 100644
--- a/sdk/lib/opentelemetry/sdk/configurator.rb
+++ b/sdk/lib/opentelemetry/sdk/configurator.rb
@@ -126,13 +126,20 @@ def add_span_processor(span_processor)
@span_processors << span_processor
end
+ # Add a log record processor to the export pipeline
+ #
+ # @param [#emit, #shutdown, #force_flush] log_record_processor A log_record_processor
+ # that satisfies the duck type #emit, #shutdown, #force_flush. See
+ # {SimpleLogRecordProcessor} for an example.
+ def add_log_record_processor(log_record_processor); end
+
# @api private
# The configure method is where we define the setup process. This allows
# us to make certain guarantees about which systems and globals are setup
# at each stage. The setup process is:
# - setup logging
# - setup propagation
- # - setup tracer_provider and meter_provider
+ # - setup tracer_provider, meter_provider, and logger_provider
# - install instrumentation
def configure
OpenTelemetry.logger = logger
@@ -142,6 +149,7 @@ def configure
tracer_provider.id_generator = @id_generator
OpenTelemetry.tracer_provider = tracer_provider
metrics_configuration_hook
+ logs_configuration_hook
install_instrumentation
end
@@ -149,6 +157,8 @@ def configure
def metrics_configuration_hook; end
+ def logs_configuration_hook; end
+
def tracer_provider
@tracer_provider ||= Trace::TracerProvider.new(resource: @resource)
end
diff --git a/sdk/lib/opentelemetry/sdk/trace/span.rb b/sdk/lib/opentelemetry/sdk/trace/span.rb
index 274ac4c24d..c1f32657f8 100644
--- a/sdk/lib/opentelemetry/sdk/trace/span.rb
+++ b/sdk/lib/opentelemetry/sdk/trace/span.rb
@@ -271,6 +271,9 @@ def finish(end_timestamp: nil)
return self
end
@end_timestamp = relative_timestamp(end_timestamp)
+ @span_processors.each do |processor|
+ processor.on_finishing(self) if processor.respond_to?(:on_finishing)
+ end
@attributes = validated_attributes(@attributes).freeze
@events.freeze
@links.freeze
diff --git a/sdk/lib/opentelemetry/sdk/trace/span_processor.rb b/sdk/lib/opentelemetry/sdk/trace/span_processor.rb
index f1874f6504..2a60c3ba53 100644
--- a/sdk/lib/opentelemetry/sdk/trace/span_processor.rb
+++ b/sdk/lib/opentelemetry/sdk/trace/span_processor.rb
@@ -23,6 +23,24 @@ class SpanProcessor
# started span.
def on_start(span, parent_context); end
+ # The on_finishing method is an experimental feature and may have breaking changes.
+ # The OpenTelemetry specification defines it as "On Ending". As `end` is a reserved
+ # keyword in Ruby, we are using `on_finishing` instead.
+ #
+ # Called when a {Span} is ending, after the end timestamp has been set
+ # but before span becomes immutable. This allows for updating the span
+ # by setting attributes or adding links and events.
+ #
+ # This method is called synchronously and should not block the current
+ # thread nor throw exceptions.
+ #
+ # This method is optional on the Span Processor interface. It will only
+ # get called if it exists within the processor.
+ #
+ # @param [Span] span the {Span} that just is ending (still mutable).
+ # @return [void]
+ def on_finishing(span); end
+
# Called when a {Span} is ended, if the {Span#recording?}
# returns true.
#
diff --git a/sdk/test/opentelemetry/sdk/trace/span_processor_test.rb b/sdk/test/opentelemetry/sdk/trace/span_processor_test.rb
index 262ad53b92..2bfc1532ba 100644
--- a/sdk/test/opentelemetry/sdk/trace/span_processor_test.rb
+++ b/sdk/test/opentelemetry/sdk/trace/span_processor_test.rb
@@ -15,6 +15,10 @@
processor.on_start(span, context)
end
+ it 'implements #on_finishing' do
+ processor.on_finishing(span)
+ end
+
it 'implements #on_finish' do
processor.on_finish(span)
end
diff --git a/test_helpers/lib/opentelemetry/test_helpers.rb b/test_helpers/lib/opentelemetry/test_helpers.rb
index 25048aa0f4..ad4cc3bbb2 100644
--- a/test_helpers/lib/opentelemetry/test_helpers.rb
+++ b/test_helpers/lib/opentelemetry/test_helpers.rb
@@ -72,5 +72,34 @@ def create_span_data(name: '', kind: nil, status: nil, parent_span_id: OpenTelem
total_recorded_events, total_recorded_links, start_timestamp, end_timestamp,
attributes, links, events, resource, instrumentation_scope, span_id, trace_id, trace_flags, tracestate)
end
+
+ def create_log_record_data(timestamp: OpenTelemetry::TestHelpers.exportable_timestamp,
+ observed_timestamp: OpenTelemetry::TestHelpers.exportable_timestamp,
+ severity_text: nil,
+ severity_number: nil,
+ body: nil,
+ attributes: nil,
+ trace_id: OpenTelemetry::Trace.generate_trace_id,
+ span_id: OpenTelemetry::Trace.generate_span_id,
+ trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT,
+ resource: nil,
+ instrumentation_scope: OpenTelemetry::SDK::InstrumentationScope.new('', 'v0.0.1'),
+ total_recorded_attributes: 0)
+ resource ||= OpenTelemetry::SDK::Resources::Resource.telemetry_sdk
+ OpenTelemetry::SDK::Logs::LogRecordData.new(
+ timestamp,
+ observed_timestamp,
+ severity_text,
+ severity_number,
+ body,
+ attributes,
+ trace_id,
+ span_id,
+ trace_flags,
+ resource,
+ instrumentation_scope,
+ total_recorded_attributes
+ )
+ end
end
end