diff --git a/.cspell.yml b/.cspell.yml index f451df771..258c22c37 100644 --- a/.cspell.yml +++ b/.cspell.yml @@ -68,6 +68,7 @@ ignoreWords: - rolldice - codegen - Dockerfiles + - otelconfig words: - autocorrection - bigdecimal diff --git a/otelconfig/.rubocop.yml b/otelconfig/.rubocop.yml new file mode 100644 index 000000000..41f6eb369 --- /dev/null +++ b/otelconfig/.rubocop.yml @@ -0,0 +1,16 @@ +inherit_from: ../contrib/rubocop.yml + +Metrics/AbcSize: + Max: 30 +Metrics/MethodLength: + Max: 50 +Metrics/PerceivedComplexity: + Max: 30 +Metrics/CyclomaticComplexity: + Max: 20 +Metrics/BlockLength: + Enabled: false +Metrics/ClassLength: + Enabled: false +Metrics/ModuleLength: + Max: 150 diff --git a/otelconfig/.yardopts b/otelconfig/.yardopts new file mode 100644 index 000000000..bb0d313dc --- /dev/null +++ b/otelconfig/.yardopts @@ -0,0 +1,9 @@ +--no-private +--title=OpenTelemetry Declarative Configuration +--markup=markdown +--main=README.md +./lib/opentelemetry/**/*.rb +./lib/opentelemetry.rb +- +README.md +CHANGELOG.md \ No newline at end of file diff --git a/otelconfig/CHANGELOG.md b/otelconfig/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/otelconfig/Gemfile b/otelconfig/Gemfile new file mode 100644 index 000000000..a0e797680 --- /dev/null +++ b/otelconfig/Gemfile @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gemspec + +group :test, :development do + gem 'minitest', '~> 6.0' + gem 'rake', '~> 13.0' + gem 'rubocop', '~> 1.86.0' + gem 'rubocop-minitest', '~> 0.39.0' + gem 'rubocop-performance', '~> 1.26.0' + gem 'rubocop-rake', '~> 0.7.1' + gem 'rubocop-rspec', '~> 3.9.0' + gem 'simplecov', '~> 0.22.0' + gem 'yard', '~> 0.9' + + # Local path overrides for gems developed in this monorepo + gem 'opentelemetry-api', path: '../api', require: false + gem 'opentelemetry-common', path: '../common', require: false + gem 'opentelemetry-exporter-otlp', path: '../exporter/otlp', require: false + gem 'opentelemetry-exporter-otlp-common', path: '../exporter/otlp-common', require: false + gem 'opentelemetry-registry', path: '../registry', require: false + gem 'opentelemetry-sdk', path: '../sdk', require: false + gem 'opentelemetry-test-helpers', path: '../test_helpers', require: false + + # Prevent bundler from downgrading google-protobuf to an incompatible + # pre-built binary (glibc) that does not run in Alpine (musl) containers. + gem 'google-protobuf', '~> 3.19' + + if RUBY_VERSION >= '3.4' + gem 'base64' + gem 'bigdecimal' + gem 'mutex_m' + end + gem 'logger' if RUBY_VERSION >= '4.0.0' +end diff --git a/otelconfig/LICENSE b/otelconfig/LICENSE new file mode 100644 index 000000000..1ef7dad2c --- /dev/null +++ b/otelconfig/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/otelconfig/README.md b/otelconfig/README.md new file mode 100644 index 000000000..54c40ca4e --- /dev/null +++ b/otelconfig/README.md @@ -0,0 +1,161 @@ +# opentelemetry-otelconfig + +The `opentelemetry-otelconfig` gem provides file-based, declarative configuration of the OpenTelemetry Ruby SDK from a single YAML file. It replaces the need to write programmatic setup code for common provider and exporter patterns. + +## What is OpenTelemetry? + +[OpenTelemetry][opentelemetry-home] is an open source observability framework, providing a general-purpose API, SDK, and related tools required for the instrumentation of cloud-native software, frameworks, and libraries. + +OpenTelemetry provides a single set of APIs, libraries, agents, and collector services to capture distributed traces, metrics, and logs from your application. You can analyze them using Prometheus, Jaeger, and other observability tools. + +## How does this gem fit in? + +The `opentelemetry-otelconfig` gem sits on top of the OpenTelemetry Ruby SDK. Instead of calling `OpenTelemetry::SDK.configure` with a block of Ruby code, you describe your desired configuration in a YAML file and let `opentelemetry-otelconfig` wire up all the opentelemetry components for you. + +It works with: + +- `opentelemetry-sdk` — tracing +- `opentelemetry-exporter-otlp` — OTLP HTTP exporters +- `opentelemetry-instrumentation-all` — auto-instrumentation + +## How do I get started? + +Install the gem using: + +```sh +gem install opentelemetry-otelconfig +``` + +Or, if you use [bundler][bundler-home], include `opentelemetry-otelconfig` in your `Gemfile`. + +### Automatic configuration via environment variable + +Set `OTEL_CONFIG_FILE` to the path of your YAML config file before requiring the gem. Configuration is applied automatically at require time. + +```sh +OTEL_CONFIG_FILE=/path/to/otel-config.yaml bundle exec ruby app.rb +``` + +```ruby +require 'opentelemetry-otelconfig' + +tracer = OpenTelemetry.tracer_provider.tracer('my_app', '1.0.0') +tracer.in_span('my-operation') do |span| + span.set_attribute('key', 'value') +end +``` + +## YAML configuration reference + +See full configuration reference in [declarative-configuration](https://opentelemetry.io/docs/languages/sdk-configuration/declarative-configuration/) + +### Disabling the SDK + +Set `disabled: true` to keep all providers as no-ops without removing the config file. This is useful for running tests or CI pipelines without telemetry overhead. + +```yaml +file_format: "1.0" +disabled: true +``` + +### Resource attributes + +Attributes can be provided as a structured array, a comma-separated string, or both. When the same key appears in both, the `attributes` array takes priority. + +```yaml +resource: + attributes: + - name: service.name + value: "my-service" + - name: deployment.environment + value: "staging" + attributes_list: "service.namespace=my-namespace,service.version=1.0.0" +``` + +### Samplers + +| Sampler | YAML key | +| ------- | -------- | +| Always on | `always_on:` | +| Always off | `always_off:` | +| Trace-ID ratio | `trace_id_ratio_based: { ratio: 0.25 }` | +| Parent-based | `parent_based: { root: ... }` | + +```yaml +tracer_provider: + sampler: + parent_based: + root: + trace_id_ratio_based: + ratio: 0.1 + remote_parent_sampled: + always_on: + remote_parent_not_sampled: + always_off: + local_parent_sampled: + always_on: + local_parent_not_sampled: + always_off: +``` + +### Propagators + +Propagators can be listed either as a YAML array or as a comma-separated string. + +```yaml +# Array form +propagator: + composite: + - tracecontext: + - baggage: + +# String form (equivalent) +propagator: + composite_list: "tracecontext,baggage" +``` + +Supported propagator names: `tracecontext`, `baggage`, `b3`, `b3multi`, `jaeger`, `xray`. + +### Auto-instrumentation + +The `instrumentation/development` key maps short library names to option hashes. An empty or omitted section installs all available instrumentation with default settings. + +```yaml +instrumentation/development: + general: + net_http: + untraced_hosts: + - localhost + rack: + untraced_endpoints: + - /healthz +``` + +Short names follow the snake_case convention of the instrumentation class suffix (e.g., `net_http` for `OpenTelemetry::Instrumentation::Net::HTTP`). + +## Examples + +A runnable example application is available in the [`example/`][example-dir] directory. It demonstrates traces configured from YAML with console output. + +```sh +cd otelconfig/example +bundle exec ruby app.rb +``` + +## How can I get involved? + +The `opentelemetry-otelconfig` 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-otelconfig` gem is distributed under the Apache 2.0 license. See [LICENSE][license-github] for more information. + +[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 +[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 diff --git a/otelconfig/Rakefile b/otelconfig/Rakefile new file mode 100644 index 000000000..d202bf4c4 --- /dev/null +++ b/otelconfig/Rakefile @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# +# SPDX-License-Identifier: Apache-2.0 + +require 'bundler/gem_tasks' +require 'rake/testtask' +require 'yard' + +require 'rubocop/rake_task' +RuboCop::RakeTask.new + +Rake::TestTask.new :test do |t| + t.libs << 'test' + t.libs << 'lib' + t.test_files = FileList['test/**/*_test.rb'] +end + +YARD::Rake::YardocTask.new do |t| + t.stats_options = ['--list-undoc'] +end + +default_tasks = + if RUBY_ENGINE == 'truffleruby' + %i[test] + else + %i[test rubocop yard] + end + +task default: default_tasks diff --git a/otelconfig/example/Gemfile b/otelconfig/example/Gemfile new file mode 100644 index 000000000..dea7fdef2 --- /dev/null +++ b/otelconfig/example/Gemfile @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +source 'https://rubygems.org' + +gem 'opentelemetry-api', path: '../../api' +gem 'opentelemetry-common', path: '../../common' +gem 'opentelemetry-exporter-otlp', path: '../../exporter/otlp' +gem 'opentelemetry-sdk', path: '../../sdk' +gem 'opentelemetry-otelconfig', path: '..' +gem 'opentelemetry-instrumentation-all' diff --git a/otelconfig/example/README.md b/otelconfig/example/README.md new file mode 100644 index 000000000..29cbf6a3f --- /dev/null +++ b/otelconfig/example/README.md @@ -0,0 +1,45 @@ +# Declarative Configuration Example + +This example shows how to configure the OpenTelemetry SDK (tracing) from a YAML +file using the `opentelemetry-otelconfig` gem — no programmatic +`OpenTelemetry::SDK.configure` block required. + +## Files + +| File | Purpose | +| ---- | ------- | +| `app.rb` | Example application — emits spans | +| `otel-config-console.yaml` | console-only exporter, works without a collector | +| `otel-config.yaml` | Include otlp_http exporter, need working collector | + +## Quick start (console output, no collector needed) + +```sh +# From this directory +bundle install +bundle exec ruby app.rb +``` + +You will see span output written to stdout. + +## How it works + +1. Set the `OTEL_CONFIG_FILE` environment variable to the path of your YAML file. +2. `require 'opentelemetry-otelconfig'` reads the file, parses it, and wires + up `TracerProvider`, propagators, and instrumentation — all in one step. +3. Use the standard OpenTelemetry API (`OpenTelemetry.tracer_provider`) as normal. + +If `OTEL_CONFIG_FILE` is not set, call `OpenTelemetry::OtelConfig.configure` +manually with a config hash, or configure programmatically using the SDK. + +## YAML config key reference + +| Section | Description | +| ------- | ----------- | +| `resource.attributes` | Service name, version, environment, and any custom resource attributes | +| `resource.attributes_list` | Comma-separated `key=value` pairs as an alternative to attributes array | +| `tracer_provider.processors` | `batch` or `simple` span processors with `console` or `otlp_http` exporters | +| `tracer_provider.sampler` | `always_on`, `always_off`, `trace_id_ratio_based`, or `parent_based` | +| `tracer_provider.limits` | Attribute, event, and link count/length limits | +| `propagator.composite` | Ordered list of propagators (`tracecontext`, `baggage`, `b3`, `b3multi`, `jaeger`, `xray`) | +| `instrumentation.general` | Enabled/disabled instrumentation libraries | diff --git a/otelconfig/example/app.rb b/otelconfig/example/app.rb new file mode 100755 index 000000000..19d2bcf97 --- /dev/null +++ b/otelconfig/example/app.rb @@ -0,0 +1,58 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +ENV['OTEL_CONFIG_FILE'] ||= File.join(__dir__, 'otel-config-console.yaml') + +require 'bundler/setup' +require 'net/http' +require 'opentelemetry-sdk' +require 'opentelemetry-instrumentation-all' +require 'opentelemetry_otelconfig' + +OpenTelemetry::OtelConfig.configure + +tracer = OpenTelemetry.tracer_provider.tracer('otelconfig-example', '1.0.0') + +tracer.in_span('process-order', attributes: { 'order.id' => 'ORD-001', 'order.items' => 3 }) do |span| + span.add_event('validation-started') + + # Simulate nested work in a child span + tracer.in_span('validate-inventory', attributes: { 'warehouse' => 'us-west-2' }) do |child| + sleep(0.01) # simulate I/O + child.set_attribute('inventory.available', true) + child.add_event('inventory-checked', attributes: { 'sku' => 'WIDGET-42', 'qty' => 10 }) + end + + span.add_event('validation-complete') + span.set_attribute('order.total_usd', 49.99) +end + +OpenTelemetry.tracer_provider.force_flush(timeout: 30) + +SITES = [ + { name: 'google.ca', uri: URI('https://www.google.ca') }, + { name: 'github.com', uri: URI('https://github.com') } +].freeze + +tracer.in_span('http-requests') do + SITES.each do |site| + tracer.in_span("GET #{site[:name]}", + attributes: { + 'http.method' => 'GET', + 'http.url' => site[:uri].to_s, + 'net.peer.name' => site[:name] + }) do |span| + response = Net::HTTP.get_response(site[:uri]) + span.set_attribute('http.status_code', response.code.to_i) + if response['content-length'] + span.set_attribute('http.response_content_length', + response['content-length'].to_i) + end + end + end +end + +OpenTelemetry.tracer_provider.shutdown diff --git a/otelconfig/example/otel-config-console.yaml b/otelconfig/example/otel-config-console.yaml new file mode 100644 index 000000000..bc67ae8b3 --- /dev/null +++ b/otelconfig/example/otel-config-console.yaml @@ -0,0 +1,46 @@ +file_format: "1.0" +disabled: false +log_level: info + +resource: + attributes: + - name: service.name + value: "declare-config-example" + - name: service.version + value: "1.0.0" + - name: deployment.environment + value: "development" + +tracer_provider: + processors: + - simple: + exporter: + console: + limits: + attribute_value_length_limit: 4096 + attribute_count_limit: 128 + event_count_limit: 128 + link_count_limit: 128 + sampler: + parent_based: + root: + always_on: + remote_parent_sampled: + always_on: + remote_parent_not_sampled: + always_off: + local_parent_sampled: + always_on: + local_parent_not_sampled: + always_off: + +propagator: + composite: + - tracecontext: + - baggage: + +instrumentation/development: + general: + net_http: + untraced_hosts: + - google.ca \ No newline at end of file diff --git a/otelconfig/example/otel-config.yaml b/otelconfig/example/otel-config.yaml new file mode 100644 index 000000000..b968b62c5 --- /dev/null +++ b/otelconfig/example/otel-config.yaml @@ -0,0 +1,64 @@ +file_format: "1.0" +disabled: false +log_level: info +attribute_limits: + attribute_value_length_limit: 4096 + attribute_count_limit: 128 +resource: + attributes: + - name: service.name + value: "test-ruby-declare-config" + - name: service.version + value: "1.0.0" + - name: deployment.environment + value: "staging" + attributes_list: "service.namespace=my-namespace,service.version=1.0.0" +tracer_provider: + processors: + - + batch: + schedule_delay: 5000 + export_timeout: 30000 + max_queue_size: 2048 + max_export_batch_size: 512 + exporter: + otlp_http: + endpoint: http://host.docker.internal:4328/v1/traces + compression: gzip + timeout: 10000 + - + simple: + exporter: + console: + limits: + attribute_value_length_limit: 4096 + attribute_count_limit: 128 + event_count_limit: 128 + link_count_limit: 128 + event_attribute_count_limit: 128 + link_attribute_count_limit: 128 + sampler: + parent_based: + root: + trace_id_ratio_based: + ratio: 1.0 + remote_parent_sampled: + always_on: + remote_parent_not_sampled: + always_off: + local_parent_sampled: + always_on: + local_parent_not_sampled: + always_off: +propagator: + composite: + - + tracecontext: + - + baggage: + composite_list: "tracecontext,baggage" +instrumentation/development: + general: + net_http: + untraced_hosts: + - google.ca \ No newline at end of file diff --git a/otelconfig/lib/opentelemetry/components/trace.rb b/otelconfig/lib/opentelemetry/components/trace.rb new file mode 100644 index 000000000..a4312344a --- /dev/null +++ b/otelconfig/lib/opentelemetry/components/trace.rb @@ -0,0 +1,180 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module OtelConfig + # Trace component builder for configuring TracerProvider from declarative config. + module Trace + module_function + + # Builds a TracerProvider from the parsed YAML tracer_provider config. + # Returns a noop-configured provider if config is nil. + def build_tracer_provider(config, resource) + return OpenTelemetry::Trace::TracerProvider.new unless config + + sampler = build_sampler(config['sampler']) + span_limits = build_span_limits(config['limits']) + + tp = OpenTelemetry::SDK::Trace::TracerProvider.new( + resource: resource, + sampler: sampler, + span_limits: span_limits + ) + + Array(config['processors']).each do |proc_cfg| + processor = build_span_processor(proc_cfg) + tp.add_span_processor(processor) if processor + rescue StandardError => e + OpenTelemetry.logger.warn("Failed to build span processor: #{e.message}") + end + + tp + end + + # Builds a span processor (simple or batch) from config hash. + def build_span_processor(proc_cfg) + raise ArgumentError, 'must not specify multiple span processor type' if proc_cfg['batch'] && proc_cfg['simple'] + + if proc_cfg['batch'] + build_batch_span_processor(proc_cfg['batch']) + elsif proc_cfg['simple'] + build_simple_span_processor(proc_cfg['simple']) + else + raise ArgumentError, 'unsupported span processor type, must be one of simple or batch' + end + end + + # Builds a BatchSpanProcessor with exporter and optional tuning options. + def build_batch_span_processor(cfg) + exporter = build_span_exporter(cfg['exporter']) + opts = { + schedule_delay: cfg['schedule_delay']&.to_f, + exporter_timeout: cfg['export_timeout']&.to_f, + max_queue_size: cfg['max_queue_size']&.to_i, + max_export_batch_size: cfg['max_export_batch_size']&.to_i + }.compact + + OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter, **opts) + end + + # Builds a SimpleSpanProcessor wrapping the configured exporter. + def build_simple_span_processor(cfg) + exporter = build_span_exporter(cfg['exporter']) + OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter) + end + + # Builds a span exporter from config; supports console and otlp_http. + def build_span_exporter(exp_cfg) + raise ArgumentError, 'no exporter config' unless exp_cfg + + configured = 0 + exporter = nil + + if exp_cfg.key?('console') + configured += 1 + exporter = OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter.new + end + + if exp_cfg['otlp_http'] + configured += 1 + exporter = build_otlp_http_span_exporter(exp_cfg['otlp_http']) + end + + raise ArgumentError, 'must not specify multiple exporters' if configured > 1 + raise ArgumentError, 'no valid span exporter' if exporter.nil? + + exporter + end + + # Builds an OTLP HTTP span exporter from the given endpoint/headers config. + def build_otlp_http_span_exporter(cfg) + opts = { + endpoint: cfg['endpoint'], + headers: cfg['headers'] || cfg['headers_list'] ? parse_headers(cfg) : nil, + compression: cfg['compression'], + timeout: cfg['timeout'] && cfg['timeout'] / 1000.0 # YAML ms → Ruby seconds + }.compact + + OpenTelemetry::Exporter::OTLP::Exporter.new(**opts) + end + + # Builds a sampler from config; defaults to ParentBased(ALWAYS_ON). + def build_sampler(sampler_cfg) + s = OpenTelemetry::SDK::Trace::Samplers + + # Default: parent-based with always_on root + return s.parent_based(root: s::ALWAYS_ON) unless sampler_cfg + + if sampler_cfg['parent_based'] + build_parent_based_sampler(sampler_cfg['parent_based']) + elsif sampler_cfg.key?('always_on') + s::ALWAYS_ON + elsif sampler_cfg.key?('always_off') + s::ALWAYS_OFF + elsif sampler_cfg['trace_id_ratio_based'] + ratio = sampler_cfg['trace_id_ratio_based']['ratio'] || 1.0 + s.trace_id_ratio_based(ratio.to_f) + else + s.parent_based(root: s::ALWAYS_ON) + end + end + + # Builds a ParentBased sampler with configurable root and remote/local delegates. + def build_parent_based_sampler(cfg) + s = OpenTelemetry::SDK::Trace::Samplers + + root = cfg['root'] ? build_sampler(cfg['root']) : s::ALWAYS_ON + + opts = { + root: root, + remote_parent_sampled: cfg['remote_parent_sampled'] && build_sampler(cfg['remote_parent_sampled']), + remote_parent_not_sampled: cfg['remote_parent_not_sampled'] && build_sampler(cfg['remote_parent_not_sampled']), + local_parent_sampled: cfg['local_parent_sampled'] && build_sampler(cfg['local_parent_sampled']), + local_parent_not_sampled: cfg['local_parent_not_sampled'] && build_sampler(cfg['local_parent_not_sampled']) + }.compact + + s.parent_based(**opts) + end + + # Builds SpanLimits from config; returns the SDK default when config is nil. + def build_span_limits(limits_cfg) + return OpenTelemetry::SDK::Trace::SpanLimits::DEFAULT unless limits_cfg + + opts = { + attribute_count_limit: limits_cfg['attribute_count_limit'], + attribute_length_limit: limits_cfg['attribute_value_length_limit'], + event_count_limit: limits_cfg['event_count_limit'], + link_count_limit: limits_cfg['link_count_limit'], + event_attribute_count_limit: limits_cfg['event_attribute_count_limit'], + link_attribute_count_limit: limits_cfg['link_attribute_count_limit'] + }.compact + + OpenTelemetry::SDK::Trace::SpanLimits.new(**opts) + end + + # Parses headers from YAML array format or headers_list string. + # Array format takes precedence over headers_list. + def parse_headers(cfg) + headers = {} + + if cfg['headers'].is_a?(Array) + cfg['headers'].each do |h| + headers[h['name']] = h['value'] if h['name'] && h['value'] + end + end + + # Fall back to headers_list only if headers array produced nothing + if headers.empty? && cfg['headers_list'].is_a?(String) + cfg['headers_list'].split(',').each do |pair| + key, value = pair.strip.split('=', 2) + headers[key] = value if key && value + end + end + + headers + end + end + end +end diff --git a/otelconfig/lib/opentelemetry/otelconfig.rb b/otelconfig/lib/opentelemetry/otelconfig.rb new file mode 100644 index 000000000..2e3d2cd7d --- /dev/null +++ b/otelconfig/lib/opentelemetry/otelconfig.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +require 'date' +require 'yaml' +require 'opentelemetry/components/trace' + +require_relative 'otelconfig/instrumentation' +require_relative 'otelconfig/propagation' +require_relative 'otelconfig/resource' + +module OpenTelemetry + # OtelConfig module handles declarative configuration of OpenTelemetry components + # from YAML files. + module OtelConfig + ENV_CONFIG_FILE = 'OTEL_CONFIG_FILE' + + class << self + # Entry point + def configure + config_path = ENV[ENV_CONFIG_FILE] + + if config_path.to_s.empty? + OpenTelemetry.logger.info('No OTEL_CONFIG_FILE defined.') + else + config = parse_config_file(config_path) + apply(config) + end + end + + # Configure directly from a file path (for testing or explicit setup). + def configure_from_file(path) + config = parse_config_file(path) + apply(config) + end + + private + + def apply(config) + return if config.nil? + + unless defined?(OpenTelemetry::SDK) + warn '[opentelemetry-otelconfig] opentelemetry-sdk is not loaded. ' \ + 'Add `gem "opentelemetry-sdk"` to your Gemfile.' + return + end + + if config['disabled'] + OpenTelemetry.logger.info('OpenTelemetry SDK disabled by configuration.') + else + resource = build_resource(config['resource']) + tracer_provider = Trace.build_tracer_provider(config['tracer_provider'], resource) + + OpenTelemetry.tracer_provider = tracer_provider + + configure_propagation(config['propagator']) + configure_instrumentation(config['instrumentation/development']) + end + end + + def parse_config_file(path) + content = File.read(path) + YAML.safe_load(content, permitted_classes: [Date, Time]) + rescue Errno::ENOENT => e + OpenTelemetry.logger.error("Config file not found: #{e.message}") + nil + rescue Psych::SyntaxError => e + OpenTelemetry.logger.error("YAML parse error: #{e.message}") + nil + end + end + end +end diff --git a/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb b/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb new file mode 100644 index 000000000..ee5d1c6f5 --- /dev/null +++ b/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + # OtelConfig module — instrumentation configuration helpers. + module OtelConfig + class << self + # Installs instrumentation libraries from the registry. + def configure_instrumentation(instrumentation_cfg) + config_map = build_instrumentation_config_map(instrumentation_cfg) + OpenTelemetry::Instrumentation.registry.install_all(config_map) + rescue NameError + OpenTelemetry.logger.warn('opentelemetry-instrumentation-all not available; skipping instrumentation install.') + end + + # Transforms the YAML instrumentation config into the flat hash that + # install_all expects: { 'OpenTelemetry::Instrumentation::Foo' => { opt: val } } + def build_instrumentation_config_map(instrumentation_cfg) + return {} unless instrumentation_cfg.is_a?(Hash) + + general = instrumentation_cfg['general'] + return {} unless general.is_a?(Hash) + + name_map = build_instrumentation_name_map + general.each_with_object({}) do |(short_name, options), result| + full_name = name_map[short_name.to_s] + unless full_name + OpenTelemetry.logger.warn("Declarative config: unknown instrumentation short name '#{short_name}' — skipping.") + next + end + result[full_name] = options.is_a?(Hash) ? options.transform_keys(&:to_sym) : {} + end + end + + # Builds a lookup table: snake_case_short_name => full_class_name + # e.g. 'net_http' => 'OpenTelemetry::Instrumentation::Net::HTTP' + def build_instrumentation_name_map + registry = OpenTelemetry::Instrumentation.registry + registry.instance_variable_get(:@instrumentation).each_with_object({}) do |inst_class, map| + inst = inst_class.instance + short = inst.name.delete_prefix('OpenTelemetry::Instrumentation::') + .gsub('::', '_') + .gsub(/([a-z\d])([A-Z])/, '\1_\2') # this is for case like AwsLambda -> aws_lambda + .downcase + map[short] = inst.name + end + rescue StandardError + {} + end + end + end +end diff --git a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb new file mode 100644 index 000000000..e20c771e9 --- /dev/null +++ b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + # OtelConfig module — propagation configuration helpers. + module OtelConfig + class << self + # Configures the global text-map propagator from the propagator_cfg hash. + def configure_propagation(propagator_cfg) + return unless propagator_cfg + + names = extract_propagator_names(propagator_cfg) + return if names.empty? + + propagators = names.filter_map { |name| resolve_propagator(name) } + return if propagators.empty? + + OpenTelemetry.propagation = + OpenTelemetry::Context::Propagation::CompositeTextMapPropagator + .compose_propagators(propagators) + end + + # Extracts an ordered list of propagator name strings from the config hash. + def extract_propagator_names(cfg) + composite = cfg['composite'] + if composite.is_a?(Array) + return composite.flat_map { |entry| entry.is_a?(Hash) ? entry.keys : entry.to_s } + end + + list = cfg['composite_list'] + return list.split(',').map(&:strip) if list.is_a?(String) + + [] + end + + # Returns a propagator instance for the given name, or nil with a warning. + def resolve_propagator(name) + case name + when 'tracecontext' + OpenTelemetry::Trace::Propagation::TraceContext.text_map_propagator + when 'baggage' + OpenTelemetry::Baggage::Propagation.text_map_propagator + when 'b3' + const_get_propagator('OpenTelemetry::Propagator::B3::Single') + when 'b3multi' + const_get_propagator('OpenTelemetry::Propagator::B3::Multi') + when 'jaeger' + const_get_propagator('OpenTelemetry::Propagator::Jaeger') + when 'ottrace' + const_get_propagator('OpenTelemetry::Propagator::OTTrace') + when 'xray' + const_get_propagator('OpenTelemetry::Propagator::XRay') + when 'google_cloud_trace_context' + const_get_propagator('OpenTelemetry::Propagator::GoogleCloudTraceContext') + else + OpenTelemetry.logger.warn("Unknown propagator: #{name}") + nil + end + end + + # Looks up a propagator class by fully-qualified name and returns its text_map_propagator. + def const_get_propagator(class_name) + Kernel.const_get(class_name).text_map_propagator + rescue NameError + OpenTelemetry.logger.warn("Propagator #{class_name} not available — is the gem installed?") + nil + end + end + end +end diff --git a/otelconfig/lib/opentelemetry/otelconfig/resource.rb b/otelconfig/lib/opentelemetry/otelconfig/resource.rb new file mode 100644 index 000000000..4c61fcc96 --- /dev/null +++ b/otelconfig/lib/opentelemetry/otelconfig/resource.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + # OtelConfig module — resource configuration helpers. + module OtelConfig + class << self + # Priority: attributes > attribute_list > detected > base + def build_resource(resource_cfg) + base = OpenTelemetry::SDK::Resources::Resource.default + + return base unless resource_cfg + + detected = build_detected_attributes(resource_cfg['detection/development']) + + explicit = {} + Array(resource_cfg['attributes']).each do |attr| + next unless attr.is_a?(Hash) && attr['name'] && !attr['value'].nil? + + explicit[attr['name']] = coerce_attribute_value(attr['value'], attr['type']) + end + + if resource_cfg['attributes_list'].is_a?(String) + resource_cfg['attributes_list'].split(',').each do |pair| + key, value = pair.strip.split('=', 2) + explicit[key] ||= value if key && value + end + end + + OpenTelemetry.logger.warn('OtelConfig: schema_url is supported; ignoring.') if resource_cfg['schema_url'] + + attrs = detected.merge(explicit) + custom = OpenTelemetry::SDK::Resources::Resource.create(attrs) + base.merge(custom) + end + + private + + # type coercion + def coerce_attribute_value(value, type) + case type + when 'string' then value.to_s + when 'bool' then coerce_bool(value) + when 'int' then Integer(value) + when 'double' then Float(value) + when 'string_array' then Array(value).map(&:to_s) + when 'bool_array' then Array(value).map { |v| coerce_bool(v) } + when 'int_array' then Array(value).map { |v| Integer(v) } + when 'double_array' then Array(value).map { |v| Float(v) } + else value # no type field → use the YAML-parsed value as-is + end + end + + def coerce_bool(value) + case value + when true, 'true', 1 then true + when false, 'false', 0 then false + else !!value + end + end + + # Extract the attributes from field 'detection/development' + def build_detected_attributes(detection_cfg) + return {} unless detection_cfg.is_a?(Hash) + + included_patterns = Array(detection_cfg.dig('attributes', 'included')) + excluded_patterns = Array(detection_cfg.dig('attributes', 'excluded')) + detector_names = Array(detection_cfg['detectors']) + .flat_map { |d| d.is_a?(Hash) ? d.keys : d.to_s } + + raw = detector_names.each_with_object({}) do |name, attrs| + attrs.merge!(run_detector(name).attribute_enumerator.to_h) + end + + # File.fnmatch: * matches any chars except /, so "process.*" covers + # "process.pid", "process.runtime.name", etc. + raw.select do |key, _| + included = included_patterns.empty? || included_patterns.any? { |pat| File.fnmatch(pat, key) } + excluded = excluded_patterns.any? { |pat| File.fnmatch(pat, key) } + included && !excluded + end + end + + # Returns a Resource for the given detector name. + def run_detector(name) + case name + when 'container' + detect_resource('OpenTelemetry::Resource::Detector::Container') + when 'aws' + # Run all AWS sub-detectors; each returns an empty resource if not on that platform. + detect_resource('OpenTelemetry::Resource::Detector::AWS', %i[ec2 ecs eks lambda]) + when 'azure' + detect_resource('OpenTelemetry::Resource::Detector::Azure') + when 'google_cloud_platform' + detect_resource('OpenTelemetry::Resource::Detector::GoogleCloudPlatform') + else + OpenTelemetry.logger.warn("OtelConfig: unknown resource detector '#{name}'; skipping.") + OpenTelemetry::SDK::Resources::Resource.create({}) + end + end + + # Looks up a resource detector class by fully-qualified name and calls detect. + def detect_resource(class_name, *args) + Kernel.const_get(class_name).detect(*args) + rescue NameError + OpenTelemetry.logger.warn("OtelConfig: resource detector '#{class_name}' is not available — is the gem installed?") + OpenTelemetry::SDK::Resources::Resource.create({}) + end + end + end +end diff --git a/otelconfig/lib/opentelemetry/otelconfig/version.rb b/otelconfig/lib/opentelemetry/otelconfig/version.rb new file mode 100644 index 000000000..7daa1850c --- /dev/null +++ b/otelconfig/lib/opentelemetry/otelconfig/version.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +module OpenTelemetry + module OtelConfig + VERSION = '0.1.0' + end +end diff --git a/otelconfig/lib/opentelemetry_otelconfig.rb b/otelconfig/lib/opentelemetry_otelconfig.rb new file mode 100644 index 000000000..089d1c939 --- /dev/null +++ b/otelconfig/lib/opentelemetry_otelconfig.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +require 'opentelemetry/otelconfig' diff --git a/otelconfig/opentelemetry-otelconfig.gemspec b/otelconfig/opentelemetry-otelconfig.gemspec new file mode 100644 index 000000000..ebaafae2c --- /dev/null +++ b/otelconfig/opentelemetry-otelconfig.gemspec @@ -0,0 +1,47 @@ +# 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/otelconfig/version' + +Gem::Specification.new do |spec| + spec.name = 'opentelemetry-otelconfig' + spec.version = OpenTelemetry::OtelConfig::VERSION + spec.authors = ['OpenTelemetry Authors'] + spec.email = ['cncf-opentelemetry-contributors@lists.cncf.io'] + + spec.summary = 'Declare Config implementation for OpenTelemetry' + 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.3' + + spec.add_development_dependency 'opentelemetry-api', '~> 1.10.0' + spec.add_development_dependency 'opentelemetry-common', '~> 0.25.0' + spec.add_development_dependency 'opentelemetry-exporter-otlp', '~> 0.34.0' + spec.add_development_dependency 'opentelemetry-instrumentation-all', '~> 0.91.0' + spec.add_development_dependency 'opentelemetry-propagator-google_cloud_trace_context', '~> 0.4.0' + spec.add_development_dependency 'opentelemetry-propagator-ottrace', '~> 0.25.0' + spec.add_development_dependency 'opentelemetry-propagator-xray', '~> 0.27.0' + spec.add_development_dependency 'opentelemetry-resource-detector-aws', '~> 0.5.0' + spec.add_development_dependency 'opentelemetry-resource-detector-azure', '~> 0.3.0' + spec.add_development_dependency 'opentelemetry-resource-detector-container', '~> 0.3.0' + spec.add_development_dependency 'opentelemetry-resource-detector-google_cloud_platform', '~> 0.4.0' + spec.add_development_dependency 'opentelemetry-sdk', '~> 1.12' + + if spec.respond_to?(:metadata) + spec.metadata['changelog_uri'] = "https://open-telemetry.github.io/opentelemetry-ruby/opentelemetry-logs-sdk/v#{OpenTelemetry::OtelConfig::VERSION}/file.CHANGELOG.html" + spec.metadata['source_code_uri'] = "https://github.com/open-telemetry/opentelemetry-ruby/tree/#{spec.name}/v#{spec.version}/logs_sdk" + 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-logs-sdk/v#{OpenTelemetry::OtelConfig::VERSION}" + end +end diff --git a/otelconfig/test/components/trace_test.rb b/otelconfig/test/components/trace_test.rb new file mode 100644 index 000000000..32f43d745 --- /dev/null +++ b/otelconfig/test/components/trace_test.rb @@ -0,0 +1,233 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::OtelConfig do + describe 'tracer_provider' do + describe 'simple processor with console exporter' do + it 'installs a SimpleSpanProcessor backed by ConsoleSpanExporter' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + tp = OpenTelemetry.tracer_provider + + _(tp).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider + + processors = tp.instance_variable_get(:@span_processors) + _(processors.size).must_equal 1 + + _(processors[0]).must_be_instance_of OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor + _(processors[0].instance_variable_get(:@span_exporter)).must_be_instance_of OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter + end + end + end + + describe 'batch processor with OTLP HTTP exporter' do + it 'installs a BatchSpanProcessor with the correct endpoint, headers, compression, and timeout' do + with_config(<<~YAML) do |path| + file_format: "1.0" + tracer_provider: + processors: + - batch: + schedule_delay: 5000 + export_timeout: 30000 + max_queue_size: 2048 + max_export_batch_size: 512 + exporter: + otlp_http: + endpoint: http://localhost:4318/v1/traces + headers: + - name: api-key + value: "secret-token" + compression: gzip + timeout: 10000 + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + processors = OpenTelemetry.tracer_provider.instance_variable_get(:@span_processors) + _(processors.size).must_equal 1 + + bsp = processors[0] + _(bsp).must_be_instance_of OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor + + exporter = bsp.instance_variable_get(:@exporter) + _(exporter).must_be_instance_of OpenTelemetry::Exporter::OTLP::Exporter + + _(exporter.instance_variable_get(:@uri).to_s).must_equal 'http://localhost:4318/v1/traces' + _(exporter.instance_variable_get(:@compression)).must_equal 'gzip' + _(exporter.instance_variable_get(:@timeout)).must_equal 10.0 + _(exporter.instance_variable_get(:@headers)['api-key']).must_equal 'secret-token' + end + end + + it 'forwards batch tuning parameters to the processor' do + with_config(<<~YAML) do |path| + file_format: "1.0" + tracer_provider: + processors: + - batch: + schedule_delay: 3000 + export_timeout: 15000 + max_queue_size: 1024 + max_export_batch_size: 256 + exporter: + otlp_http: + endpoint: http://localhost:4318/v1/traces + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + processors = OpenTelemetry.tracer_provider.instance_variable_get(:@span_processors) + _(processors.size).must_equal 1 + + bsp = processors[0] + _(bsp.instance_variable_get(:@delay_seconds) * 1000).must_equal 3000.0 + _(bsp.instance_variable_get(:@max_queue_size)).must_equal 1024 + _(bsp.instance_variable_get(:@batch_size)).must_equal 256 + end + end + end + + describe 'multiple processors' do + it 'adds processors in declaration order: batch OTLP first, simple console second' do + with_config(<<~YAML) do |path| + file_format: "1.0" + tracer_provider: + processors: + - batch: + exporter: + otlp_http: + endpoint: http://localhost:4318/v1/traces + - simple: + exporter: + console: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + processors = OpenTelemetry.tracer_provider.instance_variable_get(:@span_processors) + _(processors.size).must_equal 2 + + _(processors[0]).must_be_instance_of OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor + _(processors[0].instance_variable_get(:@exporter)).must_be_instance_of OpenTelemetry::Exporter::OTLP::Exporter + + _(processors[1]).must_be_instance_of OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor + _(processors[1].instance_variable_get(:@span_exporter)).must_be_instance_of OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter + end + end + end + + describe 'sampler configuration' do + it 'uses ALWAYS_ON when sampler is always_on' do + with_config(<<~YAML) do |path| + file_format: "1.0" + tracer_provider: + processors: + - simple: + exporter: + console: + sampler: + always_on: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider.sampler).must_equal OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON + end + end + + it 'uses ALWAYS_OFF when sampler is always_off' do + with_config(<<~YAML) do |path| + file_format: "1.0" + tracer_provider: + processors: + - simple: + exporter: + console: + sampler: + always_off: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider.sampler).must_equal OpenTelemetry::SDK::Trace::Samplers::ALWAYS_OFF + end + end + + it 'uses TraceIdRatioBased with the configured ratio' do + with_config(<<~YAML) do |path| + file_format: "1.0" + tracer_provider: + processors: + - simple: + exporter: + console: + sampler: + trace_id_ratio_based: + ratio: 0.25 + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + sampler = OpenTelemetry.tracer_provider.sampler + + _(sampler.description).must_match(/0.25/) + end + end + + it 'wraps the root sampler in ParentBased' do + with_config(<<~YAML) do |path| + file_format: "1.0" + tracer_provider: + processors: + - simple: + exporter: + console: + sampler: + parent_based: + root: + always_on: + remote_parent_sampled: + always_on: + remote_parent_not_sampled: + always_off: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + sampler = OpenTelemetry.tracer_provider.sampler + + _(sampler.description).must_match(/ParentBased/) + end + end + end + + describe 'span limits' do + it 'applies all configured limits to the TracerProvider' do + with_config(<<~YAML) do |path| + file_format: "1.0" + tracer_provider: + processors: + - simple: + exporter: + console: + limits: + attribute_value_length_limit: 512 + attribute_count_limit: 64 + event_count_limit: 32 + link_count_limit: 16 + event_attribute_count_limit: 8 + link_attribute_count_limit: 4 + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + limits = OpenTelemetry.tracer_provider + .instance_variable_get(:@span_limits) + + _(limits.attribute_length_limit).must_equal 512 + _(limits.attribute_count_limit).must_equal 64 + _(limits.event_count_limit).must_equal 32 + _(limits.link_count_limit).must_equal 16 + _(limits.event_attribute_count_limit).must_equal 8 + _(limits.link_attribute_count_limit).must_equal 4 + end + end + end + end +end diff --git a/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb b/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb new file mode 100644 index 000000000..fd04e0e2c --- /dev/null +++ b/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb @@ -0,0 +1,497 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::OtelConfig do + # Temporarily replaces build_instrumentation_name_map with a stub that + # returns +map+, then restores the original after the block. + def with_name_map(map) + original = OpenTelemetry::OtelConfig.method(:build_instrumentation_name_map) + OpenTelemetry::OtelConfig.singleton_class.undef_method(:build_instrumentation_name_map) + OpenTelemetry::OtelConfig.define_singleton_method(:build_instrumentation_name_map) { map } + yield + ensure + OpenTelemetry::OtelConfig.singleton_class.undef_method(:build_instrumentation_name_map) + OpenTelemetry::OtelConfig.define_singleton_method(:build_instrumentation_name_map, original) + end + + # Temporarily replaces OpenTelemetry::Instrumentation.registry with a fake + # registry containing +classes+, then restores the original method. + def with_registry_classes(classes) + fake_registry = Object.new + fake_registry.instance_variable_set(:@instrumentation, classes) + + original = OpenTelemetry::Instrumentation.method(:registry) + OpenTelemetry::Instrumentation.singleton_class.undef_method(:registry) + OpenTelemetry::Instrumentation.define_singleton_method(:registry) { fake_registry } + yield + ensure + OpenTelemetry::Instrumentation.singleton_class.undef_method(:registry) + OpenTelemetry::Instrumentation.define_singleton_method(:registry, original) + end + + # Builds a fake instrumentation class that responds to .instance and returns + # an object exposing a #name string. + def fake_instrumentation_class(full_name) + instance = Struct.new(:name).new(full_name) + Class.new.tap do |klass| + klass.define_singleton_method(:instance) { instance } + end + end + + # Fake name map used throughout the stubbed unit tests. + FAKE_NAME_MAP = { + 'net_http' => 'OpenTelemetry::Instrumentation::Net::HTTP', + 'rack' => 'OpenTelemetry::Instrumentation::Rack', + 'redis' => 'OpenTelemetry::Instrumentation::Redis', + 'sidekiq' => 'OpenTelemetry::Instrumentation::Sidekiq', + 'active_job' => 'OpenTelemetry::Instrumentation::ActiveJob', + 'faraday' => 'OpenTelemetry::Instrumentation::Faraday', + 'mysql2' => 'OpenTelemetry::Instrumentation::Mysql2', + 'pg' => 'OpenTelemetry::Instrumentation::PG', + 'grpc' => 'OpenTelemetry::Instrumentation::GRPC', + 'graphql' => 'OpenTelemetry::Instrumentation::GraphQL', + 'dalli' => 'OpenTelemetry::Instrumentation::Dalli', + 'action_pack' => 'OpenTelemetry::Instrumentation::ActionPack' + }.freeze + + describe 'instrumentation' do + describe 'build_instrumentation_name_map' do + it 'returns a Hash for the current registry' do + result = OpenTelemetry::OtelConfig.build_instrumentation_name_map + + _(result).must_be_kind_of Hash + end + + it 'maps full instrumentation names to snake_case short names' do + classes = [ + fake_instrumentation_class('OpenTelemetry::Instrumentation::Net::HTTP'), + fake_instrumentation_class('OpenTelemetry::Instrumentation::ActionPack'), + fake_instrumentation_class('OpenTelemetry::Instrumentation::Redis') + ] + + with_registry_classes(classes) do + result = OpenTelemetry::OtelConfig.build_instrumentation_name_map + + _(result).must_equal( + 'net_http' => 'OpenTelemetry::Instrumentation::Net::HTTP', + 'action_pack' => 'OpenTelemetry::Instrumentation::ActionPack', + 'redis' => 'OpenTelemetry::Instrumentation::Redis' + ) + end + end + + it 'joins nested module segments with underscores' do + classes = [ + fake_instrumentation_class('OpenTelemetry::Instrumentation::Foo::Bar::HTTP') + ] + + with_registry_classes(classes) do + result = OpenTelemetry::OtelConfig.build_instrumentation_name_map + + _(result['foo_bar_http']).must_equal 'OpenTelemetry::Instrumentation::Foo::Bar::HTTP' + end + end + + it 'returns {} when registry instrumentation data is invalid' do + with_registry_classes(nil) do + result = OpenTelemetry::OtelConfig.build_instrumentation_name_map + + _(result).must_equal({}) + end + end + end + + describe 'configure_from_file with instrumentation section' do + it 'does not raise when the instrumentation section is absent' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider + end + end + + it 'does not raise when instrumentation gems are not installed' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + instrumentation: + general: + net_http: + untraced_hosts: + - example.com + rack: + record_frontend_span: false + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider + end + end + + it 'does not raise for multiple instrumentations with mixed option types' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + instrumentation: + general: + redis: + peer_service: "cache-cluster" + trace_root_spans: true + db_statement: obfuscate + sidekiq: + span_naming: queue + propagation_style: link + trace_launcher_heartbeat: false + active_job: + propagation_style: child + force_flush: true + span_naming: job_class + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider + end + end + end + + # build_instrumentation_config_map — nil / invalid inputs + describe 'build_instrumentation_config_map with invalid inputs' do + it 'returns {} when config is nil' do + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map(nil)).must_equal({}) + end + + it 'returns {} when config is not a Hash' do + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map('string')).must_equal({}) + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map(42)).must_equal({}) + end + + it 'returns {} when the general key is absent' do + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map({ 'other' => {} })).must_equal({}) + end + + it 'returns {} when general is not a Hash' do + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map({ 'general' => 'flat' })).must_equal({}) + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map({ 'general' => [] })).must_equal({}) + end + + it 'returns {} when general is an empty Hash' do + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map({ 'general' => {} })).must_equal({}) + end + end + + # build_instrumentation_config_map — transformation logic (stubbed name map) + describe 'build_instrumentation_config_map with stubbed name map' do + describe 'core transformation behaviour' do + it 'maps the short name to the full class name' do + with_name_map(FAKE_NAME_MAP) do + cfg = { 'general' => { 'net_http' => {} } } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + _(result.keys).must_include 'OpenTelemetry::Instrumentation::Net::HTTP' + end + end + + it 'symbolizes option keys' do + with_name_map(FAKE_NAME_MAP) do + cfg = { 'general' => { 'net_http' => { 'untraced_hosts' => ['localhost'] } } } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::Net::HTTP'] + _(opts.keys).must_include :untraced_hosts + _(opts.keys).wont_include 'untraced_hosts' + end + end + + it 'treats nil options as an empty Hash' do + with_name_map(FAKE_NAME_MAP) do + cfg = { 'general' => { 'net_http' => nil } } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + _(result['OpenTelemetry::Instrumentation::Net::HTTP']).must_equal({}) + end + end + + it 'treats non-Hash options as an empty Hash' do + with_name_map(FAKE_NAME_MAP) do + cfg = { 'general' => { 'net_http' => 'enabled' } } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + _(result['OpenTelemetry::Instrumentation::Net::HTTP']).must_equal({}) + end + end + + it 'skips and does not include unknown short names' do + with_name_map(FAKE_NAME_MAP) do + cfg = { 'general' => { 'totally_unknown_lib' => { 'opt' => 1 } } } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + _(result).must_equal({}) + end + end + + it 'maps multiple instrumentations in one call' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'net_http' => { 'untraced_hosts' => ['internal.example.com'] }, + 'redis' => { 'peer_service' => 'cache', 'trace_root_spans' => true } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + _(result.size).must_equal 2 + _(result['OpenTelemetry::Instrumentation::Net::HTTP']).must_equal(untraced_hosts: ['internal.example.com']) + _(result['OpenTelemetry::Instrumentation::Redis']).must_equal(peer_service: 'cache', trace_root_spans: true) + end + end + end + + # Representative option shapes from each instrumentation + describe 'net_http options' do + it 'maps untraced_hosts array' do + with_name_map(FAKE_NAME_MAP) do + cfg = { 'general' => { 'net_http' => { 'untraced_hosts' => ['metrics.example.com', 'localhost'] } } } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + _(result['OpenTelemetry::Instrumentation::Net::HTTP']).must_equal( + untraced_hosts: ['metrics.example.com', 'localhost'] + ) + end + end + end + + describe 'rack options' do + it 'maps all rack options correctly' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'rack' => { + 'allowed_request_headers' => %w[X-Request-ID X-Forwarded-For], + 'allowed_response_headers' => ['X-Response-Time'], + 'untraced_endpoints' => ['/healthz', '/metrics'], + 'record_frontend_span' => true, + 'use_rack_events' => false + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::Rack'] + _(opts[:allowed_request_headers]).must_equal %w[X-Request-ID X-Forwarded-For] + _(opts[:allowed_response_headers]).must_equal ['X-Response-Time'] + _(opts[:untraced_endpoints]).must_equal ['/healthz', '/metrics'] + _(opts[:record_frontend_span]).must_equal true + _(opts[:use_rack_events]).must_equal false + end + end + end + + describe 'redis options' do + it 'maps peer_service, trace_root_spans, and db_statement' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'redis' => { + 'peer_service' => 'redis-primary', + 'trace_root_spans' => false, + 'db_statement' => 'obfuscate' + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::Redis'] + _(opts[:peer_service]).must_equal 'redis-primary' + _(opts[:trace_root_spans]).must_equal false + _(opts[:db_statement]).must_equal 'obfuscate' + end + end + end + + describe 'sidekiq options' do + it 'maps span_naming, propagation_style, and boolean trace flags' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'sidekiq' => { + 'span_naming' => 'job_class', + 'propagation_style' => 'child', + 'trace_launcher_heartbeat' => true, + 'trace_poller_enqueue' => false, + 'trace_poller_wait' => false, + 'trace_processor_process_one' => true, + 'peer_service' => 'sidekiq-workers' + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::Sidekiq'] + _(opts[:span_naming]).must_equal 'job_class' + _(opts[:propagation_style]).must_equal 'child' + _(opts[:trace_launcher_heartbeat]).must_equal true + _(opts[:trace_poller_enqueue]).must_equal false + _(opts[:trace_processor_process_one]).must_equal true + _(opts[:peer_service]).must_equal 'sidekiq-workers' + end + end + end + + describe 'active_job options' do + it 'maps propagation_style, force_flush, and span_naming' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'active_job' => { + 'propagation_style' => 'none', + 'force_flush' => true, + 'span_naming' => 'job_class' + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::ActiveJob'] + _(opts[:propagation_style]).must_equal 'none' + _(opts[:force_flush]).must_equal true + _(opts[:span_naming]).must_equal 'job_class' + end + end + end + + describe 'faraday options' do + it 'maps span_kind, peer_service, and enable_internal_instrumentation' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'faraday' => { + 'span_kind' => 'internal', + 'peer_service' => 'downstream-api', + 'enable_internal_instrumentation' => true + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::Faraday'] + _(opts[:span_kind]).must_equal 'internal' + _(opts[:peer_service]).must_equal 'downstream-api' + _(opts[:enable_internal_instrumentation]).must_equal true + end + end + end + + describe 'mysql2 options' do + it 'maps db_statement, obfuscation_limit, span_name, and peer_service' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'mysql2' => { + 'peer_service' => 'mysql-primary', + 'db_statement' => 'omit', + 'span_name' => 'db_name', + 'obfuscation_limit' => 500 + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::Mysql2'] + _(opts[:peer_service]).must_equal 'mysql-primary' + _(opts[:db_statement]).must_equal 'omit' + _(opts[:span_name]).must_equal 'db_name' + _(opts[:obfuscation_limit]).must_equal 500 + end + end + end + + describe 'pg options' do + it 'maps db_statement, obfuscation_limit, and peer_service' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'pg' => { + 'peer_service' => 'postgres-replica', + 'db_statement' => 'include', + 'obfuscation_limit' => 1000 + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::PG'] + _(opts[:peer_service]).must_equal 'postgres-replica' + _(opts[:db_statement]).must_equal 'include' + _(opts[:obfuscation_limit]).must_equal 1000 + end + end + end + + describe 'grpc options' do + it 'maps allowed_metadata_headers and peer_service' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'grpc' => { + 'allowed_metadata_headers' => %w[x-correlation-id x-tenant-id], + 'peer_service' => 'grpc-backend' + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::GRPC'] + _(opts[:allowed_metadata_headers]).must_equal %w[x-correlation-id x-tenant-id] + _(opts[:peer_service]).must_equal 'grpc-backend' + end + end + end + + describe 'graphql options' do + it 'maps schemas array and all boolean platform flags' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'graphql' => { + 'schemas' => [], + 'enable_platform_field' => true, + 'enable_platform_authorized' => false, + 'enable_platform_resolve_type' => true, + 'legacy_platform_span_names' => false, + 'legacy_tracing' => false + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::GraphQL'] + _(opts[:schemas]).must_equal [] + _(opts[:enable_platform_field]).must_equal true + _(opts[:enable_platform_authorized]).must_equal false + _(opts[:enable_platform_resolve_type]).must_equal true + end + end + end + + describe 'dalli options' do + it 'maps peer_service and db_statement' do + with_name_map(FAKE_NAME_MAP) do + cfg = { + 'general' => { + 'dalli' => { + 'peer_service' => 'memcached', + 'db_statement' => 'omit' + } + } + } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + opts = result['OpenTelemetry::Instrumentation::Dalli'] + _(opts[:peer_service]).must_equal 'memcached' + _(opts[:db_statement]).must_equal 'omit' + end + end + end + + describe 'action_pack options' do + it 'maps span_naming' do + with_name_map(FAKE_NAME_MAP) do + cfg = { 'general' => { 'action_pack' => { 'span_naming' => 'class' } } } + result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) + _(result['OpenTelemetry::Instrumentation::ActionPack']).must_equal(span_naming: 'class') + end + end + end + end + end +end diff --git a/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb new file mode 100644 index 000000000..de32a8786 --- /dev/null +++ b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb @@ -0,0 +1,255 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::OtelConfig do + describe 'propagator' do + describe 'composite array' do + it 'configures a single tracecontext propagator — correct instance and fields' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: + - tracecontext: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + propagation = OpenTelemetry.propagation + _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator + _(propagation.fields).must_include 'traceparent' + _(propagation.fields).must_include 'tracestate' + end + end + + it 'configures a single baggage propagator — correct instance and fields' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: + - baggage: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + propagation = OpenTelemetry.propagation + _(propagation).must_be_instance_of OpenTelemetry::Baggage::Propagation::TextMapPropagator + _(propagation.fields).must_include 'baggage' + end + end + + it 'composes tracecontext and baggage — correct instance, propagator order, and fields' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: + - tracecontext: + - baggage: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + propagation = OpenTelemetry.propagation + _(propagation).must_be_instance_of OpenTelemetry::Context::Propagation::CompositeTextMapPropagator + + propagators = propagation.instance_variable_get(:@propagators) + _(propagators.size).must_equal 2 + _(propagators[0]).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator + _(propagators[1]).must_be_instance_of OpenTelemetry::Baggage::Propagation::TextMapPropagator + + fields = propagation.fields + _(fields).must_include 'traceparent' + _(fields).must_include 'tracestate' + _(fields).must_include 'baggage' + _(fields.index('traceparent')).must_be :<, fields.index('baggage') + end + end + + it 'silently skips unknown names and still applies the valid propagators' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: + - tracecontext: + - nonexistent_xyz: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + propagation = OpenTelemetry.propagation + _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator + _(propagation.fields).must_include 'traceparent' + end + end + + it 'leaves propagation unconfigured when the composite array is empty' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: [] + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + fields = OpenTelemetry.propagation.fields + _(fields).wont_include 'traceparent' + _(fields).wont_include 'baggage' + end + end + end + + describe 'composite_list string' do + it 'configures tracecontext and baggage from a comma-separated list' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite_list: "tracecontext,baggage" + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + propagation = OpenTelemetry.propagation + _(propagation).must_be_instance_of OpenTelemetry::Context::Propagation::CompositeTextMapPropagator + + propagators = propagation.instance_variable_get(:@propagators) + _(propagators[0]).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator + _(propagators[1]).must_be_instance_of OpenTelemetry::Baggage::Propagation::TextMapPropagator + + _(propagation.fields).must_include 'traceparent' + _(propagation.fields).must_include 'baggage' + end + end + + it 'strips whitespace from each entry' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite_list: " tracecontext , baggage " + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + fields = OpenTelemetry.propagation.fields + _(fields).must_include 'traceparent' + _(fields).must_include 'baggage' + end + end + + it 'silently skips unknown entries and still applies the valid propagators' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite_list: "tracecontext,totally_unknown_propagator" + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + propagation = OpenTelemetry.propagation + _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator + _(propagation.fields).must_include 'traceparent' + end + end + end + + describe 'composite vs composite_list precedence' do + it 'uses composite array and ignores composite_list when both are present' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: + - tracecontext: + composite_list: "baggage" + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + propagation = OpenTelemetry.propagation + _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator + _(propagation.fields).must_include 'traceparent' + _(propagation.fields).wont_include 'baggage' + end + end + end + + describe 'optional gem propagators' do + %w[b3 b3multi jaeger ottrace google_cloud_trace_context].each do |name| + it "does not raise and keeps tracecontext when #{name} is requested" do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: + - #{name}: + - tracecontext: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.propagation.fields).must_include 'traceparent' + end + end + end + + # xray does not implement a `fields` instance method, so the composite's + # fields aggregation cannot be used. Instead we verify the propagator is + # present in the composite's @propagators array and is the correct type. + describe 'xray (gem required)' do + before { require 'opentelemetry-propagator-xray' } + + it 'configures xray alone — propagation is an XRay::TextMapPropagator instance' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: + - xray: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.propagation).must_be_instance_of \ + OpenTelemetry::Propagator::XRay::TextMapPropagator + end + end + + it 'composes xray with tracecontext — both are present in @propagators in order' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + propagator: + composite: + - xray: + - tracecontext: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + propagation = OpenTelemetry.propagation + _(propagation).must_be_instance_of \ + OpenTelemetry::Context::Propagation::CompositeTextMapPropagator + + propagators = propagation.instance_variable_get(:@propagators) + _(propagators.size).must_equal 2 + _(propagators[0]).must_be_instance_of OpenTelemetry::Propagator::XRay::TextMapPropagator + _(propagators[1]).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator + end + end + end + end + + describe 'when propagator section is absent' do + it 'leaves propagation unconfigured' do + with_config(<<~YAML) do |path| + file_format: "1.0" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + fields = OpenTelemetry.propagation.fields + _(fields).wont_include 'traceparent' + _(fields).wont_include 'baggage' + end + end + end + end +end diff --git a/otelconfig/test/opentelemetry/otelconfig/resource_test.rb b/otelconfig/test/opentelemetry/otelconfig/resource_test.rb new file mode 100644 index 000000000..50ab7ffa6 --- /dev/null +++ b/otelconfig/test/opentelemetry/otelconfig/resource_test.rb @@ -0,0 +1,516 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::OtelConfig do + describe 'resource attributes' do + describe 'attributes array with no type field' do + it 'stores a plain string value' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: service.name + value: "my-service" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'my-service' + end + end + + it 'stores a YAML-parsed integer as-is' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: instance.count + value: 3 + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['instance.count']).must_equal 3 + end + end + + it 'stores a YAML-parsed boolean as-is' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: feature.enabled + value: true + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['feature.enabled']).must_equal true + end + end + end + + describe 'attributes array with type: string' do + it 'converts an integer value to its string representation' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: build.number + value: 42 + type: string + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['build.number']).must_equal '42' + _(attrs['build.number']).must_be_kind_of String + end + end + + it 'keeps an existing string value as a string' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: service.namespace + value: "payments" + type: string + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.namespace']).must_equal 'payments' + end + end + end + + describe 'attributes array with typed fields' do + it 'coerces each supported type correctly' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: debug.enabled + value: true + type: bool + - name: debug.disabled + value: false + type: bool + - name: flag.true + value: "true" + type: bool + - name: flag.false + value: "false" + type: bool + - name: max.retries + value: 5 + type: int + - name: sample.ratio + value: 0.25 + type: double + - name: host.tags + value: [web, api, gateway] + type: string_array + - name: feature.flags + value: [true, false, true] + type: bool_array + - name: allowed.ports + value: [8080, 8443, 9000] + type: int_array + - name: cpu.percentages + value: [0.25, 0.50, 0.75] + type: double_array + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + + # bool: native YAML booleans + _(attrs['debug.enabled']).must_equal true + _(attrs['debug.disabled']).must_equal false + + # bool: string coercion + _(attrs['flag.true']).must_equal true + _(attrs['flag.false']).must_equal false + + # int + _(attrs['max.retries']).must_equal 5 + _(attrs['max.retries']).must_be_kind_of Integer + + # double + _(attrs['sample.ratio']).must_equal 0.25 + _(attrs['sample.ratio']).must_be_kind_of Float + + # string_array + _(attrs['host.tags']).must_equal %w[web api gateway] + + # bool_array + _(attrs['feature.flags']).must_equal [true, false, true] + + # int_array + _(attrs['allowed.ports']).must_equal [8080, 8443, 9000] + + # double_array + _(attrs['cpu.percentages']).must_equal [0.25, 0.50, 0.75] + end + end + end + + describe 'attributes array with invalid entries' do + it 'skips entries that have no name key' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - value: "orphan" + - name: service.name + value: "valid" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'valid' + _(attrs.value?('orphan')).must_equal false + end + end + + it 'skips entries whose value is null (YAML ~)' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: empty.key + value: ~ + - name: service.name + value: "present" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs.key?('empty.key')).must_equal false + _(attrs['service.name']).must_equal 'present' + end + end + end + + describe 'multiple attributes together' do + it 'includes all named attributes regardless of type' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: service.name + value: "inventory-api" + - name: deployment.environment + value: "production" + - name: service.version + value: "2.1.0" + - name: max.connections + value: 100 + type: int + - name: sample.ratio + value: 0.5 + type: double + - name: debug.enabled + value: false + type: bool + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'inventory-api' + _(attrs['deployment.environment']).must_equal 'production' + _(attrs['service.version']).must_equal '2.1.0' + _(attrs['max.connections']).must_equal 100 + _(attrs['sample.ratio']).must_equal 0.5 + _(attrs['debug.enabled']).must_equal false + end + end + end + + describe 'attributes_list' do + it 'parses a single key=value pair' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes_list: "env=production" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['env']).must_equal 'production' + end + end + + it 'parses multiple comma-separated key=value pairs' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes_list: "region=us-east-1,team=platform,tier=backend" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['region']).must_equal 'us-east-1' + _(attrs['team']).must_equal 'platform' + _(attrs['tier']).must_equal 'backend' + end + end + + it 'preserves a value containing = by splitting only on the first =' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes_list: "auth.token=abc=def=ghi" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['auth.token']).must_equal 'abc=def=ghi' + end + end + end + + describe 'priority: attributes array over attributes_list' do + it 'keeps the attributes-array value when both sources define the same key' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: service.name + value: "from-array" + attributes_list: "service.name=from-list" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'from-array' + end + end + + it 'still includes keys that appear only in attributes_list' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: service.name + value: "from-array" + attributes_list: "service.name=from-list,extra.key=bonus-value" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'from-array' + _(attrs['extra.key']).must_equal 'bonus-value' + end + end + + it 'still includes keys that appear only in the attributes array' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: only-in-array + value: "here" + attributes_list: "only-in-list=there" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['only-in-array']).must_equal 'here' + _(attrs['only-in-list']).must_equal 'there' + end + end + end + + describe 'schema_url' do + it 'does not raise and still applies all attributes' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + schema_url: "https://opentelemetry.io/schemas/1.21.0" + attributes: + - name: service.name + value: "schema-test" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'schema-test' + end + end + end + + describe 'detection/development' do + it 'does not raise for an unknown detector and preserves explicit attributes' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + detection/development: + detectors: + - unknown_detector_xyz: + attributes: + - name: service.name + value: "detection-test" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'detection-test' + end + end + + it 'applies included pattern filtering without raising' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + detection/development: + detectors: + - unknown_detector_xyz: + attributes: + included: + - "process.*" + excluded: [] + attributes: + - name: service.name + value: "filter-test" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'filter-test' + end + end + + it 'applies excluded pattern filtering without raising' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + detection/development: + detectors: + - unknown_detector_xyz: + attributes: + included: [] + excluded: + - "host.*" + attributes: + - name: service.name + value: "exclude-test" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'exclude-test' + end + end + end + + describe 'merged with default SDK resource' do + it 'preserves built-in telemetry.sdk.* attributes alongside custom ones' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: service.name + value: "sdk-merge-test" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(attrs['service.name']).must_equal 'sdk-merge-test' + _(attrs).must_include 'telemetry.sdk.name' + _(attrs).must_include 'telemetry.sdk.language' + end + end + end + + describe 'resource shared across all providers' do + it 'applies the same resource attributes to the tracer_provider' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: service.name + value: "shared-service" + - name: deployment.environment + value: "staging" + #{TRACER_PROVIDER_YAML} + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + tp_attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + + _(tp_attrs['service.name']).must_equal 'shared-service' + _(tp_attrs['deployment.environment']).must_equal 'staging' + end + end + end + end +end diff --git a/otelconfig/test/opentelemetry/otelconfig_test.rb b/otelconfig/test/opentelemetry/otelconfig_test.rb new file mode 100644 index 000000000..b6516d0d4 --- /dev/null +++ b/otelconfig/test/opentelemetry/otelconfig_test.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +require 'test_helper' + +describe OpenTelemetry::OtelConfig do + describe 'disabled flag' do + it 'skips SDK provider setup when disabled: true' do + with_config(<<~YAML) do |path| + file_format: "1.0" + disabled: true + tracer_provider: + processors: + - simple: + exporter: + console: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider).wont_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider + end + end + + it 'applies SDK provider setup when disabled: false' do + with_config(<<~YAML) do |path| + file_format: "1.0" + disabled: false + tracer_provider: + processors: + - simple: + exporter: + console: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider + end + end + end + + describe 'when provider sections are absent' do + it 'does not install a tracer provider' do + with_config(<<~YAML) do |path| + file_format: "1.0" + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider).wont_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider + end + end + end + + describe 'tracer_provider and propagator configured together' do + it 'creates the SDK tracer_provider with the shared resource and correct processors' do + with_config(<<~YAML) do |path| + file_format: "1.0" + resource: + attributes: + - name: service.name + value: "full-stack-test" + tracer_provider: + processors: + - simple: + exporter: + console: + propagator: + composite: + - tracecontext: + - baggage: + YAML + OpenTelemetry::OtelConfig.configure_from_file(path) + + _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider + + tp_attrs = OpenTelemetry.tracer_provider + .instance_variable_get(:@resource) + .attribute_enumerator.to_h + _(tp_attrs['service.name']).must_equal 'full-stack-test' + + fields = OpenTelemetry.propagation.fields + _(fields).must_include 'traceparent' + _(fields).must_include 'baggage' + end + end + end +end diff --git a/otelconfig/test/test_helper.rb b/otelconfig/test/test_helper.rb new file mode 100644 index 000000000..95804fa9e --- /dev/null +++ b/otelconfig/test/test_helper.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +require 'simplecov' +SimpleCov.start + +require 'bundler/setup' + +$LOAD_PATH.unshift File.join(__dir__, '..', 'lib') + +require 'opentelemetry-sdk' +require 'opentelemetry-exporter-otlp' +require 'opentelemetry/otelconfig' +require 'opentelemetry/test_helpers' + +require 'minitest/autorun' +require 'minitest/spec' +require 'tempfile' + +# Writes +yaml+ to a Tempfile and yields its path, then deletes the file. +def with_config(yaml) + tmp = Tempfile.new(['otel-config', '.yaml']) + tmp.write(yaml) + tmp.close + yield tmp.path +ensure + tmp.unlink +end + +# Reset after every test across all spec files. +Minitest::Spec.after do + tp = OpenTelemetry.tracer_provider + tp.shutdown if tp.respond_to?(:shutdown) + + OpenTelemetry::TestHelpers.reset_opentelemetry +end + +# Shared minimal tracer_provider YAML used by end-to-end tests. +TRACER_PROVIDER_YAML = <<~PROVIDER + tracer_provider: + processors: + - simple: + exporter: + console: +PROVIDER