diff --git a/api/lib/opentelemetry.rb b/api/lib/opentelemetry.rb
index 3234fd782a..f84d38916f 100644
--- a/api/lib/opentelemetry.rb
+++ b/api/lib/opentelemetry.rb
@@ -28,7 +28,11 @@ module OpenTelemetry
# @return [Object, Logger] configured Logger or a default STDOUT Logger.
def logger
- @logger ||= Logger.new($stdout, level: ENV['OTEL_LOG_LEVEL'] || Logger::INFO)
+ @logger ||= create_logger
+ # alternately: Logger.new($stdout, level: ENV['OTEL_LOG_LEVEL'] || Logger::INF, progname: 'OpenTelemetry')
+ # and clean up the log messages that are prefixed with "OpenTelemetry"
+ # if we want log records generated for OpenTelemetry logs, I think we can do that, but I imagine this is
+ # "untraced" territory
end
# @return [Callable] configured error handler or a default that logs the
@@ -69,4 +73,15 @@ def tracer_provider
def propagation
@propagation ||= Context::Propagation::NoopTextMapPropagator.new
end
+
+ private
+
+ def create_logger
+ logger = Logger.new($stdout, level: ENV['OTEL_LOG_LEVEL'] || Logger::INFO)
+ # @skip_instrumenting prevents Ruby Logger instrumentation from
+ # triggering a stack overflow. Logs emitted using OpenTelemetry.logger
+ # will not be turned into OpenTelemetry LogRecords.
+ logger.instance_variable_set(:@skip_instrumenting, true)
+ logger
+ end
end
diff --git a/exporter/otlp-logs/.rubocop.yml b/exporter/otlp-logs/.rubocop.yml
new file mode 100644
index 0000000000..46d464b94a
--- /dev/null
+++ b/exporter/otlp-logs/.rubocop.yml
@@ -0,0 +1,42 @@
+AllCops:
+ TargetRubyVersion: "3.0"
+ NewCops: disable
+ SuggestExtensions: false
+ Exclude:
+ - "lib/opentelemetry/proto/**/*"
+ - "vendor/**/*"
+
+Bundler/OrderedGems:
+ Exclude:
+ - gemfiles/**/*
+Lint/UnusedMethodArgument:
+ Enabled: false
+Lint/MissingSuper:
+ Enabled: false
+Lint/ConstantDefinitionInBlock:
+ Exclude:
+ - "test/**/*"
+Style/StringConcatenation:
+ Exclude:
+ - "test/**/*"
+Metrics/AbcSize:
+ Enabled: false
+Layout/LineLength:
+ Enabled: false
+Metrics/MethodLength:
+ Max: 20
+Metrics/ParameterLists:
+ Enabled: false
+Style/FrozenStringLiteralComment:
+ Exclude:
+ - gemfiles/**/*
+Style/ModuleFunction:
+ Enabled: false
+Style/StringLiterals:
+ Exclude:
+ - gemfiles/**/*
+Metrics/BlockLength:
+ Enabled: false
+Naming/FileName:
+ Exclude:
+ - "lib/opentelemetry-exporter-otlp.rb"
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..362c129911
--- /dev/null
+++ b/exporter/otlp-logs/Appraisals
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+(14..23).each do |i|
+ version = "3.#{i}"
+ appraise "google-protobuf-#{version}" do
+ gem 'google-protobuf', "~> #{version}"
+ end
+end
diff --git a/exporter/otlp-logs/CHANGELOG.md b/exporter/otlp-logs/CHANGELOG.md
new file mode 100644
index 0000000000..8b2d7c12e9
--- /dev/null
+++ b/exporter/otlp-logs/CHANGELOG.md
@@ -0,0 +1,182 @@
+# Release History: opentelemetry-exporter-otlp
+
+### v0.26.1 / 2023-07-29
+
+* FIXED: Regenerate v0.20.0 protos
+* ADDED: Allow google-protobuf ~> 3.14
+
+### v0.26.0 / 2023-06-13
+
+* ADDED: Use OTLP 0.20.0 protos
+
+### v0.25.0 / 2023-06-01
+
+* BREAKING CHANGE: Remove support for EoL Ruby 2.7
+
+* ADDED: Remove support for EoL Ruby 2.7
+* FIXED: Make version available to user agent header #1458
+
+### v0.24.1 / 2023-05-30
+
+* FIXED: Add Ruby 3.2 to CI and do small fix
+* FIXED: Adds User-Agent header in OTLP exporter
+
+### v0.24.0 / 2022-09-14
+
+* ADDED: Support InstrumentationScope, and update OTLP proto to 0.18.0
+* FIXED: Handle OTLP exporter 404s discretely
+* FIXED: `OTEL_EXPORTER_OTLP_ENDPOINT` appends the correct path with a trailing slash
+* FIXED: OTLP exporter demo code
+* DOCS: Update exporter default compression setting
+
+### v0.23.0 / 2022-06-23
+
+* ADDED: Report bundle size stats in exporter; also don't re-gzip unnecessarily
+
+### v0.22.0 / 2022-06-09
+
+* ADDED: Otlp grpc
+
+### v0.21.3 / 2022-05-12
+
+* (No significant changes)
+
+### v0.21.2 / 2022-01-19
+
+* FIXED: Default scheme for OTLP endpoint
+* FIXED: Remove TIMEOUT status from OTLP exporter (#1087)
+
+### v0.21.1 / 2021-12-31
+
+* FIXED: Allow OTLP Exporter compression value of `none`
+
+### v0.21.0 / 2021-12-01
+
+* ADDED: Exporter should use gzip compression by default
+
+### v0.20.6 / 2021-10-29
+
+* FIXED: Add unexpected error handlign in BSP and OTLP exporter (#995)
+* FIXED: Handle otlp exporter race condition gzip errors with retry
+
+### v0.20.5 / 2021-09-29
+
+* (No significant changes)
+
+### v0.20.4 / 2021-09-29
+
+* FIXED: OTLP Export Header Format
+
+### v0.20.3 / 2021-08-19
+
+* FIXED: OTLP exporter missing failure metrics
+
+### v0.20.2 / 2021-08-12
+
+* FIXED: Add rescue for OpenSSL errors during export
+* DOCS: Update docs to rely more on environment variable configuration
+
+### v0.20.1 / 2021-06-29
+
+* FIXED: Otlp encoding exceptions again
+
+### v0.20.0 / 2021-06-23
+
+* BREAKING CHANGE: Total order constraint on span.status=
+
+* FIXED: Total order constraint on span.status=
+
+### v0.19.0 / 2021-06-03
+
+* ADDED: Add a SSL verify mode option for the OTLP exporter
+* FIXED: Handle OTLP exporter encoding exceptions
+* DOCS: Remove the OTLP receiver legacy gRPC port(55680) references
+
+### v0.18.0 / 2021-05-21
+
+* BREAKING CHANGE: Replace Time.now with Process.clock_gettime
+
+* FIXED: Replace Time.now with Process.clock_gettime
+* FIXED: Rescue missed otlp exporter network errors
+
+### v0.17.0 / 2021-04-22
+
+* ADDED: Add zipkin exporter
+
+### v0.16.0 / 2021-03-17
+
+* BREAKING CHANGE: Implement Exporter#force_flush
+
+* ADDED: Implement Exporter#force_flush
+* FIXED: Rescue socket err in otlp exporter to prevent failures unable to connect
+* DOCS: Replace Gitter with GitHub Discussions
+
+### v0.15.0 / 2021-02-18
+
+* BREAKING CHANGE: Streamline processor pipeline
+
+* ADDED: Add otlp exporter hooks
+* FIXED: Streamline processor pipeline
+
+### v0.14.0 / 2021-02-03
+
+* (No significant changes)
+
+### v0.13.0 / 2021-01-29
+
+* BREAKING CHANGE: Spec compliance for OTLP exporter
+
+* ADDED: Add untraced wrapper to common utils
+* FIXED: Spec compliance for OTLP exporter
+* FIXED: Conditionally append path to collector endpoint
+* FIXED: OTLP path should be /v1/traces
+* FIXED: Rename OTLP env vars SPAN -> TRACES
+
+### v0.12.1 / 2021-01-13
+
+* FIXED: Updated protobuf version dependency
+
+### v0.12.0 / 2020-12-24
+
+* (No significant changes)
+
+### v0.11.0 / 2020-12-11
+
+* BREAKING CHANGE: Implement tracestate
+
+* ADDED: Implement tracestate
+* ADDED: Metrics reporting from trace export
+* FIXED: Copyright comments to not reference year
+
+### v0.10.0 / 2020-12-03
+
+* (No significant changes)
+
+### v0.9.0 / 2020-11-27
+
+* BREAKING CHANGE: Add timeout for force_flush and shutdown
+
+* ADDED: Add timeout for force_flush and shutdown
+* FIXED: Remove unused kwarg from otlp exporter retry
+
+### v0.8.0 / 2020-10-27
+
+* BREAKING CHANGE: Move context/span methods to Trace module
+* BREAKING CHANGE: Remove 'canonical' from status codes
+* BREAKING CHANGE: Assorted SpanContext fixes
+
+* FIXED: Move context/span methods to Trace module
+* FIXED: Remove 'canonical' from status codes
+* FIXED: Add gzip support to OTLP exporter
+* FIXED: Assorted SpanContext fixes
+
+### v0.7.0 / 2020-10-07
+
+* FIXED: OTLP parent_span_id should be nil for root
+* DOCS: Fix use of add_event in OTLP doc
+* DOCS: Standardize toplevel docs structure and readme
+* DOCS: Use BatchSpanProcessor in examples
+
+### v0.6.0 / 2020-09-10
+
+* Initial release.
diff --git a/exporter/otlp-logs/Gemfile b/exporter/otlp-logs/Gemfile
new file mode 100644
index 0000000000..a87d415b14
--- /dev/null
+++ b/exporter/otlp-logs/Gemfile
@@ -0,0 +1,20 @@
+# 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
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..55d09b8470
--- /dev/null
+++ b/exporter/otlp-logs/README.md
@@ -0,0 +1,154 @@
+# opentelemetry-exporter-otlp
+
+The `opentelemetry-exporter-otlp` gem provides an [OTLP](https://github.com/open-telemetry/opentelemetry-proto) exporter for OpenTelemetry for Ruby. Using `opentelemetry-exporter-otlp`, an application can configure OpenTelemetry to export collected tracing 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 and metrics from your application. You can analyze them using Prometheus, Jaeger, and other observability tools.
+
+## How does this gem fit in?
+
+The `opentelemetry-exporter-otlp` gem is a plugin that provides OTLP export. To export to the OpenTelemetry Collector, an application can include this gem along with `opentelemetry-sdk`, and configure the `SDK` to use the provided OTLP exporter as a span 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-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 span processor, and use the OpenTelemetry interfaces to produces traces and other information. Following is a basic example.
+
+```ruby
+require 'opentelemetry/sdk'
+require 'opentelemetry/exporter/otlp'
+
+# The OTLP exporter is the default, so no configuration is needed.
+# However, it could be manually selected via an environment variable if required:
+#
+# ENV['OTEL_TRACES_EXPORTER'] = 'otlp'
+#
+# You may also configure various settings via environment variables:
+# ENV['OTEL_EXPORTER_OTLP_COMPRESSION'] = 'gzip'
+
+OpenTelemetry::SDK.configure
+
+# To start a trace you need to get a Tracer from the TracerProvider
+tracer_provider = OpenTelemetry.tracer_provider
+tracer = tracer_provider.tracer('my_app_or_gem', '0.1.0')
+
+# create a span
+tracer.in_span('foo') do |span|
+ # set an attribute
+ span.set_attribute('platform', 'osx')
+ # add an event
+ span.add_event('event in bar')
+ # create bar as child of foo
+ tracer.in_span('bar') do |child_span|
+ # inspect the span
+ pp child_span
+ end
+end
+
+tracer_provider.shutdown
+```
+
+For additional examples, see the [examples on github][examples-github].
+
+## How can I configure the OTLP exporter?
+
+The collector exporter can be configured explicitly in code, or via environment variables as shown above. The configuration parameters, environment variables, and defaults are shown below.
+
+| Parameter | Environment variable | Default |
+| ------------------- | -------------------------------------------- | ----------------------------------- |
+| `endpoint:` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `"http://localhost:4318/v1/traces"` |
+| `certificate_file: `| `OTEL_EXPORTER_OTLP_CERTIFICATE` | |
+| `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` gem source is [on github][repo-github], along with related gems including `opentelemetry-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` 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 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.rb b/exporter/otlp-logs/lib/opentelemetry-exporter-otlp.rb
new file mode 100644
index 0000000000..1af5afb085
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry-exporter-otlp.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..1b9a2fa502
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs_exporter.rb
@@ -0,0 +1,385 @@
+# frozen_string_literal: true
+
+# Copyright The OpenTelemetry Authors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+require 'opentelemetry/common'
+require 'opentelemetry/sdk'
+require 'opentelemetry/sdk/logs'
+require 'net/http'
+require 'csv'
+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
+ WRITE_TIMEOUT_SUPPORTED = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6')
+ private_constant(:KEEP_ALIVE_TIMEOUT, :RETRY_COUNT, :WRITE_TIMEOUT_SUPPORTED)
+
+ 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.key?('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER')
+ OpenSSL::SSL::VERIFY_PEER
+ elsif ENV.key?('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE')
+ 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'),
+ 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::Exporter #{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)
+
+ @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 http_connection(uri, ssl_verify_mode, certificate_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.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 if WRITE_TIMEOUT_SUPPORTED
+ @http.start unless @http.started?
+ response = measure_request_duration { @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
+ redo if backoff?(retry_after: response['Retry-After'], retry_count: retry_count += 1, reason: response.code)
+ FAILURE
+ when Net::HTTPRequestTimeOut, Net::HTTPGatewayTimeOut, Net::HTTPBadGateway
+ response.body # Read and discard body
+ redo if backoff?(retry_count: retry_count += 1, reason: response.code)
+ FAILURE
+ when Net::HTTPNotFound
+ OpenTelemetry.handle_error(message: "OTLP exporter received http.code=404 for uri: '#{@path}'")
+ 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, reason: response.code)
+ else
+ @http.finish
+ FAILURE
+ end
+ rescue Net::OpenTimeout, Net::ReadTimeout
+ retry if backoff?(retry_count: retry_count += 1, reason: 'timeout')
+ return FAILURE
+ rescue OpenSSL::SSL::SSLError
+ retry if backoff?(retry_count: retry_count += 1, reason: 'openssl_error')
+ return FAILURE
+ rescue SocketError
+ retry if backoff?(retry_count: retry_count += 1, reason: 'socket_error')
+ return FAILURE
+ rescue SystemCallError => e
+ retry if backoff?(retry_count: retry_count += 1, reason: e.class.name)
+ return FAILURE
+ rescue EOFError
+ retry if backoff?(retry_count: retry_count += 1, reason: 'eof_error')
+ return FAILURE
+ rescue Zlib::DataError
+ retry if backoff?(retry_count: retry_count += 1, reason: 'zlib_error')
+ 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 if WRITE_TIMEOUT_SUPPORTED
+ 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 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 measure_request_duration
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ begin
+ response = yield
+ ensure
+ stop = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ duration_ms = 1000.0 * (stop - start)
+ end
+ end
+
+ def backoff?(retry_count:, reason:, 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.unix_nano_timestamp,
+ observed_time_unix_nano: log_record_data.unix_nano_observed_timestamp,
+ severity_number: as_otlp_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
+
+ # TODO: maybe don't translate the severity number, but translate the severity text into
+ # the number if the number is nil? Poss. change to allow for adding your own
+ # otel values?
+ def as_otlp_severity_number(severity_number)
+ case severity_number
+ when 0 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_DEBUG
+ when 1 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_INFO
+ when 2 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_WARN
+ when 3 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_ERROR
+ when 4 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_FATAL
+ when 5 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_UNSPECIFIED
+ end
+ 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..35e8a783b7
--- /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 exporter version
+ VERSION = '0.26.3'
+ 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..ceeb940e82
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb
@@ -0,0 +1,35 @@
+# 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'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("opentelemetry/proto/collector/logs/v1/logs_service.proto", :syntax => :proto3) do
+ add_message "opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest" do
+ repeated :resource_logs, :message, 1, "opentelemetry.proto.logs.v1.ResourceLogs"
+ end
+ add_message "opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse" do
+ optional :partial_success, :message, 1, "opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess"
+ end
+ add_message "opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess" do
+ optional :rejected_log_records, :int64, 1
+ optional :error_message, :string, 2
+ end
+ end
+end
+
+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..15654a2cfd
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb
@@ -0,0 +1,35 @@
+# 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'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("opentelemetry/proto/collector/metrics/v1/metrics_service.proto", :syntax => :proto3) do
+ add_message "opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest" do
+ repeated :resource_metrics, :message, 1, "opentelemetry.proto.metrics.v1.ResourceMetrics"
+ end
+ add_message "opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse" do
+ optional :partial_success, :message, 1, "opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess"
+ end
+ add_message "opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess" do
+ optional :rejected_data_points, :int64, 1
+ optional :error_message, :string, 2
+ end
+ end
+end
+
+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..4d0d62f22e
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb
@@ -0,0 +1,35 @@
+# 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'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("opentelemetry/proto/collector/trace/v1/trace_service.proto", :syntax => :proto3) do
+ add_message "opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest" do
+ repeated :resource_spans, :message, 1, "opentelemetry.proto.trace.v1.ResourceSpans"
+ end
+ add_message "opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse" do
+ optional :partial_success, :message, 1, "opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess"
+ end
+ add_message "opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess" do
+ optional :rejected_spans, :int64, 1
+ optional :error_message, :string, 2
+ end
+ end
+end
+
+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..a49580ec51
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb
@@ -0,0 +1,50 @@
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# source: opentelemetry/proto/common/v1/common.proto
+
+require 'google/protobuf'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("opentelemetry/proto/common/v1/common.proto", :syntax => :proto3) do
+ add_message "opentelemetry.proto.common.v1.AnyValue" do
+ oneof :value do
+ optional :string_value, :string, 1
+ optional :bool_value, :bool, 2
+ optional :int_value, :int64, 3
+ optional :double_value, :double, 4
+ optional :array_value, :message, 5, "opentelemetry.proto.common.v1.ArrayValue"
+ optional :kvlist_value, :message, 6, "opentelemetry.proto.common.v1.KeyValueList"
+ optional :bytes_value, :bytes, 7
+ end
+ end
+ add_message "opentelemetry.proto.common.v1.ArrayValue" do
+ repeated :values, :message, 1, "opentelemetry.proto.common.v1.AnyValue"
+ end
+ add_message "opentelemetry.proto.common.v1.KeyValueList" do
+ repeated :values, :message, 1, "opentelemetry.proto.common.v1.KeyValue"
+ end
+ add_message "opentelemetry.proto.common.v1.KeyValue" do
+ optional :key, :string, 1
+ optional :value, :message, 2, "opentelemetry.proto.common.v1.AnyValue"
+ end
+ add_message "opentelemetry.proto.common.v1.InstrumentationScope" do
+ optional :name, :string, 1
+ optional :version, :string, 2
+ repeated :attributes, :message, 3, "opentelemetry.proto.common.v1.KeyValue"
+ optional :dropped_attributes_count, :uint32, 4
+ end
+ end
+end
+
+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..2419b86bfa
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb
@@ -0,0 +1,83 @@
+# 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'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("opentelemetry/proto/logs/v1/logs.proto", :syntax => :proto3) do
+ add_message "opentelemetry.proto.logs.v1.LogsData" do
+ repeated :resource_logs, :message, 1, "opentelemetry.proto.logs.v1.ResourceLogs"
+ end
+ add_message "opentelemetry.proto.logs.v1.ResourceLogs" do
+ optional :resource, :message, 1, "opentelemetry.proto.resource.v1.Resource"
+ repeated :scope_logs, :message, 2, "opentelemetry.proto.logs.v1.ScopeLogs"
+ optional :schema_url, :string, 3
+ end
+ add_message "opentelemetry.proto.logs.v1.ScopeLogs" do
+ optional :scope, :message, 1, "opentelemetry.proto.common.v1.InstrumentationScope"
+ repeated :log_records, :message, 2, "opentelemetry.proto.logs.v1.LogRecord"
+ optional :schema_url, :string, 3
+ end
+ add_message "opentelemetry.proto.logs.v1.LogRecord" do
+ optional :time_unix_nano, :fixed64, 1
+ optional :observed_time_unix_nano, :fixed64, 11
+ optional :severity_number, :enum, 2, "opentelemetry.proto.logs.v1.SeverityNumber"
+ optional :severity_text, :string, 3
+ optional :body, :message, 5, "opentelemetry.proto.common.v1.AnyValue"
+ repeated :attributes, :message, 6, "opentelemetry.proto.common.v1.KeyValue"
+ optional :dropped_attributes_count, :uint32, 7
+ optional :flags, :fixed32, 8
+ optional :trace_id, :bytes, 9
+ optional :span_id, :bytes, 10
+ end
+ add_enum "opentelemetry.proto.logs.v1.SeverityNumber" do
+ value :SEVERITY_NUMBER_UNSPECIFIED, 0
+ value :SEVERITY_NUMBER_TRACE, 1
+ value :SEVERITY_NUMBER_TRACE2, 2
+ value :SEVERITY_NUMBER_TRACE3, 3
+ value :SEVERITY_NUMBER_TRACE4, 4
+ value :SEVERITY_NUMBER_DEBUG, 5
+ value :SEVERITY_NUMBER_DEBUG2, 6
+ value :SEVERITY_NUMBER_DEBUG3, 7
+ value :SEVERITY_NUMBER_DEBUG4, 8
+ value :SEVERITY_NUMBER_INFO, 9
+ value :SEVERITY_NUMBER_INFO2, 10
+ value :SEVERITY_NUMBER_INFO3, 11
+ value :SEVERITY_NUMBER_INFO4, 12
+ value :SEVERITY_NUMBER_WARN, 13
+ value :SEVERITY_NUMBER_WARN2, 14
+ value :SEVERITY_NUMBER_WARN3, 15
+ value :SEVERITY_NUMBER_WARN4, 16
+ value :SEVERITY_NUMBER_ERROR, 17
+ value :SEVERITY_NUMBER_ERROR2, 18
+ value :SEVERITY_NUMBER_ERROR3, 19
+ value :SEVERITY_NUMBER_ERROR4, 20
+ value :SEVERITY_NUMBER_FATAL, 21
+ value :SEVERITY_NUMBER_FATAL2, 22
+ value :SEVERITY_NUMBER_FATAL3, 23
+ value :SEVERITY_NUMBER_FATAL4, 24
+ end
+ add_enum "opentelemetry.proto.logs.v1.LogRecordFlags" do
+ value :LOG_RECORD_FLAGS_DO_NOT_USE, 0
+ value :LOG_RECORD_FLAGS_TRACE_FLAGS_MASK, 255
+ end
+ end
+end
+
+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..743381736b
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb
@@ -0,0 +1,159 @@
+# 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'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("opentelemetry/proto/metrics/v1/metrics.proto", :syntax => :proto3) do
+ add_message "opentelemetry.proto.metrics.v1.MetricsData" do
+ repeated :resource_metrics, :message, 1, "opentelemetry.proto.metrics.v1.ResourceMetrics"
+ end
+ add_message "opentelemetry.proto.metrics.v1.ResourceMetrics" do
+ optional :resource, :message, 1, "opentelemetry.proto.resource.v1.Resource"
+ repeated :scope_metrics, :message, 2, "opentelemetry.proto.metrics.v1.ScopeMetrics"
+ optional :schema_url, :string, 3
+ end
+ add_message "opentelemetry.proto.metrics.v1.ScopeMetrics" do
+ optional :scope, :message, 1, "opentelemetry.proto.common.v1.InstrumentationScope"
+ repeated :metrics, :message, 2, "opentelemetry.proto.metrics.v1.Metric"
+ optional :schema_url, :string, 3
+ end
+ add_message "opentelemetry.proto.metrics.v1.Metric" do
+ optional :name, :string, 1
+ optional :description, :string, 2
+ optional :unit, :string, 3
+ oneof :data do
+ optional :gauge, :message, 5, "opentelemetry.proto.metrics.v1.Gauge"
+ optional :sum, :message, 7, "opentelemetry.proto.metrics.v1.Sum"
+ optional :histogram, :message, 9, "opentelemetry.proto.metrics.v1.Histogram"
+ optional :exponential_histogram, :message, 10, "opentelemetry.proto.metrics.v1.ExponentialHistogram"
+ optional :summary, :message, 11, "opentelemetry.proto.metrics.v1.Summary"
+ end
+ end
+ add_message "opentelemetry.proto.metrics.v1.Gauge" do
+ repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.NumberDataPoint"
+ end
+ add_message "opentelemetry.proto.metrics.v1.Sum" do
+ repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.NumberDataPoint"
+ optional :aggregation_temporality, :enum, 2, "opentelemetry.proto.metrics.v1.AggregationTemporality"
+ optional :is_monotonic, :bool, 3
+ end
+ add_message "opentelemetry.proto.metrics.v1.Histogram" do
+ repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.HistogramDataPoint"
+ optional :aggregation_temporality, :enum, 2, "opentelemetry.proto.metrics.v1.AggregationTemporality"
+ end
+ add_message "opentelemetry.proto.metrics.v1.ExponentialHistogram" do
+ repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint"
+ optional :aggregation_temporality, :enum, 2, "opentelemetry.proto.metrics.v1.AggregationTemporality"
+ end
+ add_message "opentelemetry.proto.metrics.v1.Summary" do
+ repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.SummaryDataPoint"
+ end
+ add_message "opentelemetry.proto.metrics.v1.NumberDataPoint" do
+ repeated :attributes, :message, 7, "opentelemetry.proto.common.v1.KeyValue"
+ optional :start_time_unix_nano, :fixed64, 2
+ optional :time_unix_nano, :fixed64, 3
+ repeated :exemplars, :message, 5, "opentelemetry.proto.metrics.v1.Exemplar"
+ optional :flags, :uint32, 8
+ oneof :value do
+ optional :as_double, :double, 4
+ optional :as_int, :sfixed64, 6
+ end
+ end
+ add_message "opentelemetry.proto.metrics.v1.HistogramDataPoint" do
+ repeated :attributes, :message, 9, "opentelemetry.proto.common.v1.KeyValue"
+ optional :start_time_unix_nano, :fixed64, 2
+ optional :time_unix_nano, :fixed64, 3
+ optional :count, :fixed64, 4
+ proto3_optional :sum, :double, 5
+ repeated :bucket_counts, :fixed64, 6
+ repeated :explicit_bounds, :double, 7
+ repeated :exemplars, :message, 8, "opentelemetry.proto.metrics.v1.Exemplar"
+ optional :flags, :uint32, 10
+ proto3_optional :min, :double, 11
+ proto3_optional :max, :double, 12
+ end
+ add_message "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint" do
+ repeated :attributes, :message, 1, "opentelemetry.proto.common.v1.KeyValue"
+ optional :start_time_unix_nano, :fixed64, 2
+ optional :time_unix_nano, :fixed64, 3
+ optional :count, :fixed64, 4
+ proto3_optional :sum, :double, 5
+ optional :scale, :sint32, 6
+ optional :zero_count, :fixed64, 7
+ optional :positive, :message, 8, "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets"
+ optional :negative, :message, 9, "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets"
+ optional :flags, :uint32, 10
+ repeated :exemplars, :message, 11, "opentelemetry.proto.metrics.v1.Exemplar"
+ proto3_optional :min, :double, 12
+ proto3_optional :max, :double, 13
+ optional :zero_threshold, :double, 14
+ end
+ add_message "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets" do
+ optional :offset, :sint32, 1
+ repeated :bucket_counts, :uint64, 2
+ end
+ add_message "opentelemetry.proto.metrics.v1.SummaryDataPoint" do
+ repeated :attributes, :message, 7, "opentelemetry.proto.common.v1.KeyValue"
+ optional :start_time_unix_nano, :fixed64, 2
+ optional :time_unix_nano, :fixed64, 3
+ optional :count, :fixed64, 4
+ optional :sum, :double, 5
+ repeated :quantile_values, :message, 6, "opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile"
+ optional :flags, :uint32, 8
+ end
+ add_message "opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile" do
+ optional :quantile, :double, 1
+ optional :value, :double, 2
+ end
+ add_message "opentelemetry.proto.metrics.v1.Exemplar" do
+ repeated :filtered_attributes, :message, 7, "opentelemetry.proto.common.v1.KeyValue"
+ optional :time_unix_nano, :fixed64, 2
+ optional :span_id, :bytes, 4
+ optional :trace_id, :bytes, 5
+ oneof :value do
+ optional :as_double, :double, 3
+ optional :as_int, :sfixed64, 6
+ end
+ end
+ add_enum "opentelemetry.proto.metrics.v1.AggregationTemporality" do
+ value :AGGREGATION_TEMPORALITY_UNSPECIFIED, 0
+ value :AGGREGATION_TEMPORALITY_DELTA, 1
+ value :AGGREGATION_TEMPORALITY_CUMULATIVE, 2
+ end
+ add_enum "opentelemetry.proto.metrics.v1.DataPointFlags" do
+ value :DATA_POINT_FLAGS_DO_NOT_USE, 0
+ value :DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK, 1
+ end
+ end
+end
+
+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..a10d5ad1f5
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb
@@ -0,0 +1,25 @@
+# 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'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("opentelemetry/proto/resource/v1/resource.proto", :syntax => :proto3) do
+ add_message "opentelemetry.proto.resource.v1.Resource" do
+ repeated :attributes, :message, 1, "opentelemetry.proto.common.v1.KeyValue"
+ optional :dropped_attributes_count, :uint32, 2
+ end
+ end
+end
+
+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..48c5737352
--- /dev/null
+++ b/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb
@@ -0,0 +1,90 @@
+# 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'
+
+Google::Protobuf::DescriptorPool.generated_pool.build do
+ add_file("opentelemetry/proto/trace/v1/trace.proto", :syntax => :proto3) do
+ add_message "opentelemetry.proto.trace.v1.TracesData" do
+ repeated :resource_spans, :message, 1, "opentelemetry.proto.trace.v1.ResourceSpans"
+ end
+ add_message "opentelemetry.proto.trace.v1.ResourceSpans" do
+ optional :resource, :message, 1, "opentelemetry.proto.resource.v1.Resource"
+ repeated :scope_spans, :message, 2, "opentelemetry.proto.trace.v1.ScopeSpans"
+ optional :schema_url, :string, 3
+ end
+ add_message "opentelemetry.proto.trace.v1.ScopeSpans" do
+ optional :scope, :message, 1, "opentelemetry.proto.common.v1.InstrumentationScope"
+ repeated :spans, :message, 2, "opentelemetry.proto.trace.v1.Span"
+ optional :schema_url, :string, 3
+ end
+ add_message "opentelemetry.proto.trace.v1.Span" do
+ optional :trace_id, :bytes, 1
+ optional :span_id, :bytes, 2
+ optional :trace_state, :string, 3
+ optional :parent_span_id, :bytes, 4
+ optional :name, :string, 5
+ optional :kind, :enum, 6, "opentelemetry.proto.trace.v1.Span.SpanKind"
+ optional :start_time_unix_nano, :fixed64, 7
+ optional :end_time_unix_nano, :fixed64, 8
+ repeated :attributes, :message, 9, "opentelemetry.proto.common.v1.KeyValue"
+ optional :dropped_attributes_count, :uint32, 10
+ repeated :events, :message, 11, "opentelemetry.proto.trace.v1.Span.Event"
+ optional :dropped_events_count, :uint32, 12
+ repeated :links, :message, 13, "opentelemetry.proto.trace.v1.Span.Link"
+ optional :dropped_links_count, :uint32, 14
+ optional :status, :message, 15, "opentelemetry.proto.trace.v1.Status"
+ end
+ add_message "opentelemetry.proto.trace.v1.Span.Event" do
+ optional :time_unix_nano, :fixed64, 1
+ optional :name, :string, 2
+ repeated :attributes, :message, 3, "opentelemetry.proto.common.v1.KeyValue"
+ optional :dropped_attributes_count, :uint32, 4
+ end
+ add_message "opentelemetry.proto.trace.v1.Span.Link" do
+ optional :trace_id, :bytes, 1
+ optional :span_id, :bytes, 2
+ optional :trace_state, :string, 3
+ repeated :attributes, :message, 4, "opentelemetry.proto.common.v1.KeyValue"
+ optional :dropped_attributes_count, :uint32, 5
+ end
+ add_enum "opentelemetry.proto.trace.v1.Span.SpanKind" do
+ value :SPAN_KIND_UNSPECIFIED, 0
+ value :SPAN_KIND_INTERNAL, 1
+ value :SPAN_KIND_SERVER, 2
+ value :SPAN_KIND_CLIENT, 3
+ value :SPAN_KIND_PRODUCER, 4
+ value :SPAN_KIND_CONSUMER, 5
+ end
+ add_message "opentelemetry.proto.trace.v1.Status" do
+ optional :message, :string, 2
+ optional :code, :enum, 3, "opentelemetry.proto.trace.v1.Status.StatusCode"
+ end
+ add_enum "opentelemetry.proto.trace.v1.Status.StatusCode" do
+ value :STATUS_CODE_UNSET, 0
+ value :STATUS_CODE_OK, 1
+ value :STATUS_CODE_ERROR, 2
+ end
+ end
+end
+
+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..414843df60
--- /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 = 'OTLP exporter for the OpenTelemetry framework'
+ spec.description = 'OTLP 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.14'
+ spec.add_dependency 'opentelemetry-api', '~> 1.1'
+ spec.add_dependency 'opentelemetry-common', '~> 0.20'
+ spec.add_dependency 'opentelemetry-logs-api'
+ spec.add_dependency 'opentelemetry-logs-sdk'
+ 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/.rubocop.yml b/exporter/otlp-logs/test/.rubocop.yml
new file mode 100644
index 0000000000..3ceaea6dff
--- /dev/null
+++ b/exporter/otlp-logs/test/.rubocop.yml
@@ -0,0 +1,8 @@
+inherit_from: ../.rubocop.yml
+
+Style/MethodCallWithoutArgsParentheses:
+ Exclude:
+ - 'opentelemetry/exporter/otlp/exporter_test.rb'
+Style/BlockDelimiters:
+ Exclude:
+ - 'opentelemetry/exporter/otlp/exporter_test.rb'
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..667b24b4b5
--- /dev/null
+++ b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb
@@ -0,0 +1,696 @@
+# 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
+
+ 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.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',
+ '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'
+ _(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_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',
+ 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'
+ _(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 = _() {
+ exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: Object.new)
+ _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über')
+ }.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 = _() {
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a = ') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ }.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+
+ error = _() {
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'a = ') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ }.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+ end
+
+ it 'fails fast when header or values are not found' do
+ error = _() {
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => ',') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ }.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+
+ error = _() {
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => ',') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ }.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+ end
+
+ it 'fails fast when header values contain invalid escape characters' do
+ error = _() {
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'c=hi%F3') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ }.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+
+ error = _() {
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'c=hi%F3') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ }.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+ end
+
+ it 'fails fast when headers are invalid' do
+ error = _() {
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'this is not a header') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ end
+ }.must_raise(ArgumentError)
+ _(error.message).must_match(/headers/i)
+
+ error = _() {
+ OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'this is not a header') do
+ OpenTelemetry::Exporter::OTLP::LogsExporter.new
+ 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 }
+
+ before do
+ OpenTelemetry.logger_provider = OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: OpenTelemetry::SDK::Resources::Resource.telemetry_sdk)
+ end
+
+ 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 TIMEOUT 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
+
+ it 'returns TIMEOUT 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
+ 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)
+ 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 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 exporter received http\.code=404 for uri: '/v1/logs'}
+ )
+
+ _(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)
+ OpenTelemetry.logger_provider.add_log_record_processor(processor)
+ OpenTelemetry.logger_provider.logger.emit(body: 'test')
+ OpenTelemetry.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
+ # CHECK ME!
+ # make multiple logs
+ # send them to multiple loggers
+ # shut down the processor
+ # see what happens
+ stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200)
+ processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter)
+ logger = OpenTelemetry.logger_provider.logger('logger', 'v0.0.1')
+ # other_logger = OpenTelemetry.logger_provider.logger('other_logger')
+
+ trace_id = OpenTelemetry::Trace.generate_trace_id
+ span_id = OpenTelemetry::Trace.generate_span_id
+ trace_flags = OpenTelemetry::Trace::TraceFlags::DEFAULT
+ span_context = OpenTelemetry::Trace::SpanContext.new(trace_id: trace_id, span_id: span_id, trace_flags: trace_flags)
+ timestamp = Time.now
+ observed_timestamp = Time.now + 1
+ severity_text = 'DEBUG'
+ body = 'Test'
+ attributes = { 'b' => true }
+
+
+ OpenTelemetry.logger_provider.add_log_record_processor(processor)
+ logger.emit(body: 'test', severity_number: 0, severity_text: 'DEBUG', timestamp: timestamp, span_context: span_context)
+ OpenTelemetry.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::Logs::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: (timestamp.to_r * 1_000_000_000).to_i,
+ observed_time_unix_nano: (observed_timestamp.to_r * 1_000_000_000).to_i,
+ severity_number: Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_DEBUG,
+ severity_text: severity_text,
+ body: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 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: trace_flags.instance_variable_get(:@flags),
+ trace_id: trace_id,
+ span_id: span_id
+ ),
+ # Opentelemetry::Proto::Logs::V1::LogRecord.new(
+ # trace_id: trace_id,
+ # span_id: client_span_id,
+ # parent_span_id: child_span_id,
+ # name: 'client',
+ # kind: Opentelemetry::Proto::Logs::V1::LogRecord::SpanKind::SPAN_KIND_CLIENT,
+ # start_time_unix_nano: ((start_timestamp + 2).to_r * 1_000_000_000).to_i,
+ # end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i,
+ # status: Opentelemetry::Proto::Logs::V1::Status.new(
+ # code: Opentelemetry::Proto::Logs::V1::Status::StatusCode::STATUS_CODE_UNSET
+ # )
+ # ),
+ # Opentelemetry::Proto::Logs::V1::LogRecord.new(
+ # trace_id: trace_id,
+ # span_id: consumer_span_id,
+ # parent_span_id: child_span_id,
+ # name: 'consumer',
+ # kind: Opentelemetry::Proto::Logs::V1::LogRecord::SpanKind::SPAN_KIND_CONSUMER,
+ # start_time_unix_nano: ((start_timestamp + 5).to_r * 1_000_000_000).to_i,
+ # end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i,
+ # status: Opentelemetry::Proto::Logs::V1::Status.new(
+ # code: Opentelemetry::Proto::Logs::V1::Status::StatusCode::STATUS_CODE_UNSET
+ # )
+ # ),
+ # Opentelemetry::Proto::Logs::V1::LogRecord.new(
+ # trace_id: trace_id,
+ # span_id: child_span_id,
+ # parent_span_id: root_span_id,
+ # name: 'child',
+ # kind: Opentelemetry::Proto::Logs::V1::LogRecord::SpanKind::SPAN_KIND_PRODUCER,
+ # start_time_unix_nano: ((start_timestamp + 1).to_r * 1_000_000_000).to_i,
+ # end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i,
+ # attributes: [
+ # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'b', value: Opentelemetry::Proto::Common::V1::AnyValue.new(bool_value: true)),
+ # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'f', value: Opentelemetry::Proto::Common::V1::AnyValue.new(double_value: 1.1)),
+ # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'i', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 2)),
+ # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 's', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'val')),
+ # Opentelemetry::Proto::Common::V1::KeyValue.new(
+ # key: 'a',
+ # value: Opentelemetry::Proto::Common::V1::AnyValue.new(
+ # array_value: Opentelemetry::Proto::Common::V1::ArrayValue.new(
+ # values: [
+ # Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 3),
+ # Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 4)
+ # ]
+ # )
+ # )
+ # )
+ # ],
+ # events: [
+ # Opentelemetry::Proto::Logs::V1::LogRecord::Event.new(
+ # time_unix_nano: ((start_timestamp + 4).to_r * 1_000_000_000).to_i,
+ # name: 'event',
+ # attributes: [
+ # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'attr', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 42))
+ # ]
+ # )
+ # ],
+ # links: [
+ # Opentelemetry::Proto::Logs::V1::LogRecord::Link.new(
+ # trace_id: trace_id,
+ # span_id: root_span_id,
+ # attributes: [
+ # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'attr', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 4))
+ # ]
+ # )
+ # ],
+ # status: Opentelemetry::Proto::Logs::V1::Status.new(
+ # code: Opentelemetry::Proto::Logs::V1::Status::StatusCode::STATUS_CODE_ERROR
+ # )
+ # )
+ ]
+ ),
+ # Opentelemetry::Proto::Logs::V1::ScopeSpans.new(
+ # scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new(
+ # name: 'other_logger'
+ # ),
+ # spans: [
+ # Opentelemetry::Proto::Logs::V1::LogRecord.new(
+ # trace_id: trace_id,
+ # span_id: server_span_id,
+ # parent_span_id: client_span_id,
+ # name: 'server',
+ # kind: Opentelemetry::Proto::Logs::V1::LogRecord::SpanKind::SPAN_KIND_SERVER,
+ # start_time_unix_nano: ((start_timestamp + 3).to_r * 1_000_000_000).to_i,
+ # end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i,
+ # status: Opentelemetry::Proto::Logs::V1::Status.new(
+ # code: Opentelemetry::Proto::Logs::V1::Status::StatusCode::STATUS_CODE_UNSET
+ # )
+ # )
+ # ]
+ # )
+ ]
+ )
+ ]
+ )
+ )
+
+ 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/test_helper.rb b/exporter/otlp-logs/test/test_helper.rb
new file mode 100644
index 0000000000..2366658d98
--- /dev/null
+++ b/exporter/otlp-logs/test/test_helper.rb
@@ -0,0 +1,22 @@
+# 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'
+
+OpenTelemetry.logger = Logger.new(File::NULL)
diff --git a/logs_api/lib/opentelemetry-logs-api.rb b/logs_api/lib/opentelemetry-logs-api.rb
index 47ba36905e..723004ed22 100644
--- a/logs_api/lib/opentelemetry-logs-api.rb
+++ b/logs_api/lib/opentelemetry-logs-api.rb
@@ -7,3 +7,25 @@
require 'opentelemetry'
require_relative 'opentelemetry/logs'
require_relative 'opentelemetry/logs/version'
+
+# 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.
+module OpenTelemetry
+ # Register the global logger provider.
+ #
+ # @param [LoggerProvider] provider A logger provider to register as the
+ # global instance.
+ def logger_provider=(provider)
+ puts 'nil logger provider' if provider.nil?
+ @logger_provider = provider
+ 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..10dfe0bbf6
--- /dev/null
+++ b/logs_api/lib/opentelemetry/internal/proxy_logger.rb
@@ -0,0 +1,68 @@
+# 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
+ # Returns a new {ProxyLogger} instance.
+ #
+ # @return [ProxyLogger]
+ def initialize
+ super
+ @delegate = nil
+ end
+
+ # Set the delegate Logger. If this is called more than once, a warning will
+ # be logged and superfluous calls will be ignored.
+ #
+ # @param [Logger] logger The Logger to delegate to
+ def delegate=(logger)
+ @mutex.synchronize do
+ if @delegate.nil?
+ @delegate = logger
+ @instrument_registry.each_value { |instrument| instrument.upgrade_with(logger) }
+ else
+ OpenTelemetry.logger.warn 'Attempt to reset delegate in ProxyLogger ignored.'
+ end
+ end
+ 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
+ )
+ @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
+ )
+
+ 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/logger.rb b/logs_api/lib/opentelemetry/logs/logger.rb
index c6caa34a77..6374b296b1 100644
--- a/logs_api/lib/opentelemetry/logs/logger.rb
+++ b/logs_api/lib/opentelemetry/logs/logger.rb
@@ -8,6 +8,11 @@ module OpenTelemetry
module Logs
# No-op implementation of logger.
class Logger
+ def initialize
+ @mutex = Mutex.new
+ @instrument_registry = {}
+ end
+
# rubocop:disable Style/EmptyMethod
# Emit a {LogRecord} to the processing pipeline.
diff --git a/logs_api/lib/opentelemetry/logs/logger_provider.rb b/logs_api/lib/opentelemetry/logs/logger_provider.rb
index 13343407dc..f2d9eb6d45 100644
--- a/logs_api/lib/opentelemetry/logs/logger_provider.rb
+++ b/logs_api/lib/opentelemetry/logs/logger_provider.rb
@@ -9,7 +9,9 @@ module Logs
# No-op implementation of a logger provider.
class LoggerProvider
NOOP_LOGGER = OpenTelemetry::Logs::Logger.new
- private_constant :NOOP_LOGGER
+ # This is used in the SDK LoggerProvider
+ # I would like to make it a public constant
+ # private_constant :NOOP_LOGGER
# Returns an {OpenTelemetry::Logs::Logger} instance.
#
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..51c3d9d429
--- /dev/null
+++ b/logs_sdk/lib/opentelemetry/sdk/logs/configuration_patch.rb
@@ -0,0 +1,67 @@
+# 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
+ # default is console until other exporters built
+ 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
+ Logs::Export::BatchLogRecordProcessor.new(OpenTelemetry::Exporter::OTLP::LogsExporter.new)
+ # fetch_exporter(exporter, 'OpenTelemetry::Exporter::OTLP::Exporter')
+ 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/export.rb b/logs_sdk/lib/opentelemetry/sdk/logs/export.rb
index 414670e86c..0f8e07d749 100644
--- a/logs_sdk/lib/opentelemetry/sdk/logs/export.rb
+++ b/logs_sdk/lib/opentelemetry/sdk/logs/export.rb
@@ -3,7 +3,6 @@
# Copyright The OpenTelemetry Authors
#
# SPDX-License-Identifier: Apache-2.0
-
module OpenTelemetry
module SDK
module Logs
@@ -24,8 +23,8 @@ module Export
end
end
-require_relative 'export/console_log_record_exporter'
-require_relative 'export/in_memory_log_record_exporter'
-require_relative 'export/log_record_exporter'
require_relative 'export/simple_log_record_processor'
require_relative 'export/batch_log_record_processor'
+require_relative 'export/log_record_exporter'
+require_relative 'export/console_log_record_exporter'
+require_relative 'export/in_memory_log_record_exporter'
diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb b/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb
index 10d1dcb7ab..5d2f2d5881 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,9 @@ 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 +70,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 +84,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
@@ -98,6 +109,50 @@ def to_log_record_data
private
+ 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
+ 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
+
def to_integer_nanoseconds(timestamp)
return unless timestamp.is_a?(Time)
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.rb b/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb
index 231999a33c..f4e51c016f 100644
--- a/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb
+++ b/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb
@@ -4,6 +4,8 @@
#
# SPDX-License-Identifier: Apache-2.0
+require_relative '../../../../../logs_api/lib/opentelemetry-logs-api'
+
module OpenTelemetry
module SDK
module Logs
@@ -26,6 +28,10 @@ def initialize(name, version, logger_provider)
@logger_provider = logger_provider
end
+ def log_record_limits
+ logger_provider.log_record_limits
+ end
+
# Emit a {LogRecord} to the processing pipeline.
#
# @param [optional Time] timestamp Time when the event occurred.
diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb b/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb
index 99e7df10e5..4c13343185 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,27 +21,47 @@ 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 Array] log_record_processors The
+ # {LogRecordProcessor}s to associate with 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)
- @log_record_processors = []
+ def initialize(
+ resource: OpenTelemetry::SDK::Resources::Resource.create,
+ log_record_processors: [],
+ log_record_limits: LogRecordLimits::DEFAULT
+ )
+ @log_record_processors = 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.
+ # Creates an {OpenTelemetry::SDK::Logs::Logger} instance.
#
# @param [String] name Instrumentation package name
# @param [optional String] version Instrumentation package version
#
# @return [OpenTelemetry::SDK::Logs::Logger]
def logger(name:, version: nil)
- version ||= ''
+ if @stopped
+ OpenTelemetry.logger.warn('calling LoggerProvider#logger after shutdown, a noop logger will be returned')
+ OpenTelemetry::Logs::LoggerProvider::NOOP_LOGGER
+ else
+ version ||= ''
+
+ if !name.is_a?(String) || name.empty?
+ OpenTelemetry.logger.warn('LoggerProvider#logger called with an ' \
+ "invalid name. Name provided: #{name.inspect}")
+ end
- if !name.is_a?(String) || name.empty?
- OpenTelemetry.logger.warn('LoggerProvider#logger called with an ' \
- "invalid name. Name provided: #{name.inspect}")
+ @registry_mutex.synchronize do
+ @registry[Key.new(name, version)] ||= Logger.new(name, version, self)
+ end
end
Logger.new(name, version, self)
@@ -142,7 +165,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/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..771d4720eb 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)
@@ -80,8 +89,43 @@
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
+
+ # 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
+ skip 'uncertain about why this is failing... need to investigate'
+ 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
+
+ describe 'when stopped' do
+ it 'logs a warning' do
+ logger_provider.instance_variable_set(:@stopped, true)
+
+ OpenTelemetry::TestHelpers.with_test_logger do |log_stream|
+ logger_provider.logger(name: '')
+ assert_match(
+ /calling LoggerProvider#logger after shutdown/,
+ log_stream.string
+ )
+ end
+ end
+
+ it 'does not add a new logger to the registry' do
+ before_stopped_size = logger_provider.instance_variable_get(:@registry).keys.size
+ logger_provider.instance_variable_set(:@stopped, true)
+ logger_provider.logger(name: 'new_logger')
+ after_stopped_size = logger_provider.instance_variable_get(:@registry).keys.size
+
+ assert_equal(before_stopped_size, after_stopped_size)
+ end
end
end
diff --git a/sdk/lib/opentelemetry/sdk/configurator.rb b/sdk/lib/opentelemetry/sdk/configurator.rb
index ae1bbaf07b..86cbd2df4e 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
@@ -179,7 +189,6 @@ def wrapped_exporters_from_env # rubocop:disable Metrics/CyclomaticComplexity
when 'none' then nil
when 'otlp'
otlp_protocol = ENV['OTEL_EXPORTER_OTLP_TRACES_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, spans will not be exported."
nil
diff --git a/test_helpers/lib/opentelemetry/test_helpers.rb b/test_helpers/lib/opentelemetry/test_helpers.rb
index 25048aa0f4..b47e29847d 100644
--- a/test_helpers/lib/opentelemetry/test_helpers.rb
+++ b/test_helpers/lib/opentelemetry/test_helpers.rb
@@ -72,5 +72,19 @@ 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,
+ trace_id: OpenTelemetry::Trace.generate_trace_id,
+ span_id: OpenTelemetry::Trace.generate_span_id,
+ trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT, severity_text: nil,
+ severity_number: nil, body: nil, resource: nil,
+ instrumentation_scope: OpenTelemetry::SDK::InstrumentationScope.new('', 'v0.0.1'),
+ attributes: nil, total_recorded_attributes: 0)
+ resource ||= OpenTelemetry::SDK::Resources::Resource.telemetry_sdk
+ OpenTelemetry::SDK::Logs::LogRecordData.new(timestamp, observed_timestamp, trace_id, span_id, trace_flags,
+ severity_text, severity_number, body, resource, instrumentation_scope,
+ attributes, total_recorded_attributes)
+ end
end
end