diff --git a/api/lib/opentelemetry.rb b/api/lib/opentelemetry.rb index 3234fd782a..f84d38916f 100644 --- a/api/lib/opentelemetry.rb +++ b/api/lib/opentelemetry.rb @@ -28,7 +28,11 @@ module OpenTelemetry # @return [Object, Logger] configured Logger or a default STDOUT Logger. def logger - @logger ||= Logger.new($stdout, level: ENV['OTEL_LOG_LEVEL'] || Logger::INFO) + @logger ||= create_logger + # alternately: Logger.new($stdout, level: ENV['OTEL_LOG_LEVEL'] || Logger::INF, progname: 'OpenTelemetry') + # and clean up the log messages that are prefixed with "OpenTelemetry" + # if we want log records generated for OpenTelemetry logs, I think we can do that, but I imagine this is + # "untraced" territory end # @return [Callable] configured error handler or a default that logs the @@ -69,4 +73,15 @@ def tracer_provider def propagation @propagation ||= Context::Propagation::NoopTextMapPropagator.new end + + private + + def create_logger + logger = Logger.new($stdout, level: ENV['OTEL_LOG_LEVEL'] || Logger::INFO) + # @skip_instrumenting prevents Ruby Logger instrumentation from + # triggering a stack overflow. Logs emitted using OpenTelemetry.logger + # will not be turned into OpenTelemetry LogRecords. + logger.instance_variable_set(:@skip_instrumenting, true) + logger + end end diff --git a/exporter/otlp-logs/.rubocop.yml b/exporter/otlp-logs/.rubocop.yml new file mode 100644 index 0000000000..46d464b94a --- /dev/null +++ b/exporter/otlp-logs/.rubocop.yml @@ -0,0 +1,42 @@ +AllCops: + TargetRubyVersion: "3.0" + NewCops: disable + SuggestExtensions: false + Exclude: + - "lib/opentelemetry/proto/**/*" + - "vendor/**/*" + +Bundler/OrderedGems: + Exclude: + - gemfiles/**/* +Lint/UnusedMethodArgument: + Enabled: false +Lint/MissingSuper: + Enabled: false +Lint/ConstantDefinitionInBlock: + Exclude: + - "test/**/*" +Style/StringConcatenation: + Exclude: + - "test/**/*" +Metrics/AbcSize: + Enabled: false +Layout/LineLength: + Enabled: false +Metrics/MethodLength: + Max: 20 +Metrics/ParameterLists: + Enabled: false +Style/FrozenStringLiteralComment: + Exclude: + - gemfiles/**/* +Style/ModuleFunction: + Enabled: false +Style/StringLiterals: + Exclude: + - gemfiles/**/* +Metrics/BlockLength: + Enabled: false +Naming/FileName: + Exclude: + - "lib/opentelemetry-exporter-otlp.rb" diff --git a/exporter/otlp-logs/.yardopts b/exporter/otlp-logs/.yardopts new file mode 100644 index 0000000000..f8ba2c9cd6 --- /dev/null +++ b/exporter/otlp-logs/.yardopts @@ -0,0 +1,9 @@ +--no-private +--title=OpenTelemetry OTLP Logs Exporter +--markup=markdown +--main=README.md +./lib/opentelemetry/exporter/otlp-logs/**/*.rb +./lib/opentelemetry/exporter/otlp.rb +- +README.md +CHANGELOG.md diff --git a/exporter/otlp-logs/Appraisals b/exporter/otlp-logs/Appraisals new file mode 100644 index 0000000000..362c129911 --- /dev/null +++ b/exporter/otlp-logs/Appraisals @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +(14..23).each do |i| + version = "3.#{i}" + appraise "google-protobuf-#{version}" do + gem 'google-protobuf', "~> #{version}" + end +end diff --git a/exporter/otlp-logs/CHANGELOG.md b/exporter/otlp-logs/CHANGELOG.md new file mode 100644 index 0000000000..8b2d7c12e9 --- /dev/null +++ b/exporter/otlp-logs/CHANGELOG.md @@ -0,0 +1,182 @@ +# Release History: opentelemetry-exporter-otlp + +### v0.26.1 / 2023-07-29 + +* FIXED: Regenerate v0.20.0 protos +* ADDED: Allow google-protobuf ~> 3.14 + +### v0.26.0 / 2023-06-13 + +* ADDED: Use OTLP 0.20.0 protos + +### v0.25.0 / 2023-06-01 + +* BREAKING CHANGE: Remove support for EoL Ruby 2.7 + +* ADDED: Remove support for EoL Ruby 2.7 +* FIXED: Make version available to user agent header #1458 + +### v0.24.1 / 2023-05-30 + +* FIXED: Add Ruby 3.2 to CI and do small fix +* FIXED: Adds User-Agent header in OTLP exporter + +### v0.24.0 / 2022-09-14 + +* ADDED: Support InstrumentationScope, and update OTLP proto to 0.18.0 +* FIXED: Handle OTLP exporter 404s discretely +* FIXED: `OTEL_EXPORTER_OTLP_ENDPOINT` appends the correct path with a trailing slash +* FIXED: OTLP exporter demo code +* DOCS: Update exporter default compression setting + +### v0.23.0 / 2022-06-23 + +* ADDED: Report bundle size stats in exporter; also don't re-gzip unnecessarily + +### v0.22.0 / 2022-06-09 + +* ADDED: Otlp grpc + +### v0.21.3 / 2022-05-12 + +* (No significant changes) + +### v0.21.2 / 2022-01-19 + +* FIXED: Default scheme for OTLP endpoint +* FIXED: Remove TIMEOUT status from OTLP exporter (#1087) + +### v0.21.1 / 2021-12-31 + +* FIXED: Allow OTLP Exporter compression value of `none` + +### v0.21.0 / 2021-12-01 + +* ADDED: Exporter should use gzip compression by default + +### v0.20.6 / 2021-10-29 + +* FIXED: Add unexpected error handlign in BSP and OTLP exporter (#995) +* FIXED: Handle otlp exporter race condition gzip errors with retry + +### v0.20.5 / 2021-09-29 + +* (No significant changes) + +### v0.20.4 / 2021-09-29 + +* FIXED: OTLP Export Header Format + +### v0.20.3 / 2021-08-19 + +* FIXED: OTLP exporter missing failure metrics + +### v0.20.2 / 2021-08-12 + +* FIXED: Add rescue for OpenSSL errors during export +* DOCS: Update docs to rely more on environment variable configuration + +### v0.20.1 / 2021-06-29 + +* FIXED: Otlp encoding exceptions again + +### v0.20.0 / 2021-06-23 + +* BREAKING CHANGE: Total order constraint on span.status= + +* FIXED: Total order constraint on span.status= + +### v0.19.0 / 2021-06-03 + +* ADDED: Add a SSL verify mode option for the OTLP exporter +* FIXED: Handle OTLP exporter encoding exceptions +* DOCS: Remove the OTLP receiver legacy gRPC port(55680) references + +### v0.18.0 / 2021-05-21 + +* BREAKING CHANGE: Replace Time.now with Process.clock_gettime + +* FIXED: Replace Time.now with Process.clock_gettime +* FIXED: Rescue missed otlp exporter network errors + +### v0.17.0 / 2021-04-22 + +* ADDED: Add zipkin exporter + +### v0.16.0 / 2021-03-17 + +* BREAKING CHANGE: Implement Exporter#force_flush + +* ADDED: Implement Exporter#force_flush +* FIXED: Rescue socket err in otlp exporter to prevent failures unable to connect +* DOCS: Replace Gitter with GitHub Discussions + +### v0.15.0 / 2021-02-18 + +* BREAKING CHANGE: Streamline processor pipeline + +* ADDED: Add otlp exporter hooks +* FIXED: Streamline processor pipeline + +### v0.14.0 / 2021-02-03 + +* (No significant changes) + +### v0.13.0 / 2021-01-29 + +* BREAKING CHANGE: Spec compliance for OTLP exporter + +* ADDED: Add untraced wrapper to common utils +* FIXED: Spec compliance for OTLP exporter +* FIXED: Conditionally append path to collector endpoint +* FIXED: OTLP path should be /v1/traces +* FIXED: Rename OTLP env vars SPAN -> TRACES + +### v0.12.1 / 2021-01-13 + +* FIXED: Updated protobuf version dependency + +### v0.12.0 / 2020-12-24 + +* (No significant changes) + +### v0.11.0 / 2020-12-11 + +* BREAKING CHANGE: Implement tracestate + +* ADDED: Implement tracestate +* ADDED: Metrics reporting from trace export +* FIXED: Copyright comments to not reference year + +### v0.10.0 / 2020-12-03 + +* (No significant changes) + +### v0.9.0 / 2020-11-27 + +* BREAKING CHANGE: Add timeout for force_flush and shutdown + +* ADDED: Add timeout for force_flush and shutdown +* FIXED: Remove unused kwarg from otlp exporter retry + +### v0.8.0 / 2020-10-27 + +* BREAKING CHANGE: Move context/span methods to Trace module +* BREAKING CHANGE: Remove 'canonical' from status codes +* BREAKING CHANGE: Assorted SpanContext fixes + +* FIXED: Move context/span methods to Trace module +* FIXED: Remove 'canonical' from status codes +* FIXED: Add gzip support to OTLP exporter +* FIXED: Assorted SpanContext fixes + +### v0.7.0 / 2020-10-07 + +* FIXED: OTLP parent_span_id should be nil for root +* DOCS: Fix use of add_event in OTLP doc +* DOCS: Standardize toplevel docs structure and readme +* DOCS: Use BatchSpanProcessor in examples + +### v0.6.0 / 2020-09-10 + +* Initial release. diff --git a/exporter/otlp-logs/Gemfile b/exporter/otlp-logs/Gemfile new file mode 100644 index 0000000000..a87d415b14 --- /dev/null +++ b/exporter/otlp-logs/Gemfile @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +group :test, :development do + gem 'opentelemetry-api', path: '../../api' + gem 'opentelemetry-common', path: '../../common' + gem 'opentelemetry-logs-api', path: '../../logs_api' + gem 'opentelemetry-logs-sdk', path: '../../logs_sdk' + gem 'opentelemetry-registry', path: '../../registry' + gem 'opentelemetry-sdk', path: '../../sdk' + gem 'opentelemetry-semantic_conventions', path: '../../semantic_conventions' + gem 'opentelemetry-test-helpers', path: '../../test_helpers' +end diff --git a/exporter/otlp-logs/LICENSE b/exporter/otlp-logs/LICENSE new file mode 100644 index 0000000000..1ef7dad2c5 --- /dev/null +++ b/exporter/otlp-logs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright The OpenTelemetry Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/exporter/otlp-logs/README.md b/exporter/otlp-logs/README.md new file mode 100644 index 0000000000..55d09b8470 --- /dev/null +++ b/exporter/otlp-logs/README.md @@ -0,0 +1,154 @@ +# opentelemetry-exporter-otlp + +The `opentelemetry-exporter-otlp` gem provides an [OTLP](https://github.com/open-telemetry/opentelemetry-proto) exporter for OpenTelemetry for Ruby. Using `opentelemetry-exporter-otlp`, an application can configure OpenTelemetry to export collected tracing data to [the OpenTelemetry Collector][opentelemetry-collector-home]. + +## What is OpenTelemetry? + +[OpenTelemetry][opentelemetry-home] is an open source observability framework, providing a general-purpose API, SDK, and related tools required for the instrumentation of cloud-native software, frameworks, and libraries. + +OpenTelemetry provides a single set of APIs, libraries, agents, and collector services to capture distributed traces and metrics from your application. You can analyze them using Prometheus, Jaeger, and other observability tools. + +## How does this gem fit in? + +The `opentelemetry-exporter-otlp` gem is a plugin that provides OTLP export. To export to the OpenTelemetry Collector, an application can include this gem along with `opentelemetry-sdk`, and configure the `SDK` to use the provided OTLP exporter as a span processor. + +Generally, *libraries* that produce telemetry data should avoid depending directly on specific exporter, deferring that choice to the application developer. + +### Supported protocol version + +This gem supports the [v0.20.0 release][otel-proto-release] of OTLP. + +## How do I get started? + +Install the gem using: + +```console + +gem install opentelemetry-sdk +gem install opentelemetry-exporter-otlp + +``` + +Or, if you use [bundler][bundler-home], include `opentelemetry-sdk` in your `Gemfile`. + +Then, configure the SDK to use the OTLP exporter as a span processor, and use the OpenTelemetry interfaces to produces traces and other information. Following is a basic example. + +```ruby +require 'opentelemetry/sdk' +require 'opentelemetry/exporter/otlp' + +# The OTLP exporter is the default, so no configuration is needed. +# However, it could be manually selected via an environment variable if required: +# +# ENV['OTEL_TRACES_EXPORTER'] = 'otlp' +# +# You may also configure various settings via environment variables: +# ENV['OTEL_EXPORTER_OTLP_COMPRESSION'] = 'gzip' + +OpenTelemetry::SDK.configure + +# To start a trace you need to get a Tracer from the TracerProvider +tracer_provider = OpenTelemetry.tracer_provider +tracer = tracer_provider.tracer('my_app_or_gem', '0.1.0') + +# create a span +tracer.in_span('foo') do |span| + # set an attribute + span.set_attribute('platform', 'osx') + # add an event + span.add_event('event in bar') + # create bar as child of foo + tracer.in_span('bar') do |child_span| + # inspect the span + pp child_span + end +end + +tracer_provider.shutdown +``` + +For additional examples, see the [examples on github][examples-github]. + +## How can I configure the OTLP exporter? + +The collector exporter can be configured explicitly in code, or via environment variables as shown above. The configuration parameters, environment variables, and defaults are shown below. + +| Parameter | Environment variable | Default | +| ------------------- | -------------------------------------------- | ----------------------------------- | +| `endpoint:` | `OTEL_EXPORTER_OTLP_ENDPOINT` | `"http://localhost:4318/v1/traces"` | +| `certificate_file: `| `OTEL_EXPORTER_OTLP_CERTIFICATE` | | +| `headers:` | `OTEL_EXPORTER_OTLP_HEADERS` | | +| `compression:` | `OTEL_EXPORTER_OTLP_COMPRESSION` | `"gzip"` | +| `timeout:` | `OTEL_EXPORTER_OTLP_TIMEOUT` | `10` | +| `ssl_verify_mode:` | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER` or | `OpenSSL::SSL:VERIFY_PEER` | +| | `OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE` | | + +`ssl_verify_mode:` parameter values should be flags for server certificate verification: `OpenSSL::SSL:VERIFY_PEER` and `OpenSSL::SSL:VERIFY_NONE` are acceptable. These values can also be set using the appropriately named environment variables as shown where `VERIFY_PEER` will take precedence over `VERIFY_NONE`. Please see [the Net::HTTP docs](https://ruby-doc.org/stdlib-2.7.6/libdoc/net/http/rdoc/Net/HTTP.html#verify_mode) for more information about these flags. + +## How can I get involved? + +The `opentelemetry-exporter-otlp` gem source is [on github][repo-github], along with related gems including `opentelemetry-sdk`. + +The OpenTelemetry Ruby gems are maintained by the OpenTelemetry-Ruby special interest group (SIG). You can get involved by joining us in [GitHub Discussions][discussions-url] or attending our weekly meeting. See the [meeting calendar][community-meetings] for dates and times. For more information on this and other language SIGs, see the OpenTelemetry [community page][ruby-sig]. + +## License + +The `opentelemetry-exporter-otlp` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. + +## Working with Proto Definitions + +The OTel community maintains a [repository with protobuf definitions][otel-proto-github] that language and collector implementors use to generate code. + +Maintainers are expected to keep up to date with the latest version of protos. This guide will provide you with step-by-step instructions on updating the OTLP Exporter gem with the latest definitions. + +### System Requirements + +- [`git` 2.41+][git-install] +- [`protoc` 22.5][protoc-install] +- [Ruby 3+][ruby-downloads] + +> :warning: `protoc 23.x` *changes the Ruby code generator to emit a serialized proto instead of a DSL.* . Please ensure you use `protoc` version `22.x` in order to ensure we remain compatible with versions of protobuf prior to `google-protobuf` gem `3.18`. + +### Upgrade Proto Definitions + +**Update the target otel-proto version in the `Rakefile` that matches a release `tag` in the proto repo, e.g.** + +```ruby + # Rakefile + + # https://github.com/open-telemetry/opentelemetry-proto/tree/v0.20.0 + PROTO_VERSION = `v0.20.0` +``` + +**Generate the Ruby source files using `rake`:** + +```console + +$> bundle exec rake protobuf:generate + +``` + +**Run tests and fix any errors:** + +```console + +$> bundle exec rake test + +``` + +**Commit the chnages and open a PR!** + +[opentelemetry-collector-home]: https://opentelemetry.io/docs/collector/about/ +[opentelemetry-home]: https://opentelemetry.io +[bundler-home]: https://bundler.io +[repo-github]: https://github.com/open-telemetry/opentelemetry-ruby +[license-github]: https://github.com/open-telemetry/opentelemetry-ruby/blob/main/LICENSE +[examples-github]: https://github.com/open-telemetry/opentelemetry-ruby/tree/main/examples +[ruby-sig]: https://github.com/open-telemetry/community#ruby-sig +[community-meetings]: https://github.com/open-telemetry/community#community-meetings +[discussions-url]: https://github.com/open-telemetry/opentelemetry-ruby/discussions +[git-install]: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git +[protoc-install]: https://github.com/protocolbuffers/protobuf/releases/tag/v22.5 +[ruby-downloads]: https://www.ruby-lang.org/en/downloads/ +[otel-proto-github]: https://github.com/open-telemetry/opentelemetry-proto +[otel-proto-release]: https://github.com/open-telemetry/opentelemetry-proto/releases/tag/v0.20.0 diff --git a/exporter/otlp-logs/Rakefile b/exporter/otlp-logs/Rakefile new file mode 100644 index 0000000000..ce1edbd6e9 --- /dev/null +++ b/exporter/otlp-logs/Rakefile @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' + +require 'rubocop/rake_task' +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.libs << '../../api/lib' + t.libs << '../../sdk/lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +if RUBY_ENGINE == 'truffleruby' + task default: %i[test] +else + task default: %i[test rubocop yard] +end + +# https://github.com/open-telemetry/opentelemetry-proto/tree/v0.20.0 +PROTO_VERSION = 'v0.20.0' + +namespace :protobuf do + task :clean do + FileUtils.rm_rf('lib/opentelemetry/proto') + FileUtils.rm_rf('opentelemetry-proto') + end + + desc "Generate Ruby Source files from OTel Proto Version #{PROTO_VERSION}" + task generate: [:clean] do + system("git clone -b #{PROTO_VERSION} https://github.com/open-telemetry/opentelemetry-proto", exception: true) + Dir['opentelemetry-proto/opentelemetry/proto/**/*.proto'].each do |file| + system("protoc --ruby_out=lib/ --proto_path=opentelemetry-proto #{file.gsub('opentelemetry-proto/', '')}", exception: true) + end + FileUtils.rm_rf('opentelemetry-proto') + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry-exporter-otlp.rb b/exporter/otlp-logs/lib/opentelemetry-exporter-otlp.rb new file mode 100644 index 0000000000..1af5afb085 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry-exporter-otlp.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/exporter/otlp_logs' diff --git a/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs_exporter.rb b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs_exporter.rb new file mode 100644 index 0000000000..1b9a2fa502 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/logs_exporter.rb @@ -0,0 +1,385 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/common' +require 'opentelemetry/sdk' +require 'opentelemetry/sdk/logs' +require 'net/http' +require 'csv' +require 'zlib' + +require 'google/rpc/status_pb' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' +require 'opentelemetry/proto/logs/v1/logs_pb' +require 'opentelemetry/proto/collector/logs/v1/logs_service_pb' + +module OpenTelemetry + module Exporter + module OTLP + # An OpenTelemetry log exporter that sends log records over HTTP as Protobuf encoded OTLP ExportLogsServiceRequests. + class LogsExporter # rubocop:disable Metrics/ClassLength + SUCCESS = OpenTelemetry::SDK::Logs::Export::SUCCESS + FAILURE = OpenTelemetry::SDK::Logs::Export::FAILURE + private_constant(:SUCCESS, :FAILURE) + + # Default timeouts in seconds. + KEEP_ALIVE_TIMEOUT = 30 + RETRY_COUNT = 5 + WRITE_TIMEOUT_SUPPORTED = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.6') + private_constant(:KEEP_ALIVE_TIMEOUT, :RETRY_COUNT, :WRITE_TIMEOUT_SUPPORTED) + + ERROR_MESSAGE_INVALID_HEADERS = 'headers must be a String with comma-separated URL Encoded UTF-8 k=v pairs or a Hash' + private_constant(:ERROR_MESSAGE_INVALID_HEADERS) + + DEFAULT_USER_AGENT = "OTel-OTLP-Exporter-Ruby/#{OpenTelemetry::Exporter::OTLP::VERSION} Ruby/#{RUBY_VERSION} (#{RUBY_PLATFORM}; #{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION})".freeze + + def self.ssl_verify_mode + if ENV.key?('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER') + OpenSSL::SSL::VERIFY_PEER + elsif ENV.key?('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE') + OpenSSL::SSL::VERIFY_NONE + else + OpenSSL::SSL::VERIFY_PEER + end + end + + def initialize(endpoint: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT', default: 'http://localhost:4318/v1/logs'), + certificate_file: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE', 'OTEL_EXPORTER_OTLP_CERTIFICATE'), + ssl_verify_mode: LogsExporter.ssl_verify_mode, + headers: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_HEADERS', 'OTEL_EXPORTER_OTLP_HEADERS', default: {}), + compression: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', 'OTEL_EXPORTER_OTLP_COMPRESSION', default: 'gzip'), + timeout: OpenTelemetry::Common::Utilities.config_opt('OTEL_EXPORTER_OTLP_LOGS_TIMEOUT', 'OTEL_EXPORTER_OTLP_TIMEOUT', default: 10)) + raise ArgumentError, "invalid url for OTLP::Exporter #{endpoint}" unless OpenTelemetry::Common::Utilities.valid_url?(endpoint) + raise ArgumentError, "unsupported compression key #{compression}" unless compression.nil? || %w[gzip none].include?(compression) + + @uri = if endpoint == ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] + URI.join(endpoint, 'v1/logs') + else + URI(endpoint) + end + + @http = http_connection(@uri, ssl_verify_mode, certificate_file) + + @path = @uri.path + @headers = prepare_headers(headers) + @timeout = timeout.to_f + @compression = compression + @shutdown = false + end + + # Called to export sampled {OpenTelemetry::SDK::Logs::LogRecordData} structs. + # + # @param [Enumerable] log_record_data the + # list of recorded {OpenTelemetry::SDK::Logs::LogRecordData} structs to be + # exported. + # @param [optional Numeric] timeout An optional timeout in seconds. + # @return [Integer] the result of the export. + def export(log_record_data, timeout: nil) + OpenTelemetry.logger.error('Logs Exporter tried to export, but it has already shut down') if @shutdown + return FAILURE if @shutdown + + send_bytes(encode(log_record_data), timeout: timeout) + end + + # Called when {OpenTelemetry::SDK::Logs::LoggerProvider#force_flush} is called, if + # this exporter is registered to a {OpenTelemetry::SDK::Logs::LoggerProvider} + # object. + # + # @param [optional Numeric] timeout An optional timeout in seconds. + def force_flush(timeout: nil) + SUCCESS + end + + # Called when {OpenTelemetry::SDK::Logs::LoggerProvider#shutdown} is called, if + # this exporter is registered to a {OpenTelemetry::SDK::Logs::LoggerProvider} + # object. + # + # @param [optional Numeric] timeout An optional timeout in seconds. + def shutdown(timeout: nil) + @shutdown = true + @http.finish if @http.started? + SUCCESS + end + + private + + def http_connection(uri, ssl_verify_mode, certificate_file) + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = uri.scheme == 'https' + http.verify_mode = ssl_verify_mode + http.ca_file = certificate_file unless certificate_file.nil? + http.keep_alive_timeout = KEEP_ALIVE_TIMEOUT + http + end + + # The around_request is a private method that provides an extension + # point for the exporters network calls. The default behaviour + # is to not record these operations. + # + # An example use case would be to prepend a patch, or extend this class + # and override this method's behaviour to explicitly record the HTTP request. + # This would allow you to create log records for your export pipeline. + def around_request + OpenTelemetry::Common::Utilities.untraced { yield } # rubocop:disable Style/ExplicitBlockArgument + end + + def send_bytes(bytes, timeout:) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + return FAILURE if bytes.nil? + + request = Net::HTTP::Post.new(@path) + if @compression == 'gzip' + request.add_field('Content-Encoding', 'gzip') + body = Zlib.gzip(bytes) + else + body = bytes + end + + request.body = body + request.add_field('Content-Type', 'application/x-protobuf') + @headers.each { |key, value| request.add_field(key, value) } + + retry_count = 0 + timeout ||= @timeout + start_time = OpenTelemetry::Common::Utilities.timeout_timestamp + + around_request do + remaining_timeout = OpenTelemetry::Common::Utilities.maybe_timeout(timeout, start_time) + return FAILURE if remaining_timeout.zero? + + @http.open_timeout = remaining_timeout + @http.read_timeout = remaining_timeout + @http.write_timeout = remaining_timeout if WRITE_TIMEOUT_SUPPORTED + @http.start unless @http.started? + response = measure_request_duration { @http.request(request) } + + case response + when Net::HTTPOK + response.body # Read and discard body + SUCCESS + when Net::HTTPServiceUnavailable, Net::HTTPTooManyRequests + response.body # Read and discard body + redo if backoff?(retry_after: response['Retry-After'], retry_count: retry_count += 1, reason: response.code) + FAILURE + when Net::HTTPRequestTimeOut, Net::HTTPGatewayTimeOut, Net::HTTPBadGateway + response.body # Read and discard body + redo if backoff?(retry_count: retry_count += 1, reason: response.code) + FAILURE + when Net::HTTPNotFound + OpenTelemetry.handle_error(message: "OTLP exporter received http.code=404 for uri: '#{@path}'") + FAILURE + when Net::HTTPBadRequest, Net::HTTPClientError, Net::HTTPServerError + log_status(response.body) + FAILURE + when Net::HTTPRedirection + @http.finish + handle_redirect(response['location']) + redo if backoff?(retry_after: 0, retry_count: retry_count += 1, reason: response.code) + else + @http.finish + FAILURE + end + rescue Net::OpenTimeout, Net::ReadTimeout + retry if backoff?(retry_count: retry_count += 1, reason: 'timeout') + return FAILURE + rescue OpenSSL::SSL::SSLError + retry if backoff?(retry_count: retry_count += 1, reason: 'openssl_error') + return FAILURE + rescue SocketError + retry if backoff?(retry_count: retry_count += 1, reason: 'socket_error') + return FAILURE + rescue SystemCallError => e + retry if backoff?(retry_count: retry_count += 1, reason: e.class.name) + return FAILURE + rescue EOFError + retry if backoff?(retry_count: retry_count += 1, reason: 'eof_error') + return FAILURE + rescue Zlib::DataError + retry if backoff?(retry_count: retry_count += 1, reason: 'zlib_error') + return FAILURE + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#send_bytes') + return FAILURE + end + ensure + # Reset timeouts to defaults for the next call. + @http.open_timeout = @timeout + @http.read_timeout = @timeout + @http.write_timeout = @timeout if WRITE_TIMEOUT_SUPPORTED + end + + def handle_redirect(location) + # TODO: figure out destination and reinitialize @http and @path + end + + def log_status(body) + status = Google::Rpc::Status.decode(body) + details = status.details.map do |detail| + klass_or_nil = ::Google::Protobuf::DescriptorPool.generated_pool.lookup(detail.type_name).msgclass + detail.unpack(klass_or_nil) if klass_or_nil + end.compact + OpenTelemetry.handle_error(message: "OTLP exporter received rpc.Status{message=#{status.message}, details=#{details}}") + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: 'unexpected error decoding rpc.Status in OTLP::Exporter#log_status') + end + + def measure_request_duration + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + begin + response = yield + ensure + stop = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration_ms = 1000.0 * (stop - start) + end + end + + def backoff?(retry_count:, reason:, retry_after: nil) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + return false if retry_count > RETRY_COUNT + + sleep_interval = nil + unless retry_after.nil? + sleep_interval = + begin + Integer(retry_after) + rescue ArgumentError + nil + end + sleep_interval ||= + begin + Time.httpdate(retry_after) - Time.now + rescue # rubocop:disable Style/RescueStandardError + nil + end + sleep_interval = nil unless sleep_interval&.positive? + end + sleep_interval ||= rand(2**retry_count) + + sleep(sleep_interval) + true + end + + def encode(log_record_data) # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.encode( + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.new( + resource_logs: log_record_data + .group_by(&:resource) + .map do |resource, log_record_datas| + Opentelemetry::Proto::Logs::V1::ResourceLogs.new( + resource: Opentelemetry::Proto::Resource::V1::Resource.new( + attributes: resource.attribute_enumerator.map { |key, value| as_otlp_key_value(key, value) } + ), + scope_logs: log_record_datas + .group_by(&:instrumentation_scope) + .map do |il, lrd| + Opentelemetry::Proto::Logs::V1::ScopeLogs.new( + scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new( + name: il.name, + version: il.version + ), + log_records: lrd.map { |lr| as_otlp_log_record(lr) } + ) + end + ) + end + ) + ) + rescue StandardError => e + OpenTelemetry.handle_error(exception: e, message: 'unexpected error in OTLP::Exporter#encode') + nil + end + + def as_otlp_log_record(log_record_data) + Opentelemetry::Proto::Logs::V1::LogRecord.new( + time_unix_nano: log_record_data.unix_nano_timestamp, + observed_time_unix_nano: log_record_data.unix_nano_observed_timestamp, + severity_number: as_otlp_severity_number(log_record_data.severity_number), + severity_text: log_record_data.severity_text, + body: as_otlp_any_value(log_record_data.body), + attributes: log_record_data.attributes&.map { |k, v| as_otlp_key_value(k, v) }, + dropped_attributes_count: log_record_data.total_recorded_attributes - log_record_data.attributes&.size.to_i, + flags: log_record_data.trace_flags.instance_variable_get(:@flags), + trace_id: log_record_data.trace_id, + span_id: log_record_data.span_id + ) + end + + def as_otlp_key_value(key, value) + Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value(value)) + rescue Encoding::UndefinedConversionError => e + encoded_value = value.encode('UTF-8', invalid: :replace, undef: :replace, replace: '�') + OpenTelemetry.handle_error(exception: e, message: "encoding error for key #{key} and value #{encoded_value}") + Opentelemetry::Proto::Common::V1::KeyValue.new(key: key, value: as_otlp_any_value('Encoding Error')) + end + + def as_otlp_any_value(value) + result = Opentelemetry::Proto::Common::V1::AnyValue.new + case value + when String + result.string_value = value + when Integer + result.int_value = value + when Float + result.double_value = value + when true, false + result.bool_value = value + when Array + values = value.map { |element| as_otlp_any_value(element) } + result.array_value = Opentelemetry::Proto::Common::V1::ArrayValue.new(values: values) + end + result + end + + # TODO: maybe don't translate the severity number, but translate the severity text into + # the number if the number is nil? Poss. change to allow for adding your own + # otel values? + def as_otlp_severity_number(severity_number) + case severity_number + when 0 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_DEBUG + when 1 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_INFO + when 2 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_WARN + when 3 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_ERROR + when 4 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_FATAL + when 5 then Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_UNSPECIFIED + end + end + + def prepare_headers(config_headers) + headers = case config_headers + when String then parse_headers(config_headers) + when Hash then config_headers.dup + else + raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS + end + + headers['User-Agent'] = "#{headers.fetch('User-Agent', '')} #{DEFAULT_USER_AGENT}".strip + + headers + end + + def parse_headers(raw) + entries = raw.split(',') + raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if entries.empty? + + entries.each_with_object({}) do |entry, headers| + k, v = entry.split('=', 2).map(&CGI.method(:unescape)) + begin + k = k.to_s.strip + v = v.to_s.strip + rescue Encoding::CompatibilityError + raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS + rescue ArgumentError => e + raise e, ERROR_MESSAGE_INVALID_HEADERS + end + raise ArgumentError, ERROR_MESSAGE_INVALID_HEADERS if k.empty? || v.empty? + + headers[k] = v + end + end + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/version.rb b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/version.rb new file mode 100644 index 0000000000..35e8a783b7 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp/version.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Exporter + module OTLP + ## Current OpenTelemetry OTLP exporter version + VERSION = '0.26.3' + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/exporter/otlp_logs.rb b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp_logs.rb new file mode 100644 index 0000000000..b1c041c5ed --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/exporter/otlp_logs.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/exporter/otlp/version' +require 'opentelemetry/exporter/otlp/logs_exporter' + +# OpenTelemetry is an open source observability framework, providing a +# general-purpose API, SDK, and related tools required for the instrumentation +# of cloud-native software, frameworks, and libraries. +# +# The OpenTelemetry module provides global accessors for telemetry objects. +# See the documentation for the `opentelemetry-api` gem for details. +module OpenTelemetry +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb new file mode 100644 index 0000000000..ceeb940e82 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/logs/v1/logs_service_pb.rb @@ -0,0 +1,35 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/logs/v1/logs_service.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/logs/v1/logs_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/collector/logs/v1/logs_service.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest" do + repeated :resource_logs, :message, 1, "opentelemetry.proto.logs.v1.ResourceLogs" + end + add_message "opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse" do + optional :partial_success, :message, 1, "opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess" + end + add_message "opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess" do + optional :rejected_log_records, :int64, 1 + optional :error_message, :string, 2 + end + end +end + +module Opentelemetry + module Proto + module Collector + module Logs + module V1 + ExportLogsServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.logs.v1.ExportLogsServiceRequest").msgclass + ExportLogsServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.logs.v1.ExportLogsServiceResponse").msgclass + ExportLogsPartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.logs.v1.ExportLogsPartialSuccess").msgclass + end + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb new file mode 100644 index 0000000000..15654a2cfd --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/metrics/v1/metrics_service_pb.rb @@ -0,0 +1,35 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/metrics/v1/metrics_service.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/metrics/v1/metrics_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/collector/metrics/v1/metrics_service.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest" do + repeated :resource_metrics, :message, 1, "opentelemetry.proto.metrics.v1.ResourceMetrics" + end + add_message "opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse" do + optional :partial_success, :message, 1, "opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess" + end + add_message "opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess" do + optional :rejected_data_points, :int64, 1 + optional :error_message, :string, 2 + end + end +end + +module Opentelemetry + module Proto + module Collector + module Metrics + module V1 + ExportMetricsServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceRequest").msgclass + ExportMetricsServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.metrics.v1.ExportMetricsServiceResponse").msgclass + ExportMetricsPartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.metrics.v1.ExportMetricsPartialSuccess").msgclass + end + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb new file mode 100644 index 0000000000..4d0d62f22e --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/collector/trace/v1/trace_service_pb.rb @@ -0,0 +1,35 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/collector/trace/v1/trace_service.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/trace/v1/trace_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/collector/trace/v1/trace_service.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest" do + repeated :resource_spans, :message, 1, "opentelemetry.proto.trace.v1.ResourceSpans" + end + add_message "opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse" do + optional :partial_success, :message, 1, "opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess" + end + add_message "opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess" do + optional :rejected_spans, :int64, 1 + optional :error_message, :string, 2 + end + end +end + +module Opentelemetry + module Proto + module Collector + module Trace + module V1 + ExportTraceServiceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest").msgclass + ExportTraceServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.trace.v1.ExportTraceServiceResponse").msgclass + ExportTracePartialSuccess = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.collector.trace.v1.ExportTracePartialSuccess").msgclass + end + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb new file mode 100644 index 0000000000..a49580ec51 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/common/v1/common_pb.rb @@ -0,0 +1,50 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/common/v1/common.proto + +require 'google/protobuf' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/common/v1/common.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.common.v1.AnyValue" do + oneof :value do + optional :string_value, :string, 1 + optional :bool_value, :bool, 2 + optional :int_value, :int64, 3 + optional :double_value, :double, 4 + optional :array_value, :message, 5, "opentelemetry.proto.common.v1.ArrayValue" + optional :kvlist_value, :message, 6, "opentelemetry.proto.common.v1.KeyValueList" + optional :bytes_value, :bytes, 7 + end + end + add_message "opentelemetry.proto.common.v1.ArrayValue" do + repeated :values, :message, 1, "opentelemetry.proto.common.v1.AnyValue" + end + add_message "opentelemetry.proto.common.v1.KeyValueList" do + repeated :values, :message, 1, "opentelemetry.proto.common.v1.KeyValue" + end + add_message "opentelemetry.proto.common.v1.KeyValue" do + optional :key, :string, 1 + optional :value, :message, 2, "opentelemetry.proto.common.v1.AnyValue" + end + add_message "opentelemetry.proto.common.v1.InstrumentationScope" do + optional :name, :string, 1 + optional :version, :string, 2 + repeated :attributes, :message, 3, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 4 + end + end +end + +module Opentelemetry + module Proto + module Common + module V1 + AnyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.AnyValue").msgclass + ArrayValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.ArrayValue").msgclass + KeyValueList = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.KeyValueList").msgclass + KeyValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.KeyValue").msgclass + InstrumentationScope = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.common.v1.InstrumentationScope").msgclass + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb new file mode 100644 index 0000000000..2419b86bfa --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/logs/v1/logs_pb.rb @@ -0,0 +1,83 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/logs/v1/logs.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/logs/v1/logs.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.logs.v1.LogsData" do + repeated :resource_logs, :message, 1, "opentelemetry.proto.logs.v1.ResourceLogs" + end + add_message "opentelemetry.proto.logs.v1.ResourceLogs" do + optional :resource, :message, 1, "opentelemetry.proto.resource.v1.Resource" + repeated :scope_logs, :message, 2, "opentelemetry.proto.logs.v1.ScopeLogs" + optional :schema_url, :string, 3 + end + add_message "opentelemetry.proto.logs.v1.ScopeLogs" do + optional :scope, :message, 1, "opentelemetry.proto.common.v1.InstrumentationScope" + repeated :log_records, :message, 2, "opentelemetry.proto.logs.v1.LogRecord" + optional :schema_url, :string, 3 + end + add_message "opentelemetry.proto.logs.v1.LogRecord" do + optional :time_unix_nano, :fixed64, 1 + optional :observed_time_unix_nano, :fixed64, 11 + optional :severity_number, :enum, 2, "opentelemetry.proto.logs.v1.SeverityNumber" + optional :severity_text, :string, 3 + optional :body, :message, 5, "opentelemetry.proto.common.v1.AnyValue" + repeated :attributes, :message, 6, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 7 + optional :flags, :fixed32, 8 + optional :trace_id, :bytes, 9 + optional :span_id, :bytes, 10 + end + add_enum "opentelemetry.proto.logs.v1.SeverityNumber" do + value :SEVERITY_NUMBER_UNSPECIFIED, 0 + value :SEVERITY_NUMBER_TRACE, 1 + value :SEVERITY_NUMBER_TRACE2, 2 + value :SEVERITY_NUMBER_TRACE3, 3 + value :SEVERITY_NUMBER_TRACE4, 4 + value :SEVERITY_NUMBER_DEBUG, 5 + value :SEVERITY_NUMBER_DEBUG2, 6 + value :SEVERITY_NUMBER_DEBUG3, 7 + value :SEVERITY_NUMBER_DEBUG4, 8 + value :SEVERITY_NUMBER_INFO, 9 + value :SEVERITY_NUMBER_INFO2, 10 + value :SEVERITY_NUMBER_INFO3, 11 + value :SEVERITY_NUMBER_INFO4, 12 + value :SEVERITY_NUMBER_WARN, 13 + value :SEVERITY_NUMBER_WARN2, 14 + value :SEVERITY_NUMBER_WARN3, 15 + value :SEVERITY_NUMBER_WARN4, 16 + value :SEVERITY_NUMBER_ERROR, 17 + value :SEVERITY_NUMBER_ERROR2, 18 + value :SEVERITY_NUMBER_ERROR3, 19 + value :SEVERITY_NUMBER_ERROR4, 20 + value :SEVERITY_NUMBER_FATAL, 21 + value :SEVERITY_NUMBER_FATAL2, 22 + value :SEVERITY_NUMBER_FATAL3, 23 + value :SEVERITY_NUMBER_FATAL4, 24 + end + add_enum "opentelemetry.proto.logs.v1.LogRecordFlags" do + value :LOG_RECORD_FLAGS_DO_NOT_USE, 0 + value :LOG_RECORD_FLAGS_TRACE_FLAGS_MASK, 255 + end + end +end + +module Opentelemetry + module Proto + module Logs + module V1 + LogsData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.logs.v1.LogsData").msgclass + ResourceLogs = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.logs.v1.ResourceLogs").msgclass + ScopeLogs = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.logs.v1.ScopeLogs").msgclass + LogRecord = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.logs.v1.LogRecord").msgclass + SeverityNumber = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.logs.v1.SeverityNumber").enummodule + LogRecordFlags = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.logs.v1.LogRecordFlags").enummodule + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb new file mode 100644 index 0000000000..743381736b --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/metrics/v1/metrics_pb.rb @@ -0,0 +1,159 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/metrics/v1/metrics.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/metrics/v1/metrics.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.metrics.v1.MetricsData" do + repeated :resource_metrics, :message, 1, "opentelemetry.proto.metrics.v1.ResourceMetrics" + end + add_message "opentelemetry.proto.metrics.v1.ResourceMetrics" do + optional :resource, :message, 1, "opentelemetry.proto.resource.v1.Resource" + repeated :scope_metrics, :message, 2, "opentelemetry.proto.metrics.v1.ScopeMetrics" + optional :schema_url, :string, 3 + end + add_message "opentelemetry.proto.metrics.v1.ScopeMetrics" do + optional :scope, :message, 1, "opentelemetry.proto.common.v1.InstrumentationScope" + repeated :metrics, :message, 2, "opentelemetry.proto.metrics.v1.Metric" + optional :schema_url, :string, 3 + end + add_message "opentelemetry.proto.metrics.v1.Metric" do + optional :name, :string, 1 + optional :description, :string, 2 + optional :unit, :string, 3 + oneof :data do + optional :gauge, :message, 5, "opentelemetry.proto.metrics.v1.Gauge" + optional :sum, :message, 7, "opentelemetry.proto.metrics.v1.Sum" + optional :histogram, :message, 9, "opentelemetry.proto.metrics.v1.Histogram" + optional :exponential_histogram, :message, 10, "opentelemetry.proto.metrics.v1.ExponentialHistogram" + optional :summary, :message, 11, "opentelemetry.proto.metrics.v1.Summary" + end + end + add_message "opentelemetry.proto.metrics.v1.Gauge" do + repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.NumberDataPoint" + end + add_message "opentelemetry.proto.metrics.v1.Sum" do + repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.NumberDataPoint" + optional :aggregation_temporality, :enum, 2, "opentelemetry.proto.metrics.v1.AggregationTemporality" + optional :is_monotonic, :bool, 3 + end + add_message "opentelemetry.proto.metrics.v1.Histogram" do + repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.HistogramDataPoint" + optional :aggregation_temporality, :enum, 2, "opentelemetry.proto.metrics.v1.AggregationTemporality" + end + add_message "opentelemetry.proto.metrics.v1.ExponentialHistogram" do + repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint" + optional :aggregation_temporality, :enum, 2, "opentelemetry.proto.metrics.v1.AggregationTemporality" + end + add_message "opentelemetry.proto.metrics.v1.Summary" do + repeated :data_points, :message, 1, "opentelemetry.proto.metrics.v1.SummaryDataPoint" + end + add_message "opentelemetry.proto.metrics.v1.NumberDataPoint" do + repeated :attributes, :message, 7, "opentelemetry.proto.common.v1.KeyValue" + optional :start_time_unix_nano, :fixed64, 2 + optional :time_unix_nano, :fixed64, 3 + repeated :exemplars, :message, 5, "opentelemetry.proto.metrics.v1.Exemplar" + optional :flags, :uint32, 8 + oneof :value do + optional :as_double, :double, 4 + optional :as_int, :sfixed64, 6 + end + end + add_message "opentelemetry.proto.metrics.v1.HistogramDataPoint" do + repeated :attributes, :message, 9, "opentelemetry.proto.common.v1.KeyValue" + optional :start_time_unix_nano, :fixed64, 2 + optional :time_unix_nano, :fixed64, 3 + optional :count, :fixed64, 4 + proto3_optional :sum, :double, 5 + repeated :bucket_counts, :fixed64, 6 + repeated :explicit_bounds, :double, 7 + repeated :exemplars, :message, 8, "opentelemetry.proto.metrics.v1.Exemplar" + optional :flags, :uint32, 10 + proto3_optional :min, :double, 11 + proto3_optional :max, :double, 12 + end + add_message "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint" do + repeated :attributes, :message, 1, "opentelemetry.proto.common.v1.KeyValue" + optional :start_time_unix_nano, :fixed64, 2 + optional :time_unix_nano, :fixed64, 3 + optional :count, :fixed64, 4 + proto3_optional :sum, :double, 5 + optional :scale, :sint32, 6 + optional :zero_count, :fixed64, 7 + optional :positive, :message, 8, "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets" + optional :negative, :message, 9, "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets" + optional :flags, :uint32, 10 + repeated :exemplars, :message, 11, "opentelemetry.proto.metrics.v1.Exemplar" + proto3_optional :min, :double, 12 + proto3_optional :max, :double, 13 + optional :zero_threshold, :double, 14 + end + add_message "opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets" do + optional :offset, :sint32, 1 + repeated :bucket_counts, :uint64, 2 + end + add_message "opentelemetry.proto.metrics.v1.SummaryDataPoint" do + repeated :attributes, :message, 7, "opentelemetry.proto.common.v1.KeyValue" + optional :start_time_unix_nano, :fixed64, 2 + optional :time_unix_nano, :fixed64, 3 + optional :count, :fixed64, 4 + optional :sum, :double, 5 + repeated :quantile_values, :message, 6, "opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile" + optional :flags, :uint32, 8 + end + add_message "opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile" do + optional :quantile, :double, 1 + optional :value, :double, 2 + end + add_message "opentelemetry.proto.metrics.v1.Exemplar" do + repeated :filtered_attributes, :message, 7, "opentelemetry.proto.common.v1.KeyValue" + optional :time_unix_nano, :fixed64, 2 + optional :span_id, :bytes, 4 + optional :trace_id, :bytes, 5 + oneof :value do + optional :as_double, :double, 3 + optional :as_int, :sfixed64, 6 + end + end + add_enum "opentelemetry.proto.metrics.v1.AggregationTemporality" do + value :AGGREGATION_TEMPORALITY_UNSPECIFIED, 0 + value :AGGREGATION_TEMPORALITY_DELTA, 1 + value :AGGREGATION_TEMPORALITY_CUMULATIVE, 2 + end + add_enum "opentelemetry.proto.metrics.v1.DataPointFlags" do + value :DATA_POINT_FLAGS_DO_NOT_USE, 0 + value :DATA_POINT_FLAGS_NO_RECORDED_VALUE_MASK, 1 + end + end +end + +module Opentelemetry + module Proto + module Metrics + module V1 + MetricsData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.MetricsData").msgclass + ResourceMetrics = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ResourceMetrics").msgclass + ScopeMetrics = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ScopeMetrics").msgclass + Metric = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Metric").msgclass + Gauge = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Gauge").msgclass + Sum = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Sum").msgclass + Histogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Histogram").msgclass + ExponentialHistogram = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ExponentialHistogram").msgclass + Summary = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Summary").msgclass + NumberDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.NumberDataPoint").msgclass + HistogramDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.HistogramDataPoint").msgclass + ExponentialHistogramDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint").msgclass + ExponentialHistogramDataPoint::Buckets = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint.Buckets").msgclass + SummaryDataPoint = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.SummaryDataPoint").msgclass + SummaryDataPoint::ValueAtQuantile = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.SummaryDataPoint.ValueAtQuantile").msgclass + Exemplar = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.Exemplar").msgclass + AggregationTemporality = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.AggregationTemporality").enummodule + DataPointFlags = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.metrics.v1.DataPointFlags").enummodule + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb new file mode 100644 index 0000000000..a10d5ad1f5 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/resource/v1/resource_pb.rb @@ -0,0 +1,25 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/resource/v1/resource.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/resource/v1/resource.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.resource.v1.Resource" do + repeated :attributes, :message, 1, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 2 + end + end +end + +module Opentelemetry + module Proto + module Resource + module V1 + Resource = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.resource.v1.Resource").msgclass + end + end + end +end diff --git a/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb b/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb new file mode 100644 index 0000000000..48c5737352 --- /dev/null +++ b/exporter/otlp-logs/lib/opentelemetry/proto/trace/v1/trace_pb.rb @@ -0,0 +1,90 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: opentelemetry/proto/trace/v1/trace.proto + +require 'google/protobuf' + +require 'opentelemetry/proto/common/v1/common_pb' +require 'opentelemetry/proto/resource/v1/resource_pb' + +Google::Protobuf::DescriptorPool.generated_pool.build do + add_file("opentelemetry/proto/trace/v1/trace.proto", :syntax => :proto3) do + add_message "opentelemetry.proto.trace.v1.TracesData" do + repeated :resource_spans, :message, 1, "opentelemetry.proto.trace.v1.ResourceSpans" + end + add_message "opentelemetry.proto.trace.v1.ResourceSpans" do + optional :resource, :message, 1, "opentelemetry.proto.resource.v1.Resource" + repeated :scope_spans, :message, 2, "opentelemetry.proto.trace.v1.ScopeSpans" + optional :schema_url, :string, 3 + end + add_message "opentelemetry.proto.trace.v1.ScopeSpans" do + optional :scope, :message, 1, "opentelemetry.proto.common.v1.InstrumentationScope" + repeated :spans, :message, 2, "opentelemetry.proto.trace.v1.Span" + optional :schema_url, :string, 3 + end + add_message "opentelemetry.proto.trace.v1.Span" do + optional :trace_id, :bytes, 1 + optional :span_id, :bytes, 2 + optional :trace_state, :string, 3 + optional :parent_span_id, :bytes, 4 + optional :name, :string, 5 + optional :kind, :enum, 6, "opentelemetry.proto.trace.v1.Span.SpanKind" + optional :start_time_unix_nano, :fixed64, 7 + optional :end_time_unix_nano, :fixed64, 8 + repeated :attributes, :message, 9, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 10 + repeated :events, :message, 11, "opentelemetry.proto.trace.v1.Span.Event" + optional :dropped_events_count, :uint32, 12 + repeated :links, :message, 13, "opentelemetry.proto.trace.v1.Span.Link" + optional :dropped_links_count, :uint32, 14 + optional :status, :message, 15, "opentelemetry.proto.trace.v1.Status" + end + add_message "opentelemetry.proto.trace.v1.Span.Event" do + optional :time_unix_nano, :fixed64, 1 + optional :name, :string, 2 + repeated :attributes, :message, 3, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 4 + end + add_message "opentelemetry.proto.trace.v1.Span.Link" do + optional :trace_id, :bytes, 1 + optional :span_id, :bytes, 2 + optional :trace_state, :string, 3 + repeated :attributes, :message, 4, "opentelemetry.proto.common.v1.KeyValue" + optional :dropped_attributes_count, :uint32, 5 + end + add_enum "opentelemetry.proto.trace.v1.Span.SpanKind" do + value :SPAN_KIND_UNSPECIFIED, 0 + value :SPAN_KIND_INTERNAL, 1 + value :SPAN_KIND_SERVER, 2 + value :SPAN_KIND_CLIENT, 3 + value :SPAN_KIND_PRODUCER, 4 + value :SPAN_KIND_CONSUMER, 5 + end + add_message "opentelemetry.proto.trace.v1.Status" do + optional :message, :string, 2 + optional :code, :enum, 3, "opentelemetry.proto.trace.v1.Status.StatusCode" + end + add_enum "opentelemetry.proto.trace.v1.Status.StatusCode" do + value :STATUS_CODE_UNSET, 0 + value :STATUS_CODE_OK, 1 + value :STATUS_CODE_ERROR, 2 + end + end +end + +module Opentelemetry + module Proto + module Trace + module V1 + TracesData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.TracesData").msgclass + ResourceSpans = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.ResourceSpans").msgclass + ScopeSpans = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.ScopeSpans").msgclass + Span = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Span").msgclass + Span::Event = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Span.Event").msgclass + Span::Link = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Span.Link").msgclass + Span::SpanKind = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Span.SpanKind").enummodule + Status = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Status").msgclass + Status::StatusCode = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("opentelemetry.proto.trace.v1.Status.StatusCode").enummodule + end + end + end +end diff --git a/exporter/otlp-logs/opentelemetry-exporter-otlp-logs.gemspec b/exporter/otlp-logs/opentelemetry-exporter-otlp-logs.gemspec new file mode 100644 index 0000000000..414843df60 --- /dev/null +++ b/exporter/otlp-logs/opentelemetry-exporter-otlp-logs.gemspec @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'opentelemetry/exporter/otlp/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-exporter-otlp-logs' + spec.version = OpenTelemetry::Exporter::OTLP::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'OTLP exporter for the OpenTelemetry framework' + spec.description = 'OTLP exporter for the OpenTelemetry framework' + spec.homepage = 'https://github.com/open-telemetry/opentelemetry-ruby' + spec.license = 'Apache-2.0' + + spec.files = ::Dir.glob('lib/**/*.rb') + + ::Dir.glob('*.md') + + ['LICENSE', '.yardopts'] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 3.0' + + spec.add_dependency 'googleapis-common-protos-types', '~> 1.3' + spec.add_dependency 'google-protobuf', '~> 3.14' + spec.add_dependency 'opentelemetry-api', '~> 1.1' + spec.add_dependency 'opentelemetry-common', '~> 0.20' + spec.add_dependency 'opentelemetry-logs-api' + spec.add_dependency 'opentelemetry-logs-sdk' + spec.add_dependency 'opentelemetry-sdk' + spec.add_dependency 'opentelemetry-semantic_conventions' + + spec.add_development_dependency 'appraisal', '~> 2.2.0' + spec.add_development_dependency 'bundler', '>= 1.17' + spec.add_development_dependency 'faraday', '~> 0.13' + spec.add_development_dependency 'minitest', '~> 5.0' + spec.add_development_dependency 'opentelemetry-test-helpers' + spec.add_development_dependency 'pry-byebug' unless RUBY_ENGINE == 'jruby' + spec.add_development_dependency 'rake', '~> 12.0' + spec.add_development_dependency 'rubocop', '~> 1.3' + spec.add_development_dependency 'simplecov', '~> 0.17' + spec.add_development_dependency 'webmock', '~> 3.7.6' + spec.add_development_dependency 'yard', '~> 0.9' + spec.add_development_dependency 'yard-doctest', '~> 0.1.6' + + if spec.respond_to?(:metadata) + spec.metadata['changelog_uri'] = "https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-exporter-otlp/v#{OpenTelemetry::Exporter::OTLP::VERSION}/file.CHANGELOG.html" + spec.metadata['source_code_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby/tree/main/exporter/otlp' + spec.metadata['bug_tracker_uri'] = 'https://github.com/open-telemetry/opentelemetry-ruby/issues' + spec.metadata['documentation_uri'] = "https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-exporter-otlp/v#{OpenTelemetry::Exporter::OTLP::VERSION}" + end +end diff --git a/exporter/otlp-logs/test/.rubocop.yml b/exporter/otlp-logs/test/.rubocop.yml new file mode 100644 index 0000000000..3ceaea6dff --- /dev/null +++ b/exporter/otlp-logs/test/.rubocop.yml @@ -0,0 +1,8 @@ +inherit_from: ../.rubocop.yml + +Style/MethodCallWithoutArgsParentheses: + Exclude: + - 'opentelemetry/exporter/otlp/exporter_test.rb' +Style/BlockDelimiters: + Exclude: + - 'opentelemetry/exporter/otlp/exporter_test.rb' diff --git a/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb new file mode 100644 index 0000000000..667b24b4b5 --- /dev/null +++ b/exporter/otlp-logs/test/opentelemetry/exporter/otlp/logs_exporter_test.rb @@ -0,0 +1,696 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 +require 'test_helper' +require 'google/protobuf/wrappers_pb' +require 'google/protobuf/well_known_types' + +describe OpenTelemetry::Exporter::OTLP::LogsExporter do + SUCCESS = OpenTelemetry::SDK::Logs::Export::SUCCESS + FAILURE = OpenTelemetry::SDK::Logs::Export::FAILURE + VERSION = OpenTelemetry::Exporter::OTLP::VERSION + DEFAULT_USER_AGENT = OpenTelemetry::Exporter::OTLP::LogsExporter::DEFAULT_USER_AGENT + + describe '#initialize' do + it 'initializes with defaults' do + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new + _(exp).wont_be_nil + _(exp.instance_variable_get(:@headers)).must_equal('User-Agent' => DEFAULT_USER_AGENT) + _(exp.instance_variable_get(:@timeout)).must_equal 10.0 + _(exp.instance_variable_get(:@path)).must_equal '/v1/logs' + _(exp.instance_variable_get(:@compression)).must_equal 'gzip' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_be_nil + _(http.use_ssl?).must_equal false + _(http.address).must_equal 'localhost' + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_PEER + _(http.port).must_equal 4318 + end + + it 'provides a useful, spec-compliant default user agent header' do + # spec compliance: OTLP Exporter name and version + _(DEFAULT_USER_AGENT).must_match("OTel-OTLP-Exporter-Ruby/#{VERSION}") + # bonus: incredibly useful troubleshooting information + _(DEFAULT_USER_AGENT).must_match("Ruby/#{RUBY_VERSION}") + _(DEFAULT_USER_AGENT).must_match(RUBY_PLATFORM) + _(DEFAULT_USER_AGENT).must_match("#{RUBY_ENGINE}/#{RUBY_ENGINE_VERSION}") + end + + it 'refuses invalid endpoint' do + assert_raises ArgumentError do + OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'not a url') + end + end + + it 'uses endpoints path if provided' do + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'https://localhost/custom/path') + _(exp.instance_variable_get(:@path)).must_equal '/custom/path' + end + + it 'only allows gzip compression or none' do + assert_raises ArgumentError do + OpenTelemetry::Exporter::OTLP::LogsExporter.new(compression: 'flate') + end + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(compression: nil) + _(exp.instance_variable_get(:@compression)).must_be_nil + + %w[gzip none].each do |compression| + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(compression: compression) + _(exp.instance_variable_get(:@compression)).must_equal(compression) + end + + [ + { envar: 'OTEL_EXPORTER_OTLP_COMPRESSION', value: 'gzip' }, + { envar: 'OTEL_EXPORTER_OTLP_COMPRESSION', value: 'none' }, + { envar: 'OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', value: 'gzip' }, + { envar: 'OTEL_EXPORTER_OTLP_LOGS_COMPRESSION', value: 'none' } + ].each do |example| + OpenTelemetry::TestHelpers.with_env(example[:envar] => example[:value]) do + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new + _(exp.instance_variable_get(:@compression)).must_equal(example[:value]) + end + end + end + + it 'sets parameters from the environment' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234', + 'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar', + 'OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d', + 'OTEL_EXPORTER_OTLP_COMPRESSION' => 'gzip', + 'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true', + 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'User-Agent' => DEFAULT_USER_AGENT) + _(exp.instance_variable_get(:@timeout)).must_equal 11.0 + _(exp.instance_variable_get(:@path)).must_equal '/v1/logs' + _(exp.instance_variable_get(:@compression)).must_equal 'gzip' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_equal '/foo/bar' + _(http.use_ssl?).must_equal true + _(http.address).must_equal 'localhost' + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE + _(http.port).must_equal 1234 + end + + it 'prefers explicit parameters rather than the environment' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234', + 'OTEL_EXPORTER_OTLP_CERTIFICATE' => '/foo/bar', + 'OTEL_EXPORTER_OTLP_HEADERS' => 'a:b,c:d', + 'OTEL_EXPORTER_OTLP_COMPRESSION' => 'flate', + 'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER' => 'true', + 'OTEL_EXPORTER_OTLP_TIMEOUT' => '11') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'http://localhost:4321', + certificate_file: '/baz', + headers: { 'x' => 'y' }, + compression: 'gzip', + ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE, + timeout: 12) + end + _(exp.instance_variable_get(:@headers)).must_equal('x' => 'y', 'User-Agent' => DEFAULT_USER_AGENT) + _(exp.instance_variable_get(:@timeout)).must_equal 12.0 + _(exp.instance_variable_get(:@path)).must_equal '' + _(exp.instance_variable_get(:@compression)).must_equal 'gzip' + http = exp.instance_variable_get(:@http) + _(http.ca_file).must_equal '/baz' + _(http.use_ssl?).must_equal false + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE + _(http.address).must_equal 'localhost' + _(http.port).must_equal 4321 + end + + it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT has a trailing slash' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234/' + ) do + OpenTelemetry::Exporter::OTLP::LogsExporter.new() + end + _(exp.instance_variable_get(:@path)).must_equal '/v1/logs' + end + + it 'appends the correct path if OTEL_EXPORTER_OTLP_ENDPOINT does not have a trailing slash' do + exp = OpenTelemetry::TestHelpers.with_env( + 'OTEL_EXPORTER_OTLP_ENDPOINT' => 'https://localhost:1234' + ) do + OpenTelemetry::Exporter::OTLP::LogsExporter.new() + end + _(exp.instance_variable_get(:@path)).must_equal '/v1/logs' + end + + it 'restricts explicit headers to a String or Hash' do + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: { 'token' => 'über' }) + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: 'token=%C3%BCber') + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + + error = _() { + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: Object.new) + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über') + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + + it 'ignores later mutations of a headers Hash parameter' do + a_hash_to_mutate_later = { 'token' => 'über' } + exp = OpenTelemetry::Exporter::OTLP::LogsExporter.new(headers: a_hash_to_mutate_later) + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + + a_hash_to_mutate_later['token'] = 'unter' + a_hash_to_mutate_later['oops'] = 'i forgot to add this, too' + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + end + + describe 'Headers Environment Variable' do + it 'allows any number of the equal sign (=) characters in the value' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d==,e=f') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd==', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT) + + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'a=b,c=d==,e=f') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd==', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT) + end + + it 'trims any leading or trailing whitespaces in keys and values' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a = b ,c=d , e=f') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT) + + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'a = b ,c=d , e=f') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('a' => 'b', 'c' => 'd', 'e' => 'f', 'User-Agent' => DEFAULT_USER_AGENT) + end + + it 'decodes values as URL encoded UTF-8 strings' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'token=%C3%BCber') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => '%C3%BCber=token') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('über' => 'token', 'User-Agent' => DEFAULT_USER_AGENT) + + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'token=%C3%BCber') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => '%C3%BCber=token') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('über' => 'token', 'User-Agent' => DEFAULT_USER_AGENT) + end + + it 'appends the default user agent to one provided in config' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'User-Agent=%C3%BCber/3.2.1') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('User-Agent' => "über/3.2.1 #{DEFAULT_USER_AGENT}") + end + + it 'prefers LOGS specific variable' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a=b,c=d==,e=f', 'OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'token=%C3%BCber') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + _(exp.instance_variable_get(:@headers)).must_equal('token' => 'über', 'User-Agent' => DEFAULT_USER_AGENT) + end + + it 'fails fast when header values are missing' do + error = _() { + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'a = ') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + + error = _() { + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'a = ') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + + it 'fails fast when header or values are not found' do + error = _() { + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => ',') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + + error = _() { + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => ',') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + + it 'fails fast when header values contain invalid escape characters' do + error = _() { + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'c=hi%F3') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + + error = _() { + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'c=hi%F3') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + + it 'fails fast when headers are invalid' do + error = _() { + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_HEADERS' => 'this is not a header') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + + error = _() { + OpenTelemetry::TestHelpers.with_env('OTEL_EXPORTER_OTLP_LOGS_HEADERS' => 'this is not a header') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + }.must_raise(ArgumentError) + _(error.message).must_match(/headers/i) + end + end + end + + describe 'ssl_verify_mode:' do + it 'can be set to VERIFY_NONE by an envvar' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + http = exp.instance_variable_get(:@http) + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_NONE + end + + it 'can be set to VERIFY_PEER by an envvar' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER' => 'true') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + http = exp.instance_variable_get(:@http) + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_PEER + end + + it 'VERIFY_PEER will override VERIFY_NONE' do + exp = OpenTelemetry::TestHelpers.with_env('OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_NONE' => 'true', + 'OTEL_RUBY_EXPORTER_OTLP_SSL_VERIFY_PEER' => 'true') do + OpenTelemetry::Exporter::OTLP::LogsExporter.new + end + http = exp.instance_variable_get(:@http) + _(http.verify_mode).must_equal OpenSSL::SSL::VERIFY_PEER + end + end + + describe '#export' do + let(:exporter) { OpenTelemetry::Exporter::OTLP::LogsExporter.new } + + before do + OpenTelemetry.logger_provider = OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: OpenTelemetry::SDK::Resources::Resource.telemetry_sdk) + end + + it 'integrates with collector' do + skip unless ENV['TRACING_INTEGRATION_TEST'] + WebMock.disable_net_connect!(allow: 'localhost') + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + exporter = OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'http://localhost:4318', compression: 'gzip') + result = exporter.export([log_record_data]) + _(result).must_equal(SUCCESS) + end + + it 'retries on timeout' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_timeout.then.to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data]) + _(result).must_equal(SUCCESS) + end + + it 'returns TIMEOUT on timeout' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data], timeout: 0) + _(result).must_equal(FAILURE) + end + + it 'returns FAILURE on unexpected exceptions' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_raise('something unexpected') + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data], timeout: 1) + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: unexpected error in OTLP::Exporter#send_bytes - something unexpected/ + ) + + _(result).must_equal(FAILURE) + ensure + OpenTelemetry.logger = logger + end + + it 'handles encoding failures' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.stub(:encode, ->(_) { raise 'a little hell' }) do + _(exporter.export([log_record_data], timeout: 1)).must_equal(FAILURE) + end + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: unexpected error in OTLP::Exporter#encode - a little hell/ + ) + ensure + OpenTelemetry.logger = logger + end + + it 'returns TIMEOUT on timeout after retrying' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_timeout.then.to_raise('this should not be reached') + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + + @retry_count = 0 + backoff_stubbed_call = lambda do |**_args| + sleep(0.10) + @retry_count += 1 + true + end + + exporter.stub(:backoff?, backoff_stubbed_call) do + _(exporter.export([log_record_data], timeout: 0.1)).must_equal(FAILURE) + end + ensure + @retry_count = 0 + end + + it 'returns FAILURE when shutdown' do + exporter.shutdown + result = exporter.export(nil) + _(result).must_equal(FAILURE) + end + + it 'returns FAILURE when encryption to receiver endpoint fails' do + exporter = OpenTelemetry::Exporter::OTLP::LogsExporter.new(endpoint: 'https://localhost:4318/v1/logs') + stub_request(:post, 'https://localhost:4318/v1/logs').to_raise(OpenSSL::SSL::SSLError.new('enigma wedged')) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + exporter.stub(:backoff?, ->(**_) { false }) do + _(exporter.export([log_record_data])).must_equal(FAILURE) + end + end + + it 'exports a log_record_data' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data]) + _(result).must_equal(SUCCESS) + end + + it 'handles encoding errors with poise and grace' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data(total_recorded_attributes: 1, attributes: { 'a' => "\xC2".dup.force_encoding(::Encoding::ASCII_8BIT) }) + + result = exporter.export([log_record_data]) + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: encoding error for key a and value �/ + ) + + _(result).must_equal(SUCCESS) + ensure + OpenTelemetry.logger = logger + end + + it 'logs rpc.Status on bad request' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + details = [::Google::Protobuf::Any.pack(::Google::Protobuf::StringValue.new(value: 'you are a bad request'))] + status = ::Google::Rpc::Status.encode(::Google::Rpc::Status.new(code: 1, message: 'bad request', details: details)) + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 400, body: status, headers: { 'Content-Type' => 'application/x-protobuf' }) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + + result = exporter.export([log_record_data]) + + _(log_stream.string).must_match( + /ERROR -- : OpenTelemetry error: OTLP exporter received rpc.Status{message=bad request, details=\[.*you are a bad request.*\]}/ + ) + + _(result).must_equal(FAILURE) + ensure + OpenTelemetry.logger = logger + end + + it 'logs a specific message when there is a 404' do + log_stream = StringIO.new + logger = OpenTelemetry.logger + OpenTelemetry.logger = ::Logger.new(log_stream) + + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 404, body: "Not Found\n") + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + + result = exporter.export([log_record_data]) + + _(log_stream.string).must_match( + %r{ERROR -- : OpenTelemetry error: OTLP exporter received http\.code=404 for uri: '/v1/logs'} + ) + + _(result).must_equal(FAILURE) + ensure + OpenTelemetry.logger = logger + end + + it 'handles Zlib gzip compression errors' do + stub_request(:post, 'http://localhost:4318/v1/logs').to_raise(Zlib::DataError.new('data error')) + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + exporter.stub(:backoff?, ->(**_) { false }) do + _(exporter.export([log_record_data])).must_equal(FAILURE) + end + end + + it 'exports a log record from a logger' do + stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter, max_queue_size: 1, max_export_batch_size: 1) + OpenTelemetry.logger_provider.add_log_record_processor(processor) + OpenTelemetry.logger_provider.logger.emit(body: 'test') + OpenTelemetry.logger_provider.shutdown + assert_requested(stub_post) + end + + it 'compresses with gzip if enabled' do + exporter = OpenTelemetry::Exporter::OTLP::LogsExporter.new(compression: 'gzip') + stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return do |request| + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(Zlib.gunzip(request.body)) + { status: 200 } + end + + log_record_data = OpenTelemetry::TestHelpers.create_log_record_data + result = exporter.export([log_record_data]) + + _(result).must_equal(SUCCESS) + assert_requested(stub_post) + end + + it 'batches per resource' do + etsr = nil + stub_post = stub_request(:post, 'http://localhost:4318/v1/logs').to_return do |request| + proto = Zlib.gunzip(request.body) + etsr = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.decode(proto) + { status: 200 } + end + + log_record_data1 = OpenTelemetry::TestHelpers.create_log_record_data(resource: OpenTelemetry::SDK::Resources::Resource.create('k1' => 'v1')) + log_record_data2 = OpenTelemetry::TestHelpers.create_log_record_data(resource: OpenTelemetry::SDK::Resources::Resource.create('k2' => 'v2')) + + result = exporter.export([log_record_data1, log_record_data2]) + + _(result).must_equal(SUCCESS) + assert_requested(stub_post) + _(etsr.resource_logs.length).must_equal(2) + end + + it 'translates all the things' do + # CHECK ME! + # make multiple logs + # send them to multiple loggers + # shut down the processor + # see what happens + stub_request(:post, 'http://localhost:4318/v1/logs').to_return(status: 200) + processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter) + logger = OpenTelemetry.logger_provider.logger('logger', 'v0.0.1') + # other_logger = OpenTelemetry.logger_provider.logger('other_logger') + + trace_id = OpenTelemetry::Trace.generate_trace_id + span_id = OpenTelemetry::Trace.generate_span_id + trace_flags = OpenTelemetry::Trace::TraceFlags::DEFAULT + span_context = OpenTelemetry::Trace::SpanContext.new(trace_id: trace_id, span_id: span_id, trace_flags: trace_flags) + timestamp = Time.now + observed_timestamp = Time.now + 1 + severity_text = 'DEBUG' + body = 'Test' + attributes = { 'b' => true } + + + OpenTelemetry.logger_provider.add_log_record_processor(processor) + logger.emit(body: 'test', severity_number: 0, severity_text: 'DEBUG', timestamp: timestamp, span_context: span_context) + OpenTelemetry.logger_provider.shutdown + + encoded_etsr = Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.encode( + Opentelemetry::Proto::Collector::Logs::V1::ExportLogsServiceRequest.new( + resource_logs: [ + Opentelemetry::Proto::Logs::V1::ResourceLogs.new( + resource: Opentelemetry::Proto::Resource::V1::Resource.new( + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.name', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'opentelemetry')), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.language', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'ruby')), + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'telemetry.sdk.version', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: OpenTelemetry::SDK::Logs::VERSION)) + ] + ), + scope_logs: [ + Opentelemetry::Proto::Logs::V1::ScopeLogs.new( + scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new( + name: 'logger', + version: 'v0.0.1' + ), + log_records: [ + Opentelemetry::Proto::Logs::V1::LogRecord.new( + time_unix_nano: (timestamp.to_r * 1_000_000_000).to_i, + observed_time_unix_nano: (observed_timestamp.to_r * 1_000_000_000).to_i, + severity_number: Opentelemetry::Proto::Logs::V1::SeverityNumber::SEVERITY_NUMBER_DEBUG, + severity_text: severity_text, + body: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: body), + attributes: [ + Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'b', value: Opentelemetry::Proto::Common::V1::AnyValue.new(bool_value: true)), + ], + dropped_attributes_count: 0, + flags: trace_flags.instance_variable_get(:@flags), + trace_id: trace_id, + span_id: span_id + ), + # Opentelemetry::Proto::Logs::V1::LogRecord.new( + # trace_id: trace_id, + # span_id: client_span_id, + # parent_span_id: child_span_id, + # name: 'client', + # kind: Opentelemetry::Proto::Logs::V1::LogRecord::SpanKind::SPAN_KIND_CLIENT, + # start_time_unix_nano: ((start_timestamp + 2).to_r * 1_000_000_000).to_i, + # end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i, + # status: Opentelemetry::Proto::Logs::V1::Status.new( + # code: Opentelemetry::Proto::Logs::V1::Status::StatusCode::STATUS_CODE_UNSET + # ) + # ), + # Opentelemetry::Proto::Logs::V1::LogRecord.new( + # trace_id: trace_id, + # span_id: consumer_span_id, + # parent_span_id: child_span_id, + # name: 'consumer', + # kind: Opentelemetry::Proto::Logs::V1::LogRecord::SpanKind::SPAN_KIND_CONSUMER, + # start_time_unix_nano: ((start_timestamp + 5).to_r * 1_000_000_000).to_i, + # end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i, + # status: Opentelemetry::Proto::Logs::V1::Status.new( + # code: Opentelemetry::Proto::Logs::V1::Status::StatusCode::STATUS_CODE_UNSET + # ) + # ), + # Opentelemetry::Proto::Logs::V1::LogRecord.new( + # trace_id: trace_id, + # span_id: child_span_id, + # parent_span_id: root_span_id, + # name: 'child', + # kind: Opentelemetry::Proto::Logs::V1::LogRecord::SpanKind::SPAN_KIND_PRODUCER, + # start_time_unix_nano: ((start_timestamp + 1).to_r * 1_000_000_000).to_i, + # end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i, + # attributes: [ + # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'b', value: Opentelemetry::Proto::Common::V1::AnyValue.new(bool_value: true)), + # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'f', value: Opentelemetry::Proto::Common::V1::AnyValue.new(double_value: 1.1)), + # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'i', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 2)), + # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 's', value: Opentelemetry::Proto::Common::V1::AnyValue.new(string_value: 'val')), + # Opentelemetry::Proto::Common::V1::KeyValue.new( + # key: 'a', + # value: Opentelemetry::Proto::Common::V1::AnyValue.new( + # array_value: Opentelemetry::Proto::Common::V1::ArrayValue.new( + # values: [ + # Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 3), + # Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 4) + # ] + # ) + # ) + # ) + # ], + # events: [ + # Opentelemetry::Proto::Logs::V1::LogRecord::Event.new( + # time_unix_nano: ((start_timestamp + 4).to_r * 1_000_000_000).to_i, + # name: 'event', + # attributes: [ + # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'attr', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 42)) + # ] + # ) + # ], + # links: [ + # Opentelemetry::Proto::Logs::V1::LogRecord::Link.new( + # trace_id: trace_id, + # span_id: root_span_id, + # attributes: [ + # Opentelemetry::Proto::Common::V1::KeyValue.new(key: 'attr', value: Opentelemetry::Proto::Common::V1::AnyValue.new(int_value: 4)) + # ] + # ) + # ], + # status: Opentelemetry::Proto::Logs::V1::Status.new( + # code: Opentelemetry::Proto::Logs::V1::Status::StatusCode::STATUS_CODE_ERROR + # ) + # ) + ] + ), + # Opentelemetry::Proto::Logs::V1::ScopeSpans.new( + # scope: Opentelemetry::Proto::Common::V1::InstrumentationScope.new( + # name: 'other_logger' + # ), + # spans: [ + # Opentelemetry::Proto::Logs::V1::LogRecord.new( + # trace_id: trace_id, + # span_id: server_span_id, + # parent_span_id: client_span_id, + # name: 'server', + # kind: Opentelemetry::Proto::Logs::V1::LogRecord::SpanKind::SPAN_KIND_SERVER, + # start_time_unix_nano: ((start_timestamp + 3).to_r * 1_000_000_000).to_i, + # end_time_unix_nano: (end_timestamp.to_r * 1_000_000_000).to_i, + # status: Opentelemetry::Proto::Logs::V1::Status.new( + # code: Opentelemetry::Proto::Logs::V1::Status::StatusCode::STATUS_CODE_UNSET + # ) + # ) + # ] + # ) + ] + ) + ] + ) + ) + + assert_requested(:post, 'http://localhost:4318/v1/logs') do |req| + req.body == Zlib.gzip(encoded_etsr) + end + end + end +end diff --git a/exporter/otlp-logs/test/test_helper.rb b/exporter/otlp-logs/test/test_helper.rb new file mode 100644 index 0000000000..2366658d98 --- /dev/null +++ b/exporter/otlp-logs/test/test_helper.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +if RUBY_ENGINE == 'ruby' + require 'simplecov' + SimpleCov.start do + enable_coverage :branch + add_filter '/test/' + end + + SimpleCov.minimum_coverage 85 +end + +require 'opentelemetry-test-helpers' +require 'opentelemetry/exporter/otlp_logs' +require 'minitest/autorun' +require 'webmock/minitest' + +OpenTelemetry.logger = Logger.new(File::NULL) diff --git a/logs_api/lib/opentelemetry-logs-api.rb b/logs_api/lib/opentelemetry-logs-api.rb index 47ba36905e..723004ed22 100644 --- a/logs_api/lib/opentelemetry-logs-api.rb +++ b/logs_api/lib/opentelemetry-logs-api.rb @@ -7,3 +7,25 @@ require 'opentelemetry' require_relative 'opentelemetry/logs' require_relative 'opentelemetry/logs/version' + +# OpenTelemetry is an open source observability framework, providing a +# general-purpose API, SDK, and related tools required for the instrumentation +# of cloud-native software, frameworks, and libraries. +# +# The OpenTelemetry module provides global accessors for telemetry objects. +module OpenTelemetry + # Register the global logger provider. + # + # @param [LoggerProvider] provider A logger provider to register as the + # global instance. + def logger_provider=(provider) + puts 'nil logger provider' if provider.nil? + @logger_provider = provider + end + + # @return [Object, Logs::LoggerProvider] registered logger provider or a + # default no-op implementation of the logger provider. + def logger_provider + @mutex.synchronize { @logger_provider } + end +end diff --git a/logs_api/lib/opentelemetry/internal/proxy_logger.rb b/logs_api/lib/opentelemetry/internal/proxy_logger.rb new file mode 100644 index 0000000000..10dfe0bbf6 --- /dev/null +++ b/logs_api/lib/opentelemetry/internal/proxy_logger.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Internal + # @api private + # + # {ProxyLogger} is an implementation of {OpenTelemetry::Logs::Logger}. It is returned from + # the ProxyLoggerProvider until a delegate logger provider is installed. After the delegate + # logger provider is installed, the ProxyLogger will delegate to the corresponding "real" + # logger. + class ProxyLogger < Logs::Logger + # Returns a new {ProxyLogger} instance. + # + # @return [ProxyLogger] + def initialize + super + @delegate = nil + end + + # Set the delegate Logger. If this is called more than once, a warning will + # be logged and superfluous calls will be ignored. + # + # @param [Logger] logger The Logger to delegate to + def delegate=(logger) + @mutex.synchronize do + if @delegate.nil? + @delegate = logger + @instrument_registry.each_value { |instrument| instrument.upgrade_with(logger) } + else + OpenTelemetry.logger.warn 'Attempt to reset delegate in ProxyLogger ignored.' + end + end + end + + def on_emit( + timestamp: nil, + observed_timestamp: nil, + severity_number: nil, + severity_text: nil, + body: nil, + trace_id: nil, + span_id: nil, + trace_flags: nil, + attributes: nil, + context: nil + ) + @delegate.on_emit( + timestamp: nil, + observed_timestamp: nil, + severity_number: nil, + severity_text: nil, + body: nil, + trace_id: nil, + span_id: nil, + trace_flags: nil, + attributes: nil, + context: nil + ) + + super + end + end + end +end diff --git a/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb b/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb new file mode 100644 index 0000000000..0cbedc9d62 --- /dev/null +++ b/logs_api/lib/opentelemetry/internal/proxy_logger_provider.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module Internal + # @api private + # + # {ProxyLoggerProvider} is an implementation of {OpenTelemetry::Logs::LoggerProvider}. + # It is the default global logger provider returned by OpenTelemetry.logger_provider. + # It delegates to a "real" LoggerProvider after the global logger provider is registered. + # It returns {ProxyLogger} instances until the delegate is installed. + class ProxyLoggerProvider < Logs::LoggerProvider + Key = Struct.new(:name, :version) + private_constant(:Key) + # Returns a new {ProxyLoggerProvider} instance. + # + # @return [ProxyLoggerProvider] + def initialize + super + + @mutex = Mutex.new + @registry = {} + @delegate = nil + end + + # Set the delegate logger provider. If this is called more than once, a warning will + # be logged and superfluous calls will be ignored. + # + # @param [LoggerProvider] provider The logger provider to delegate to + def delegate=(provider) + unless @delegate.nil? + OpenTelemetry.logger.warn 'Attempt to reset delegate in ProxyLoggerProvider ignored.' + return + end + + @mutex.synchronize do + @delegate = provider + @registry.each { |key, logger| logger.delegate = provider.logger(key.name, key.version) } + end + end + + # Returns a {Logger} instance. + # + # @param [optional String] name Instrumentation package name + # @param [optional String] version Instrumentation package version + # + # @return [Logger] + def logger(name = nil, version = nil) + @mutex.synchronize do + return @delegate.logger(name, version) unless @delegate.nil? + + @registry[Key.new(name, version)] ||= ProxyLogger.new + end + end + end + end +end diff --git a/logs_api/lib/opentelemetry/logs/logger.rb b/logs_api/lib/opentelemetry/logs/logger.rb index c6caa34a77..6374b296b1 100644 --- a/logs_api/lib/opentelemetry/logs/logger.rb +++ b/logs_api/lib/opentelemetry/logs/logger.rb @@ -8,6 +8,11 @@ module OpenTelemetry module Logs # No-op implementation of logger. class Logger + def initialize + @mutex = Mutex.new + @instrument_registry = {} + end + # rubocop:disable Style/EmptyMethod # Emit a {LogRecord} to the processing pipeline. diff --git a/logs_api/lib/opentelemetry/logs/logger_provider.rb b/logs_api/lib/opentelemetry/logs/logger_provider.rb index 13343407dc..f2d9eb6d45 100644 --- a/logs_api/lib/opentelemetry/logs/logger_provider.rb +++ b/logs_api/lib/opentelemetry/logs/logger_provider.rb @@ -9,7 +9,9 @@ module Logs # No-op implementation of a logger provider. class LoggerProvider NOOP_LOGGER = OpenTelemetry::Logs::Logger.new - private_constant :NOOP_LOGGER + # This is used in the SDK LoggerProvider + # I would like to make it a public constant + # private_constant :NOOP_LOGGER # Returns an {OpenTelemetry::Logs::Logger} instance. # diff --git a/logs_sdk/lib/opentelemetry/sdk/logs.rb b/logs_sdk/lib/opentelemetry/sdk/logs.rb index fbd978c68c..b310d6aa5a 100644 --- a/logs_sdk/lib/opentelemetry/sdk/logs.rb +++ b/logs_sdk/lib/opentelemetry/sdk/logs.rb @@ -5,12 +5,14 @@ # SPDX-License-Identifier: Apache-2.0 require_relative 'logs/version' +require_relative 'logs/configuration_patch' require_relative 'logs/logger' require_relative 'logs/logger_provider' require_relative 'logs/log_record' require_relative 'logs/log_record_data' require_relative 'logs/log_record_processor' require_relative 'logs/export' +require_relative 'logs/log_record_limits' module OpenTelemetry module SDK diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/configuration_patch.rb b/logs_sdk/lib/opentelemetry/sdk/logs/configuration_patch.rb new file mode 100644 index 0000000000..51c3d9d429 --- /dev/null +++ b/logs_sdk/lib/opentelemetry/sdk/logs/configuration_patch.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/sdk/configurator' + +module OpenTelemetry + module SDK + module Logs + # The ConfiguratorPatch implements a hook to configure the logs + # portion of the SDK. + module ConfiguratorPatch + def add_log_record_processor(log_record_processor) + @log_record_processors << log_record_processor + end + + private + + def initialize + super + @log_record_processors = [] + end + + # The logs_configuration_hook method is where we define the setup process + # for logs SDK. + def logs_configuration_hook + OpenTelemetry.logger_provider = Logs::LoggerProvider.new(resource: @resource) + configure_log_record_processors + end + + def configure_log_record_processors + processors = @log_record_processors.empty? ? wrapped_log_exporters_from_env.compact : @log_record_processors + processors.each { |p| OpenTelemetry.logger_provider.add_log_record_processor(p) } + end + + def wrapped_log_exporters_from_env + # default is console until other exporters built + exporters = ENV.fetch('OTEL_LOGS_EXPORTER', 'console') + + exporters.split(',').map do |exporter| + case exporter.strip + when 'none' then nil + when 'console' then Logs::Export::SimpleLogRecordProcessor.new(Logs::Export::ConsoleLogRecordExporter.new) + when 'otlp' + otlp_protocol = ENV['OTEL_EXPORTER_OTLP_LOGS_PROTOCOL'] || ENV['OTEL_EXPORTER_OTLP_PROTOCOL'] || 'http/protobuf' + + if otlp_protocol != 'http/protobuf' + OpenTelemetry.logger.warn "The #{otlp_protocol} transport protocol is not supported by the OTLP exporter, log_records will not be exported." + nil + else + Logs::Export::BatchLogRecordProcessor.new(OpenTelemetry::Exporter::OTLP::LogsExporter.new) + # fetch_exporter(exporter, 'OpenTelemetry::Exporter::OTLP::Exporter') + end + else + OpenTelemetry.logger.warn "The #{exporter} exporter is unknown and cannot be configured, log records will not be exported" + nil + end + end + end + end + end + end +end + +OpenTelemetry::SDK::Configurator.prepend(OpenTelemetry::SDK::Logs::ConfiguratorPatch) diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/export.rb b/logs_sdk/lib/opentelemetry/sdk/logs/export.rb index 414670e86c..0f8e07d749 100644 --- a/logs_sdk/lib/opentelemetry/sdk/logs/export.rb +++ b/logs_sdk/lib/opentelemetry/sdk/logs/export.rb @@ -3,7 +3,6 @@ # Copyright The OpenTelemetry Authors # # SPDX-License-Identifier: Apache-2.0 - module OpenTelemetry module SDK module Logs @@ -24,8 +23,8 @@ module Export end end -require_relative 'export/console_log_record_exporter' -require_relative 'export/in_memory_log_record_exporter' -require_relative 'export/log_record_exporter' require_relative 'export/simple_log_record_processor' require_relative 'export/batch_log_record_processor' +require_relative 'export/log_record_exporter' +require_relative 'export/console_log_record_exporter' +require_relative 'export/in_memory_log_record_exporter' diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb b/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb index 10d1dcb7ab..5d2f2d5881 100644 --- a/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb +++ b/logs_sdk/lib/opentelemetry/sdk/logs/log_record.rb @@ -9,6 +9,10 @@ module SDK module Logs # Implementation of OpenTelemetry::Logs::LogRecord that records log events. class LogRecord < OpenTelemetry::Logs::LogRecord + EMPTY_ATTRIBUTES = {}.freeze + + private_constant :EMPTY_ATTRIBUTES + attr_accessor :timestamp, :observed_timestamp, :severity_text, @@ -49,6 +53,9 @@ class LogRecord < OpenTelemetry::Logs::LogRecord # source of the log, desrived from the LoggerProvider. # @param [optional OpenTelemetry::SDK::InstrumentationScope] instrumentation_scope # The instrumentation scope, derived from the emitting Logger + # @param [optional] OpenTelemetry::SDK::LogRecordLimits] log_record_limits + # Attribute limits + # # # # @return [LogRecord] @@ -63,7 +70,8 @@ def initialize( span_id: nil, trace_flags: nil, resource: nil, - instrumentation_scope: nil + instrumentation_scope: nil, + log_record_limits: nil ) @timestamp = timestamp @observed_timestamp = observed_timestamp || timestamp || Time.now @@ -76,7 +84,10 @@ def initialize( @trace_flags = trace_flags @resource = resource @instrumentation_scope = instrumentation_scope + @log_record_limits = log_record_limits || LogRecordLimits::DEFAULT @total_recorded_attributes = @attributes&.size || 0 + + trim_attributes(@attributes) end def to_log_record_data @@ -98,6 +109,50 @@ def to_log_record_data private + def trim_attributes(attributes) + return if attributes.nil? + + # truncate total attributes + truncate_attributes(attributes, @log_record_limits.attribute_count_limit) + + # truncate attribute values + truncate_attribute_values(attributes, @log_record_limits.attribute_length_limit) + + # validate attributes + validate_attributes(attributes) + + nil + end + + def truncate_attributes(attributes, attribute_limit) + excess = attributes.size - attribute_limit + excess.times { attributes.shift } if excess.positive? + end + + def validate_attributes(attrs) + # Similar to Internal.valid_attributes?, but with different messages + attrs.keep_if do |k, v| + if !Internal.valid_key?(k) + OpenTelemetry.handle_error(message: "invalid log record attribute key type #{k.class} on record: '#{body}'") + return false + elsif !Internal.valid_value?(v) + OpenTelemetry.handle_error(message: "invalid log record attribute value type #{v.class} for key '#{k}' on record: '#{body}'") + return false + end + + true + end + end + + def truncate_attribute_values(attributes, attribute_length_limit) + return EMPTY_ATTRIBUTES if attributes.nil? + return attributes if attribute_length_limit.nil? + + attributes.transform_values! { |value| OpenTelemetry::Common::Utilities.truncate_attribute_value(value, attribute_length_limit) } + + attributes + end + def to_integer_nanoseconds(timestamp) return unless timestamp.is_a?(Time) diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/log_record_limits.rb b/logs_sdk/lib/opentelemetry/sdk/logs/log_record_limits.rb new file mode 100644 index 0000000000..6440b7a4aa --- /dev/null +++ b/logs_sdk/lib/opentelemetry/sdk/logs/log_record_limits.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module SDK + module Logs + # Class that holds log record attribute limit parameters. + class LogRecordLimits + # The global default max number of attributes per {LogRecord}. + attr_reader :attribute_count_limit + + # The global default max length of attribute value per {LogRecord}. + attr_reader :attribute_length_limit + + # Returns a {LogRecordLimits} with the desired values. + # + # @return [LogRecordLimits] with the desired values. + # @raise [ArgumentError] if any of the max numbers are not positive. + def initialize(attribute_count_limit: Integer(OpenTelemetry::Common::Utilities.config_opt( + 'OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT', + 'OTEL_ATTRIBUTE_COUNT_LIMIT', + default: 128 + )), + attribute_length_limit: OpenTelemetry::Common::Utilities.config_opt( + 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT', + 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' + )) + raise ArgumentError, 'attribute_count_limit must be positive' unless attribute_count_limit.positive? + raise ArgumentError, 'attribute_length_limit must not be less than 32' unless attribute_length_limit.nil? || Integer(attribute_length_limit) >= 32 + + @attribute_count_limit = attribute_count_limit + @attribute_length_limit = attribute_length_limit.nil? ? nil : Integer(attribute_length_limit) + end + + # The default {LogRecordLimits}. + DEFAULT = new + end + end + end +end diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb b/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb index 231999a33c..f4e51c016f 100644 --- a/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb +++ b/logs_sdk/lib/opentelemetry/sdk/logs/logger.rb @@ -4,6 +4,8 @@ # # SPDX-License-Identifier: Apache-2.0 +require_relative '../../../../../logs_api/lib/opentelemetry-logs-api' + module OpenTelemetry module SDK module Logs @@ -26,6 +28,10 @@ def initialize(name, version, logger_provider) @logger_provider = logger_provider end + def log_record_limits + logger_provider.log_record_limits + end + # Emit a {LogRecord} to the processing pipeline. # # @param [optional Time] timestamp Time when the event occurred. diff --git a/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb b/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb index 99e7df10e5..4c13343185 100644 --- a/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb +++ b/logs_sdk/lib/opentelemetry/sdk/logs/logger_provider.rb @@ -9,6 +9,9 @@ module SDK module Logs # The SDK implementation of OpenTelemetry::Logs::LoggerProvider. class LoggerProvider < OpenTelemetry::Logs::LoggerProvider + Key = Struct.new(:name, :version) + private_constant(:Key) + UNEXPECTED_ERROR_MESSAGE = 'unexpected error in ' \ 'OpenTelemetry::SDK::Logs::LoggerProvider#%s' @@ -18,27 +21,47 @@ class LoggerProvider < OpenTelemetry::Logs::LoggerProvider # # @param [optional Resource] resource The resource to associate with # new LogRecords created by {Logger}s created by this LoggerProvider. + # @param [optional Array] log_record_processors The + # {LogRecordProcessor}s to associate with this LoggerProvider. + # @param [optional LogRecordLimits] log_record_limits The limits for + # attributes count and attribute length for LogRecords. # # @return [OpenTelemetry::SDK::Logs::LoggerProvider] - def initialize(resource: OpenTelemetry::SDK::Resources::Resource.create) - @log_record_processors = [] + def initialize( + resource: OpenTelemetry::SDK::Resources::Resource.create, + log_record_processors: [], + log_record_limits: LogRecordLimits::DEFAULT + ) + @log_record_processors = log_record_processors + @log_record_limits = log_record_limits @mutex = Mutex.new @resource = resource @stopped = false + @registry = {} + @registry_mutex = Mutex.new end - # Returns an {OpenTelemetry::SDK::Logs::Logger} instance. + # Creates an {OpenTelemetry::SDK::Logs::Logger} instance. # # @param [String] name Instrumentation package name # @param [optional String] version Instrumentation package version # # @return [OpenTelemetry::SDK::Logs::Logger] def logger(name:, version: nil) - version ||= '' + if @stopped + OpenTelemetry.logger.warn('calling LoggerProvider#logger after shutdown, a noop logger will be returned') + OpenTelemetry::Logs::LoggerProvider::NOOP_LOGGER + else + version ||= '' + + if !name.is_a?(String) || name.empty? + OpenTelemetry.logger.warn('LoggerProvider#logger called with an ' \ + "invalid name. Name provided: #{name.inspect}") + end - if !name.is_a?(String) || name.empty? - OpenTelemetry.logger.warn('LoggerProvider#logger called with an ' \ - "invalid name. Name provided: #{name.inspect}") + @registry_mutex.synchronize do + @registry[Key.new(name, version)] ||= Logger.new(name, version, self) + end end Logger.new(name, version, self) @@ -142,7 +165,8 @@ def on_emit(timestamp: nil, span_id: span_id, trace_flags: trace_flags, resource: @resource, - instrumentation_scope: instrumentation_scope) + instrumentation_scope: instrumentation_scope, + log_record_limits: @log_record_limits) @log_record_processors.each { |processor| processor.on_emit(log_record, context) } end diff --git a/logs_sdk/test/opentelemetry/sdk/logs/log_record_limits_test.rb b/logs_sdk/test/opentelemetry/sdk/logs/log_record_limits_test.rb new file mode 100644 index 0000000000..04c6238276 --- /dev/null +++ b/logs_sdk/test/opentelemetry/sdk/logs/log_record_limits_test.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::SDK::Logs::LogRecordLimits do + let(:log_record_limits) { OpenTelemetry::SDK::Logs::LogRecordLimits.new } + + describe '#initialize' do + it 'provides defaults' do + _(log_record_limits.attribute_count_limit).must_equal 128 + _(log_record_limits.attribute_length_limit).must_be_nil + end + + it 'prioritizes specific environment varibles for attribute value length limits' do + OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '35', + 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '33') do + _(log_record_limits.attribute_length_limit).must_equal 33 + end + end + + it 'uses general attribute value length limits in the absence of more specific ones' do + OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '35') do + _(log_record_limits.attribute_length_limit).must_equal 35 + end + end + + it 'reflects environment variables' do + OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1', + 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32') do + _(log_record_limits.attribute_count_limit).must_equal 1 + _(log_record_limits.attribute_length_limit).must_equal 32 + end + end + + it 'reflects explicit overrides' do + OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1', + 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '4') do + log_record_limits = OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_count_limit: 10, + attribute_length_limit: 32) + _(log_record_limits.attribute_count_limit).must_equal 10 + _(log_record_limits.attribute_length_limit).must_equal 32 + end + end + + it 'reflects generic attribute env vars' do + OpenTelemetry::TestHelpers.with_env('OTEL_ATTRIBUTE_COUNT_LIMIT' => '1', + 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32') do + _(log_record_limits.attribute_count_limit).must_equal 1 + _(log_record_limits.attribute_length_limit).must_equal 32 + end + end + + it 'prefers model-specific attribute env vars over generic attribute env vars' do + OpenTelemetry::TestHelpers.with_env('OTEL_LOG_RECORD_ATTRIBUTE_COUNT_LIMIT' => '1', + 'OTEL_ATTRIBUTE_COUNT_LIMIT' => '2', + 'OTEL_LOG_RECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '32', + 'OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT' => '33') do + _(log_record_limits.attribute_count_limit).must_equal 1 + _(log_record_limits.attribute_length_limit).must_equal 32 + end + end + + it 'raises if attribute_count_limit is not positive' do + assert_raises ArgumentError do + OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_count_limit: -1) + end + end + + it 'raises if attribute_length_limit is less than 32' do + assert_raises ArgumentError do + OpenTelemetry::SDK::Logs::LogRecordLimits.new(attribute_length_limit: 31) + end + end + end +end diff --git a/logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb b/logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb index ca38350e1f..71a2557e9d 100644 --- a/logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb +++ b/logs_sdk/test/opentelemetry/sdk/logs/log_record_test.rb @@ -92,5 +92,85 @@ assert_equal(args[:instrumentation_scope], log_record_data.instrumentation_scope) end end + + describe 'attribute limits' do + it 'uses the limits set by the logger provider via the logger' do + # Spy on the console output + captured_stdout = StringIO.new + original_stdout = $stdout + $stdout = captured_stdout + + # Create the LoggerProvider with the console exporter and an attribute limit of 1 + limits = Logs::LogRecordLimits.new(attribute_count_limit: 1) + logger_provider = Logs::LoggerProvider.new(log_record_limits: limits) + console_exporter = Logs::Export::SimpleLogRecordProcessor.new(Logs::Export::ConsoleLogRecordExporter.new) + logger_provider.add_log_record_processor(console_exporter) + + # Create a logger that uses the given LoggerProvider + logger = Logs::Logger.new('', '', logger_provider) + + # Emit a log from that logger, with attribute count exceeding the limit + logger.on_emit(attributes: { 'a' => 'a', 'b' => 'b' }) + + # Look at the captured output to see if the attributes have been truncated + assert_match(/attributes={"b"=>"b"}/, captured_stdout.string) + refute_match(/"a"=>"a"/, captured_stdout.string) + + # Return STDOUT to its normal output + $stdout = original_stdout + end + + it 'emits an error message if attribute key is invalid' do + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + logger.on_emit(attributes: { a: 'a' }) + assert_match(/invalid log record attribute key type Symbol/, log_stream.string) + end + end + + it 'emits an error message if the attribute value is invalid' do + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + logger.on_emit(attributes: { 'a' => Class.new }) + assert_match(/invalid log record attribute value type Class/, log_stream.string) + end + end + + it 'uses the default limits if none provided' do + log_record = Logs::LogRecord.new + default = Logs::LogRecordLimits::DEFAULT + + assert_equal(default.attribute_count_limit, log_record.instance_variable_get(:@log_record_limits).attribute_count_limit) + # default length is nil + assert_nil(log_record.instance_variable_get(:@log_record_limits).attribute_length_limit) + end + + it 'trims the oldest attributes' do + limits = Logs::LogRecordLimits.new(attribute_count_limit: 1) + attributes = { 'old' => 'old', 'new' => 'new' } + log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: attributes) + + assert_equal({ 'new' => 'new' }, log_record.attributes) + end + end + + describe 'attribute value limit' do + it 'truncates the values that are too long' do + length_limit = 32 + too_long = 'a' * (length_limit + 1) + just_right = 'a' * (length_limit - 3) # truncation removes 3 chars for the '...' + limits = Logs::LogRecordLimits.new(attribute_length_limit: length_limit) + log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: { 'key' => too_long }) + + assert_equal({ 'key' => "#{just_right}..." }, log_record.attributes) + end + + it 'does not alter values within the range' do + length_limit = 32 + within_range = 'a' * length_limit + limits = Logs::LogRecordLimits.new(attribute_length_limit: length_limit) + log_record = Logs::LogRecord.new(log_record_limits: limits, attributes: { 'key' => within_range }) + + assert_equal({ 'key' => within_range }, log_record.attributes) + end + end end end diff --git a/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb b/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb index 023a9a67c9..771d4720eb 100644 --- a/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb +++ b/logs_sdk/test/opentelemetry/sdk/logs/logger_provider_test.rb @@ -24,6 +24,15 @@ end end + describe '#initialize' do + it 'activates a default LogRecordLimits' do + assert_equal( + OpenTelemetry::SDK::Logs::LogRecordLimits::DEFAULT, + logger_provider.instance_variable_get(:@log_record_limits) + ) + end + end + describe '#add_log_record_processor' do it "adds the processor to the logger provider's processors" do assert_equal(0, logger_provider.instance_variable_get(:@log_record_processors).length) @@ -80,8 +89,43 @@ name = 'name' version = 'version' logger = logger_provider.logger(name: name, version: version) - assert_equal(logger.instance_variable_get(:@instrumentation_scope).name, name) - assert_equal(logger.instance_variable_get(:@instrumentation_scope).version, version) + assert_equal(name, logger.instance_variable_get(:@instrumentation_scope).name) + assert_equal(version, logger.instance_variable_get(:@instrumentation_scope).version) + end + + # On the first call, create a logger with an empty string for name and + # version and add to the registry. The second call returns that logger + # from the registry instead of creating a new instance. + it 'reuses the same logger if called twice when name and version are nil' do + skip 'uncertain about why this is failing... need to investigate' + logger = logger_provider.logger(name: nil, version: nil) + logger2 = logger_provider.logger(name: nil, version: nil) + + assert_instance_of(Logs::Logger, logger) + assert_same(logger, logger2) + end + + describe 'when stopped' do + it 'logs a warning' do + logger_provider.instance_variable_set(:@stopped, true) + + OpenTelemetry::TestHelpers.with_test_logger do |log_stream| + logger_provider.logger(name: '') + assert_match( + /calling LoggerProvider#logger after shutdown/, + log_stream.string + ) + end + end + + it 'does not add a new logger to the registry' do + before_stopped_size = logger_provider.instance_variable_get(:@registry).keys.size + logger_provider.instance_variable_set(:@stopped, true) + logger_provider.logger(name: 'new_logger') + after_stopped_size = logger_provider.instance_variable_get(:@registry).keys.size + + assert_equal(before_stopped_size, after_stopped_size) + end end end diff --git a/sdk/lib/opentelemetry/sdk/configurator.rb b/sdk/lib/opentelemetry/sdk/configurator.rb index ae1bbaf07b..86cbd2df4e 100644 --- a/sdk/lib/opentelemetry/sdk/configurator.rb +++ b/sdk/lib/opentelemetry/sdk/configurator.rb @@ -126,13 +126,20 @@ def add_span_processor(span_processor) @span_processors << span_processor end + # Add a log record processor to the export pipeline + # + # @param [#emit, #shutdown, #force_flush] log_record_processor A log_record_processor + # that satisfies the duck type #emit, #shutdown, #force_flush. See + # {SimpleLogRecordProcessor} for an example. + def add_log_record_processor(log_record_processor); end + # @api private # The configure method is where we define the setup process. This allows # us to make certain guarantees about which systems and globals are setup # at each stage. The setup process is: # - setup logging # - setup propagation - # - setup tracer_provider and meter_provider + # - setup tracer_provider, meter_provider, and logger_provider # - install instrumentation def configure OpenTelemetry.logger = logger @@ -142,6 +149,7 @@ def configure tracer_provider.id_generator = @id_generator OpenTelemetry.tracer_provider = tracer_provider metrics_configuration_hook + logs_configuration_hook install_instrumentation end @@ -149,6 +157,8 @@ def configure def metrics_configuration_hook; end + def logs_configuration_hook; end + def tracer_provider @tracer_provider ||= Trace::TracerProvider.new(resource: @resource) end @@ -179,7 +189,6 @@ def wrapped_exporters_from_env # rubocop:disable Metrics/CyclomaticComplexity when 'none' then nil when 'otlp' otlp_protocol = ENV['OTEL_EXPORTER_OTLP_TRACES_PROTOCOL'] || ENV['OTEL_EXPORTER_OTLP_PROTOCOL'] || 'http/protobuf' - if otlp_protocol != 'http/protobuf' OpenTelemetry.logger.warn "The #{otlp_protocol} transport protocol is not supported by the OTLP exporter, spans will not be exported." nil diff --git a/test_helpers/lib/opentelemetry/test_helpers.rb b/test_helpers/lib/opentelemetry/test_helpers.rb index 25048aa0f4..b47e29847d 100644 --- a/test_helpers/lib/opentelemetry/test_helpers.rb +++ b/test_helpers/lib/opentelemetry/test_helpers.rb @@ -72,5 +72,19 @@ def create_span_data(name: '', kind: nil, status: nil, parent_span_id: OpenTelem total_recorded_events, total_recorded_links, start_timestamp, end_timestamp, attributes, links, events, resource, instrumentation_scope, span_id, trace_id, trace_flags, tracestate) end + + def create_log_record_data(timestamp: OpenTelemetry::TestHelpers.exportable_timestamp, + observed_timestamp: OpenTelemetry::TestHelpers.exportable_timestamp, + trace_id: OpenTelemetry::Trace.generate_trace_id, + span_id: OpenTelemetry::Trace.generate_span_id, + trace_flags: OpenTelemetry::Trace::TraceFlags::DEFAULT, severity_text: nil, + severity_number: nil, body: nil, resource: nil, + instrumentation_scope: OpenTelemetry::SDK::InstrumentationScope.new('', 'v0.0.1'), + attributes: nil, total_recorded_attributes: 0) + resource ||= OpenTelemetry::SDK::Resources::Resource.telemetry_sdk + OpenTelemetry::SDK::Logs::LogRecordData.new(timestamp, observed_timestamp, trace_id, span_id, trace_flags, + severity_text, severity_number, body, resource, instrumentation_scope, + attributes, total_recorded_attributes) + end end end