diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1eadcc02c..e5b495679 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,7 @@ jobs: - opentelemetry-exporter-otlp-grpc - opentelemetry-exporter-otlp-http - opentelemetry-exporter-zipkin + - opentelemetry-exporter-otlp-logs - opentelemetry-exporter-otlp-metrics os: - ubuntu-latest @@ -134,8 +135,9 @@ jobs: echo "skip=false" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-common" ]] && echo "skip=true" >> $GITHUB_OUTPUT - [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-http" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-grpc" ]] && echo "skip=true" >> $GITHUB_OUTPUT + [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-http" ]] && echo "skip=true" >> $GITHUB_OUTPUT + [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-logs" ]] && echo "skip=true" >> $GITHUB_OUTPUT [[ "${{ matrix.gem }}" == "opentelemetry-exporter-otlp-metrics" ]] && echo "skip=true" >> $GITHUB_OUTPUT # This is essentially a bash script getting evaluated, so we need to return true or the whole job fails. true diff --git a/exporter/otlp-logs/.rubocop.yml b/exporter/otlp-logs/.rubocop.yml new file mode 100644 index 000000000..5c3cff545 --- /dev/null +++ b/exporter/otlp-logs/.rubocop.yml @@ -0,0 +1,19 @@ +inherit_from: + - ../../contrib/rubocop.yml + +AllCops: + Exclude: + - "lib/opentelemetry/proto/**/*" + - "vendor/**/*" + +Naming/FileName: + Exclude: + - lib/opentelemetry-exporter-otlp-logs.rb +Style/StringLiterals: + Exclude: + - gemfiles/**/* +Style/FrozenStringLiteralComment: + Exclude: + - gemfiles/**/* +Metrics/BlockLength: + Enabled: false diff --git a/exporter/otlp-logs/.yardopts b/exporter/otlp-logs/.yardopts new file mode 100644 index 000000000..f8ba2c9cd --- /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 000000000..b59a40a8b --- /dev/null +++ b/exporter/otlp-logs/Appraisals @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +appraise 'google-protobuf-3.18.0' do + gem 'google-protobuf', '~> 3.18.0' +end + +appraise 'google-protobuf-3.25.0' do + gem 'google-protobuf', '~> 3.25.0' +end + +appraise 'google-protobuf-4.29.0' do + gem 'google-protobuf', '~> 4.29.0' +end + +appraise 'google-protobuf-latest' do + gem 'google-protobuf' +end diff --git a/exporter/otlp-logs/CHANGELOG.md b/exporter/otlp-logs/CHANGELOG.md new file mode 100644 index 000000000..d1805e931 --- /dev/null +++ b/exporter/otlp-logs/CHANGELOG.md @@ -0,0 +1 @@ +# Release History: opentelemetry-exporter-otlp-logs diff --git a/exporter/otlp-logs/Gemfile b/exporter/otlp-logs/Gemfile new file mode 100644 index 000000000..fa879e23f --- /dev/null +++ b/exporter/otlp-logs/Gemfile @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +group :test, :development do + gem 'opentelemetry-api', path: '../../api' + gem 'opentelemetry-common', path: '../../common' + gem 'opentelemetry-logs-api', path: '../../logs_api' + gem 'opentelemetry-logs-sdk', path: '../../logs_sdk' + gem 'opentelemetry-registry', path: '../../registry' + gem 'opentelemetry-sdk', path: '../../sdk' + gem 'opentelemetry-semantic_conventions', path: '../../semantic_conventions' + gem 'opentelemetry-test-helpers', path: '../../test_helpers' +end + +group :test do + gem 'minitest-stub-const' +end diff --git a/exporter/otlp-logs/LICENSE b/exporter/otlp-logs/LICENSE new file mode 100644 index 000000000..1ef7dad2c --- /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 000000000..ed45d75fa --- /dev/null +++ b/exporter/otlp-logs/README.md @@ -0,0 +1,146 @@ +# opentelemetry-exporter-otlp-logs + +The `opentelemetry-exporter-otlp-logs` gem provides an [OTLP](https://github.com/open-telemetry/opentelemetry-proto) logs exporter for OpenTelemetry for Ruby. Using `opentelemetry-exporter-otlp-logs`, an application can configure OpenTelemetry to export collected log record data to [the OpenTelemetry Collector][opentelemetry-collector-home]. + +## What is OpenTelemetry? + +[OpenTelemetry][opentelemetry-home] is an open source observability framework, providing a general-purpose API, SDK, and related tools required for the instrumentation of cloud-native software, frameworks, and libraries. + +OpenTelemetry provides a single set of APIs, libraries, agents, and collector services to capture distributed traces, metrics and logs from your application. You can analyze them using Prometheus, Jaeger, and other observability tools. + +## How does this gem fit in? + +The `opentelemetry-exporter-otlp-logs` gem is a plugin that provides OTLP export. To export to the OpenTelemetry Collector, an application can include this gem along with `opentelemetry-logs-sdk` and configure the SDK to use the provided OTLP exporter with a log record processor. + +Generally, *libraries* that produce telemetry data should avoid depending directly on specific exporter, deferring that choice to the application developer. + +### Supported protocol version + +This gem supports the [v0.20.0 release][otel-proto-release] of OTLP. + +## How do I get started? + +Install the gem using: + +```console + +gem install opentelemetry-logs-sdk +gem install opentelemetry-exporter-otlp + +``` + +Or, if you use [bundler][bundler-home], include `opentelemetry-sdk` in your `Gemfile`. + +Then, configure the SDK to use the OTLP exporter as a log record processor, and use the OpenTelemetry interfaces to produce log records. The following is a basic example. + +```ruby +require 'opentelemetry-logs-sdk' +require 'opentelemetry/exporter/otlp_logs' + +# Create a LoggerProvider +logger_provider = OpenTelemetry::SDK::Logs::LoggerProvider.new +# Create a batching processor configured to export to the OTLP exporter +processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new) +# Add the processor to the LoggerProvider +logger_provider.add_log_record_processor(processor) +# Access a Logger for your library from your LoggerProvider +logger = logger_provider.logger(name: 'my_app_or_gem', version: '0.1.0') + +# Use your Logger to emit a log record +logger.on_emit( + timestamp: Time.now, + severity_text: 'INFO', + body: 'Thuja plicata', + attributes: { 'cedar' => true }, +) + +logger_provider.shutdown +``` + +For additional examples, see the [examples on github][examples-github]. + +## How can I configure the OTLP logs exporter? + +The collector exporter can be configured explicitly in code, or via environment variables. The configuration parameters, environment variables, and defaults are shown below. + +| Parameter | Environment variable | Default | +| ------------------------- | -------------------------------------------- | ----------------------------------- | +| `endpoint:` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `"http://localhost:4318/v1/logs"` | +| `certificate_file:` | `OTEL_EXPORTER_OTLP_CERTIFICATE` | | +| `client_certificate_file` | `OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE` | | +| `client_key_file` | `OTEL_EXPORTER_OTLP_CLIENT_KEY` | | +| `headers:` | `OTEL_EXPORTER_OTLP_HEADERS` | | +| `compression:` | `OTEL_EXPORTER_OTLP_COMPRESSION` | `"gzip"` | +| `timeout:` | `OTEL_EXPORTER_OTLP_TIMEOUT` | `10` | +| `ssl_verify_mode:` | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER` or | `OpenSSL::SSL:VERIFY_PEER` | +| | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE` | | + +`ssl_verify_mode:` parameter values should be flags for server certificate verification: `OpenSSL::SSL:VERIFY_PEER` and `OpenSSL::SSL:VERIFY_NONE` are acceptable. These values can also be set using the appropriately named environment variables as shown where `VERIFY_PEER` will take precedence over `VERIFY_NONE`. Please see [the Net::HTTP docs](https://ruby-doc.org/stdlib-2.7.6/libdoc/net/http/rdoc/Net/HTTP.html#verify_mode) for more information about these flags. + +## How can I get involved? + +The `opentelemetry-exporter-otlp-logs` gem source is [on github][repo-github], along with related gems including `opentelemetry-logs-sdk`. + +The OpenTelemetry Ruby gems are maintained by the OpenTelemetry-Ruby special interest group (SIG). You can get involved by joining us in [GitHub Discussions][discussions-url] or attending our weekly meeting. See the [meeting calendar][community-meetings] for dates and times. For more information on this and other language SIGs, see the OpenTelemetry [community page][ruby-sig]. + +## License + +The `opentelemetry-exporter-otlp-logs` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. + +## Working with Proto Definitions + +The OTel community maintains a [repository with protobuf definitions][otel-proto-github] that language and collector implementors use to generate code. + +Maintainers are expected to keep up to date with the latest version of protos. This guide will provide you with step-by-step instructions on updating the OTLP Logs Exporter gem with the latest definitions. + +### System Requirements + +- [`git` 2.41+][git-install] +- [`protoc` 22.5][protoc-install] +- [Ruby 3+][ruby-downloads] + +> :warning: `protoc 23.x` *changes the Ruby code generator to emit a serialized proto instead of a DSL.* . Please ensure you use `protoc` version `22.x` in order to ensure we remain compatible with versions of protobuf prior to `google-protobuf` gem `3.18`. + +### Upgrade Proto Definitions + +**Update the target otel-proto version in the `Rakefile` that matches a release `tag` in the proto repo, e.g.** + +```ruby + # Rakefile + + # https://github.com/open-telemetry/opentelemetry-proto/tree/v0.20.0 + PROTO_VERSION = `v0.20.0` +``` + +**Generate the Ruby source files using `rake`:** + +```console + +$> bundle exec rake protobuf:generate + +``` + +**Run tests and fix any errors:** + +```console + +$> bundle exec rake test + +``` + +**Commit the chnages and open a PR!** + +[opentelemetry-collector-home]: https://opentelemetry.io/docs/collector/about/ +[opentelemetry-home]: https://opentelemetry.io +[bundler-home]: https://bundler.io +[repo-github]: https://github.com/open-telemetry/opentelemetry-ruby +[license-github]: https://github.com/open-telemetry/opentelemetry-ruby/blob/main/LICENSE +[examples-github]: https://github.com/open-telemetry/opentelemetry-ruby/tree/main/examples +[ruby-sig]: https://github.com/open-telemetry/community#ruby-sig +[community-meetings]: https://github.com/open-telemetry/community#community-meetings +[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions +[git-install]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git +[protoc-install]: https://github.com/protocolbuffers/protobuf/releases/tag/v22.5 +[ruby-downloads]: https://www.ruby-lang.org/en/downloads/ +[otel-proto-github]: https://github.com/open-telemetry/opentelemetry-proto +[otel-proto-release]: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.20.0 diff --git a/exporter/otlp-logs/Rakefile b/exporter/otlp-logs/Rakefile new file mode 100644 index 000000000..ce1edbd6e --- /dev/null +++ b/exporter/otlp-logs/Rakefile @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' + +require 'rubocop/rake_task' +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.libs << '../../api/lib' + t.libs << '../../sdk/lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +if RUBY_ENGINE == 'truffleruby' + task default: %i[test] +else + task default: %i[test rubocop yard] +end + +# https://github.com/open-telemetry/opentelemetry-proto/tree/v0.20.0 +PROTO_VERSION = 'v0.20.0' + +namespace :protobuf do + task :clean do + FileUtils.rm_rf('lib/opentelemetry/proto') + FileUtils.rm_rf('opentelemetry-proto') + end + + desc "Generate Ruby Source files from OTel Proto Version #{PROTO_VERSION}" + task generate: [:clean] do + system("git clone -b #{PROTO_VERSION} https://github.com/open-telemetry/opentelemetry-proto", exception: true) + Dir['opentelemetry-proto/opentelemetry/proto/**/*.proto'].each do |file| + system("protoc --ruby_out=lib/ --proto_path=opentelemetry-proto #{file.gsub('opentelemetry-proto/', '')}", exception: true) + end + FileUtils.rm_rf('opentelemetry-proto') + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry-exporter-otlp-logs.rb b/exporter/otlp-logs/lib/opentelemetry-exporter-otlp-logs.rb new file mode 100644 index 000000000..1af5afb08 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry-exporter-otlp-logs.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/exporter/otlp_logs' diff --git a/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs/logs_exporter.rb b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs/logs_exporter.rb new file mode 100644 index 000000000..419e7b4d3 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs/logs_exporter.rb @@ -0,0 +1,379 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/common' +require 'opentelemetry/sdk' +require 'opentelemetry-logs-api' # the sdk isn't loading the api, but not sure why +require 'opentelemetry/sdk/logs' +require 'net/http' +require 'zlib' + +require 'google/rpc/status_pb' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' +require 'opentelemetry/proto/logs/v1/logs_pb' +require 'opentelemetry/proto/collector/logs/v1/logs_service_pb' + +module OpenTelemetry + module Exporter + module OTLP + module Logs + # An OpenTelemetry log exporter that sends log records over HTTP as Protobuf encoded OTLP ExportLogsServiceRequests. + class LogsExporter # rubocop:disable Metrics/ClassLength + SUCCESS = OpenTelemetry::SDK::Logs::Export::SUCCESS + FAILURE = OpenTelemetry::SDK::Logs::Export::FAILURE + private_constant(:SUCCESS, :FAILURE) + + # Default timeouts in seconds. + KEEP_ALIVE_TIMEOUT = 30 + RETRY_COUNT = 5 + private_constant(:KEEP_ALIVE_TIMEOUT, :RETRY_COUNT) + + ERROR_MESSAGE_INVALID_HEADERS = 'headers must be a String with comma-separated URL Encoded UTF-8 k=v pairs or a Hash' + private_constant(:ERROR_MESSAGE_INVALID_HEADERS) + + DEFAULT_USER_AGENT = "OTel-OTLP-Exporter-Ruby/#{OpenTelemetry::Exporter::OTLP::Logs::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})".freeze + + def self.ssl_verify_mode + if ENV['OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER'] == 'true' + OpenSSL::SSL::VERIFY_PEER + elsif ENV['OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE'] == 'true' + OpenSSL::SSL::VERIFY_NONE + else + OpenSSL::SSL::VERIFY_PEER + end + end + + def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'http://localhost:4318/v1/logs'), + certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'), + client_certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE'), + client_key_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY', 'OTEL_EXPORTER_OTLP_CLIENT_KEY'), + ssl_verify_mode: LogsExporter.ssl_verify_mode, + headers: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', default: {}), + compression: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION', default: 'gzip'), + timeout: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10)) + raise ArgumentError, "invalid url for OTLP::Logs::LogsExporter #{endpoint}" unless OpenTelemetry::Common::Utilities.valid_url?(endpoint) + raise ArgumentError, "unsupported compression key #{compression}" unless compression.nil? || %w[gzip none].include?(compression) + + @uri = if endpoint == ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] + URI.join(endpoint, 'v1/logs') + else + URI(endpoint) + end + + @http = http_connection(@uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file) + + @path = @uri.path + @headers = prepare_headers(headers) + @timeout = timeout.to_f + @compression = compression + @shutdown = false + end + + # Called to export sampled {OpenTelemetry::SDK::Logs::LogRecordData} structs. + # + # @param [Enumerable] log_record_data the + # list of recorded {OpenTelemetry::SDK::Logs::LogRecordData} structs to be + # exported. + # @param [optional Numeric] timeout An optional timeout in seconds. + # @return [Integer] the result of the export. + def export(log_record_data, timeout: nil) + OpenTelemetry.logger.error('Logs Exporter tried to export, but it has already shut down') if @shutdown + return FAILURE if @shutdown + + send_bytes(encode(log_record_data), timeout: timeout) + end + + # Called when {OpenTelemetry::SDK::Logs::LoggerProvider#force_flush} is called, if + # this exporter is registered to a {OpenTelemetry::SDK::Logs::LoggerProvider} + # object. + # + # @param [optional Numeric] timeout An optional timeout in seconds. + def force_flush(timeout: nil) + SUCCESS + end + + # Called when {OpenTelemetry::SDK::Logs::LoggerProvider#shutdown} is called, if + # this exporter is registered to a {OpenTelemetry::SDK::Logs::LoggerProvider} + # object. + # + # @param [optional Numeric] timeout An optional timeout in seconds. + def shutdown(timeout: nil) + @shutdown = true + @http.finish if @http.started? + SUCCESS + end + + private + + def handle_http_error(response) + OpenTelemetry.handle_error(message: "OTLP logs exporter received #{response.class.name}, http.code=#{response.code}, for uri: '#{@path}'") + end + + def http_connection(uri, ssl_verify_mode, certificate_file, client_certificate_file, client_key_file) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == 'https' + http.verify_mode = ssl_verify_mode + http.ca_file = certificate_file unless certificate_file.nil? + http.cert = OpenSSL::X509::Certificate.new(File.read(client_certificate_file)) unless client_certificate_file.nil? + http.key = OpenSSL::PKey::RSA.new(File.read(client_key_file)) unless client_key_file.nil? + http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT + http + end + + # The around_request is a private method that provides an extension + # point for the exporters network calls. The default behaviour + # is to not record these operations. + # + # An example use case would be to prepend a patch, or extend this class + # and override this method's behaviour to explicitly record the HTTP request. + # This would allow you to create log records for your export pipeline. + def around_request + OpenTelemetry::Common::Utilities.untraced { yield } # rubocop:disable Style/ExplicitBlockArgument + end + + def send_bytes(bytes, timeout:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + return FAILURE if bytes.nil? + + request = Net::HTTP::Post.new(@path) + if @compression == 'gzip' + request.add_field('Content-Encoding', 'gzip') + body = Zlib.gzip(bytes) + else + body = bytes + end + + request.body = body + request.add_field('Content-Type', 'application/x-protobuf') + @headers.each { |key, value| request.add_field(key, value) } + + retry_count = 0 + timeout ||= @timeout + start_time = OpenTelemetry::Common::Utilities.timeout_timestamp + + around_request do + remaining_timeout = OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start_time) + return FAILURE if remaining_timeout.zero? + + @http.open_timeout = remaining_timeout + @http.read_timeout = remaining_timeout + @http.write_timeout = remaining_timeout + @http.start unless @http.started? + response = @http.request(request) + + case response + when Net::HTTPOK + response.body # Read and discard body + SUCCESS + when Net::HTTPServiceUnavailable, Net::HTTPTooManyRequests + response.body # Read and discard body + handle_http_error(response) + redo if backoff?(retry_after: response['Retry-After'], retry_count: retry_count += 1) + FAILURE + when Net::HTTPRequestTimeOut, Net::HTTPGatewayTimeOut, Net::HTTPBadGateway + response.body # Read and discard body + handle_http_error(response) + redo if backoff?(retry_count: retry_count += 1) + FAILURE + when Net::HTTPNotFound + handle_http_error(response) + FAILURE + when Net::HTTPBadRequest, Net::HTTPClientError, Net::HTTPServerError + log_status(response.body) + FAILURE + when Net::HTTPRedirection + @http.finish + handle_redirect(response['location']) + redo if backoff?(retry_after: 0, retry_count: retry_count += 1) + else + @http.finish + handle_http_error(response) + FAILURE + end + rescue Net::OpenTimeout, Net::ReadTimeout => e + OpenTelemetry.handle_error(exception: e) + retry if backoff?(retry_count: retry_count += 1) + return FAILURE + rescue OpenSSL::SSL::SSLError => e + OpenTelemetry.handle_error(exception: e) + retry if backoff?(retry_count: retry_count += 1) + return FAILURE + rescue SocketError => e + OpenTelemetry.handle_error(exception: e) + retry if backoff?(retry_count: retry_count += 1) + return FAILURE + rescue SystemCallError => e + retry if backoff?(retry_count: retry_count += 1) + OpenTelemetry.handle_error(exception: e) + return FAILURE + rescue EOFError => e + OpenTelemetry.handle_error(exception: e) + retry if backoff?(retry_count: retry_count += 1) + return FAILURE + rescue Zlib::DataError => e + OpenTelemetry.handle_error(exception: e) + retry if backoff?(retry_count: retry_count += 1) + return FAILURE + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#send_bytes') + return FAILURE + end + ensure + # Reset timeouts to defaults for the next call. + @http.open_timeout = @timeout + @http.read_timeout = @timeout + @http.write_timeout = @timeout + end + + def handle_redirect(location) + # TODO: figure out destination and reinitialize @http and @path + end + + def log_status(body) + status = Google::Rpc::Status.decode(body) + details = status.details.map do |detail| + klass_or_nil = ::Google::Protobuf::DescriptorPool.generated_pool.lookup(detail.type_name).msgclass + detail.unpack(klass_or_nil) if klass_or_nil + end.compact + OpenTelemetry.handle_error(message: "OTLP logs exporter received rpc.Status{message=#{status.message}, details=#{details}}") + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: 'unexpected error decoding rpc.Status in OTLP::Exporter#log_status') + end + + def backoff?(retry_count:, retry_after: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + return false if retry_count > RETRY_COUNT + + sleep_interval = nil + unless retry_after.nil? + sleep_interval = + begin + Integer(retry_after) + rescue ArgumentError + nil + end + sleep_interval ||= + begin + Time.httpdate(retry_after) - Time.now + rescue # rubocop:disable Style/RescueStandardError + nil + end + sleep_interval = nil unless sleep_interval&.positive? + end + sleep_interval ||= rand(2**retry_count) + + sleep(sleep_interval) + true + end + + def encode(log_record_data) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.encode( + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.new( + resource_logs: log_record_data + .group_by(&:resource) + .map do |resource, log_record_datas| + Opentelemetry::Proto::Logs::V1::ResourceLogs.new( + resource: Opentelemetry::Proto::Resource::V1::Resource.new( + attributes: resource.attribute_enumerator.map { |key, value| as_otlp_key_value(key, value) } + ), + scope_logs: log_record_datas + .group_by(&:instrumentation_scope) + .map do |il, lrd| + Opentelemetry::Proto::Logs::V1::ScopeLogs.new( + scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new( + name: il.name, + version: il.version + ), + log_records: lrd.map { |lr| as_otlp_log_record(lr) } + ) + end + ) + end + ) + ) + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#encode') + nil + end + + def as_otlp_log_record(log_record_data) + Opentelemetry::Proto::Logs::V1::LogRecord.new( + time_unix_nano: log_record_data.timestamp, + observed_time_unix_nano: log_record_data.observed_timestamp, + severity_number: log_record_data.severity_number, + severity_text: log_record_data.severity_text, + body: as_otlp_any_value(log_record_data.body), + attributes: log_record_data.attributes&.map { |k, v| as_otlp_key_value(k, v) }, + dropped_attributes_count: log_record_data.total_recorded_attributes - log_record_data.attributes&.size.to_i, + flags: log_record_data.trace_flags.instance_variable_get(:@flags), + trace_id: log_record_data.trace_id, + span_id: log_record_data.span_id + ) + end + + def as_otlp_key_value(key, value) + Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value(value)) + rescue Encoding::UndefinedConversionError => e + encoded_value = value.encode('UTF-8', invalid: :replace, undef: :replace, replace: '�') + OpenTelemetry.handle_error(exception: e, message: "encoding error for key #{key} and value #{encoded_value}") + Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value('Encoding Error')) + end + + def as_otlp_any_value(value) + result = Opentelemetry::Proto::Common::V1::AnyValue.new + case value + when String + result.string_value = value + when Integer + result.int_value = value + when Float + result.double_value = value + when true, false + result.bool_value = value + when Array + values = value.map { |element| as_otlp_any_value(element) } + result.array_value = Opentelemetry::Proto::Common::V1::ArrayValue.new(values: values) + end + result + end + + def prepare_headers(config_headers) + headers = case config_headers + when String then parse_headers(config_headers) + when Hash then config_headers.dup + else + raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS + end + + headers['User-Agent'] = "#{headers.fetch('User-Agent', '')} #{DEFAULT_USER_AGENT}".strip + + headers + end + + def parse_headers(raw) + entries = raw.split(',') + raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if entries.empty? + + entries.each_with_object({}) do |entry, headers| + k, v = entry.split('=', 2).map(&CGI.method(:unescape)) + begin + k = k.to_s.strip + v = v.to_s.strip + rescue Encoding::CompatibilityError + raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS + rescue ArgumentError => e + raise e, ERROR_MESSAGE_INVALID_HEADERS + end + raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if k.empty? || v.empty? + + headers[k] = v + end + end + end + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs/version.rb b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs/version.rb new file mode 100644 index 000000000..34fa4aba2 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs/version.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Exporter + module OTLP + module Logs + ## Current OpenTelemetry OTLP logs exporter version + VERSION = '0.1.0' + end + 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 000000000..4638347ef --- /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/logs/version' +require 'opentelemetry/exporter/otlp/logs/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 000000000..cdefc01c2 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/logs/v1/logs_service.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/logs/v1/logs_pb' + +descriptor_data = "\n8opentelemetry/proto/collector/logs/v1/logs_service.proto\x12%opentelemetry.proto.collector.logs.v1\x1a&opentelemetry/proto/logs/v1/logs.proto\"\\\n\x18\x45xportLogsServiceRequest\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"u\n\x19\x45xportLogsServiceResponse\x12X\n\x0fpartial_success\x18\x01 \x01(\x0b\x32?.opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess\"O\n\x18\x45xportLogsPartialSuccess\x12\x1c\n\x14rejected_log_records\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x9d\x01\n\x0bLogsService\x12\x8d\x01\n\x06\x45xport\x12?.opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest\x1a@.opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse\"\x00\x42\x98\x01\n(io.opentelemetry.proto.collector.logs.v1B\x10LogsServiceProtoP\x01Z0go.opentelemetry.io/proto/otlp/collector/logs/v1\xaa\x02%OpenTelemetry.Proto.Collector.Logs.V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Opentelemetry + module Proto + module Collector + module Logs + module V1 + ExportLogsServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest').msgclass + ExportLogsServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse').msgclass + ExportLogsPartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess').msgclass + end + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb new file mode 100644 index 000000000..593d752e7 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/metrics/v1/metrics_service.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/metrics/v1/metrics_pb' + +descriptor_data = "\n>opentelemetry/proto/collector/metrics/v1/metrics_service.proto\x12(opentelemetry.proto.collector.metrics.v1\x1a,opentelemetry/proto/metrics/v1/metrics.proto\"h\n\x1b\x45xportMetricsServiceRequest\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"~\n\x1c\x45xportMetricsServiceResponse\x12^\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess\"R\n\x1b\x45xportMetricsPartialSuccess\x12\x1c\n\x14rejected_data_points\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xac\x01\n\x0eMetricsService\x12\x99\x01\n\x06\x45xport\x12\x45.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest\x1a\x46.opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse\"\x00\x42\xa4\x01\n+io.opentelemetry.proto.collector.metrics.v1B\x13MetricsServiceProtoP\x01Z3go.opentelemetry.io/proto/otlp/collector/metrics/v1\xaa\x02(OpenTelemetry.Proto.Collector.Metrics.V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Opentelemetry + module Proto + module Collector + module Metrics + module V1 + ExportMetricsServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest').msgclass + ExportMetricsServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse').msgclass + ExportMetricsPartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess').msgclass + end + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb new file mode 100644 index 000000000..ae8226063 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/trace/v1/trace_service.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/trace/v1/trace_pb' + +descriptor_data = "\n:opentelemetry/proto/collector/trace/v1/trace_service.proto\x12&opentelemetry.proto.collector.trace.v1\x1a(opentelemetry/proto/trace/v1/trace.proto\"`\n\x19\x45xportTraceServiceRequest\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"x\n\x1a\x45xportTraceServiceResponse\x12Z\n\x0fpartial_success\x18\x01 \x01(\x0b\x32\x41.opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess\"J\n\x19\x45xportTracePartialSuccess\x12\x16\n\x0erejected_spans\x18\x01 \x01(\x03\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xa2\x01\n\x0cTraceService\x12\x91\x01\n\x06\x45xport\x12\x41.opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest\x1a\x42.opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse\"\x00\x42\x9c\x01\n)io.opentelemetry.proto.collector.trace.v1B\x11TraceServiceProtoP\x01Z1go.opentelemetry.io/proto/otlp/collector/trace/v1\xaa\x02&OpenTelemetry.Proto.Collector.Trace.V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Opentelemetry + module Proto + module Collector + module Trace + module V1 + ExportTraceServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest').msgclass + ExportTraceServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse').msgclass + ExportTracePartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess').msgclass + end + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb new file mode 100644 index 000000000..919181359 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/common/v1/common.proto + +require 'google/protobuf' + +descriptor_data = "\n*opentelemetry/proto/common/v1/common.proto\x12\x1dopentelemetry.proto.common.v1\"\x8c\x02\n\x08\x41nyValue\x12\x16\n\x0cstring_value\x18\x01 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x02 \x01(\x08H\x00\x12\x13\n\tint_value\x18\x03 \x01(\x03H\x00\x12\x16\n\x0c\x64ouble_value\x18\x04 \x01(\x01H\x00\x12@\n\x0b\x61rray_value\x18\x05 \x01(\x0b\x32).opentelemetry.proto.common.v1.ArrayValueH\x00\x12\x43\n\x0ckvlist_value\x18\x06 \x01(\x0b\x32+.opentelemetry.proto.common.v1.KeyValueListH\x00\x12\x15\n\x0b\x62ytes_value\x18\x07 \x01(\x0cH\x00\x42\x07\n\x05value\"E\n\nArrayValue\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"G\n\x0cKeyValueList\x12\x37\n\x06values\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\"O\n\x08KeyValue\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x36\n\x05value\x18\x02 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\"\x94\x01\n\x14InstrumentationScope\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x07version\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\rB{\n io.opentelemetry.proto.common.v1B\x0b\x43ommonProtoP\x01Z(go.opentelemetry.io/proto/otlp/common/v1\xaa\x02\x1dOpenTelemetry.Proto.Common.V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Opentelemetry + module Proto + module Common + module V1 + AnyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.AnyValue').msgclass + ArrayValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.ArrayValue').msgclass + KeyValueList = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.KeyValueList').msgclass + KeyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.KeyValue').msgclass + InstrumentationScope = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.common.v1.InstrumentationScope').msgclass + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb new file mode 100644 index 000000000..2e59dad95 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/logs/v1/logs.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' + +descriptor_data = "\n&opentelemetry/proto/logs/v1/logs.proto\x12\x1bopentelemetry.proto.logs.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"L\n\x08LogsData\x12@\n\rresource_logs\x18\x01 \x03(\x0b\x32).opentelemetry.proto.logs.v1.ResourceLogs\"\xa3\x01\n\x0cResourceLogs\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12:\n\nscope_logs\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.ScopeLogs\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\xa0\x01\n\tScopeLogs\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12;\n\x0blog_records\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.logs.v1.LogRecord\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xef\x02\n\tLogRecord\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x1f\n\x17observed_time_unix_nano\x18\x0b \x01(\x06\x12\x44\n\x0fseverity_number\x18\x02 \x01(\x0e\x32+.opentelemetry.proto.logs.v1.SeverityNumber\x12\x15\n\rseverity_text\x18\x03 \x01(\t\x12\x35\n\x04\x62ody\x18\x05 \x01(\x0b\x32\'.opentelemetry.proto.common.v1.AnyValue\x12;\n\nattributes\x18\x06 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x07 \x01(\r\x12\r\n\x05\x66lags\x18\x08 \x01(\x07\x12\x10\n\x08trace_id\x18\t \x01(\x0c\x12\x0f\n\x07span_id\x18\n \x01(\x0cJ\x04\x08\x04\x10\x05*\xc3\x05\n\x0eSeverityNumber\x12\x1f\n\x1bSEVERITY_NUMBER_UNSPECIFIED\x10\x00\x12\x19\n\x15SEVERITY_NUMBER_TRACE\x10\x01\x12\x1a\n\x16SEVERITY_NUMBER_TRACE2\x10\x02\x12\x1a\n\x16SEVERITY_NUMBER_TRACE3\x10\x03\x12\x1a\n\x16SEVERITY_NUMBER_TRACE4\x10\x04\x12\x19\n\x15SEVERITY_NUMBER_DEBUG\x10\x05\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG2\x10\x06\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG3\x10\x07\x12\x1a\n\x16SEVERITY_NUMBER_DEBUG4\x10\x08\x12\x18\n\x14SEVERITY_NUMBER_INFO\x10\t\x12\x19\n\x15SEVERITY_NUMBER_INFO2\x10\n\x12\x19\n\x15SEVERITY_NUMBER_INFO3\x10\x0b\x12\x19\n\x15SEVERITY_NUMBER_INFO4\x10\x0c\x12\x18\n\x14SEVERITY_NUMBER_WARN\x10\r\x12\x19\n\x15SEVERITY_NUMBER_WARN2\x10\x0e\x12\x19\n\x15SEVERITY_NUMBER_WARN3\x10\x0f\x12\x19\n\x15SEVERITY_NUMBER_WARN4\x10\x10\x12\x19\n\x15SEVERITY_NUMBER_ERROR\x10\x11\x12\x1a\n\x16SEVERITY_NUMBER_ERROR2\x10\x12\x12\x1a\n\x16SEVERITY_NUMBER_ERROR3\x10\x13\x12\x1a\n\x16SEVERITY_NUMBER_ERROR4\x10\x14\x12\x19\n\x15SEVERITY_NUMBER_FATAL\x10\x15\x12\x1a\n\x16SEVERITY_NUMBER_FATAL2\x10\x16\x12\x1a\n\x16SEVERITY_NUMBER_FATAL3\x10\x17\x12\x1a\n\x16SEVERITY_NUMBER_FATAL4\x10\x18*Y\n\x0eLogRecordFlags\x12\x1f\n\x1bLOG_RECORD_FLAGS_DO_NOT_USE\x10\x00\x12&\n!LOG_RECORD_FLAGS_TRACE_FLAGS_MASK\x10\xff\x01\x42s\n\x1eio.opentelemetry.proto.logs.v1B\tLogsProtoP\x01Z&go.opentelemetry.io/proto/otlp/logs/v1\xaa\x02\x1bOpenTelemetry.Proto.Logs.V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Opentelemetry + module Proto + module Logs + module V1 + LogsData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.LogsData').msgclass + ResourceLogs = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.ResourceLogs').msgclass + ScopeLogs = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.ScopeLogs').msgclass + LogRecord = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.LogRecord').msgclass + SeverityNumber = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.SeverityNumber').enummodule + LogRecordFlags = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.logs.v1.LogRecordFlags').enummodule + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb new file mode 100644 index 000000000..8ce952127 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/metrics/v1/metrics.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' + +descriptor_data = "\n,opentelemetry/proto/metrics/v1/metrics.proto\x12\x1eopentelemetry.proto.metrics.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"X\n\x0bMetricsData\x12I\n\x10resource_metrics\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.ResourceMetrics\"\xaf\x01\n\x0fResourceMetrics\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12\x43\n\rscope_metrics\x18\x02 \x03(\x0b\x32,.opentelemetry.proto.metrics.v1.ScopeMetrics\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x9f\x01\n\x0cScopeMetrics\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x37\n\x07metrics\x18\x02 \x03(\x0b\x32&.opentelemetry.proto.metrics.v1.Metric\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\x92\x03\n\x06Metric\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x0c\n\x04unit\x18\x03 \x01(\t\x12\x36\n\x05gauge\x18\x05 \x01(\x0b\x32%.opentelemetry.proto.metrics.v1.GaugeH\x00\x12\x32\n\x03sum\x18\x07 \x01(\x0b\x32#.opentelemetry.proto.metrics.v1.SumH\x00\x12>\n\thistogram\x18\t \x01(\x0b\x32).opentelemetry.proto.metrics.v1.HistogramH\x00\x12U\n\x15\x65xponential_histogram\x18\n \x01(\x0b\x32\x34.opentelemetry.proto.metrics.v1.ExponentialHistogramH\x00\x12:\n\x07summary\x18\x0b \x01(\x0b\x32\'.opentelemetry.proto.metrics.v1.SummaryH\x00\x42\x06\n\x04\x64\x61taJ\x04\x08\x04\x10\x05J\x04\x08\x06\x10\x07J\x04\x08\x08\x10\t\"M\n\x05Gauge\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\"\xba\x01\n\x03Sum\x12\x44\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32/.opentelemetry.proto.metrics.v1.NumberDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\x12\x14\n\x0cis_monotonic\x18\x03 \x01(\x08\"\xad\x01\n\tHistogram\x12G\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x32.opentelemetry.proto.metrics.v1.HistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"\xc3\x01\n\x14\x45xponentialHistogram\x12R\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32=.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint\x12W\n\x17\x61ggregation_temporality\x18\x02 \x01(\x0e\x32\x36.opentelemetry.proto.metrics.v1.AggregationTemporality\"P\n\x07Summary\x12\x45\n\x0b\x64\x61ta_points\x18\x01 \x03(\x0b\x32\x30.opentelemetry.proto.metrics.v1.SummaryDataPoint\"\x86\x02\n\x0fNumberDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\x13\n\tas_double\x18\x04 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12;\n\texemplars\x18\x05 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\x08 \x01(\rB\x07\n\x05valueJ\x04\x08\x01\x10\x02\"\xe6\x02\n\x12HistogramDataPoint\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\x15\n\rbucket_counts\x18\x06 \x03(\x06\x12\x17\n\x0f\x65xplicit_bounds\x18\x07 \x03(\x01\x12;\n\texemplars\x18\x08 \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\r\n\x05\x66lags\x18\n \x01(\r\x12\x10\n\x03min\x18\x0b \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\x0c \x01(\x01H\x02\x88\x01\x01\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_maxJ\x04\x08\x01\x10\x02\"\xda\x04\n\x1d\x45xponentialHistogramDataPoint\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x10\n\x03sum\x18\x05 \x01(\x01H\x00\x88\x01\x01\x12\r\n\x05scale\x18\x06 \x01(\x11\x12\x12\n\nzero_count\x18\x07 \x01(\x06\x12W\n\x08positive\x18\x08 \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12W\n\x08negative\x18\t \x01(\x0b\x32\x45.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets\x12\r\n\x05\x66lags\x18\n \x01(\r\x12;\n\texemplars\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.metrics.v1.Exemplar\x12\x10\n\x03min\x18\x0c \x01(\x01H\x01\x88\x01\x01\x12\x10\n\x03max\x18\r \x01(\x01H\x02\x88\x01\x01\x12\x16\n\x0ezero_threshold\x18\x0e \x01(\x01\x1a\x30\n\x07\x42uckets\x12\x0e\n\x06offset\x18\x01 \x01(\x11\x12\x15\n\rbucket_counts\x18\x02 \x03(\x04\x42\x06\n\x04_sumB\x06\n\x04_minB\x06\n\x04_max\"\xc5\x02\n\x10SummaryDataPoint\x12;\n\nattributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x1c\n\x14start_time_unix_nano\x18\x02 \x01(\x06\x12\x16\n\x0etime_unix_nano\x18\x03 \x01(\x06\x12\r\n\x05\x63ount\x18\x04 \x01(\x06\x12\x0b\n\x03sum\x18\x05 \x01(\x01\x12Y\n\x0fquantile_values\x18\x06 \x03(\x0b\x32@.opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile\x12\r\n\x05\x66lags\x18\x08 \x01(\r\x1a\x32\n\x0fValueAtQuantile\x12\x10\n\x08quantile\x18\x01 \x01(\x01\x12\r\n\x05value\x18\x02 \x01(\x01J\x04\x08\x01\x10\x02\"\xc1\x01\n\x08\x45xemplar\x12\x44\n\x13\x66iltered_attributes\x18\x07 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12\x16\n\x0etime_unix_nano\x18\x02 \x01(\x06\x12\x13\n\tas_double\x18\x03 \x01(\x01H\x00\x12\x10\n\x06\x61s_int\x18\x06 \x01(\x10H\x00\x12\x0f\n\x07span_id\x18\x04 \x01(\x0c\x12\x10\n\x08trace_id\x18\x05 \x01(\x0c\x42\x07\n\x05valueJ\x04\x08\x01\x10\x02*\x8c\x01\n\x16\x41ggregationTemporality\x12\'\n#AGGREGATION_TEMPORALITY_UNSPECIFIED\x10\x00\x12!\n\x1d\x41GGREGATION_TEMPORALITY_DELTA\x10\x01\x12&\n\"AGGREGATION_TEMPORALITY_CUMULATIVE\x10\x02*^\n\x0e\x44\x61taPointFlags\x12\x1f\n\x1b\x44\x41TA_POINT_FLAGS_DO_NOT_USE\x10\x00\x12+\n\'DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK\x10\x01\x42\x7f\n!io.opentelemetry.proto.metrics.v1B\x0cMetricsProtoP\x01Z)go.opentelemetry.io/proto/otlp/metrics/v1\xaa\x02\x1eOpenTelemetry.Proto.Metrics.V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Opentelemetry + module Proto + module Metrics + module V1 + MetricsData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.MetricsData').msgclass + ResourceMetrics = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ResourceMetrics').msgclass + ScopeMetrics = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ScopeMetrics').msgclass + Metric = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Metric').msgclass + Gauge = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Gauge').msgclass + Sum = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Sum').msgclass + Histogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Histogram').msgclass + ExponentialHistogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ExponentialHistogram').msgclass + Summary = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Summary').msgclass + NumberDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.NumberDataPoint').msgclass + HistogramDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.HistogramDataPoint').msgclass + ExponentialHistogramDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint').msgclass + ExponentialHistogramDataPoint::Buckets = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets').msgclass + SummaryDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.SummaryDataPoint').msgclass + SummaryDataPoint::ValueAtQuantile = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile').msgclass + Exemplar = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.Exemplar').msgclass + AggregationTemporality = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.AggregationTemporality').enummodule + DataPointFlags = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.metrics.v1.DataPointFlags').enummodule + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb new file mode 100644 index 000000000..adfb9add9 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/resource/v1/resource.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' + +descriptor_data = "\n.opentelemetry/proto/resource/v1/resource.proto\x12\x1fopentelemetry.proto.resource.v1\x1a*opentelemetry/proto/common/v1/common.proto\"i\n\x08Resource\x12;\n\nattributes\x18\x01 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x02 \x01(\rB\x83\x01\n\"io.opentelemetry.proto.resource.v1B\rResourceProtoP\x01Z*go.opentelemetry.io/proto/otlp/resource/v1\xaa\x02\x1fOpenTelemetry.Proto.Resource.V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Opentelemetry + module Proto + module Resource + module V1 + Resource = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.resource.v1.Resource').msgclass + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb new file mode 100644 index 000000000..43b719989 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/trace/v1/trace.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' + +descriptor_data = "\n(opentelemetry/proto/trace/v1/trace.proto\x12\x1copentelemetry.proto.trace.v1\x1a*opentelemetry/proto/common/v1/common.proto\x1a.opentelemetry/proto/resource/v1/resource.proto\"Q\n\nTracesData\x12\x43\n\x0eresource_spans\x18\x01 \x03(\x0b\x32+.opentelemetry.proto.trace.v1.ResourceSpans\"\xa7\x01\n\rResourceSpans\x12;\n\x08resource\x18\x01 \x01(\x0b\x32).opentelemetry.proto.resource.v1.Resource\x12=\n\x0bscope_spans\x18\x02 \x03(\x0b\x32(.opentelemetry.proto.trace.v1.ScopeSpans\x12\x12\n\nschema_url\x18\x03 \x01(\tJ\x06\x08\xe8\x07\x10\xe9\x07\"\x97\x01\n\nScopeSpans\x12\x42\n\x05scope\x18\x01 \x01(\x0b\x32\x33.opentelemetry.proto.common.v1.InstrumentationScope\x12\x31\n\x05spans\x18\x02 \x03(\x0b\x32\".opentelemetry.proto.trace.v1.Span\x12\x12\n\nschema_url\x18\x03 \x01(\t\"\xe6\x07\n\x04Span\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12\x16\n\x0eparent_span_id\x18\x04 \x01(\x0c\x12\x0c\n\x04name\x18\x05 \x01(\t\x12\x39\n\x04kind\x18\x06 \x01(\x0e\x32+.opentelemetry.proto.trace.v1.Span.SpanKind\x12\x1c\n\x14start_time_unix_nano\x18\x07 \x01(\x06\x12\x1a\n\x12\x65nd_time_unix_nano\x18\x08 \x01(\x06\x12;\n\nattributes\x18\t \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\n \x01(\r\x12\x38\n\x06\x65vents\x18\x0b \x03(\x0b\x32(.opentelemetry.proto.trace.v1.Span.Event\x12\x1c\n\x14\x64ropped_events_count\x18\x0c \x01(\r\x12\x36\n\x05links\x18\r \x03(\x0b\x32\'.opentelemetry.proto.trace.v1.Span.Link\x12\x1b\n\x13\x64ropped_links_count\x18\x0e \x01(\r\x12\x34\n\x06status\x18\x0f \x01(\x0b\x32$.opentelemetry.proto.trace.v1.Status\x1a\x8c\x01\n\x05\x45vent\x12\x16\n\x0etime_unix_nano\x18\x01 \x01(\x06\x12\x0c\n\x04name\x18\x02 \x01(\t\x12;\n\nattributes\x18\x03 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x04 \x01(\r\x1a\x9d\x01\n\x04Link\x12\x10\n\x08trace_id\x18\x01 \x01(\x0c\x12\x0f\n\x07span_id\x18\x02 \x01(\x0c\x12\x13\n\x0btrace_state\x18\x03 \x01(\t\x12;\n\nattributes\x18\x04 \x03(\x0b\x32\'.opentelemetry.proto.common.v1.KeyValue\x12 \n\x18\x64ropped_attributes_count\x18\x05 \x01(\r\"\x99\x01\n\x08SpanKind\x12\x19\n\x15SPAN_KIND_UNSPECIFIED\x10\x00\x12\x16\n\x12SPAN_KIND_INTERNAL\x10\x01\x12\x14\n\x10SPAN_KIND_SERVER\x10\x02\x12\x14\n\x10SPAN_KIND_CLIENT\x10\x03\x12\x16\n\x12SPAN_KIND_PRODUCER\x10\x04\x12\x16\n\x12SPAN_KIND_CONSUMER\x10\x05\"\xae\x01\n\x06Status\x12\x0f\n\x07message\x18\x02 \x01(\t\x12=\n\x04\x63ode\x18\x03 \x01(\x0e\x32/.opentelemetry.proto.trace.v1.Status.StatusCode\"N\n\nStatusCode\x12\x15\n\x11STATUS_CODE_UNSET\x10\x00\x12\x12\n\x0eSTATUS_CODE_OK\x10\x01\x12\x15\n\x11STATUS_CODE_ERROR\x10\x02J\x04\x08\x01\x10\x02\x42w\n\x1fio.opentelemetry.proto.trace.v1B\nTraceProtoP\x01Z\'go.opentelemetry.io/proto/otlp/trace/v1\xaa\x02\x1cOpenTelemetry.Proto.Trace.V1b\x06proto3" + +pool = Google::Protobuf::DescriptorPool.generated_pool +pool.add_serialized_file(descriptor_data) + +module Opentelemetry + module Proto + module Trace + module V1 + TracesData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.TracesData').msgclass + ResourceSpans = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.ResourceSpans').msgclass + ScopeSpans = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.ScopeSpans').msgclass + Span = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Span').msgclass + Span::Event = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Span.Event').msgclass + Span::Link = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Span.Link').msgclass + Span::SpanKind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Span.SpanKind').enummodule + Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Status').msgclass + Status::StatusCode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup('opentelemetry.proto.trace.v1.Status.StatusCode').enummodule + end + end + end +end diff --git a/exporter/otlp-logs/opentelemetry-exporter-otlp-logs.gemspec b/exporter/otlp-logs/opentelemetry-exporter-otlp-logs.gemspec new file mode 100644 index 000000000..d1c69fcee --- /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/logs/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-exporter-otlp-logs' + spec.version = OpenTelemetry::Exporter::OTLP::Logs::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'Experimental OTLP Logs exporter for the OpenTelemetry framework' + spec.description = 'Experimental OTLP Logs exporter for the OpenTelemetry framework' + spec.homepage = 'https://github.com/open-telemetry/opentelemetry-ruby' + spec.license = 'Apache-2.0' + + spec.files = ::Dir.glob('lib/**/*.rb') + + ::Dir.glob('*.md') + + ['LICENSE', '.yardopts'] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 3.0' + + spec.add_dependency 'googleapis-common-protos-types', '~> 1.3' + spec.add_dependency 'google-protobuf', '>= 3.18' + spec.add_dependency 'opentelemetry-api', '~> 1.1' + spec.add_dependency 'opentelemetry-common', '~> 0.20' + spec.add_dependency 'opentelemetry-logs-api', '~> 0.1' + spec.add_dependency 'opentelemetry-logs-sdk', '~> 0.1' + spec.add_dependency 'opentelemetry-sdk' + spec.add_dependency 'opentelemetry-semantic_conventions' + + spec.add_development_dependency 'appraisal', '~> 2.2.0' + spec.add_development_dependency 'bundler', '>= 1.17' + spec.add_development_dependency 'faraday', '~> 0.13' + spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'opentelemetry-test-helpers' + spec.add_development_dependency 'pry-byebug' unless RUBY_ENGINE == 'jruby' + spec.add_development_dependency 'rake', '~> 12.0' + spec.add_development_dependency 'rubocop', '~> 1.3' + spec.add_development_dependency 'simplecov', '~> 0.17' + spec.add_development_dependency 'webmock', '~> 3.7.6' + spec.add_development_dependency 'yard', '~> 0.9' + spec.add_development_dependency 'yard-doctest', '~> 0.1.6' + + if spec.respond_to?(:metadata) + spec.metadata['changelog_uri'] = "https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-exporter-otlp/v#{OpenTelemetry::Exporter::OTLP::Logs::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::Logs::VERSION}" + end +end diff --git a/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb new file mode 100644 index 000000000..450574e82 --- /dev/null +++ b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb @@ -0,0 +1,748 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 +require 'test_helper' +require 'google/protobuf/wrappers_pb' +require 'google/protobuf/well_known_types' + +describe OpenTelemetry::Exporter::OTLP::Logs::LogsExporter do + SUCCESS = OpenTelemetry::SDK::Logs::Export::SUCCESS + FAILURE = OpenTelemetry::SDK::Logs::Export::FAILURE + VERSION = OpenTelemetry::Exporter::OTLP::Logs::VERSION + DEFAULT_USER_AGENT = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter::DEFAULT_USER_AGENT + CLIENT_CERT_A_PATH = File.dirname(__FILE__) + '/mtls-client-a.pem' + CLIENT_CERT_A = OpenSSL::X509::Certificate.new(File.read(CLIENT_CERT_A_PATH)) + CLIENT_KEY_A = OpenSSL::PKey::RSA.new(File.read(CLIENT_CERT_A_PATH)) + CLIENT_CERT_B_PATH = File.dirname(__FILE__) + '/mtls-client-b.pem' + CLIENT_CERT_B = OpenSSL::X509::Certificate.new(File.read(CLIENT_CERT_B_PATH)) + CLIENT_KEY_B = OpenSSL::PKey::RSA.new(File.read(CLIENT_CERT_B_PATH)) + + describe '#initialize' do + it 'initializes with defaults' do + exp = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + _(exp).wont_be_nil + _(exp.instance_variable_get(:@headers)).must_equal('User-Agent' => DEFAULT_USER_AGENT) + _(exp.instance_variable_get(:@timeout)).must_equal 10.0 + _(exp.instance_variable_get(:@path)).must_equal '/v1/logs' + _(exp.instance_variable_get(:@compression)).must_equal 'gzip' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_be_nil + _(http.cert).must_be_nil + _(http.key).must_be_nil + _(http.use_ssl?).must_equal false + _(http.address).must_equal 'localhost' + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_PEER + _(http.port).must_equal 4318 + end + + it 'provides a useful, spec-compliant default user agent header' do + # spec compliance: OTLP Exporter name and version + _(DEFAULT_USER_AGENT).must_match("OTel-OTLP-Exporter-Ruby/#{VERSION}") + # bonus: incredibly useful troubleshooting information + _(DEFAULT_USER_AGENT).must_match("Ruby/#{RUBY_VERSION}") + _(DEFAULT_USER_AGENT).must_match(RUBY_PLATFORM) + _(DEFAULT_USER_AGENT).must_match("#{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION}") + end + + it 'refuses invalid endpoint' do + assert_raises ArgumentError do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(endpoint: 'not a url') + end + end + + it 'uses endpoints path if provided' do + exp = OpenTelemetry::Exporter::OTLP::Logs::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::Logs::LogsExporter.new(compression: 'flate') + end + exp = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(compression: nil) + _(exp.instance_variable_get(:@compression)).must_be_nil + + %w[gzip none].each do |compression| + exp = OpenTelemetry::Exporter::OTLP::Logs::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::Logs::LogsExporter.new + _(exp.instance_variable_get(:@compression)).must_equal(example[:value]) + end + end + end + + it 'sets parameters from the environment' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234', + 'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar/cacert', + 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE' => CLIENT_CERT_A_PATH, + 'OTEL_EXPORTER_OTLP_CLIENT_KEY' => CLIENT_CERT_A_PATH, + 'OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d', + 'OTEL_EXPORTER_OTLP_COMPRESSION' => 'gzip', + 'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true', + 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'User-Agent' => DEFAULT_USER_AGENT) + _(exp.instance_variable_get(:@timeout)).must_equal 11.0 + _(exp.instance_variable_get(:@path)).must_equal '/v1/logs' + _(exp.instance_variable_get(:@compression)).must_equal 'gzip' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_equal '/foo/bar/cacert' + # Quality check fails in JRuby + _(http.cert).must_equal CLIENT_CERT_A unless RUBY_ENGINE == 'jruby' + _(http.key.params).must_equal CLIENT_KEY_A.params + _(http.use_ssl?).must_equal true + _(http.address).must_equal 'localhost' + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE + _(http.port).must_equal 1234 + end + + it 'prefers explicit parameters rather than the environment' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234', + 'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar', + 'OTEL_EXPORTER_OTLP_CLIENT_CERTIFICATE' => CLIENT_CERT_A_PATH, + 'OTEL_EXPORTER_OTLP_CLIENT_KEY' => CLIENT_CERT_A_PATH, + 'OTEL_EXPORTER_OTLP_HEADERS' => 'a:b,c:d', + 'OTEL_EXPORTER_OTLP_COMPRESSION' => 'flate', + 'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER' => 'true', + 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(endpoint: 'http://localhost:4321', + certificate_file: '/baz', + client_certificate_file: CLIENT_CERT_B_PATH, + client_key_file: CLIENT_CERT_B_PATH, + headers: { 'x' => 'y' }, + compression: 'gzip', + ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE, + timeout: 12) + end + _(exp.instance_variable_get(:@headers)).must_equal('x' => 'y', 'User-Agent' => DEFAULT_USER_AGENT) + _(exp.instance_variable_get(:@timeout)).must_equal 12.0 + _(exp.instance_variable_get(:@path)).must_equal '' + _(exp.instance_variable_get(:@compression)).must_equal 'gzip' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_equal '/baz' + # equality check fails in JRuby + _(http.cert).must_equal CLIENT_CERT_B unless RUBY_ENGINE == 'jruby' + _(http.key.params).must_equal CLIENT_KEY_B.params + _(http.use_ssl?).must_equal false + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE + _(http.address).must_equal 'localhost' + _(http.port).must_equal 4321 + end + + it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT has a trailing slash' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/' + ) do + OpenTelemetry::Exporter::OTLP::Logs::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::Logs::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::Logs::LogsExporter.new(headers: { 'token' => 'über' }) + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + + exp = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(headers: 'token=%C3%BCber') + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + + error = _ do + exp = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(headers: Object.new) + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über') + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + + it 'ignores later mutations of a headers Hash parameter' do + a_hash_to_mutate_later = { 'token' => 'über' } + exp = OpenTelemetry::Exporter::OTLP::Logs::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::Logs::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::Logs::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::Logs::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::Logs::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::Logs::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::Logs::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::Logs::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::Logs::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::Logs::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::Logs::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + end + + it 'fails fast when header values are missing' do + error = _ do + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a = ') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + + error = _ do + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'a = ') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + + it 'fails fast when header or values are not found' do + error = _ do + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => ',') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + + error = _ do + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => ',') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + + it 'fails fast when header values contain invalid escape characters' do + error = _ do + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'c=hi%F3') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + + error = _ do + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'c=hi%F3') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + + it 'fails fast when headers are invalid' do + error = _ do + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'this is not a header') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + + error = _ do + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'this is not a header') do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new + end + end.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + end + end + + describe 'ssl_verify_mode:' do + it 'can be set to VERIFY_NONE by an envvar' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true') do + OpenTelemetry::Exporter::OTLP::Logs::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::Logs::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::Logs::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::Logs::LogsExporter.new } + # TODO: replace with a before block to set a global logger provider through OpenTelemetry.logger_provider when the API code is merged + let(:logger_provider) { OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: OpenTelemetry::SDK::Resources::Resource.telemetry_sdk) } + + it 'integrates with collector' do + skip unless ENV['TRACING_INTEGRATION_TEST'] + WebMock.disable_net_connect!(allow: 'localhost') + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + exporter = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(endpoint: 'http://localhost:4318', compression: 'gzip') + result = exporter.export([log_record_data]) + _(result).must_equal(SUCCESS) + end + + it 'retries on timeout' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_timeout.then.to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data]) + _(result).must_equal(SUCCESS) + end + + it 'returns FAILURE on timeout' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data], timeout: 0) + _(result).must_equal(FAILURE) + end + + it 'returns FAILURE on unexpected exceptions' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_raise('something unexpected') + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data], timeout: 1) + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: unexpected error in OTLP::Exporter#send_bytes - something unexpected/ + ) + + _(result).must_equal(FAILURE) + ensure + OpenTelemetry.logger = logger + end + + it 'handles encoding failures' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.stub(:encode, ->(_) { raise 'a little hell' }) do + _(exporter.export([log_record_data], timeout: 1)).must_equal(FAILURE) + end + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: unexpected error in OTLP::Exporter#encode - a little hell/ + ) + ensure + OpenTelemetry.logger = logger + end + + { 'Net::HTTPServiceUnavailable' => 503, + 'Net::HTTPTooManyRequests' => 429, + 'Net::HTTPRequestTimeout' => 408, + 'Net::HTTPGatewayTimeout' => 504, + 'Net::HTTPBadGateway' => 502, + 'Net::HTTPNotFound' => 404 }.each do |klass, code| + it "logs an error and returns FAILURE with #{code}s" do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.stub_const(:RETRY_COUNT, 0) do + log_stream = StringIO.new + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: code) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + _(exporter.export([log_record_data])).must_equal(FAILURE) + _(log_stream.string).must_match( + %r{ERROR -- : OpenTelemetry error: OTLP logs exporter received #{klass}, http.code=#{code}, for uri: '/v1/logs'} + ) + end + end + end + + [ + Net::OpenTimeout, + Net::ReadTimeout, + OpenSSL::SSL::SSLError, + SocketError, + EOFError, + Zlib::DataError + ].each do |error| + it "logs error and returns FAILURE when #{error} is raised" do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.stub_const(:RETRY_COUNT, 0) do + log_stream = StringIO.new + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_raise(error.send(:new)) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + _(exporter.export([log_record_data])).must_equal(FAILURE) + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: #{error}/ + ) + end + end + end + + it 'works with a SystemCallError' do + OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.stub_const(:RETRY_COUNT, 0) do + log_stream = StringIO.new + OpenTelemetry.logger = ::Logger.new(log_stream) + stub_request(:post, 'http://localhost:4318/v1/logs').to_raise(SystemCallError.new('Failed to open TCP connection', 61)) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + _(exporter.export([log_record_data])).must_equal(FAILURE) + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error:.*Failed to open TCP connection/ + ) + end + end + + it 'returns FAILURE on timeout after retrying' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_timeout.then.to_raise('this should not be reached') + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + + @retry_count = 0 + backoff_stubbed_call = lambda do |**_args| + sleep(0.10) + @retry_count += 1 + true + end + + exporter.stub(:backoff?, backoff_stubbed_call) do + _(exporter.export([log_record_data], timeout: 0.1)).must_equal(FAILURE) + end + ensure + @retry_count = 0 + end + + it 'returns FAILURE when shutdown' do + exporter.shutdown + result = exporter.export(nil) + _(result).must_equal(FAILURE) + end + + it 'returns FAILURE when encryption to receiver endpoint fails' do + log_stream = StringIO.new + OpenTelemetry.logger = ::Logger.new(log_stream) + + exporter = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(endpoint: 'https://localhost:4318/v1/logs') + stub_request(:post, 'https://localhost:4318/v1/logs').to_raise(OpenSSL::SSL::SSLError.new('enigma wedged')) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + exporter.stub(:backoff?, ->(**_) { false }) do + _(exporter.export([log_record_data])).must_equal(FAILURE) + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: enigma wedged/ + ) + end + end + + it 'exports a log_record_data' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data]) + _(result).must_equal(SUCCESS) + end + + it 'handles encoding errors with poise and grace' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data(total_recorded_attributes: 1, attributes: { 'a' => "\xC2".dup.force_encoding(::Encoding::ASCII_8BIT) }) + + result = exporter.export([log_record_data]) + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: encoding error for key a and value �/ + ) + + _(result).must_equal(SUCCESS) + ensure + OpenTelemetry.logger = logger + end + + it 'logs rpc.Status on bad request' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + details = [::Google::Protobuf::Any.pack(::Google::Protobuf::StringValue.new(value: 'you are a bad request'))] + status = ::Google::Rpc::Status.encode(::Google::Rpc::Status.new(code: 1, message: 'bad request', details: details)) + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 400, body: status, headers: { 'Content-Type' => 'application/x-protobuf' }) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + + result = exporter.export([log_record_data]) + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: OTLP logs exporter received rpc.Status{message=bad request, details=\[.*you are a bad request.*\]}/ + ) + + _(result).must_equal(FAILURE) + ensure + OpenTelemetry.logger = logger + end + + it 'logs a specific message when there is a 404' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 404, body: "Not Found\n") + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + + result = exporter.export([log_record_data]) + + _(log_stream.string).must_match( + %r{ERROR -- : OpenTelemetry error: OTLP logs exporter received Net::HTTPNotFound, http.code=404, for uri: '/v1/logs'\n} + ) + + _(result).must_equal(FAILURE) + ensure + OpenTelemetry.logger = logger + end + + it 'handles Zlib gzip compression errors' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_raise(Zlib::DataError.new('data error')) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + exporter.stub(:backoff?, ->(**_) { false }) do + _(exporter.export([log_record_data])).must_equal(FAILURE) + end + end + + it 'exports a log record from a logger' do + stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter, max_queue_size: 1, max_export_batch_size: 1) + logger_provider.add_log_record_processor(processor) + logger_provider.logger(name: 'test').on_emit(body: 'test') + logger_provider.shutdown + assert_requested(stub_post) + end + + it 'compresses with gzip if enabled' do + exporter = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(compression: 'gzip') + stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return do |request| + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(Zlib.gunzip(request.body)) + { status: 200 } + end + + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data]) + + _(result).must_equal(SUCCESS) + assert_requested(stub_post) + end + + it 'batches per resource' do + etsr = nil + stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return do |request| + proto = Zlib.gunzip(request.body) + etsr = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(proto) + { status: 200 } + end + + log_record_data1 = OpenTelemetry::TestHelpers.create_log_record_data(resource: OpenTelemetry::SDK::Resources::Resource.create('k1' => 'v1')) + log_record_data2 = OpenTelemetry::TestHelpers.create_log_record_data(resource: OpenTelemetry::SDK::Resources::Resource.create('k2' => 'v2')) + + result = exporter.export([log_record_data1, log_record_data2]) + + _(result).must_equal(SUCCESS) + assert_requested(stub_post) + _(etsr.resource_logs.length).must_equal(2) + end + + it 'translates all the things' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter) + logger = logger_provider.logger(name: 'logger', version: 'v0.0.1') + other_logger = logger_provider.logger(name: 'other_logger', version: 'v0.1.0') + + lr1 = { + timestamp: Time.now, + observed_timestamp: Time.now + 1, + severity_text: 'DEBUG', + severity_number: 5, + body: 'log_1', + attributes: { 'b' => true }, + trace_id: OpenTelemetry::Trace.generate_trace_id, + span_id: OpenTelemetry::Trace.generate_span_id, + trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT, + context: OpenTelemetry::Context.current + } + + lr2 = { + timestamp: Time.now + 2, + observed_timestamp: Time.now + 3, + severity_text: 'WARN', + severity_number: 13, + body: 'log_1', + attributes: { 'a' => false }, + trace_id: OpenTelemetry::Trace.generate_trace_id, + span_id: OpenTelemetry::Trace.generate_span_id, + trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT, + context: OpenTelemetry::Context.current + } + + lr3 = { + timestamp: Time.now + 4, + observed_timestamp: Time.now + 5, + severity_text: 'ERROR', + severity_number: 17, + body: 'log_1', + attributes: { 'c' => 12_345 }, + trace_id: OpenTelemetry::Trace.generate_trace_id, + span_id: OpenTelemetry::Trace.generate_span_id, + trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT, + context: OpenTelemetry::Context.current + } + + logger_provider.add_log_record_processor(processor) + logger.on_emit(**lr1) + logger.on_emit(**lr2) + other_logger.on_emit(**lr3) + logger_provider.shutdown + encoded_etsr = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.encode( + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.new( + resource_logs: [ + Opentelemetry::Proto::Logs::V1::ResourceLogs.new( + resource: Opentelemetry::Proto::Resource::V1::Resource.new( + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.name', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'opentelemetry')), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.language', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'ruby')), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.version', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: OpenTelemetry::SDK::VERSION)) + ] + ), + scope_logs: [ + Opentelemetry::Proto::Logs::V1::ScopeLogs.new( + scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new( + name: 'logger', + version: 'v0.0.1' + ), + log_records: [ + Opentelemetry::Proto::Logs::V1::LogRecord.new( + time_unix_nano: (lr1[:timestamp].to_r * 1_000_000_000).to_i, + observed_time_unix_nano: (lr1[:observed_timestamp].to_r * 1_000_000_000).to_i, + severity_number: 5, + severity_text: lr1[:severity_text], + body: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: lr1[:body]), + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'b', value: Opentelemetry::Proto::Common::V1::AnyValue.new(bool_value: true)) + ], + dropped_attributes_count: 0, + flags: lr1[:trace_flags].instance_variable_get(:@flags), + trace_id: lr1[:trace_id], + span_id: lr1[:span_id] + ), + Opentelemetry::Proto::Logs::V1::LogRecord.new( + time_unix_nano: (lr2[:timestamp].to_r * 1_000_000_000).to_i, + observed_time_unix_nano: (lr2[:observed_timestamp].to_r * 1_000_000_000).to_i, + severity_number: 13, + severity_text: lr2[:severity_text], + body: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: lr2[:body]), + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'a', value: Opentelemetry::Proto::Common::V1::AnyValue.new(bool_value: false)) + ], + dropped_attributes_count: 0, + flags: lr2[:trace_flags].instance_variable_get(:@flags), + trace_id: lr2[:trace_id], + span_id: lr2[:span_id] + ) + ] + ), + Opentelemetry::Proto::Logs::V1::ScopeLogs.new( + scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new( + name: 'other_logger', + version: 'v0.1.0' + ), + log_records: [ + Opentelemetry::Proto::Logs::V1::LogRecord.new( + time_unix_nano: (lr3[:timestamp].to_r * 1_000_000_000).to_i, + observed_time_unix_nano: (lr3[:observed_timestamp].to_r * 1_000_000_000).to_i, + severity_number: 17, + severity_text: lr3[:severity_text], + body: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: lr3[:body]), + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'c', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 12_345)) + ], + dropped_attributes_count: 0, + flags: lr3[:trace_flags].instance_variable_get(:@flags), + trace_id: lr3[:trace_id], + span_id: lr3[:span_id] + ) + ] + ) + ] + ) + ] + ) + ) + + assert_requested(:post, 'http://localhost:4318/v1/logs') do |req| + Zlib.gunzip(req.body) == encoded_etsr + end + end + end +end diff --git a/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-a.pem b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-a.pem new file mode 100644 index 000000000..ac4e9c42f --- /dev/null +++ b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-a.pem @@ -0,0 +1,50 @@ +-----BEGIN CERTIFICATE----- +MIIDmzCCAoOgAwIBAgIUGfmv/4kRRFbg319TIHkcwDMC3pUwDQYJKoZIhvcNAQEL +BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNYS5leGFtcGxlLnRs +ZDAeFw0yNDAzMDYyMjM4MzNaFw0yNDAzMDcyMjM4MzNaMF0xCzAJBgNVBAYTAkFV +MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz +IFB0eSBMdGQxFjAUBgNVBAMMDWEuZXhhbXBsZS50bGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCaJG6fvdjFs9cnbF8i3wzO3VPUFH4hbAg5pV6rs81s +LuJnnlG3WX1sxYQGASqLKIzPiz3g4nKFaBfXvBYXo7M/AuJ2tEspedIcvdTqwx2k +owaaX7Y9lSx+h1OhovrviCqrqX5t9cIZWSeTU1bETcoTEd9/5usVe3XeaqmY8mAP +OA0dBKeotGIOxtTEP23CxW1AJwWPLZC1go8ycvsTQfmeif+g+6BOcKeZxkayhCvo +Ous+dt2dXa3x3yROe8ffZ5lAkPBLHfEUOSk/zpQnlkGzVrbXP4LCxLDQ8PlD3qEm +HbCK+c29mNeTjeoye5EVeQDO0ATiNh1/vSlMPpOY93IdAgMBAAGjUzBRMB0GA1Ud +DgQWBBQ5kq4dpjcQ8JSiyYrPswAoeltJJTAfBgNVHSMEGDAWgBQ5kq4dpjcQ8JSi +yYrPswAoeltJJTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBt +E0JyqwqL63ZrlxiBnnYrk0LoCuXjss7B9p3+6F5v0qKxxC+fpAVoh3CTRkDJRerz +ORXECXJ4drBetvje/dTX+5pNSLOOyQfOCaSohO4S82xNLSpFd6LcjYsfN8he482w +E1wuLoi9abktDVmX+sNVeiUUeuDMyqm51NRAlmzDhxTOPvqljdFZXRQO3X00qiQV +YJs7e1xql0R7DbrwOG5J2lenCwfj51ngmIpGxAaU3eLMAqLT6AZHhZxiATzCtCL9 +2cAKOrW/O8dRVhuWbCjFCIJIRIPrThbMaaw6p4mMkED7dnbOZofXLDgM+pxT5/eU +8DnQZlgXAkIlZY+9r3oG +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCaJG6fvdjFs9cn +bF8i3wzO3VPUFH4hbAg5pV6rs81sLuJnnlG3WX1sxYQGASqLKIzPiz3g4nKFaBfX +vBYXo7M/AuJ2tEspedIcvdTqwx2kowaaX7Y9lSx+h1OhovrviCqrqX5t9cIZWSeT +U1bETcoTEd9/5usVe3XeaqmY8mAPOA0dBKeotGIOxtTEP23CxW1AJwWPLZC1go8y +cvsTQfmeif+g+6BOcKeZxkayhCvoOus+dt2dXa3x3yROe8ffZ5lAkPBLHfEUOSk/ +zpQnlkGzVrbXP4LCxLDQ8PlD3qEmHbCK+c29mNeTjeoye5EVeQDO0ATiNh1/vSlM +PpOY93IdAgMBAAECggEAGE1LJaM8VHs0njIPU8Of1NN/Pm4WrrVGHLSOVvrLldVU +e6qxznrs8+O2G24+o176yFP3Jwf8rzzImYo9X2+/OF1/j+CAAyOCNWbWdUba2xSa +22bgqBfnQnGahV7ZOj+ZHqRj2vlGp1FvlGIsyVlMVTJZruQcxy/GVxEw+PypmWxp +u1MOjYEZWjvqJuxTjjnYDcQezfy9Qu0JNOF4+cVVKydewGApmcdDGThcDltxWizM +n144vgcfR97g2jDKs0GxSAuCbfYo2xoetei5iEVKmlXI/OuUIS78LSud9oBOoYPF ++5MVYOISTNcE+/GEp/BNTq6E8Kk4A1dhaYMvC2qXUQKBgQDKAtFfVP4DusMQd4PD +oixZ50GENohG4uGCDVBT/RJXUDueil5G4h9iSGxSt0nVOe3nBv5sHMgAARX3S+Pl +717tbzDgLXqfEqhCj9Kow7BtOussfSQ8hwxBazh9GEva48/Bx+OXFLcpPAcmc9WU +oQzCCb1yeZ7gsK6yNiLJTQGYCQKBgQDDVokcPHseqFJMOdd9Hr4Ci9gshbZj4htb +EXiUf50PP0Qco1VJ0MnPW9hateEdRYGVjLeCKHi1XHi8ARQQRJXVI4BI9yiIt8VO +snnFiEYJ/wgq4lyO8eWeNUaimFvhKHDBcz5tKwtmS4CGf7eAHdTB6R0izLtxkXcs +6+ZiO/bGdQKBgHdXVNPKBUq0wdpvkMM5gpQWP6lZAgdGr8zCCsujfXthpecSfYHI +wpuwh3YSXCcA0yAiDJpYInuGKLDw/5DuahlBEBHQLFnfjtHL37rd6NOO9DJTN94e +NkpLipK0kNOetDUZ3sV5cn+EvACme+4TetMDKA2B9i9tkbcsrj5YJPHpAoGAc5Gh +MTl/RlYjysF2AqrLlEoUrdK2ZEYEFU8y3fjafYjazW69KR0EKVCXoqN0+pKC5m4I +rFMxh2ucau7gZfeOBjoozgKc0raXX8YsUXgcqBFhTa37QP9Q8NdoYQ5vXblFbM64 +InKTHgSRmAG8GWqM0+UNvecPB1QfBE7VUU1U5XkCgYA34SlJVa1es7hifeAtb2XC +jVsHeEwcVnVq4S8aNo51taBJqPR9QIg4bssj4QmaMJntyQl94CE7NM11OssQ4rez +lY+BZGSmkuEFybqJ5CwHsKk+Cjdm4agqqU/uupOxFPxEzcD2YDgFto7RMPDP/Daf +iH9tE2qrnzQvE43+caLAuQ== +-----END PRIVATE KEY----- diff --git a/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-b.pem b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-b.pem new file mode 100644 index 000000000..0371b0be7 --- /dev/null +++ b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/mtls-client-b.pem @@ -0,0 +1,50 @@ +-----BEGIN CERTIFICATE----- +MIIDmzCCAoOgAwIBAgIUALrFiUtkMZj2wNSxHvtR+KBoBfowDQYJKoZIhvcNAQEL +BQAwXTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEWMBQGA1UEAwwNYi5leGFtcGxlLnRs +ZDAeFw0yNDAzMDYyMjM5NDdaFw0yNDAzMDcyMjM5NDdaMF0xCzAJBgNVBAYTAkFV +MRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRz +IFB0eSBMdGQxFjAUBgNVBAMMDWIuZXhhbXBsZS50bGQwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQC78NVeBRf/WK39UM1BpiQROA8mV8YXB8ugGB7AlOC2 +uLmVdWknMoS155afr4u0DICyNGDkxhWxyqG9UEsBOBmlNZSg85ewjdSMSBRh6Lxk +uWP9mVS0f1nRVMMBYNbkEPIJ5T2IRCwHP6/gpNKO9prH06atLCs6HP8y0cLKCWNJ +WixJJpT5goRldEBKTIUtOfM8Sa7ktoYeEvmGmjXjgP9pcdlmC3pjfzTk4+HH3cKL +RGG4dlhSTrOjVNBL30GWQjiNCM2fAHugUcrcGsmXhbBzmkBa8Rs6mI0ZJQIa/bWv +6KfWJ1eDF+VIVyhaQPeEkLgatP5NyuaqafvBrTMT1/GHAgMBAAGjUzBRMB0GA1Ud +DgQWBBQPlAhA673kZnZ9rvRwDejlc2kcjDAfBgNVHSMEGDAWgBQPlAhA673kZnZ9 +rvRwDejlc2kcjDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBx +OOFo6UxqdtFrN7BZPV00e2tlz3on21JlTxlk92fL0lHL86XJPMr1znBg0OkD1riw +OYJQ47wLZAgYIhs8UTrcZIZd1xwbJ4fjSDRFHOnI2BHJ/5pR9/NFsrOgeOorLbZf +x6aa+mQt1qYltTsH8gA1PP5syUcHmlSVLk5NWreMaHEEU8THVsyhxlv8yE+Zuh5y +JX1KpH4Eo3ekM4RwGuqjbtMgumD8gQf4lHysMEyemQOnebKxViz0bfLfEOMYIXBf +DaavjgfinXAQnOhItlHXisuAIDxSajnyR0kvTDdRbZBruRpUeKBcENQO95D4b5uA +BNnkf1CmWZoTwwqvDdNZ +-----END CERTIFICATE----- +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC78NVeBRf/WK39 +UM1BpiQROA8mV8YXB8ugGB7AlOC2uLmVdWknMoS155afr4u0DICyNGDkxhWxyqG9 +UEsBOBmlNZSg85ewjdSMSBRh6LxkuWP9mVS0f1nRVMMBYNbkEPIJ5T2IRCwHP6/g +pNKO9prH06atLCs6HP8y0cLKCWNJWixJJpT5goRldEBKTIUtOfM8Sa7ktoYeEvmG +mjXjgP9pcdlmC3pjfzTk4+HH3cKLRGG4dlhSTrOjVNBL30GWQjiNCM2fAHugUcrc +GsmXhbBzmkBa8Rs6mI0ZJQIa/bWv6KfWJ1eDF+VIVyhaQPeEkLgatP5NyuaqafvB +rTMT1/GHAgMBAAECggEACftFJGnaLReaoK+KlzKptmMR0D7X0x/43UHVWvO36Oca +rtNo3Z9ok6NUc/AGLW5/Ofe02wb3iVUdDfRBEjQr4y2wp8kIL2T4+ueryB9s518+ +mO4XJOxL6uWOOHkvheFYhCoGq0FjopGWrQmpR9UqbJ59uwjI1Zygo878LMV0M9Bc +5ABaF06yRVyZRH5lAC8zihBUsleE5jUM4FULHRUZFUVJx8ivFtJnLqLuywMOlkq4 +yY4Vf0M77dwVVY5enbnTbKEI25Csf47Gp9i5JS2jXY9ORPNjpYb8LHvp8NZTNczd +mGxQsWd6pWohnGkZdbJqQoQxMfCNYev5ejf7bJ1AoQKBgQDkpFtsIA0ExmcefIsE +ChuEpFUXko8wjNeCWFOjJx+Sed9RaRaQiSYOKmWwDjaP7dlJwUC0RIRy65gOGGnf +PUHOd7nEBtyDIjpSsjWWBjfsUEAqrhze0qpXSPmBzhNlOdsN01VzTgTNiTpea7hi +izgYjd1kLvJ5p+CV5NIxNhp+qQKBgQDSbbyKf6vPDNIZb9V/ef10Co31ahDVwlFa +P3FrZv+9eDMJQfXtkRTlu5Auo+TDaFFSMjrb5rTJcEuzBwHyyodqPvPsYC33/DVQ +jGYVqjQuG5q473DNebtinn5JR40ZfHiJlpx2Ms5xdbPIhN1efTRXmYIov0AIaKuR +on9LE8X8rwKBgFsyIzTxY7/v0tmaG2i1D1zMnxQT5QEcbCkVSebdh/5IlgZGwDVO +PtuPlZevU5v85ppAdqpwWdPsnG2i1zevmzvbDUFe6z1yvYiWhEEeodej+rQLVoCZ +zk+aT8qyg5HwjarqDD89czT380wN8zF7DhjdHN0EzLoxd6bR6fSu+8phAoGAV4v0 +PyLy1gedeZu/lXOpcRfbC9l++5AGzKdMhsSpbaiOgzGAIcCUkye/ysfBK1NBUhM3 +zblkSdKAjBFETEDaqedbEGLLfTre644eArF3WB9/9aUYp0QYI+WQ4Of12j6g341b +twlYPngbvjcY6nDoz/E757v55gW2K7cRgqjNXF0CgYEAxNfclcdKbUtGAsttZdY3 ++dcdBtqcLvpYlMsZPQaxNppyKBI5svtK715FsVbmLhINqiNo1aKIA5M3E2P88Fa2 +nqVrKsBOn3gCe+GFlFeWwNAfRlfmP1ZUHDp07zvNtRm4ZR/3hdAze4DbyWv58LfL +WSjqCjjBeurblkRv2QTXu2k= +-----END PRIVATE KEY----- diff --git a/exporter/otlp-logs/test/test_helper.rb b/exporter/otlp-logs/test/test_helper.rb new file mode 100644 index 000000000..2ab9f6136 --- /dev/null +++ b/exporter/otlp-logs/test/test_helper.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +if RUBY_ENGINE == 'ruby' + require 'simplecov' + SimpleCov.start do + enable_coverage :branch + add_filter '/test/' + end + + SimpleCov.minimum_coverage 85 +end + +require 'opentelemetry-test-helpers' +require 'opentelemetry/exporter/otlp_logs' +require 'minitest/autorun' +require 'webmock/minitest' +require 'minitest/stub_const' + +OpenTelemetry.logger = Logger.new(File::NULL) diff --git a/exporter/otlp-metrics/CHANGELOG.md b/exporter/otlp-metrics/CHANGELOG.md index b42812d56..774137ffd 100644 --- a/exporter/otlp-metrics/CHANGELOG.md +++ b/exporter/otlp-metrics/CHANGELOG.md @@ -1,5 +1,11 @@ # Release History: opentelemetry-exporter-otlp-metrics +### v0.2.0 / 2024-11-19 + +* ADDED: Add basic metrics view +* FIXED: Remove Metrics OTLP exporter `Util#measure_request_duration` and duplicate files +* FIXED: Add mTLS for metrics exporter + ### v0.1.0 / 2024-08-27 Initial release. diff --git a/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/metrics_exporter.rb b/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/metrics_exporter.rb index a6150fefb..36705bbf7 100644 --- a/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/metrics_exporter.rb +++ b/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/metrics_exporter.rb @@ -284,13 +284,20 @@ def histogram_data_point(hdp) end def number_data_point(ndp) - Opentelemetry::Proto::Metrics::V1::NumberDataPoint.new( + args = { attributes: ndp.attributes.map { |k, v| as_otlp_key_value(k, v) }, - as_int: ndp.value, start_time_unix_nano: ndp.start_time_unix_nano, time_unix_nano: ndp.time_unix_nano, exemplars: ndp.exemplars # exemplars not implemented yet from metrics sdk - ) + } + + if ndp.value.is_a?(Float) + args[:as_double] = ndp.value + else + args[:as_int] = ndp.value + end + + Opentelemetry::Proto::Metrics::V1::NumberDataPoint.new(**args) end # may not need this diff --git a/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/version.rb b/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/version.rb index 2d326a358..9e526448a 100644 --- a/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/version.rb +++ b/exporter/otlp-metrics/lib/opentelemetry/exporter/otlp/metrics/version.rb @@ -9,7 +9,7 @@ module Exporter module OTLP module Metrics ## Current OpenTelemetry OTLP exporter version - VERSION = '0.1.0' + VERSION = '0.2.0' end end end diff --git a/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/metrics_exporter_test.rb b/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/metrics_exporter_test.rb index 93176ce33..6076a80a2 100644 --- a/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/metrics_exporter_test.rb +++ b/exporter/otlp-metrics/test/opentelemetry/exporter/otlp/metrics/metrics_exporter_test.rb @@ -463,6 +463,23 @@ OpenTelemetry.logger = logger end + it 'is able to encode NumberDataPoint with Integer or Float value' do + stub_request(:post, 'http://localhost:4318/v1/metrics').to_return(status: 200) + + [1, 0.1234].each do |value| + ndp = OpenTelemetry::SDK::Metrics::Aggregation::NumberDataPoint.new + ndp.attributes = { 'a' => 'b' } + ndp.start_time_unix_nano = 0 + ndp.time_unix_nano = 0 + ndp.value = value + + metrics_data = create_metrics_data(data_points: [ndp]) + + result = exporter.export([metrics_data]) + _(result).must_equal(METRICS_SUCCESS) + end + end + it 'logs rpc.Status on bad request' do log_stream = StringIO.new logger = OpenTelemetry.logger diff --git a/logs_api/lib/opentelemetry-logs-api.rb b/logs_api/lib/opentelemetry-logs-api.rb index 47ba36905..03b09d7fe 100644 --- a/logs_api/lib/opentelemetry-logs-api.rb +++ b/logs_api/lib/opentelemetry-logs-api.rb @@ -5,5 +5,37 @@ # SPDX-License-Identifier: Apache-2.0 require 'opentelemetry' -require_relative 'opentelemetry/logs' -require_relative 'opentelemetry/logs/version' +require 'opentelemetry/logs' +require 'opentelemetry/logs/version' +require 'opentelemetry/internal/proxy_logger_provider' +require 'opentelemetry/internal/proxy_logger' + +# OpenTelemetry is an open source observability framework, providing a +# general-purpose API, SDK, and related tools required for the instrumentation +# of cloud-native software, frameworks, and libraries. +# +# The OpenTelemetry module in the Logs API gem provides global accessors +# for logs-related objects. +module OpenTelemetry + @logger_provider = Internal::ProxyLoggerProvider.new + + # Register the global logger provider. + # + # @param [LoggerProvider] provider A logger provider to register as the + # global instance. + def logger_provider=(provider) + @mutex.synchronize do + if @logger_provider.instance_of? Internal::ProxyLoggerProvider + logger.debug("Upgrading default proxy logger provider to #{provider.class}") + @logger_provider.delegate = provider + end + @logger_provider = provider + end + end + + # @return [Object, Logs::LoggerProvider] registered logger provider or a + # default no-op implementation of the logger provider. + def logger_provider + @mutex.synchronize { @logger_provider } + end +end diff --git a/logs_api/lib/opentelemetry/internal/proxy_logger.rb b/logs_api/lib/opentelemetry/internal/proxy_logger.rb new file mode 100644 index 000000000..a392c3dbc --- /dev/null +++ b/logs_api/lib/opentelemetry/internal/proxy_logger.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Internal + # @api private + # + # {ProxyLogger} is an implementation of {OpenTelemetry::Logs::Logger}. It is returned from + # the ProxyLoggerProvider until a delegate logger provider is installed. After the delegate + # logger provider is installed, the ProxyLogger will delegate to the corresponding "real" + # logger. + class ProxyLogger < Logs::Logger + attr_writer :delegate + + # Returns a new {ProxyLogger} instance. + # + # @return [ProxyLogger] + def initialize + @delegate = nil + end + + def on_emit( + timestamp: nil, + observed_timestamp: nil, + severity_number: nil, + severity_text: nil, + body: nil, + trace_id: nil, + span_id: nil, + trace_flags: nil, + attributes: nil, + context: nil + ) + unless @delegate.nil? + return @delegate.on_emit( + timestamp: nil, + observed_timestamp: nil, + severity_number: nil, + severity_text: nil, + body: nil, + trace_id: nil, + span_id: nil, + trace_flags: nil, + attributes: nil, + context: nil + ) + end + + super + end + end + end +end diff --git a/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb b/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb new file mode 100644 index 000000000..0cbedc9d6 --- /dev/null +++ b/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Internal + # @api private + # + # {ProxyLoggerProvider} is an implementation of {OpenTelemetry::Logs::LoggerProvider}. + # It is the default global logger provider returned by OpenTelemetry.logger_provider. + # It delegates to a "real" LoggerProvider after the global logger provider is registered. + # It returns {ProxyLogger} instances until the delegate is installed. + class ProxyLoggerProvider < Logs::LoggerProvider + Key = Struct.new(:name, :version) + private_constant(:Key) + # Returns a new {ProxyLoggerProvider} instance. + # + # @return [ProxyLoggerProvider] + def initialize + super + + @mutex = Mutex.new + @registry = {} + @delegate = nil + end + + # Set the delegate logger provider. If this is called more than once, a warning will + # be logged and superfluous calls will be ignored. + # + # @param [LoggerProvider] provider The logger provider to delegate to + def delegate=(provider) + unless @delegate.nil? + OpenTelemetry.logger.warn 'Attempt to reset delegate in ProxyLoggerProvider ignored.' + return + end + + @mutex.synchronize do + @delegate = provider + @registry.each { |key, logger| logger.delegate = provider.logger(key.name, key.version) } + end + end + + # Returns a {Logger} instance. + # + # @param [optional String] name Instrumentation package name + # @param [optional String] version Instrumentation package version + # + # @return [Logger] + def logger(name = nil, version = nil) + @mutex.synchronize do + return @delegate.logger(name, version) unless @delegate.nil? + + @registry[Key.new(name, version)] ||= ProxyLogger.new + end + end + end + end +end diff --git a/logs_api/lib/opentelemetry/logs.rb b/logs_api/lib/opentelemetry/logs.rb index 0669bb21d..ade58537c 100644 --- a/logs_api/lib/opentelemetry/logs.rb +++ b/logs_api/lib/opentelemetry/logs.rb @@ -4,11 +4,6 @@ # # SPDX-License-Identifier: Apache-2.0 -require_relative 'logs/log_record' -require_relative 'logs/logger' -require_relative 'logs/logger_provider' -require_relative 'logs/severity_number' - module OpenTelemetry # The Logs API records a timestamped record with metadata. # In OpenTelemetry, any data that is not part of a distributed trace or a @@ -20,3 +15,8 @@ module OpenTelemetry module Logs end end + +require 'opentelemetry/logs/log_record' +require 'opentelemetry/logs/logger' +require 'opentelemetry/logs/logger_provider' +require 'opentelemetry/logs/severity_number' diff --git a/logs_api/test/opentelemetry_logs_api_test.rb b/logs_api/test/opentelemetry_logs_api_test.rb new file mode 100644 index 000000000..9034a2b30 --- /dev/null +++ b/logs_api/test/opentelemetry_logs_api_test.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry do + class CustomLogRecord < OpenTelemetry::Logs::LogRecord + end + + class CustomLogger < OpenTelemetry::Logs::Logger + def on_emit(*) + CustomLogRecord.new + end + end + + class CustomLoggerProvider < OpenTelemetry::Logs::LoggerProvider + def logger(name = nil, version = nil) + CustomLogger.new + end + end + + describe '.logger_provider' do + after do + # Ensure we don't leak custom logger factories and loggers to other tests + OpenTelemetry.logger_provider = OpenTelemetry::Internal::ProxyLoggerProvider.new + end + + it 'returns a Logs::LoggerProvider by default' do + logger_provider = OpenTelemetry.logger_provider + _(logger_provider).must_be_kind_of(OpenTelemetry::Logs::LoggerProvider) + end + + it 'returns the same instance when accessed multiple times' do + _(OpenTelemetry.logger_provider).must_equal(OpenTelemetry.logger_provider) + end + + it 'returns user-specified logger provider' do + custom_logger_provider = CustomLoggerProvider.new + OpenTelemetry.logger_provider = custom_logger_provider + _(OpenTelemetry.logger_provider).must_equal(custom_logger_provider) + end + end + + describe '.logger_provider=' do + after do + # Ensure we don't leak custom logger factories and loggers to other tests + OpenTelemetry.logger_provider = OpenTelemetry::Internal::ProxyLoggerProvider.new + end + + it 'has a default proxy logger' do + refute_nil OpenTelemetry.logger_provider.logger + end + + it 'upgrades default loggers to *real* loggers' do + # proxy loggers do not emit any log records, nor does the API logger + # the on_emit method is empty + default_logger = OpenTelemetry.logger_provider.logger + _(default_logger.on_emit(body: 'test')).must_be_instance_of(NilClass) + OpenTelemetry.logger_provider = CustomLoggerProvider.new + _(default_logger.on_emit(body: 'test')).must_be_instance_of(CustomLogRecord) + end + + it 'upgrades the default logger provider to a *real* logger provider' do + default_logger_provider = OpenTelemetry.logger_provider + OpenTelemetry.logger_provider = CustomLoggerProvider.new + _(default_logger_provider.logger).must_be_instance_of(CustomLogger) + end + end +end diff --git a/logs_sdk/lib/opentelemetry-logs-sdk.rb b/logs_sdk/lib/opentelemetry-logs-sdk.rb index 32c03df1f..63c750228 100644 --- a/logs_sdk/lib/opentelemetry-logs-sdk.rb +++ b/logs_sdk/lib/opentelemetry-logs-sdk.rb @@ -5,4 +5,5 @@ # SPDX-License-Identifier: Apache-2.0 require 'opentelemetry/sdk' +require 'opentelemetry-logs-api' require 'opentelemetry/sdk/logs' diff --git a/logs_sdk/lib/opentelemetry/sdk/logs.rb b/logs_sdk/lib/opentelemetry/sdk/logs.rb index 6003e3228..b310d6aa5 100644 --- a/logs_sdk/lib/opentelemetry/sdk/logs.rb +++ b/logs_sdk/lib/opentelemetry/sdk/logs.rb @@ -5,6 +5,7 @@ # 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' 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 000000000..09186d169 --- /dev/null +++ b/logs_sdk/lib/opentelemetry/sdk/logs/configuration_patch.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk/configurator' + +module OpenTelemetry + module SDK + module Logs + # The ConfiguratorPatch implements a hook to configure the logs portion + # of the SDK. + module ConfiguratorPatch + def add_log_record_processor(log_record_processor) + @log_record_processors << log_record_processor + end + + private + + def initialize + super + @log_record_processors = [] + end + + # The logs_configuration_hook method is where we define the setup + # process for logs SDK. + def logs_configuration_hook + OpenTelemetry.logger_provider = Logs::LoggerProvider.new(resource: @resource) + configure_log_record_processors + end + + def configure_log_record_processors + processors = @log_record_processors.empty? ? wrapped_log_exporters_from_env.compact : @log_record_processors + processors.each { |p| OpenTelemetry.logger_provider.add_log_record_processor(p) } + end + + def wrapped_log_exporters_from_env + # TODO: set default to OTLP to match traces, default is console until other exporters merged + exporters = ENV.fetch('OTEL_LOGS_EXPORTER', 'console') + + exporters.split(',').map do |exporter| + case exporter.strip + when 'none' then nil + when 'console' then Logs::Export::SimpleLogRecordProcessor.new(Logs::Export::ConsoleLogRecordExporter.new) + when 'otlp' + otlp_protocol = ENV['OTEL_EXPORTER_OTLP_LOGS_PROTOCOL'] || ENV['OTEL_EXPORTER_OTLP_PROTOCOL'] || 'http/protobuf' + + if otlp_protocol != 'http/protobuf' + OpenTelemetry.logger.warn "The #{otlp_protocol} transport protocol is not supported by the OTLP exporter, log_records will not be exported." + nil + else + begin + Logs::Export::BatchLogRecordProcessor.new(OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new) + rescue NameError + OpenTelemetry.logger.warn 'The otlp logs exporter cannot be configured - please add opentelemetry-exporter-otlp-logs to your Gemfile. Logs will not be exported' + nil + end + end + else + OpenTelemetry.logger.warn "The #{exporter} exporter is unknown and cannot be configured, log records will not be exported" + nil + end + end + end + end + end + end +end + +OpenTelemetry::SDK::Configurator.prepend(OpenTelemetry::SDK::Logs::ConfiguratorPatch) diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/export/batch_log_record_processor.rb b/logs_sdk/lib/opentelemetry/sdk/logs/export/batch_log_record_processor.rb index 65f99a151..d60920a0f 100644 --- a/logs_sdk/lib/opentelemetry/sdk/logs/export/batch_log_record_processor.rb +++ b/logs_sdk/lib/opentelemetry/sdk/logs/export/batch_log_record_processor.rb @@ -40,7 +40,6 @@ def initialize(exporter, max_queue_size: Integer(ENV.fetch('OTEL_BLRP_MAX_QUEUE_SIZE', 2048)), max_export_batch_size: Integer(ENV.fetch('OTEL_BLRP_MAX_EXPORT_BATCH_SIZE', 512)), start_thread_on_boot: String(ENV['OTEL_RUBY_BLRP_START_THREAD_ON_BOOT']) !~ /false/i) - unless max_export_batch_size <= max_queue_size raise ArgumentError, 'max_export_batch_size much be less than or equal to max_queue_size' diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb b/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb index 231999a33..b05da2212 100644 --- a/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb +++ b/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb @@ -71,7 +71,6 @@ def on_emit(timestamp: nil, span_id: nil, trace_flags: nil, context: OpenTelemetry::Context.current) - current_span = OpenTelemetry::Trace.current_span(context) span_context = current_span.context unless current_span == OpenTelemetry::Trace::Span::INVALID diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb b/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb index 2f28e2480..35d7d5dc1 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' @@ -28,6 +31,8 @@ def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create, log_rec @mutex = Mutex.new @resource = resource @stopped = false + @registry = {} + @registry_mutex = Mutex.new end # Returns an {OpenTelemetry::SDK::Logs::Logger} instance. @@ -44,7 +49,9 @@ def logger(name:, version: nil) "invalid name. Name provided: #{name.inspect}") end - Logger.new(name, version, self) + @registry_mutex.synchronize do + @registry[Key.new(name, version)] ||= Logger.new(name, version, self) + end end # Adds a new log record processor to this LoggerProvider's @@ -134,6 +141,7 @@ def on_emit(timestamp: nil, trace_flags: nil, instrumentation_scope: nil, context: nil) + return if @stopped log_record = LogRecord.new(timestamp: timestamp, observed_timestamp: observed_timestamp, diff --git a/logs_sdk/test/opentelemetry/sdk/logs/export/batch_log_record_processor_test.rb b/logs_sdk/test/opentelemetry/sdk/logs/export/batch_log_record_processor_test.rb index a7ed0c362..cff2b718d 100644 --- a/logs_sdk/test/opentelemetry/sdk/logs/export/batch_log_record_processor_test.rb +++ b/logs_sdk/test/opentelemetry/sdk/logs/export/batch_log_record_processor_test.rb @@ -186,20 +186,23 @@ def to_log_record_data end it 'removes the older log records from the batch if full' do - processor = BatchLogRecordProcessor.new(TestExporter.new, max_queue_size: 1, max_export_batch_size: 1) + # Windows intermittently fails if we don't set this + OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_BLRP_START_THREAD_ON_BOOT' => 'false') do + processor = BatchLogRecordProcessor.new(TestExporter.new, max_queue_size: 1, max_export_batch_size: 1) - # Don't actually try to export, we're looking at the log records array - processor.stub(:work, nil) do - older_log_record = TestLogRecord.new - newest_log_record = TestLogRecord.new + # Don't actually try to export, we're looking at the log records array + processor.stub(:work, nil) do + older_log_record = TestLogRecord.new + newest_log_record = TestLogRecord.new - processor.on_emit(older_log_record, mock_context) - processor.on_emit(newest_log_record, mock_context) + processor.on_emit(older_log_record, mock_context) + processor.on_emit(newest_log_record, mock_context) - records = processor.instance_variable_get(:@log_records) + records = processor.instance_variable_get(:@log_records) - assert_includes(records, newest_log_record) - refute_includes(records, older_log_record) + assert_includes(records, newest_log_record) + refute_includes(records, older_log_record) + 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 a2ee0e59c..992a4e817 100644 --- a/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb +++ b/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb @@ -223,5 +223,32 @@ def mock_context.value(key); OpenTelemetry::Trace::Span::INVALID; end # rubocop: mock_log_record_processor.verify end end + + it 'does not emit if the provider is stopped' do + logger_provider.instance_variable_set(:@stopped, true) + mock_log_record = Minitest::Mock.new + + OpenTelemetry::SDK::Logs::LogRecord.stub :new, mock_log_record do + logger_provider.add_log_record_processor(mock_log_record_processor) + mock_log_record_processor.expect(:on_emit, nil, [mock_log_record, mock_context]) + + logger_provider.on_emit(**args) + # The verify should fail because on_emit should never call new on LogRecord + assert_raises(MockExpectationError) { mock_log_record_processor.verify } + end + end + end + + describe 'instrument registry' do + # On the first call, create a logger with an empty string for name and + # version and add to the registry. The second call returns that logger + # from the registry instead of creating a new instance. + it 'reuses the same logger if called twice when name and version are nil' do + logger = logger_provider.logger(name: nil, version: nil) + logger2 = logger_provider.logger(name: nil, version: nil) + + assert_instance_of(Logs::Logger, logger) + assert_same(logger, logger2) + end end end diff --git a/metrics_sdk/CHANGELOG.md b/metrics_sdk/CHANGELOG.md index d8be534a1..6a6ffa15e 100644 --- a/metrics_sdk/CHANGELOG.md +++ b/metrics_sdk/CHANGELOG.md @@ -1,5 +1,9 @@ # Release History: opentelemetry-metrics-sdk +### v0.4.0 / 2024-11-19 + +* ADDED: Update metrics configuration patch + ### v0.3.0 / 2024-10-22 * ADDED: Add basic metrics view diff --git a/metrics_sdk/Gemfile b/metrics_sdk/Gemfile index a777c7f06..c4b8797f4 100644 --- a/metrics_sdk/Gemfile +++ b/metrics_sdk/Gemfile @@ -16,6 +16,7 @@ gem 'opentelemetry-sdk', path: '../sdk' gem 'opentelemetry-test-helpers', path: '../test_helpers' group :test, :development do + gem 'opentelemetry-exporter-otlp-metrics', path: '../exporter/otlp-metrics' unless RUBY_ENGINE == 'jruby' gem 'pry' gem 'pry-byebug' unless RUBY_ENGINE == 'jruby' end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb index 0d357e1d4..d379e0a9a 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/explicit_bucket_histogram.rb @@ -25,7 +25,6 @@ def initialize( boundaries: DEFAULT_BOUNDARIES, record_min_max: true ) - @aggregation_temporality = aggregation_temporality.to_sym @boundaries = boundaries && !boundaries.empty? ? boundaries.sort : nil @record_min_max = record_min_max diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/histogram_data_point.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/histogram_data_point.rb index 212c5ccd0..d6e47c137 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/histogram_data_point.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/histogram_data_point.rb @@ -14,7 +14,7 @@ module Aggregation :start_time_unix_nano, # Integer nanoseconds since Epoch :time_unix_nano, # Integer nanoseconds since Epoch :count, # Integer count is the number of values in the population. Must be non-negative. - :sum, # Integer sum of the values in the population. If count is zero then this field then this field must be zero + :sum, # Float sum of the values in the population. If count is zero then this field must be zero. :bucket_counts, # optional Array[Integer] field contains the count values of histogram for each bucket. :explicit_bounds, # Array[Float] specifies buckets with explicitly defined bounds for values. :exemplars, # optional List of exemplars collected from measurements that were used to form the data point diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/number_data_point.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/number_data_point.rb index ae6e37eac..f76de98a9 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/number_data_point.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/aggregation/number_data_point.rb @@ -11,7 +11,7 @@ module Aggregation NumberDataPoint = Struct.new(:attributes, # Hash{String => String, Numeric, Boolean, Array} :start_time_unix_nano, # Integer nanoseconds since Epoch :time_unix_nano, # Integer nanoseconds since Epoch - :value, # Integer + :value, # Numeric :exemplars) # optional List of exemplars collected from measurements that were used to form the data point end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb index ac529e67f..f9ee2c07f 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/configuration_patch.rb @@ -10,12 +10,47 @@ module Metrics # The ConfiguratorPatch implements a hook to configure the metrics # portion of the SDK. module ConfiguratorPatch + def add_metric_reader(metric_reader) + @metric_readers << metric_reader + end + private - # The metrics_configuration_hook method is where we define the setup process - # for metrics SDK. + def initialize + super + @metric_readers = [] + end + + # The metrics_configuration_hook method is where we define the setup process for the metrics SDK. def metrics_configuration_hook OpenTelemetry.meter_provider = Metrics::MeterProvider.new(resource: @resource) + configure_metric_readers + end + + def configure_metric_readers + readers = @metric_readers.empty? ? wrapped_metric_exporters_from_env.compact : @metric_readers + readers.each { |r| OpenTelemetry.meter_provider.add_metric_reader(r) } + end + + def wrapped_metric_exporters_from_env + exporters = ENV.fetch('OTEL_METRICS_EXPORTER', 'otlp') + exporters.split(',').map do |exporter| + case exporter.strip + when 'none' then nil + when 'console' then OpenTelemetry.meter_provider.add_metric_reader(Metrics::Export::PeriodicMetricReader.new(exporter: Metrics::Export::ConsoleMetricPullExporter.new)) + when 'in-memory' then OpenTelemetry.meter_provider.add_metric_reader(Metrics::Export::InMemoryMetricPullExporter.new) + when 'otlp' + begin + OpenTelemetry.meter_provider.add_metric_reader(Metrics::Export::PeriodicMetricReader.new(exporter: OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.new)) + rescue NameError + OpenTelemetry.logger.warn 'The otlp metrics exporter cannot be configured - please add opentelemetry-exporter-otlp-metrics to your Gemfile, metrics will not be exported' + nil + end + else + OpenTelemetry.logger.warn "The #{exporter} exporter is unknown and cannot be configured, metrics will not be exported" + nil + end + end end end end diff --git a/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb b/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb index 17e0d9570..0256af7fa 100644 --- a/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb +++ b/metrics_sdk/lib/opentelemetry/sdk/metrics/version.rb @@ -8,7 +8,7 @@ module OpenTelemetry module SDK module Metrics # Current OpenTelemetry metrics sdk version - VERSION = '0.3.0' + VERSION = '0.4.0' end end end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb new file mode 100644 index 000000000..88cd4bdaa --- /dev/null +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/configuration_patch_test.rb @@ -0,0 +1,127 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' +require 'opentelemetry-exporter-otlp-metrics' unless RUBY_ENGINE == 'jruby' + +describe OpenTelemetry::SDK::Metrics::ConfiguratorPatch do + let(:configurator) { OpenTelemetry::SDK::Configurator.new } + let(:default_resource_attributes) do + { + 'telemetry.sdk.name' => 'opentelemetry', + 'telemetry.sdk.language' => 'ruby', + 'telemetry.sdk.version' => OpenTelemetry::SDK::VERSION, + 'process.pid' => Process.pid, + 'process.command' => $PROGRAM_NAME, + 'process.runtime.name' => RUBY_ENGINE, + 'process.runtime.version' => RUBY_VERSION, + 'process.runtime.description' => RUBY_DESCRIPTION, + 'service.name' => 'unknown_service' + } + end + + describe '#configure' do + describe 'meter_provider' do + it 'is an instance of SDK::Metrics::MeterProvider' do + configurator.configure + + _(OpenTelemetry.meter_provider).must_be_instance_of( + OpenTelemetry::SDK::Metrics::MeterProvider + ) + end + end + + describe 'metric readers' do + it 'defaults to a periodic reader with an otlp exporter' do + skip 'OTLP exporter not compatible with JRuby' if RUBY_ENGINE == 'jruby' + + configurator.configure + + assert_equal 1, OpenTelemetry.meter_provider.metric_readers.size + reader = OpenTelemetry.meter_provider.metric_readers[0] + + assert_instance_of OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader, reader + assert_instance_of OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter, reader.instance_variable_get(:@exporter) + end + + it 'can be set by environment variable' do + OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'console') do + configurator.configure + end + + assert_equal 1, OpenTelemetry.meter_provider.metric_readers.size + + reader = OpenTelemetry.meter_provider.metric_readers[0] + + assert_instance_of OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader, reader + assert_instance_of OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter, reader.instance_variable_get(:@exporter) + end + + it 'supports "none" as an environment variable' do + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'none') do + configurator.configure + end + + assert_empty OpenTelemetry.meter_provider.metric_readers + + refute_match(/The none exporter is unknown and cannot be configured/, log_stream.string) + end + end + + it 'supports multiple exporters passed by environment variable' do + OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'console,in-memory') do + configurator.configure + end + + assert_equal 2, OpenTelemetry.meter_provider.metric_readers.size + + reader1 = OpenTelemetry.meter_provider.metric_readers[0] + reader2 = OpenTelemetry.meter_provider.metric_readers[1] + + assert_instance_of OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader, reader1 + assert_instance_of OpenTelemetry::SDK::Metrics::Export::ConsoleMetricPullExporter, reader1.instance_variable_get(:@exporter) + + assert_instance_of OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter, reader2 + end + + it 'defaults to noop with invalid env var' do + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'unladen_swallow') do + configurator.configure + end + + assert_empty OpenTelemetry.meter_provider.metric_readers + assert_match(/The unladen_swallow exporter is unknown and cannot be configured/, log_stream.string) + end + end + + it 'rescues NameErrors when otlp set to env var and the library is not installed' do + if RUBY_ENGINE == 'jruby' + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'otlp') do + configurator.configure + end + + assert_empty OpenTelemetry.meter_provider.metric_readers + assert_match(/The otlp metrics exporter cannot be configured - please add opentelemetry-exporter-otlp-metrics to your Gemfile, metrics will not be exported/, log_stream.string) + end + else + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + OpenTelemetry::Exporter::OTLP::Metrics::MetricsExporter.stub(:new, -> { raise NameError }) do + OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'otlp') do + configurator.configure + end + + assert_empty OpenTelemetry.meter_provider.metric_readers + assert_match(/The otlp metrics exporter cannot be configured - please add opentelemetry-exporter-otlp-metrics to your Gemfile, metrics will not be exported/, log_stream.string) + end + end + end + end + end + end +end diff --git a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb index a82513a3c..72f04e08c 100644 --- a/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb +++ b/metrics_sdk/test/opentelemetry/sdk/metrics/meter_provider_test.rb @@ -9,7 +9,11 @@ describe OpenTelemetry::SDK::Metrics::MeterProvider do before do reset_metrics_sdk - OpenTelemetry::SDK.configure + # The MeterProvider tests deal with mock MetricReaders that are manually added + # Run the tests without the configuration patch interfering with the setup + OpenTelemetry::TestHelpers.with_env('OTEL_METRICS_EXPORTER' => 'none') do + OpenTelemetry::SDK.configure + end end describe '#meter' do diff --git a/metrics_sdk/test/test_helper.rb b/metrics_sdk/test/test_helper.rb index 99aa4deee..11076bde1 100644 --- a/metrics_sdk/test/test_helper.rb +++ b/metrics_sdk/test/test_helper.rb @@ -33,3 +33,6 @@ def with_test_logger ensure OpenTelemetry.logger = original_logger end + +# Suppress warn-level logs about a missing OTLP exporter for traces +ENV['OTEL_TRACES_EXPORTER'] = 'none' diff --git a/sdk/lib/opentelemetry/sdk/configurator.rb b/sdk/lib/opentelemetry/sdk/configurator.rb index ae1bbaf07..94a5020eb 100644 --- a/sdk/lib/opentelemetry/sdk/configurator.rb +++ b/sdk/lib/opentelemetry/sdk/configurator.rb @@ -126,13 +126,20 @@ def add_span_processor(span_processor) @span_processors << span_processor end + # Add a log record processor to the export pipeline + # + # @param [#emit, #shutdown, #force_flush] log_record_processor A log_record_processor + # that satisfies the duck type #emit, #shutdown, #force_flush. See + # {SimpleLogRecordProcessor} for an example. + def add_log_record_processor(log_record_processor); end + # @api private # The configure method is where we define the setup process. This allows # us to make certain guarantees about which systems and globals are setup # at each stage. The setup process is: # - setup logging # - setup propagation - # - setup tracer_provider and meter_provider + # - setup tracer_provider, meter_provider, and logger_provider # - install instrumentation def configure OpenTelemetry.logger = logger @@ -142,6 +149,7 @@ def configure tracer_provider.id_generator = @id_generator OpenTelemetry.tracer_provider = tracer_provider metrics_configuration_hook + logs_configuration_hook install_instrumentation end @@ -149,6 +157,8 @@ def configure def metrics_configuration_hook; end + def logs_configuration_hook; end + def tracer_provider @tracer_provider ||= Trace::TracerProvider.new(resource: @resource) end diff --git a/sdk/lib/opentelemetry/sdk/trace/span_limits.rb b/sdk/lib/opentelemetry/sdk/trace/span_limits.rb index 4f3e9cd2b..449705d83 100644 --- a/sdk/lib/opentelemetry/sdk/trace/span_limits.rb +++ b/sdk/lib/opentelemetry/sdk/trace/span_limits.rb @@ -41,7 +41,6 @@ def initialize(attribute_count_limit: Integer(OpenTelemetry::Common::Utilities.c event_attribute_count_limit: Integer(OpenTelemetry::Common::Utilities.config_opt('OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT', default: 128)), event_attribute_length_limit: OpenTelemetry::Common::Utilities.config_opt('OTEL_EVENT_ATTRIBUTE_VALUE_LENGTH_LIMIT', 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT'), link_attribute_count_limit: Integer(OpenTelemetry::Common::Utilities.config_opt('OTEL_LINK_ATTRIBUTE_COUNT_LIMIT', default: 128))) - 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 raise ArgumentError, 'event_count_limit must be positive' unless event_count_limit.positive? diff --git a/test_helpers/lib/opentelemetry/test_helpers.rb b/test_helpers/lib/opentelemetry/test_helpers.rb index 25048aa0f..ad4cc3bbb 100644 --- a/test_helpers/lib/opentelemetry/test_helpers.rb +++ b/test_helpers/lib/opentelemetry/test_helpers.rb @@ -72,5 +72,34 @@ def create_span_data(name: '', kind: nil, status: nil, parent_span_id: OpenTelem total_recorded_events, total_recorded_links, start_timestamp, end_timestamp, attributes, links, events, resource, instrumentation_scope, span_id, trace_id, trace_flags, tracestate) end + + def create_log_record_data(timestamp: OpenTelemetry::TestHelpers.exportable_timestamp, + observed_timestamp: OpenTelemetry::TestHelpers.exportable_timestamp, + severity_text: nil, + severity_number: nil, + body: nil, + attributes: nil, + trace_id: OpenTelemetry::Trace.generate_trace_id, + span_id: OpenTelemetry::Trace.generate_span_id, + trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT, + resource: nil, + instrumentation_scope: OpenTelemetry::SDK::InstrumentationScope.new('', 'v0.0.1'), + total_recorded_attributes: 0) + resource ||= OpenTelemetry::SDK::Resources::Resource.telemetry_sdk + OpenTelemetry::SDK::Logs::LogRecordData.new( + timestamp, + observed_timestamp, + severity_text, + severity_number, + body, + attributes, + trace_id, + span_id, + trace_flags, + resource, + instrumentation_scope, + total_recorded_attributes + ) + end end end