From 8d11a1b3b64aa90b4a839b0da3af6dee89cbcbef Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Sun, 24 May 2026 23:11:20 -0400 Subject: [PATCH 01/10] feat(chore): declarative config for trace --- otelconfig/.rubocop.yml | 16 + otelconfig/.yardopts | 9 + otelconfig/CHANGELOG.md | 0 otelconfig/Gemfile | 41 ++ otelconfig/LICENSE | 201 +++++++ otelconfig/README.md | 161 ++++++ otelconfig/Rakefile | 31 ++ otelconfig/example/Gemfile | 13 + otelconfig/example/README.md | 45 ++ otelconfig/example/app.rb | 58 ++ otelconfig/example/otel-config-console.yaml | 46 ++ otelconfig/example/otel-config.yaml | 64 +++ .../lib/opentelemetry/components/trace.rb | 180 ++++++ otelconfig/lib/opentelemetry/otelconfig.rb | 75 +++ .../otelconfig/instrumentation.rb | 54 ++ .../opentelemetry/otelconfig/propagation.rb | 72 +++ .../lib/opentelemetry/otelconfig/resource.rb | 113 ++++ .../lib/opentelemetry/otelconfig/version.rb | 10 + otelconfig/lib/opentelemetry_otelconfig.rb | 6 + otelconfig/opentelemetry-otelconfig.gemspec | 47 ++ otelconfig/test/components/trace_test.rb | 233 ++++++++ .../otelconfig/instrumentation_test.rb | 497 +++++++++++++++++ .../otelconfig/propagation_test.rb | 274 +++++++++ .../opentelemetry/otelconfig/resource_test.rb | 520 ++++++++++++++++++ .../test/opentelemetry/otelconfig_test.rb | 88 +++ otelconfig/test/test_helper.rb | 47 ++ 26 files changed, 2901 insertions(+) create mode 100644 otelconfig/.rubocop.yml create mode 100644 otelconfig/.yardopts create mode 100644 otelconfig/CHANGELOG.md create mode 100644 otelconfig/Gemfile create mode 100644 otelconfig/LICENSE create mode 100644 otelconfig/README.md create mode 100644 otelconfig/Rakefile create mode 100644 otelconfig/example/Gemfile create mode 100644 otelconfig/example/README.md create mode 100755 otelconfig/example/app.rb create mode 100644 otelconfig/example/otel-config-console.yaml create mode 100644 otelconfig/example/otel-config.yaml create mode 100644 otelconfig/lib/opentelemetry/components/trace.rb create mode 100644 otelconfig/lib/opentelemetry/otelconfig.rb create mode 100644 otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb create mode 100644 otelconfig/lib/opentelemetry/otelconfig/propagation.rb create mode 100644 otelconfig/lib/opentelemetry/otelconfig/resource.rb create mode 100644 otelconfig/lib/opentelemetry/otelconfig/version.rb create mode 100644 otelconfig/lib/opentelemetry_otelconfig.rb create mode 100644 otelconfig/opentelemetry-otelconfig.gemspec create mode 100644 otelconfig/test/components/trace_test.rb create mode 100644 otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb create mode 100644 otelconfig/test/opentelemetry/otelconfig/propagation_test.rb create mode 100644 otelconfig/test/opentelemetry/otelconfig/resource_test.rb create mode 100644 otelconfig/test/opentelemetry/otelconfig_test.rb create mode 100644 otelconfig/test/test_helper.rb diff --git a/otelconfig/.rubocop.yml b/otelconfig/.rubocop.yml new file mode 100644 index 0000000000..41f6eb369f --- /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 0000000000..bb0d313dcf --- /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 0000000000..e69de29bb2 diff --git a/otelconfig/Gemfile b/otelconfig/Gemfile new file mode 100644 index 0000000000..a0e7976801 --- /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 0000000000..1ef7dad2c5 --- /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 0000000000..635edc7f70 --- /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 +[example-dir]: https://github.com/open-telemetry/opentelemetry-ruby/tree/main/otelconfig/example +[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 0000000000..d202bf4c43 --- /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 0000000000..dea7fdef2a --- /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 0000000000..2be07b4424 --- /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 0000000000..19d2bcf972 --- /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 0000000000..bc67ae8b38 --- /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 0000000000..9a4969a211 --- /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: 0.0001 + 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 0000000000..a4312344a7 --- /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 0000000000..2e3d2cd7de --- /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 0000000000..ee5d1c6f51 --- /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 0000000000..e20c771e9e --- /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 0000000000..4c61fcc968 --- /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 0000000000..7daa1850c6 --- /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 0000000000..089d1c9395 --- /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 0000000000..ebaafae2c7 --- /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 0000000000..32f43d7454 --- /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 0000000000..fd04e0e2c6 --- /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 0000000000..148f22dad6 --- /dev/null +++ b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb @@ -0,0 +1,274 @@ +# 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 + + # ------------------------------------------------------------------------- + # composite_list string format + # ------------------------------------------------------------------------- + + 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 + + # ------------------------------------------------------------------------- + # composite array takes priority over composite_list + # ------------------------------------------------------------------------- + + 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 + + # ------------------------------------------------------------------------- + # optional gem propagators + # b3, b3multi, jaeger, ottrace, xray, google_cloud_trace_context + # Each may or may not have its gem installed; the test verifies no crash and + # that tracecontext (always available) is still applied. + # ------------------------------------------------------------------------- + + 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 + + # ------------------------------------------------------------------------- + # no propagator section + # ------------------------------------------------------------------------- + + 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 0000000000..80acbb2621 --- /dev/null +++ b/otelconfig/test/opentelemetry/otelconfig/resource_test.rb @@ -0,0 +1,520 @@ +# 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 + + # ------------------------------------------------------------------------- + # attributes array — type: string + # ------------------------------------------------------------------------- + + 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 0000000000..b6516d0d46 --- /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 0000000000..95804fa9e1 --- /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 From 76e310fd3f70d2e8f0a75296719c8f897ba9d6c8 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 25 May 2026 00:42:52 -0400 Subject: [PATCH 02/10] use ratio 1 --- otelconfig/example/otel-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/otelconfig/example/otel-config.yaml b/otelconfig/example/otel-config.yaml index 9a4969a211..b968b62c55 100644 --- a/otelconfig/example/otel-config.yaml +++ b/otelconfig/example/otel-config.yaml @@ -41,7 +41,7 @@ tracer_provider: parent_based: root: trace_id_ratio_based: - ratio: 0.0001 + ratio: 1.0 remote_parent_sampled: always_on: remote_parent_not_sampled: From 08d74c7a860e90a93d4a3878ea7614500c9163c7 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 25 May 2026 11:28:08 -0400 Subject: [PATCH 03/10] remove unnecessary comments --- .../otelconfig/propagation_test.rb | 19 ------------------- .../opentelemetry/otelconfig/resource_test.rb | 4 ---- 2 files changed, 23 deletions(-) diff --git a/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb index 148f22dad6..de32a87868 100644 --- a/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb +++ b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb @@ -101,10 +101,6 @@ end end - # ------------------------------------------------------------------------- - # composite_list string format - # ------------------------------------------------------------------------- - describe 'composite_list string' do it 'configures tracecontext and baggage from a comma-separated list' do with_config(<<~YAML) do |path| @@ -158,10 +154,6 @@ end end - # ------------------------------------------------------------------------- - # composite array takes priority over composite_list - # ------------------------------------------------------------------------- - 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| @@ -182,13 +174,6 @@ end end - # ------------------------------------------------------------------------- - # optional gem propagators - # b3, b3multi, jaeger, ottrace, xray, google_cloud_trace_context - # Each may or may not have its gem installed; the test verifies no crash and - # that tracecontext (always available) is still applied. - # ------------------------------------------------------------------------- - 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 @@ -252,10 +237,6 @@ end end - # ------------------------------------------------------------------------- - # no propagator section - # ------------------------------------------------------------------------- - describe 'when propagator section is absent' do it 'leaves propagation unconfigured' do with_config(<<~YAML) do |path| diff --git a/otelconfig/test/opentelemetry/otelconfig/resource_test.rb b/otelconfig/test/opentelemetry/otelconfig/resource_test.rb index 80acbb2621..50ab7ffa6c 100644 --- a/otelconfig/test/opentelemetry/otelconfig/resource_test.rb +++ b/otelconfig/test/opentelemetry/otelconfig/resource_test.rb @@ -63,10 +63,6 @@ end end - # ------------------------------------------------------------------------- - # attributes array — type: string - # ------------------------------------------------------------------------- - describe 'attributes array with type: string' do it 'converts an integer value to its string representation' do with_config(<<~YAML) do |path| From 9b032d4d488711d29182ee48081159f81c9d39ef Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 25 May 2026 11:34:26 -0400 Subject: [PATCH 04/10] lint --- .cspell.yml | 1 + otelconfig/README.md | 4 ++-- otelconfig/example/README.md | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.cspell.yml b/.cspell.yml index f451df7710..258c22c379 100644 --- a/.cspell.yml +++ b/.cspell.yml @@ -68,6 +68,7 @@ ignoreWords: - rolldice - codegen - Dockerfiles + - otelconfig words: - autocorrection - bigdecimal diff --git a/otelconfig/README.md b/otelconfig/README.md index 635edc7f70..54c40ca4ea 100644 --- a/otelconfig/README.md +++ b/otelconfig/README.md @@ -48,6 +48,7 @@ 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. @@ -74,7 +75,7 @@ resource: ### Samplers | Sampler | YAML key | -|---|---| +| ------- | -------- | | Always on | `always_on:` | | Always off | `always_off:` | | Trace-ID ratio | `trace_id_ratio_based: { ratio: 0.25 }` | @@ -155,7 +156,6 @@ The `opentelemetry-otelconfig` gem is distributed under the Apache 2.0 license. [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 -[example-dir]: https://github.com/open-telemetry/opentelemetry-ruby/tree/main/otelconfig/example [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/example/README.md b/otelconfig/example/README.md index 2be07b4424..29cbf6a3fd 100644 --- a/otelconfig/example/README.md +++ b/otelconfig/example/README.md @@ -1,13 +1,13 @@ # Declarative Configuration Example -This example shows how to configure the OpenTelemetry SDK (tracing) from a YAML -file using the `opentelemetry-otelconfig` gem — no programmatic +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 | @@ -35,7 +35,7 @@ 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 | From eeba8525a75691395b6442bbdf26f07218e702b6 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 15 Jun 2026 10:48:19 -0400 Subject: [PATCH 05/10] return struct with providers and propagators --- otelconfig/example/app.rb | 4 +- otelconfig/lib/opentelemetry/otelconfig.rb | 10 ++- .../lib/opentelemetry/otelconfig/constants.rb | 12 ++++ .../otelconfig/instrumentation.rb | 7 ++- .../opentelemetry/otelconfig/propagation.rb | 4 +- otelconfig/opentelemetry-otelconfig.gemspec | 7 +-- otelconfig/test/components/trace_test.rb | 27 +++++--- .../otelconfig/instrumentation_test.rb | 61 +++++++++--------- .../otelconfig/propagation_test.rb | 52 +++++++++++---- .../opentelemetry/otelconfig/resource_test.rb | 63 ++++++++++++------- .../test/opentelemetry/otelconfig_test.rb | 7 ++- 11 files changed, 167 insertions(+), 87 deletions(-) create mode 100644 otelconfig/lib/opentelemetry/otelconfig/constants.rb diff --git a/otelconfig/example/app.rb b/otelconfig/example/app.rb index 19d2bcf972..161314d922 100755 --- a/otelconfig/example/app.rb +++ b/otelconfig/example/app.rb @@ -12,7 +12,9 @@ require 'opentelemetry-instrumentation-all' require 'opentelemetry_otelconfig' -OpenTelemetry::OtelConfig.configure +sdk = OpenTelemetry::OtelConfig.configure +OpenTelemetry.tracer_provider = sdk.tracer_provider +OpenTelemetry.propagation = sdk.propagator if sdk&.propagator tracer = OpenTelemetry.tracer_provider.tracer('otelconfig-example', '1.0.0') diff --git a/otelconfig/lib/opentelemetry/otelconfig.rb b/otelconfig/lib/opentelemetry/otelconfig.rb index 2e3d2cd7de..1ad9935445 100644 --- a/otelconfig/lib/opentelemetry/otelconfig.rb +++ b/otelconfig/lib/opentelemetry/otelconfig.rb @@ -10,6 +10,7 @@ require_relative 'otelconfig/instrumentation' require_relative 'otelconfig/propagation' require_relative 'otelconfig/resource' +require_relative 'otelconfig/constants' module OpenTelemetry # OtelConfig module handles declarative configuration of OpenTelemetry components @@ -53,10 +54,15 @@ def apply(config) resource = build_resource(config['resource']) tracer_provider = Trace.build_tracer_provider(config['tracer_provider'], resource) - OpenTelemetry.tracer_provider = tracer_provider + propagators = configure_propagation(config['propagator']) - configure_propagation(config['propagator']) configure_instrumentation(config['instrumentation/development']) + + RubySDK.new( + :tracer_provider => tracer_provider, + :propagator => propagators, + :resource => resource + ) end end diff --git a/otelconfig/lib/opentelemetry/otelconfig/constants.rb b/otelconfig/lib/opentelemetry/otelconfig/constants.rb new file mode 100644 index 0000000000..7251c2f0ea --- /dev/null +++ b/otelconfig/lib/opentelemetry/otelconfig/constants.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +RubySDK = Struct.new( + :tracer_provider, + :meter_provider, + :logger_provider, + :resource, + :propagator +) diff --git a/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb b/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb index ee5d1c6f51..207a2338f8 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb @@ -11,6 +11,7 @@ class << self def configure_instrumentation(instrumentation_cfg) config_map = build_instrumentation_config_map(instrumentation_cfg) OpenTelemetry::Instrumentation.registry.install_all(config_map) + config_map rescue NameError OpenTelemetry.logger.warn('opentelemetry-instrumentation-all not available; skipping instrumentation install.') end @@ -20,11 +21,11 @@ def configure_instrumentation(instrumentation_cfg) def build_instrumentation_config_map(instrumentation_cfg) return {} unless instrumentation_cfg.is_a?(Hash) - general = instrumentation_cfg['general'] - return {} unless general.is_a?(Hash) + ruby_instrumentation = instrumentation_cfg['ruby'] + return {} unless ruby_instrumentation.is_a?(Hash) name_map = build_instrumentation_name_map - general.each_with_object({}) do |(short_name, options), result| + ruby_instrumentation.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.") diff --git a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb index e20c771e9e..10d59a4fc3 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb @@ -17,9 +17,7 @@ def configure_propagation(propagator_cfg) propagators = names.filter_map { |name| resolve_propagator(name) } return if propagators.empty? - OpenTelemetry.propagation = - OpenTelemetry::Context::Propagation::CompositeTextMapPropagator - .compose_propagators(propagators) + return OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators(propagators) end # Extracts an ordered list of propagator name strings from the config hash. diff --git a/otelconfig/opentelemetry-otelconfig.gemspec b/otelconfig/opentelemetry-otelconfig.gemspec index ebaafae2c7..b83fb9a714 100644 --- a/otelconfig/opentelemetry-otelconfig.gemspec +++ b/otelconfig/opentelemetry-otelconfig.gemspec @@ -38,10 +38,9 @@ Gem::Specification.new do |spec| 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['changelog_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}/file/CHANGELOG.md" + spec.metadata['source_code_uri'] = "https://github.com/open-telemetry/opentelemetry-ruby/tree/#{spec.name}/v#{spec.version}/otelconfig" 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}" + spec.metadata['documentation_uri'] = "https://rubydoc.info/gems/#{spec.name}/#{spec.version}" end end diff --git a/otelconfig/test/components/trace_test.rb b/otelconfig/test/components/trace_test.rb index 32f43d7454..731c2d83a1 100644 --- a/otelconfig/test/components/trace_test.rb +++ b/otelconfig/test/components/trace_test.rb @@ -13,7 +13,8 @@ file_format: "1.0" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider tp = OpenTelemetry.tracer_provider _(tp).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider @@ -47,7 +48,8 @@ compression: gzip timeout: 10000 YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider processors = OpenTelemetry.tracer_provider.instance_variable_get(:@span_processors) _(processors.size).must_equal 1 @@ -79,7 +81,8 @@ otlp_http: endpoint: http://localhost:4318/v1/traces YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider processors = OpenTelemetry.tracer_provider.instance_variable_get(:@span_processors) _(processors.size).must_equal 1 @@ -106,7 +109,8 @@ exporter: console: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider processors = OpenTelemetry.tracer_provider.instance_variable_get(:@span_processors) _(processors.size).must_equal 2 @@ -132,7 +136,8 @@ sampler: always_on: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider _(OpenTelemetry.tracer_provider.sampler).must_equal OpenTelemetry::SDK::Trace::Samplers::ALWAYS_ON end @@ -149,7 +154,8 @@ sampler: always_off: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider _(OpenTelemetry.tracer_provider.sampler).must_equal OpenTelemetry::SDK::Trace::Samplers::ALWAYS_OFF end @@ -167,7 +173,8 @@ trace_id_ratio_based: ratio: 0.25 YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider sampler = OpenTelemetry.tracer_provider.sampler _(sampler.description).must_match(/0.25/) @@ -191,7 +198,8 @@ remote_parent_not_sampled: always_off: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider sampler = OpenTelemetry.tracer_provider.sampler _(sampler.description).must_match(/ParentBased/) @@ -216,7 +224,8 @@ event_attribute_count_limit: 8 link_attribute_count_limit: 4 YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider limits = OpenTelemetry.tracer_provider .instance_variable_get(:@span_limits) diff --git a/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb b/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb index fd04e0e2c6..ea5d394eca 100644 --- a/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb +++ b/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb @@ -111,7 +111,8 @@ def fake_instrumentation_class(full_name) file_format: "1.0" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider end @@ -122,14 +123,15 @@ def fake_instrumentation_class(full_name) file_format: "1.0" #{TRACER_PROVIDER_YAML} instrumentation: - general: + ruby: net_http: untraced_hosts: - example.com rack: record_frontend_span: false YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider end @@ -140,7 +142,7 @@ def fake_instrumentation_class(full_name) file_format: "1.0" #{TRACER_PROVIDER_YAML} instrumentation: - general: + ruby: redis: peer_service: "cache-cluster" trace_root_spans: true @@ -154,7 +156,8 @@ def fake_instrumentation_class(full_name) force_flush: true span_naming: job_class YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider end @@ -172,17 +175,17 @@ def fake_instrumentation_class(full_name) _(OpenTelemetry::OtelConfig.build_instrumentation_config_map(42)).must_equal({}) end - it 'returns {} when the general key is absent' do + it 'returns {} when the ruby 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({}) + it 'returns {} when ruby is not a Hash' do + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map({ 'ruby' => 'flat' })).must_equal({}) + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map({ 'ruby' => [] })).must_equal({}) end - it 'returns {} when general is an empty Hash' do - _(OpenTelemetry::OtelConfig.build_instrumentation_config_map({ 'general' => {} })).must_equal({}) + it 'returns {} when ruby is an empty Hash' do + _(OpenTelemetry::OtelConfig.build_instrumentation_config_map({ 'ruby' => {} })).must_equal({}) end end @@ -191,7 +194,7 @@ def fake_instrumentation_class(full_name) 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' => {} } } + cfg = { 'ruby' => { 'net_http' => {} } } result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) _(result.keys).must_include 'OpenTelemetry::Instrumentation::Net::HTTP' end @@ -199,7 +202,7 @@ def fake_instrumentation_class(full_name) it 'symbolizes option keys' do with_name_map(FAKE_NAME_MAP) do - cfg = { 'general' => { 'net_http' => { 'untraced_hosts' => ['localhost'] } } } + cfg = { 'ruby' => { '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 @@ -209,7 +212,7 @@ def fake_instrumentation_class(full_name) it 'treats nil options as an empty Hash' do with_name_map(FAKE_NAME_MAP) do - cfg = { 'general' => { 'net_http' => nil } } + cfg = { 'ruby' => { 'net_http' => nil } } result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) _(result['OpenTelemetry::Instrumentation::Net::HTTP']).must_equal({}) end @@ -217,7 +220,7 @@ def fake_instrumentation_class(full_name) it 'treats non-Hash options as an empty Hash' do with_name_map(FAKE_NAME_MAP) do - cfg = { 'general' => { 'net_http' => 'enabled' } } + cfg = { 'ruby' => { 'net_http' => 'enabled' } } result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) _(result['OpenTelemetry::Instrumentation::Net::HTTP']).must_equal({}) end @@ -225,7 +228,7 @@ def fake_instrumentation_class(full_name) it 'skips and does not include unknown short names' do with_name_map(FAKE_NAME_MAP) do - cfg = { 'general' => { 'totally_unknown_lib' => { 'opt' => 1 } } } + cfg = { 'ruby' => { 'totally_unknown_lib' => { 'opt' => 1 } } } result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) _(result).must_equal({}) end @@ -234,7 +237,7 @@ def fake_instrumentation_class(full_name) it 'maps multiple instrumentations in one call' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'net_http' => { 'untraced_hosts' => ['internal.example.com'] }, 'redis' => { 'peer_service' => 'cache', 'trace_root_spans' => true } } @@ -251,7 +254,7 @@ def fake_instrumentation_class(full_name) 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'] } } } + cfg = { 'ruby' => { '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'] @@ -264,7 +267,7 @@ def fake_instrumentation_class(full_name) it 'maps all rack options correctly' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'rack' => { 'allowed_request_headers' => %w[X-Request-ID X-Forwarded-For], 'allowed_response_headers' => ['X-Response-Time'], @@ -289,7 +292,7 @@ def fake_instrumentation_class(full_name) it 'maps peer_service, trace_root_spans, and db_statement' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'redis' => { 'peer_service' => 'redis-primary', 'trace_root_spans' => false, @@ -310,7 +313,7 @@ def fake_instrumentation_class(full_name) it 'maps span_naming, propagation_style, and boolean trace flags' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'sidekiq' => { 'span_naming' => 'job_class', 'propagation_style' => 'child', @@ -338,7 +341,7 @@ def fake_instrumentation_class(full_name) it 'maps propagation_style, force_flush, and span_naming' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'active_job' => { 'propagation_style' => 'none', 'force_flush' => true, @@ -359,7 +362,7 @@ def fake_instrumentation_class(full_name) it 'maps span_kind, peer_service, and enable_internal_instrumentation' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'faraday' => { 'span_kind' => 'internal', 'peer_service' => 'downstream-api', @@ -380,7 +383,7 @@ def fake_instrumentation_class(full_name) it 'maps db_statement, obfuscation_limit, span_name, and peer_service' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'mysql2' => { 'peer_service' => 'mysql-primary', 'db_statement' => 'omit', @@ -403,7 +406,7 @@ def fake_instrumentation_class(full_name) it 'maps db_statement, obfuscation_limit, and peer_service' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'pg' => { 'peer_service' => 'postgres-replica', 'db_statement' => 'include', @@ -424,7 +427,7 @@ def fake_instrumentation_class(full_name) it 'maps allowed_metadata_headers and peer_service' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'grpc' => { 'allowed_metadata_headers' => %w[x-correlation-id x-tenant-id], 'peer_service' => 'grpc-backend' @@ -443,7 +446,7 @@ def fake_instrumentation_class(full_name) it 'maps schemas array and all boolean platform flags' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'graphql' => { 'schemas' => [], 'enable_platform_field' => true, @@ -468,7 +471,7 @@ def fake_instrumentation_class(full_name) it 'maps peer_service and db_statement' do with_name_map(FAKE_NAME_MAP) do cfg = { - 'general' => { + 'ruby' => { 'dalli' => { 'peer_service' => 'memcached', 'db_statement' => 'omit' @@ -486,7 +489,7 @@ def fake_instrumentation_class(full_name) describe 'action_pack options' do it 'maps span_naming' do with_name_map(FAKE_NAME_MAP) do - cfg = { 'general' => { 'action_pack' => { 'span_naming' => 'class' } } } + cfg = { 'ruby' => { 'action_pack' => { 'span_naming' => 'class' } } } result = OpenTelemetry::OtelConfig.build_instrumentation_config_map(cfg) _(result['OpenTelemetry::Instrumentation::ActionPack']).must_equal(span_naming: 'class') end diff --git a/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb index de32a87868..9ec593060a 100644 --- a/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb +++ b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb @@ -16,7 +16,9 @@ composite: - tracecontext: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator @@ -33,7 +35,9 @@ composite: - baggage: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation _(propagation).must_be_instance_of OpenTelemetry::Baggage::Propagation::TextMapPropagator @@ -50,7 +54,9 @@ - tracecontext: - baggage: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation _(propagation).must_be_instance_of OpenTelemetry::Context::Propagation::CompositeTextMapPropagator @@ -77,7 +83,9 @@ - tracecontext: - nonexistent_xyz: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator @@ -92,7 +100,9 @@ propagator: composite: [] YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator fields = OpenTelemetry.propagation.fields _(fields).wont_include 'traceparent' @@ -109,7 +119,9 @@ propagator: composite_list: "tracecontext,baggage" YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation _(propagation).must_be_instance_of OpenTelemetry::Context::Propagation::CompositeTextMapPropagator @@ -130,7 +142,9 @@ propagator: composite_list: " tracecontext , baggage " YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator fields = OpenTelemetry.propagation.fields _(fields).must_include 'traceparent' @@ -145,7 +159,9 @@ propagator: composite_list: "tracecontext,totally_unknown_propagator" YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator @@ -164,7 +180,9 @@ - tracecontext: composite_list: "baggage" YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator @@ -185,7 +203,9 @@ - #{name}: - tracecontext: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator _(OpenTelemetry.propagation.fields).must_include 'traceparent' end @@ -206,7 +226,9 @@ composite: - xray: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator _(OpenTelemetry.propagation).must_be_instance_of \ OpenTelemetry::Propagator::XRay::TextMapPropagator @@ -222,7 +244,9 @@ - xray: - tracecontext: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation _(propagation).must_be_instance_of \ @@ -243,7 +267,9 @@ file_format: "1.0" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator if sdk.propagator fields = OpenTelemetry.propagation.fields _(fields).wont_include 'traceparent' diff --git a/otelconfig/test/opentelemetry/otelconfig/resource_test.rb b/otelconfig/test/opentelemetry/otelconfig/resource_test.rb index 50ab7ffa6c..687b20da47 100644 --- a/otelconfig/test/opentelemetry/otelconfig/resource_test.rb +++ b/otelconfig/test/opentelemetry/otelconfig/resource_test.rb @@ -17,7 +17,8 @@ value: "my-service" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -35,7 +36,8 @@ value: 3 #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -53,7 +55,8 @@ value: true #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -74,7 +77,8 @@ type: string #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -94,7 +98,8 @@ type: string #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -142,7 +147,8 @@ type: double_array #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -190,7 +196,8 @@ value: "valid" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -211,7 +218,8 @@ value: "present" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -245,7 +253,8 @@ type: bool #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -268,7 +277,8 @@ attributes_list: "env=production" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -284,7 +294,8 @@ attributes_list: "region=us-east-1,team=platform,tier=backend" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -302,7 +313,8 @@ attributes_list: "auth.token=abc=def=ghi" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -323,7 +335,8 @@ attributes_list: "service.name=from-list" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -342,7 +355,8 @@ attributes_list: "service.name=from-list,extra.key=bonus-value" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -362,7 +376,8 @@ attributes_list: "only-in-list=there" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -384,7 +399,8 @@ value: "schema-test" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -407,7 +423,8 @@ value: "detection-test" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -432,7 +449,8 @@ value: "filter-test" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -457,7 +475,8 @@ value: "exclude-test" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -477,7 +496,8 @@ value: "sdk-merge-test" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) @@ -501,7 +521,8 @@ value: "staging" #{TRACER_PROVIDER_YAML} YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider tp_attrs = OpenTelemetry.tracer_provider .instance_variable_get(:@resource) diff --git a/otelconfig/test/opentelemetry/otelconfig_test.rb b/otelconfig/test/opentelemetry/otelconfig_test.rb index b6516d0d46..4b71a12c8f 100644 --- a/otelconfig/test/opentelemetry/otelconfig_test.rb +++ b/otelconfig/test/opentelemetry/otelconfig_test.rb @@ -33,7 +33,8 @@ exporter: console: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider end @@ -70,7 +71,9 @@ - tracecontext: - baggage: YAML - OpenTelemetry::OtelConfig.configure_from_file(path) + sdk = OpenTelemetry::OtelConfig.configure_from_file(path) + OpenTelemetry.tracer_provider = sdk.tracer_provider + OpenTelemetry.propagation = sdk.propagator _(OpenTelemetry.tracer_provider).must_be_instance_of OpenTelemetry::SDK::Trace::TracerProvider From 479dd93b7cb8e2f77b157247fb95791910aaf252 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 15 Jun 2026 10:54:14 -0400 Subject: [PATCH 06/10] gemspec --- otelconfig/Gemfile | 29 ++++++++++++++------- otelconfig/Rakefile | 7 ++--- otelconfig/opentelemetry-otelconfig.gemspec | 13 --------- 3 files changed, 21 insertions(+), 28 deletions(-) diff --git a/otelconfig/Gemfile b/otelconfig/Gemfile index a0e7976801..fb73416e10 100644 --- a/otelconfig/Gemfile +++ b/otelconfig/Gemfile @@ -9,15 +9,15 @@ 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' + gem 'minitest', '6.0.6' + gem 'rake', '13.4.2' + gem 'rubocop', '1.87.0' + gem 'rubocop-minitest', '0.39.1' + gem 'rubocop-performance', '1.26.1' + gem 'rubocop-rake', '0.7.1' + gem 'rubocop-rspec', '3.10.2' + gem 'simplecov', '0.22.0' + gem 'yard', '0.9.44' # Local path overrides for gems developed in this monorepo gem 'opentelemetry-api', path: '../api', require: false @@ -28,9 +28,18 @@ group :test, :development do gem 'opentelemetry-sdk', path: '../sdk', require: false gem 'opentelemetry-test-helpers', path: '../test_helpers', require: false + gem 'opentelemetry-instrumentation-all', '0.94.0' + gem 'opentelemetry-propagator-google_cloud_trace_context', '0.4.0' + gem 'opentelemetry-propagator-ottrace', '0.25.0' + gem 'opentelemetry-propagator-xray', '0.27.0' + gem 'opentelemetry-resource-detector-aws', '0.7.0' + gem 'opentelemetry-resource-detector-azure', '0.4.0' + gem 'opentelemetry-resource-detector-container', '0.4.0' + gem 'opentelemetry-resource-detector-google_cloud_platform', '0.4.0' + # 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' + gem 'google-protobuf', '3.25.8' if RUBY_VERSION >= '3.4' gem 'base64' diff --git a/otelconfig/Rakefile b/otelconfig/Rakefile index d202bf4c43..f6abdf82ad 100644 --- a/otelconfig/Rakefile +++ b/otelconfig/Rakefile @@ -7,9 +7,10 @@ require 'bundler/gem_tasks' require 'rake/testtask' require 'yard' - require 'rubocop/rake_task' + RuboCop::RakeTask.new +YARD::Rake::YardocTask.new Rake::TestTask.new :test do |t| t.libs << 'test' @@ -17,10 +18,6 @@ Rake::TestTask.new :test do |t| 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] diff --git a/otelconfig/opentelemetry-otelconfig.gemspec b/otelconfig/opentelemetry-otelconfig.gemspec index b83fb9a714..7c2fe9b829 100644 --- a/otelconfig/opentelemetry-otelconfig.gemspec +++ b/otelconfig/opentelemetry-otelconfig.gemspec @@ -24,19 +24,6 @@ Gem::Specification.new do |spec| 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://rubydoc.info/gems/#{spec.name}/#{spec.version}/file/CHANGELOG.md" spec.metadata['source_code_uri'] = "https://github.com/open-telemetry/opentelemetry-ruby/tree/#{spec.name}/v#{spec.version}/otelconfig" From eed4d9c58db292d46f19d093f384900a6787586a Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 15 Jun 2026 12:30:23 -0400 Subject: [PATCH 07/10] autogenerated constants for in-memory struct --- otelconfig/Rakefile | 2 + .../lib/opentelemetry/components/trace.rb | 96 +- .../{otelconfig => constants}/constants.rb | 2 + .../constants/generated_constants.rb | 1330 +++++++++++++++++ otelconfig/lib/opentelemetry/otelconfig.rb | 20 +- .../otelconfig/instrumentation.rb | 12 +- .../opentelemetry/otelconfig/propagation.rb | 32 +- .../lib/opentelemetry/otelconfig/resource.rb | 35 +- otelconfig/lib/tasks/generate_constants.rake | 248 +++ .../otelconfig/instrumentation_test.rb | 4 +- .../otelconfig/propagation_test.rb | 17 +- 11 files changed, 1702 insertions(+), 96 deletions(-) rename otelconfig/lib/opentelemetry/{otelconfig => constants}/constants.rb (84%) create mode 100644 otelconfig/lib/opentelemetry/constants/generated_constants.rb create mode 100644 otelconfig/lib/tasks/generate_constants.rake diff --git a/otelconfig/Rakefile b/otelconfig/Rakefile index f6abdf82ad..698e47f718 100644 --- a/otelconfig/Rakefile +++ b/otelconfig/Rakefile @@ -9,6 +9,8 @@ require 'rake/testtask' require 'yard' require 'rubocop/rake_task' +Dir.glob(File.join(__dir__, 'lib/tasks/**/*.rake')).each { |f| load f } + RuboCop::RakeTask.new YARD::Rake::YardocTask.new diff --git a/otelconfig/lib/opentelemetry/components/trace.rb b/otelconfig/lib/opentelemetry/components/trace.rb index a4312344a7..c0ed921f75 100644 --- a/otelconfig/lib/opentelemetry/components/trace.rb +++ b/otelconfig/lib/opentelemetry/components/trace.rb @@ -14,8 +14,8 @@ module Trace 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']) + sampler = build_sampler(config.sampler) + span_limits = build_span_limits(config.limits) tp = OpenTelemetry::SDK::Trace::TracerProvider.new( resource: resource, @@ -23,7 +23,7 @@ def build_tracer_provider(config, resource) span_limits: span_limits ) - Array(config['processors']).each do |proc_cfg| + Array(config.processors).each do |proc_cfg| processor = build_span_processor(proc_cfg) tp.add_span_processor(processor) if processor rescue StandardError => e @@ -35,12 +35,12 @@ def build_tracer_provider(config, resource) # 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'] + 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']) + 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 @@ -48,12 +48,12 @@ def build_span_processor(proc_cfg) # Builds a BatchSpanProcessor with exporter and optional tuning options. def build_batch_span_processor(cfg) - exporter = build_span_exporter(cfg['exporter']) + 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 + 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) @@ -61,7 +61,7 @@ def build_batch_span_processor(cfg) # Builds a SimpleSpanProcessor wrapping the configured exporter. def build_simple_span_processor(cfg) - exporter = build_span_exporter(cfg['exporter']) + exporter = build_span_exporter(cfg.exporter) OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(exporter) end @@ -72,14 +72,14 @@ def build_span_exporter(exp_cfg) configured = 0 exporter = nil - if exp_cfg.key?('console') + if exp_cfg.console configured += 1 exporter = OpenTelemetry::SDK::Trace::Export::ConsoleSpanExporter.new end - if exp_cfg['otlp_http'] + if exp_cfg.otlp_http configured += 1 - exporter = build_otlp_http_span_exporter(exp_cfg['otlp_http']) + exporter = build_otlp_http_span_exporter(exp_cfg.otlp_http) end raise ArgumentError, 'must not specify multiple exporters' if configured > 1 @@ -90,11 +90,12 @@ def build_span_exporter(exp_cfg) # Builds an OTLP HTTP span exporter from the given endpoint/headers config. def build_otlp_http_span_exporter(cfg) + headers = headers_to_hash(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 + endpoint: cfg.endpoint, + headers: headers.empty? ? nil : headers, + compression: cfg.compression, + timeout: cfg.timeout && cfg.timeout / 1000.0 # YAML ms → Ruby seconds }.compact OpenTelemetry::Exporter::OTLP::Exporter.new(**opts) @@ -107,14 +108,14 @@ def build_sampler(sampler_cfg) # 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') + if sampler_cfg.parent_based + build_parent_based_sampler(sampler_cfg.parent_based) + elsif sampler_cfg.always_on s::ALWAYS_ON - elsif sampler_cfg.key?('always_off') + elsif sampler_cfg.always_off s::ALWAYS_OFF - elsif sampler_cfg['trace_id_ratio_based'] - ratio = sampler_cfg['trace_id_ratio_based']['ratio'] || 1.0 + 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) @@ -125,14 +126,14 @@ def build_sampler(sampler_cfg) def build_parent_based_sampler(cfg) s = OpenTelemetry::SDK::Trace::Samplers - root = cfg['root'] ? build_sampler(cfg['root']) : s::ALWAYS_ON + 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']) + 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) @@ -143,32 +144,29 @@ 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'] + 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) + # Converts the OTLP exporter headers (array of NameStringValuePair structs) + # into a flat Hash. Falls back to the comma-separated headers_list string. + def headers_to_hash(cfg) headers = {} - if cfg['headers'].is_a?(Array) - cfg['headers'].each do |h| - headers[h['name']] = h['value'] if h['name'] && h['value'] - end + Array(cfg.headers).each do |pair| + headers[pair.name] = pair.value if pair&.name 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) + if headers.empty? && cfg.headers_list.is_a?(String) + cfg.headers_list.split(',').each do |entry| + key, value = entry.strip.split('=', 2) headers[key] = value if key && value end end diff --git a/otelconfig/lib/opentelemetry/otelconfig/constants.rb b/otelconfig/lib/opentelemetry/constants/constants.rb similarity index 84% rename from otelconfig/lib/opentelemetry/otelconfig/constants.rb rename to otelconfig/lib/opentelemetry/constants/constants.rb index 7251c2f0ea..75c8187ccc 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/constants.rb +++ b/otelconfig/lib/opentelemetry/constants/constants.rb @@ -3,6 +3,8 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +require_relative 'generated_constants' + RubySDK = Struct.new( :tracer_provider, :meter_provider, diff --git a/otelconfig/lib/opentelemetry/constants/generated_constants.rb b/otelconfig/lib/opentelemetry/constants/generated_constants.rb new file mode 100644 index 0000000000..0fff222893 --- /dev/null +++ b/otelconfig/lib/opentelemetry/constants/generated_constants.rb @@ -0,0 +1,1330 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# +# DO NOT EDIT — generated by `bundle exec rake generate:constants` +# Schema: open-telemetry/opentelemetry-configuration v1.0.0-rc.3 +# Structs: 74 (one per object definition in the schema) +# To regenerate: bundle exec rake generate:constants + + +Aggregation = Struct.new( + :default, + :drop, + :explicit_bucket_histogram, + :base2_exponential_bucket_histogram, + :last_value, + :sum, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + default: h.key?('default'), + drop: h.key?('drop'), + explicit_bucket_histogram: ExplicitBucketHistogramAggregation.from_hash(h['explicit_bucket_histogram']), + base2_exponential_bucket_histogram: Base2ExponentialBucketHistogramAggregation.from_hash(h['base2_exponential_bucket_histogram']), + last_value: h.key?('last_value'), + sum: h.key?('sum') + ) + end +end + +AttributeLimits = Struct.new( + :attribute_value_length_limit, + :attribute_count_limit, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + attribute_value_length_limit: h['attribute_value_length_limit'], + attribute_count_limit: h['attribute_count_limit'] + ) + end +end + +AttributeNameValue = Struct.new( + :name, + :value, + :type, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + name: h['name'], + value: h['value'], + type: h['type'] + ) + end +end + +Base2ExponentialBucketHistogramAggregation = Struct.new( + :max_scale, + :max_size, + :record_min_max, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + max_scale: h['max_scale'], + max_size: h['max_size'], + record_min_max: h['record_min_max'] + ) + end +end + +BatchLogRecordProcessor = Struct.new( + :schedule_delay, + :export_timeout, + :max_queue_size, + :max_export_batch_size, + :exporter, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + schedule_delay: h['schedule_delay'], + export_timeout: h['export_timeout'], + max_queue_size: h['max_queue_size'], + max_export_batch_size: h['max_export_batch_size'], + exporter: LogRecordExporter.from_hash(h['exporter']) + ) + end +end + +BatchSpanProcessor = Struct.new( + :schedule_delay, + :export_timeout, + :max_queue_size, + :max_export_batch_size, + :exporter, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + schedule_delay: h['schedule_delay'], + export_timeout: h['export_timeout'], + max_queue_size: h['max_queue_size'], + max_export_batch_size: h['max_export_batch_size'], + exporter: SpanExporter.from_hash(h['exporter']) + ) + end +end + +CardinalityLimits = Struct.new( + :default, + :counter, + :gauge, + :histogram, + :observable_counter, + :observable_gauge, + :observable_up_down_counter, + :up_down_counter, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + default: h['default'], + counter: h['counter'], + gauge: h['gauge'], + histogram: h['histogram'], + observable_counter: h['observable_counter'], + observable_gauge: h['observable_gauge'], + observable_up_down_counter: h['observable_up_down_counter'], + up_down_counter: h['up_down_counter'] + ) + end +end + +ConsoleMetricExporter = Struct.new( + :temporality_preference, + :default_histogram_aggregation, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + temporality_preference: h['temporality_preference'], + default_histogram_aggregation: h['default_histogram_aggregation'] + ) + end +end + +ExperimentalComposableParentThresholdSampler = Struct.new( + :root, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + root: ExperimentalComposableSampler.from_hash(h['root']) + ) + end +end + +ExperimentalComposableProbabilitySampler = Struct.new( + :ratio, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + ratio: h['ratio'] + ) + end +end + +ExperimentalComposableRuleBasedSampler = Struct.new( + :rules, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + rules: Array(h['rules']).filter_map { |e| ExperimentalComposableRuleBasedSamplerRule.from_hash(e) } + ) + end +end + +ExperimentalComposableRuleBasedSamplerRule = Struct.new( + :attribute_values, + :attribute_patterns, + :span_kinds, + :parent, + :sampler, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + attribute_values: ExperimentalComposableRuleBasedSamplerRuleAttributeValues.from_hash(h['attribute_values']), + attribute_patterns: ExperimentalComposableRuleBasedSamplerRuleAttributePatterns.from_hash(h['attribute_patterns']), + span_kinds: h['span_kinds'], + parent: h['parent'], + sampler: ExperimentalComposableSampler.from_hash(h['sampler']) + ) + end +end + +ExperimentalComposableRuleBasedSamplerRuleAttributePatterns = Struct.new( + :key, + :included, + :excluded, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + key: h['key'], + included: h['included'], + excluded: h['excluded'] + ) + end +end + +ExperimentalComposableRuleBasedSamplerRuleAttributeValues = Struct.new( + :key, + :values, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + key: h['key'], + values: h['values'] + ) + end +end + +ExperimentalComposableSampler = Struct.new( + :always_off, + :always_on, + :parent_threshold, + :probability, + :rule_based, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + always_off: h.key?('always_off'), + always_on: h.key?('always_on'), + parent_threshold: ExperimentalComposableParentThresholdSampler.from_hash(h['parent_threshold']), + probability: ExperimentalComposableProbabilitySampler.from_hash(h['probability']), + rule_based: ExperimentalComposableRuleBasedSampler.from_hash(h['rule_based']) + ) + end +end + +ExperimentalGeneralInstrumentation = Struct.new( + :peer, + :http, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + peer: ExperimentalPeerInstrumentation.from_hash(h['peer']), + http: ExperimentalHttpInstrumentation.from_hash(h['http']) + ) + end +end + +ExperimentalHttpClientInstrumentation = Struct.new( + :request_captured_headers, + :response_captured_headers, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + request_captured_headers: h['request_captured_headers'], + response_captured_headers: h['response_captured_headers'] + ) + end +end + +ExperimentalHttpInstrumentation = Struct.new( + :client, + :server, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + client: ExperimentalHttpClientInstrumentation.from_hash(h['client']), + server: ExperimentalHttpServerInstrumentation.from_hash(h['server']) + ) + end +end + +ExperimentalHttpServerInstrumentation = Struct.new( + :request_captured_headers, + :response_captured_headers, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + request_captured_headers: h['request_captured_headers'], + response_captured_headers: h['response_captured_headers'] + ) + end +end + +ExperimentalInstrumentation = Struct.new( + :general, + :cpp, + :dotnet, + :erlang, + :go, + :java, + :js, + :php, + :python, + :ruby, + :rust, + :swift, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + general: ExperimentalGeneralInstrumentation.from_hash(h['general']), + cpp: h['cpp'], + dotnet: h['dotnet'], + erlang: h['erlang'], + go: h['go'], + java: h['java'], + js: h['js'], + php: h['php'], + python: h['python'], + ruby: h['ruby'], + rust: h['rust'], + swift: h['swift'] + ) + end +end + +ExperimentalJaegerRemoteSampler = Struct.new( + :endpoint, + :interval, + :initial_sampler, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + endpoint: h['endpoint'], + interval: h['interval'], + initial_sampler: Sampler.from_hash(h['initial_sampler']) + ) + end +end + +ExperimentalLoggerConfig = Struct.new( + :disabled, + :minimum_severity, + :trace_based, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + disabled: h['disabled'], + minimum_severity: h['minimum_severity'], + trace_based: h['trace_based'] + ) + end +end + +ExperimentalLoggerConfigurator = Struct.new( + :default_config, + :loggers, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + default_config: ExperimentalLoggerConfig.from_hash(h['default_config']), + loggers: Array(h['loggers']).filter_map { |e| ExperimentalLoggerMatcherAndConfig.from_hash(e) } + ) + end +end + +ExperimentalLoggerMatcherAndConfig = Struct.new( + :name, + :config, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + name: h['name'], + config: ExperimentalLoggerConfig.from_hash(h['config']) + ) + end +end + +ExperimentalMeterConfig = Struct.new( + :disabled, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + disabled: h['disabled'] + ) + end +end + +ExperimentalMeterConfigurator = Struct.new( + :default_config, + :meters, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + default_config: ExperimentalMeterConfig.from_hash(h['default_config']), + meters: Array(h['meters']).filter_map { |e| ExperimentalMeterMatcherAndConfig.from_hash(e) } + ) + end +end + +ExperimentalMeterMatcherAndConfig = Struct.new( + :name, + :config, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + name: h['name'], + config: ExperimentalMeterConfig.from_hash(h['config']) + ) + end +end + +ExperimentalOtlpFileExporter = Struct.new( + :output_stream, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + output_stream: h['output_stream'] + ) + end +end + +ExperimentalOtlpFileMetricExporter = Struct.new( + :output_stream, + :temporality_preference, + :default_histogram_aggregation, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + output_stream: h['output_stream'], + temporality_preference: h['temporality_preference'], + default_histogram_aggregation: h['default_histogram_aggregation'] + ) + end +end + +ExperimentalPeerInstrumentation = Struct.new( + :service_mapping, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + service_mapping: Array(h['service_mapping']).filter_map { |e| ExperimentalPeerServiceMapping.from_hash(e) } + ) + end +end + +ExperimentalPeerServiceMapping = Struct.new( + :peer, + :service, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + peer: h['peer'], + service: h['service'] + ) + end +end + +ExperimentalProbabilitySampler = Struct.new( + :ratio, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + ratio: h['ratio'] + ) + end +end + +ExperimentalPrometheusMetricExporter = Struct.new( + :host, + :port, + :without_scope_info, + :without_target_info, + :with_resource_constant_labels, + :translation_strategy, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + host: h['host'], + port: h['port'], + without_scope_info: h['without_scope_info'], + without_target_info: h['without_target_info'], + with_resource_constant_labels: IncludeExclude.from_hash(h['with_resource_constant_labels']), + translation_strategy: h['translation_strategy'] + ) + end +end + +ExperimentalResourceDetection = Struct.new( + :attributes, + :detectors, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + attributes: IncludeExclude.from_hash(h['attributes']), + detectors: Array(h['detectors']).filter_map { |e| ExperimentalResourceDetector.from_hash(e) } + ) + end +end + +ExperimentalResourceDetector = Struct.new( + :container, + :host, + :process, + :service, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + container: h.key?('container'), + host: h.key?('host'), + process: h.key?('process'), + service: h.key?('service') + ) + end +end + +ExperimentalTracerConfig = Struct.new( + :disabled, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + disabled: h['disabled'] + ) + end +end + +ExperimentalTracerConfigurator = Struct.new( + :default_config, + :tracers, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + default_config: ExperimentalTracerConfig.from_hash(h['default_config']), + tracers: Array(h['tracers']).filter_map { |e| ExperimentalTracerMatcherAndConfig.from_hash(e) } + ) + end +end + +ExperimentalTracerMatcherAndConfig = Struct.new( + :name, + :config, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + name: h['name'], + config: ExperimentalTracerConfig.from_hash(h['config']) + ) + end +end + +ExplicitBucketHistogramAggregation = Struct.new( + :boundaries, + :record_min_max, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + boundaries: h['boundaries'], + record_min_max: h['record_min_max'] + ) + end +end + +GrpcTls = Struct.new( + :ca_file, + :key_file, + :cert_file, + :insecure, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + ca_file: h['ca_file'], + key_file: h['key_file'], + cert_file: h['cert_file'], + insecure: h['insecure'] + ) + end +end + +HttpTls = Struct.new( + :ca_file, + :key_file, + :cert_file, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + ca_file: h['ca_file'], + key_file: h['key_file'], + cert_file: h['cert_file'] + ) + end +end + +IncludeExclude = Struct.new( + :included, + :excluded, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + included: h['included'], + excluded: h['excluded'] + ) + end +end + +LogRecordExporter = Struct.new( + :otlp_http, + :otlp_grpc, + :otlp_file_development, + :console, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + otlp_http: OtlpHttpExporter.from_hash(h['otlp_http']), + otlp_grpc: OtlpGrpcExporter.from_hash(h['otlp_grpc']), + otlp_file_development: ExperimentalOtlpFileExporter.from_hash(h['otlp_file/development']), + console: h.key?('console') + ) + end +end + +LogRecordLimits = Struct.new( + :attribute_value_length_limit, + :attribute_count_limit, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + attribute_value_length_limit: h['attribute_value_length_limit'], + attribute_count_limit: h['attribute_count_limit'] + ) + end +end + +LogRecordProcessor = Struct.new( + :batch, + :simple, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + batch: BatchLogRecordProcessor.from_hash(h['batch']), + simple: SimpleLogRecordProcessor.from_hash(h['simple']) + ) + end +end + +LoggerProvider = Struct.new( + :processors, + :limits, + :logger_configurator_development, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + processors: Array(h['processors']).filter_map { |e| LogRecordProcessor.from_hash(e) }, + limits: LogRecordLimits.from_hash(h['limits']), + logger_configurator_development: ExperimentalLoggerConfigurator.from_hash(h['logger_configurator/development']) + ) + end +end + +MeterProvider = Struct.new( + :readers, + :views, + :exemplar_filter, + :meter_configurator_development, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + readers: Array(h['readers']).filter_map { |e| MetricReader.from_hash(e) }, + views: Array(h['views']).filter_map { |e| View.from_hash(e) }, + exemplar_filter: h['exemplar_filter'], + meter_configurator_development: ExperimentalMeterConfigurator.from_hash(h['meter_configurator/development']) + ) + end +end + +MetricProducer = Struct.new( + :opencensus, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + opencensus: h.key?('opencensus') + ) + end +end + +MetricReader = Struct.new( + :periodic, + :pull, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + periodic: PeriodicMetricReader.from_hash(h['periodic']), + pull: PullMetricReader.from_hash(h['pull']) + ) + end +end + +NameStringValuePair = Struct.new( + :name, + :value, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + name: h['name'], + value: h['value'] + ) + end +end + +OpenTelemetryConfiguration = Struct.new( + :file_format, + :disabled, + :log_level, + :attribute_limits, + :logger_provider, + :meter_provider, + :propagator, + :tracer_provider, + :resource, + :instrumentation_development, + :distribution, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + file_format: h['file_format'], + disabled: h['disabled'], + log_level: h['log_level'], + attribute_limits: AttributeLimits.from_hash(h['attribute_limits']), + logger_provider: LoggerProvider.from_hash(h['logger_provider']), + meter_provider: MeterProvider.from_hash(h['meter_provider']), + propagator: Propagator.from_hash(h['propagator']), + tracer_provider: TracerProvider.from_hash(h['tracer_provider']), + resource: Resource.from_hash(h['resource']), + instrumentation_development: ExperimentalInstrumentation.from_hash(h['instrumentation/development']), + distribution: h['distribution'] + ) + end +end + +OtlpGrpcExporter = Struct.new( + :endpoint, + :tls, + :headers, + :headers_list, + :compression, + :timeout, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + endpoint: h['endpoint'], + tls: GrpcTls.from_hash(h['tls']), + headers: Array(h['headers']).filter_map { |e| NameStringValuePair.from_hash(e) }, + headers_list: h['headers_list'], + compression: h['compression'], + timeout: h['timeout'] + ) + end +end + +OtlpGrpcMetricExporter = Struct.new( + :endpoint, + :tls, + :headers, + :headers_list, + :compression, + :timeout, + :temporality_preference, + :default_histogram_aggregation, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + endpoint: h['endpoint'], + tls: GrpcTls.from_hash(h['tls']), + headers: Array(h['headers']).filter_map { |e| NameStringValuePair.from_hash(e) }, + headers_list: h['headers_list'], + compression: h['compression'], + timeout: h['timeout'], + temporality_preference: h['temporality_preference'], + default_histogram_aggregation: h['default_histogram_aggregation'] + ) + end +end + +OtlpHttpExporter = Struct.new( + :endpoint, + :tls, + :headers, + :headers_list, + :compression, + :timeout, + :encoding, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + endpoint: h['endpoint'], + tls: HttpTls.from_hash(h['tls']), + headers: Array(h['headers']).filter_map { |e| NameStringValuePair.from_hash(e) }, + headers_list: h['headers_list'], + compression: h['compression'], + timeout: h['timeout'], + encoding: h['encoding'] + ) + end +end + +OtlpHttpMetricExporter = Struct.new( + :endpoint, + :tls, + :headers, + :headers_list, + :compression, + :timeout, + :encoding, + :temporality_preference, + :default_histogram_aggregation, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + endpoint: h['endpoint'], + tls: HttpTls.from_hash(h['tls']), + headers: Array(h['headers']).filter_map { |e| NameStringValuePair.from_hash(e) }, + headers_list: h['headers_list'], + compression: h['compression'], + timeout: h['timeout'], + encoding: h['encoding'], + temporality_preference: h['temporality_preference'], + default_histogram_aggregation: h['default_histogram_aggregation'] + ) + end +end + +ParentBasedSampler = Struct.new( + :root, + :remote_parent_sampled, + :remote_parent_not_sampled, + :local_parent_sampled, + :local_parent_not_sampled, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + root: Sampler.from_hash(h['root']), + remote_parent_sampled: Sampler.from_hash(h['remote_parent_sampled']), + remote_parent_not_sampled: Sampler.from_hash(h['remote_parent_not_sampled']), + local_parent_sampled: Sampler.from_hash(h['local_parent_sampled']), + local_parent_not_sampled: Sampler.from_hash(h['local_parent_not_sampled']) + ) + end +end + +PeriodicMetricReader = Struct.new( + :interval, + :timeout, + :exporter, + :producers, + :cardinality_limits, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + interval: h['interval'], + timeout: h['timeout'], + exporter: PushMetricExporter.from_hash(h['exporter']), + producers: Array(h['producers']).filter_map { |e| MetricProducer.from_hash(e) }, + cardinality_limits: CardinalityLimits.from_hash(h['cardinality_limits']) + ) + end +end + +Propagator = Struct.new( + :composite, + :composite_list, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + composite: Array(h['composite']).filter_map { |e| TextMapPropagator.from_hash(e) }, + composite_list: h['composite_list'] + ) + end +end + +PullMetricExporter = Struct.new( + :prometheus_development, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + prometheus_development: ExperimentalPrometheusMetricExporter.from_hash(h['prometheus/development']) + ) + end +end + +PullMetricReader = Struct.new( + :exporter, + :producers, + :cardinality_limits, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + exporter: PullMetricExporter.from_hash(h['exporter']), + producers: Array(h['producers']).filter_map { |e| MetricProducer.from_hash(e) }, + cardinality_limits: CardinalityLimits.from_hash(h['cardinality_limits']) + ) + end +end + +PushMetricExporter = Struct.new( + :otlp_http, + :otlp_grpc, + :otlp_file_development, + :console, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + otlp_http: OtlpHttpMetricExporter.from_hash(h['otlp_http']), + otlp_grpc: OtlpGrpcMetricExporter.from_hash(h['otlp_grpc']), + otlp_file_development: ExperimentalOtlpFileMetricExporter.from_hash(h['otlp_file/development']), + console: ConsoleMetricExporter.from_hash(h['console']) + ) + end +end + +Resource = Struct.new( + :attributes, + :detection_development, + :schema_url, + :attributes_list, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + attributes: Array(h['attributes']).filter_map { |e| AttributeNameValue.from_hash(e) }, + detection_development: ExperimentalResourceDetection.from_hash(h['detection/development']), + schema_url: h['schema_url'], + attributes_list: h['attributes_list'] + ) + end +end + +Sampler = Struct.new( + :always_off, + :always_on, + :composite_development, + :jaeger_remote_development, + :parent_based, + :probability_development, + :trace_id_ratio_based, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + always_off: h.key?('always_off'), + always_on: h.key?('always_on'), + composite_development: ExperimentalComposableSampler.from_hash(h['composite/development']), + jaeger_remote_development: ExperimentalJaegerRemoteSampler.from_hash(h['jaeger_remote/development']), + parent_based: ParentBasedSampler.from_hash(h['parent_based']), + probability_development: ExperimentalProbabilitySampler.from_hash(h['probability/development']), + trace_id_ratio_based: TraceIdRatioBasedSampler.from_hash(h['trace_id_ratio_based']) + ) + end +end + +SimpleLogRecordProcessor = Struct.new( + :exporter, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + exporter: LogRecordExporter.from_hash(h['exporter']) + ) + end +end + +SimpleSpanProcessor = Struct.new( + :exporter, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + exporter: SpanExporter.from_hash(h['exporter']) + ) + end +end + +SpanExporter = Struct.new( + :otlp_http, + :otlp_grpc, + :otlp_file_development, + :console, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + otlp_http: OtlpHttpExporter.from_hash(h['otlp_http']), + otlp_grpc: OtlpGrpcExporter.from_hash(h['otlp_grpc']), + otlp_file_development: ExperimentalOtlpFileExporter.from_hash(h['otlp_file/development']), + console: h.key?('console') + ) + end +end + +SpanLimits = Struct.new( + :attribute_value_length_limit, + :attribute_count_limit, + :event_count_limit, + :link_count_limit, + :event_attribute_count_limit, + :link_attribute_count_limit, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + attribute_value_length_limit: h['attribute_value_length_limit'], + attribute_count_limit: h['attribute_count_limit'], + event_count_limit: h['event_count_limit'], + link_count_limit: h['link_count_limit'], + event_attribute_count_limit: h['event_attribute_count_limit'], + link_attribute_count_limit: h['link_attribute_count_limit'] + ) + end +end + +SpanProcessor = Struct.new( + :batch, + :simple, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + batch: BatchSpanProcessor.from_hash(h['batch']), + simple: SimpleSpanProcessor.from_hash(h['simple']) + ) + end +end + +TextMapPropagator = Struct.new( + :tracecontext, + :baggage, + :b3, + :b3multi, + :jaeger, + :ottrace, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + tracecontext: h.key?('tracecontext'), + baggage: h.key?('baggage'), + b3: h.key?('b3'), + b3multi: h.key?('b3multi'), + jaeger: h.key?('jaeger'), + ottrace: h.key?('ottrace') + ) + end +end + +TraceIdRatioBasedSampler = Struct.new( + :ratio, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + ratio: h['ratio'] + ) + end +end + +TracerProvider = Struct.new( + :processors, + :limits, + :sampler, + :tracer_configurator_development, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + processors: Array(h['processors']).filter_map { |e| SpanProcessor.from_hash(e) }, + limits: SpanLimits.from_hash(h['limits']), + sampler: Sampler.from_hash(h['sampler']), + tracer_configurator_development: ExperimentalTracerConfigurator.from_hash(h['tracer_configurator/development']) + ) + end +end + +View = Struct.new( + :selector, + :stream, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + selector: ViewSelector.from_hash(h['selector']), + stream: ViewStream.from_hash(h['stream']) + ) + end +end + +ViewSelector = Struct.new( + :instrument_name, + :instrument_type, + :unit, + :meter_name, + :meter_version, + :meter_schema_url, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + instrument_name: h['instrument_name'], + instrument_type: h['instrument_type'], + unit: h['unit'], + meter_name: h['meter_name'], + meter_version: h['meter_version'], + meter_schema_url: h['meter_schema_url'] + ) + end +end + +ViewStream = Struct.new( + :name, + :description, + :aggregation, + :aggregation_cardinality_limit, + :attribute_keys, + keyword_init: true +) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + name: h['name'], + description: h['description'], + aggregation: Aggregation.from_hash(h['aggregation']), + aggregation_cardinality_limit: h['aggregation_cardinality_limit'], + attribute_keys: IncludeExclude.from_hash(h['attribute_keys']) + ) + end +end diff --git a/otelconfig/lib/opentelemetry/otelconfig.rb b/otelconfig/lib/opentelemetry/otelconfig.rb index 1ad9935445..56078077ac 100644 --- a/otelconfig/lib/opentelemetry/otelconfig.rb +++ b/otelconfig/lib/opentelemetry/otelconfig.rb @@ -10,7 +10,7 @@ require_relative 'otelconfig/instrumentation' require_relative 'otelconfig/propagation' require_relative 'otelconfig/resource' -require_relative 'otelconfig/constants' +require_relative 'constants/constants' module OpenTelemetry # OtelConfig module handles declarative configuration of OpenTelemetry components @@ -48,27 +48,27 @@ def apply(config) return end - if config['disabled'] + 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) + resource = build_resource(config.resource) + tracer_provider = Trace.build_tracer_provider(config.tracer_provider, resource) - propagators = configure_propagation(config['propagator']) + propagators = configure_propagation(config.propagator) - configure_instrumentation(config['instrumentation/development']) + configure_instrumentation(config.instrumentation_development) RubySDK.new( - :tracer_provider => tracer_provider, - :propagator => propagators, - :resource => resource + tracer_provider: tracer_provider, + propagator: propagators, + resource: resource ) end end def parse_config_file(path) content = File.read(path) - YAML.safe_load(content, permitted_classes: [Date, Time]) + OpenTelemetryConfiguration.from_hash(YAML.safe_load(content, permitted_classes: [Date, Time])) rescue Errno::ENOENT => e OpenTelemetry.logger.error("Config file not found: #{e.message}") nil diff --git a/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb b/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb index 207a2338f8..8246668039 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/instrumentation.rb @@ -18,10 +18,16 @@ def configure_instrumentation(instrumentation_cfg) # Transforms the YAML instrumentation config into the flat hash that # install_all expects: { 'OpenTelemetry::Instrumentation::Foo' => { opt: val } } + # + # Accepts either the schema-generated ExperimentalInstrumentation struct + # (reading its +ruby+ language map) or a raw Hash with a 'ruby' key. def build_instrumentation_config_map(instrumentation_cfg) - return {} unless instrumentation_cfg.is_a?(Hash) - - ruby_instrumentation = instrumentation_cfg['ruby'] + ruby_instrumentation = + if instrumentation_cfg.is_a?(Hash) + instrumentation_cfg['ruby'] + elsif instrumentation_cfg.respond_to?(:ruby) + instrumentation_cfg.ruby + end return {} unless ruby_instrumentation.is_a?(Hash) name_map = build_instrumentation_name_map diff --git a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb index 10d59a4fc3..4e80b7bf36 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb @@ -11,26 +11,30 @@ class << self 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) } + propagators = extract_propagator_names(propagator_cfg) return if propagators.empty? - return OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators(propagators) + composite_propagators = propagators.filter_map { |name| resolve_propagator(name) } + return if composite_propagators.empty? + + return OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators(composite_propagators) end - # Extracts an ordered list of propagator name strings from the config hash. + # Extracts an ordered, deduplicated list of propagator name strings from + # the config. Names from +composite+ come first, then any additional names + # from +composite_list+ that were not already included. + # + # +composite+ is an array of TextMapPropagator structs whose set presence + # flags (e.g. tracecontext:, baggage:) identify each propagator. 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 + propagators = [] + Array(cfg.composite).each do |entry| + next unless entry - list = cfg['composite_list'] - return list.split(',').map(&:strip) if list.is_a?(String) - - [] + entry.members.each { |m| propagators << m.to_s if entry[m] } + end + propagators += cfg.composite_list.split(',').map(&:strip) if cfg.composite_list.is_a?(String) + propagators.uniq end # Returns a propagator instance for the given name, or nil with a warning. diff --git a/otelconfig/lib/opentelemetry/otelconfig/resource.rb b/otelconfig/lib/opentelemetry/otelconfig/resource.rb index 4c61fcc968..9913d90bea 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/resource.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/resource.rb @@ -13,23 +13,23 @@ def build_resource(resource_cfg) return base unless resource_cfg - detected = build_detected_attributes(resource_cfg['detection/development']) + 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? + Array(resource_cfg.attributes).each do |attr| + next unless attr.name && !attr.value.nil? - explicit[attr['name']] = coerce_attribute_value(attr['value'], attr['type']) + 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| + 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'] + 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) @@ -61,14 +61,13 @@ def coerce_bool(value) end end - # Extract the attributes from field 'detection/development' + # Extract the attributes from an ExperimentalResourceDetection struct. def build_detected_attributes(detection_cfg) - return {} unless detection_cfg.is_a?(Hash) + return {} unless detection_cfg - 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 } + included_patterns = Array(detection_cfg.attributes&.included) + excluded_patterns = Array(detection_cfg.attributes&.excluded) + detector_names = detector_names_from(detection_cfg.detectors) raw = detector_names.each_with_object({}) do |name, attrs| attrs.merge!(run_detector(name).attribute_enumerator.to_h) @@ -83,6 +82,16 @@ def build_detected_attributes(detection_cfg) end end + # Flattens an array of ExperimentalResourceDetector structs into the list + # of detector names whose presence flag is set (e.g. container, host). + def detector_names_from(detectors) + Array(detectors).flat_map do |detector| + next [] unless detector + + detector.members.select { |m| detector[m] }.map(&:to_s) + end + end + # Returns a Resource for the given detector name. def run_detector(name) case name diff --git a/otelconfig/lib/tasks/generate_constants.rake b/otelconfig/lib/tasks/generate_constants.rake new file mode 100644 index 0000000000..511e6d1921 --- /dev/null +++ b/otelconfig/lib/tasks/generate_constants.rake @@ -0,0 +1,248 @@ +# frozen_string_literal: true + +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 +# +# rake generate:constants +# +# Downloads the upstream opentelemetry-configuration JSON Schema (YAML format) +# and regenerates lib/opentelemetry/constants/generated_constants.rb from it. +# +# Usage: +# bundle exec rake generate:constants +# bundle exec rake generate:constants SCHEMA_VERSION=v1.0.0-rc.3 +# bundle exec rake generate:constants SCHEMA_DIR=/path/to/local/schema + +require 'fileutils' +require 'open-uri' +require 'tmpdir' +require 'zlib' +require 'stringio' +require 'yaml' +require 'rubygems/package' + +SCHEMA_VERSION = ENV.fetch('SCHEMA_VERSION', 'v1.0.0-rc.3') +SCHEMA_TARBALL = "https://api.github.com/repos/open-telemetry/opentelemetry-configuration/tarball/#{SCHEMA_VERSION}" +OUTPUT_FILE = File.expand_path('../opentelemetry/constants/generated_constants.rb', __dir__) + +# --------------------------------------------------------------------------- +# Pure-Ruby tarball downloader/extractor (no curl/wget/tar dependency) +# --------------------------------------------------------------------------- +module SchemaDownloader + # Downloads the gzip tarball at +url+ and extracts every file under a + # top-level +schema/+ directory into +dest_dir+, flattening the leading + # "-/" path component that GitHub tarballs include. + def self.fetch_schema(url, dest_dir) + FileUtils.mkdir_p(dest_dir) + + raw = URI.open(url, 'rb', 'User-Agent' => 'opentelemetry-otelconfig-generator').read # rubocop:disable Security/Open + gz = Zlib::GzipReader.new(StringIO.new(raw)) + + Gem::Package::TarReader.new(gz) do |tar| + tar.each do |entry| + next unless entry.file? + + rel = entry.full_name.split('/', 2).last + next unless rel&.start_with?('schema/') + + File.binwrite(File.join(dest_dir, File.basename(rel)), entry.read) + end + end + + dest_dir + end +end + +# --------------------------------------------------------------------------- +# Schema loader: merges the split schema files into one flat $defs table and +# resolves the file-level $ref aliases (e.g. `Resource: { $ref: resource.yaml }`). +# --------------------------------------------------------------------------- +module SchemaReader + ROOT_FILE = 'opentelemetry_configuration.yaml' + + def self.load_yaml(path) + YAML.safe_load(File.read(path)) + end + + # Returns a single Hash mapping every definition name to its schema body. + def self.build_defs(schema_dir) + file_roots = {} + defs = {} + + Dir.glob(File.join(schema_dir, '*.yaml')).each do |f| + next if File.basename(f).start_with?('meta_schema') + + doc = load_yaml(f) + next unless doc.is_a?(Hash) + + file_roots[File.basename(f)] = doc + (doc['$defs'] || {}).each { |name, body| defs[name] = body } + end + + root = file_roots.fetch(ROOT_FILE, {}) + + # Resolve `Name: { $ref: some_file.yaml }` aliases to the file's root schema. + (root['$defs'] || {}).each do |name, body| + next unless body.is_a?(Hash) && body['$ref'].is_a?(String) && body['$ref'].end_with?('.yaml') + + target = file_roots[body['$ref']] + defs[name] = target if target + end + + # The root configuration object itself. + defs['OpenTelemetryConfiguration'] = root.reject { |k, _| k == '$defs' } + defs + end +end + +# --------------------------------------------------------------------------- +# Code generator: schema $defs → Ruby Struct definitions (purely schema-driven) +# --------------------------------------------------------------------------- +module ConstantsGenerator + module_function + + # Ruby Struct members must be valid identifiers. Schema keys like + # "detection/development" become "detection_development". + def field_name(key) + key.gsub(%r{[/.\-]}, '_').gsub(/[^a-zA-Z0-9_]/, '_') + end + + # Extracts the definition name from a $ref string, or nil. + # Handles "#/$defs/Foo" and "common.yaml#/$defs/Foo". + def ref_name(schema) + return nil unless schema.is_a?(Hash) + + ref = schema['$ref'] + return nil unless ref.is_a?(String) && ref.include?('#/$defs/') + + ref.split('#/$defs/').last + end + + # A definition becomes a generated Struct when it declares (non-empty) properties. + def struct?(defs, name) + body = defs[name] + body.is_a?(Hash) && body['properties'].is_a?(Hash) && !body['properties'].empty? + end + + # A "marker" is an empty object (no properties, no enum, additionalProperties + # not a map) — its presence is meaningful, its value is nil/{} (e.g. console:, + # always_on:, tracecontext:). These map to a presence boolean. + def marker?(defs, name) + body = defs[name] + return false unless body.is_a?(Hash) + return false if body['properties'] + return false if body['enum'] + + add = body['additionalProperties'] + object_type = body['type'] == 'object' || (body['type'].is_a?(Array) && body['type'].include?('object')) + object_type && (add == false || add.nil?) + end + + def array_schema?(prop) + Array(prop['type']).include?('array') || prop.key?('items') + end + + # Ruby expression that builds the value for one property given local var `h`. + def value_expr(prop, defs, key) + direct = ref_name(prop) + + if direct && struct?(defs, direct) + "#{direct}.from_hash(h['#{key}'])" + elsif direct && marker?(defs, direct) + "h.key?('#{key}')" + elsif array_schema?(prop) + item_ref = ref_name(prop['items'] || {}) + if item_ref && struct?(defs, item_ref) + "Array(h['#{key}']).filter_map { |e| #{item_ref}.from_hash(e) }" + else + "h['#{key}']" + end + else + "h['#{key}']" + end + end + + def render_struct(name, body, defs) + props = body['properties'] + members = props.keys.map { |k| ":#{field_name(k)}" }.join(",\n ") + + assignments = props.map do |key, prop| + " #{field_name(key)}: #{value_expr(prop, defs, key)}" + end.join(",\n") + + <<~RUBY + #{name} = Struct.new( + #{members}, + keyword_init: true + ) do + def self.from_hash(h) + return nil unless h.is_a?(Hash) + + new( + #{assignments} + ) + end + end + RUBY + end + + def generate(defs) + names = defs.keys.select { |n| struct?(defs, n) }.sort + parts = [header(names.size)] + names.each { |name| parts << render_struct(name, defs[name], defs) } + parts.join("\n") + end + + def header(count) + <<~RUBY + # frozen_string_literal: true + + # Copyright The OpenTelemetry Authors + # SPDX-License-Identifier: Apache-2.0 + # + # DO NOT EDIT — generated by `bundle exec rake generate:constants` + # Schema: open-telemetry/opentelemetry-configuration #{SCHEMA_VERSION} + # Structs: #{count} (one per object definition in the schema) + # To regenerate: bundle exec rake generate:constants + + RUBY + end +end + +# --------------------------------------------------------------------------- +# Rake tasks +# --------------------------------------------------------------------------- +namespace :generate do + desc "Download OTel configuration schema (#{SCHEMA_VERSION}) and regenerate lib/opentelemetry/constants/generated_constants.rb" + task :constants do + schema_dir = ENV['SCHEMA_DIR'] + + if schema_dir + puts "Using local schema directory: #{schema_dir}" + else + tmpdir = Dir.mktmpdir('otel-schema-') + schema_dir = File.join(tmpdir, 'schema') + + puts "Downloading schema #{SCHEMA_VERSION} from GitHub..." + begin + SchemaDownloader.fetch_schema(SCHEMA_TARBALL, schema_dir) + rescue StandardError => e + abort "Failed to download schema: #{e.class}: #{e.message}" + end + + puts "Schema extracted to #{schema_dir}" + end + + puts 'Loading and merging schema files...' + defs = SchemaReader.build_defs(schema_dir) + object_count = defs.keys.count { |n| ConstantsGenerator.struct?(defs, n) } + puts "Found #{defs.size} definitions (#{object_count} object structs)." + + puts 'Generating constants...' + output = ConstantsGenerator.generate(defs) + + FileUtils.mkdir_p(File.dirname(OUTPUT_FILE)) + File.write(OUTPUT_FILE, output) + puts "Written to #{OUTPUT_FILE}" + end +end diff --git a/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb b/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb index ea5d394eca..bfac9f623d 100644 --- a/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb +++ b/otelconfig/test/opentelemetry/otelconfig/instrumentation_test.rb @@ -122,7 +122,7 @@ def fake_instrumentation_class(full_name) with_config(<<~YAML) do |path| file_format: "1.0" #{TRACER_PROVIDER_YAML} - instrumentation: + instrumentation/development: ruby: net_http: untraced_hosts: @@ -141,7 +141,7 @@ def fake_instrumentation_class(full_name) with_config(<<~YAML) do |path| file_format: "1.0" #{TRACER_PROVIDER_YAML} - instrumentation: + instrumentation/development: ruby: redis: peer_service: "cache-cluster" diff --git a/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb index 9ec593060a..1c1fa89366 100644 --- a/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb +++ b/otelconfig/test/opentelemetry/otelconfig/propagation_test.rb @@ -170,24 +170,31 @@ end end - describe 'composite vs composite_list precedence' do - it 'uses composite array and ignores composite_list when both are present' do + describe 'composite and composite_list merging' do + it 'merges both sources and deduplicates — composite names come first' do with_config(<<~YAML) do |path| file_format: "1.0" #{TRACER_PROVIDER_YAML} propagator: composite: - tracecontext: - composite_list: "baggage" + composite_list: "baggage,tracecontext" YAML sdk = OpenTelemetry::OtelConfig.configure_from_file(path) OpenTelemetry.tracer_provider = sdk.tracer_provider OpenTelemetry.propagation = sdk.propagator if sdk.propagator propagation = OpenTelemetry.propagation - _(propagation).must_be_instance_of OpenTelemetry::Trace::Propagation::TraceContext::TextMapPropagator + _(propagation).must_be_instance_of OpenTelemetry::Context::Propagation::CompositeTextMapPropagator + + propagators = propagation.instance_variable_get(:@propagators) + # tracecontext first (from composite), baggage second (from composite_list), no duplicate tracecontext + _(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 + _(propagation.fields).must_include 'traceparent' - _(propagation.fields).wont_include 'baggage' + _(propagation.fields).must_include 'baggage' end end end From 235fa86c2f714190e3957f4c293bff11847911fe Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 15 Jun 2026 14:07:17 -0400 Subject: [PATCH 08/10] update --- .../constants/generated_constants.rb | 48 ++++++++++++++----- .../opentelemetry/otelconfig/propagation.rb | 10 ++-- .../lib/opentelemetry/otelconfig/resource.rb | 9 ++-- otelconfig/lib/tasks/generate_constants.rake | 45 ++++++++--------- 4 files changed, 67 insertions(+), 45 deletions(-) diff --git a/otelconfig/lib/opentelemetry/constants/generated_constants.rb b/otelconfig/lib/opentelemetry/constants/generated_constants.rb index 0fff222893..55bd03e701 100644 --- a/otelconfig/lib/opentelemetry/constants/generated_constants.rb +++ b/otelconfig/lib/opentelemetry/constants/generated_constants.rb @@ -263,6 +263,7 @@ def self.from_hash(h) :parent_threshold, :probability, :rule_based, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -273,7 +274,8 @@ def self.from_hash(h) always_on: h.key?('always_on'), parent_threshold: ExperimentalComposableParentThresholdSampler.from_hash(h['parent_threshold']), probability: ExperimentalComposableProbabilitySampler.from_hash(h['probability']), - rule_based: ExperimentalComposableRuleBasedSampler.from_hash(h['rule_based']) + rule_based: ExperimentalComposableRuleBasedSampler.from_hash(h['rule_based']), + additional_properties: h.reject { |k, _| ['always_off', 'always_on', 'parent_threshold', 'probability', 'rule_based'].include?(k) } ) end end @@ -594,6 +596,7 @@ def self.from_hash(h) :host, :process, :service, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -603,7 +606,8 @@ def self.from_hash(h) container: h.key?('container'), host: h.key?('host'), process: h.key?('process'), - service: h.key?('service') + service: h.key?('service'), + additional_properties: h.reject { |k, _| ['container', 'host', 'process', 'service'].include?(k) } ) end end @@ -722,6 +726,7 @@ def self.from_hash(h) :otlp_grpc, :otlp_file_development, :console, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -731,7 +736,8 @@ def self.from_hash(h) otlp_http: OtlpHttpExporter.from_hash(h['otlp_http']), otlp_grpc: OtlpGrpcExporter.from_hash(h['otlp_grpc']), otlp_file_development: ExperimentalOtlpFileExporter.from_hash(h['otlp_file/development']), - console: h.key?('console') + console: h.key?('console'), + additional_properties: h.reject { |k, _| ['otlp_http', 'otlp_grpc', 'otlp_file/development', 'console'].include?(k) } ) end end @@ -754,6 +760,7 @@ def self.from_hash(h) LogRecordProcessor = Struct.new( :batch, :simple, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -761,7 +768,8 @@ def self.from_hash(h) new( batch: BatchLogRecordProcessor.from_hash(h['batch']), - simple: SimpleLogRecordProcessor.from_hash(h['simple']) + simple: SimpleLogRecordProcessor.from_hash(h['simple']), + additional_properties: h.reject { |k, _| ['batch', 'simple'].include?(k) } ) end end @@ -804,13 +812,15 @@ def self.from_hash(h) MetricProducer = Struct.new( :opencensus, + :additional_properties, keyword_init: true ) do def self.from_hash(h) return nil unless h.is_a?(Hash) new( - opencensus: h.key?('opencensus') + opencensus: h.key?('opencensus'), + additional_properties: h.reject { |k, _| ['opencensus'].include?(k) } ) end end @@ -857,6 +867,7 @@ def self.from_hash(h) :resource, :instrumentation_development, :distribution, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -873,7 +884,8 @@ def self.from_hash(h) tracer_provider: TracerProvider.from_hash(h['tracer_provider']), resource: Resource.from_hash(h['resource']), instrumentation_development: ExperimentalInstrumentation.from_hash(h['instrumentation/development']), - distribution: h['distribution'] + distribution: h['distribution'], + additional_properties: h.reject { |k, _| ['file_format', 'disabled', 'log_level', 'attribute_limits', 'logger_provider', 'meter_provider', 'propagator', 'tracer_provider', 'resource', 'instrumentation/development', 'distribution'].include?(k) } ) end end @@ -1041,13 +1053,15 @@ def self.from_hash(h) PullMetricExporter = Struct.new( :prometheus_development, + :additional_properties, keyword_init: true ) do def self.from_hash(h) return nil unless h.is_a?(Hash) new( - prometheus_development: ExperimentalPrometheusMetricExporter.from_hash(h['prometheus/development']) + prometheus_development: ExperimentalPrometheusMetricExporter.from_hash(h['prometheus/development']), + additional_properties: h.reject { |k, _| ['prometheus/development'].include?(k) } ) end end @@ -1074,6 +1088,7 @@ def self.from_hash(h) :otlp_grpc, :otlp_file_development, :console, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -1083,7 +1098,8 @@ def self.from_hash(h) otlp_http: OtlpHttpMetricExporter.from_hash(h['otlp_http']), otlp_grpc: OtlpGrpcMetricExporter.from_hash(h['otlp_grpc']), otlp_file_development: ExperimentalOtlpFileMetricExporter.from_hash(h['otlp_file/development']), - console: ConsoleMetricExporter.from_hash(h['console']) + console: ConsoleMetricExporter.from_hash(h['console']), + additional_properties: h.reject { |k, _| ['otlp_http', 'otlp_grpc', 'otlp_file/development', 'console'].include?(k) } ) end end @@ -1115,6 +1131,7 @@ def self.from_hash(h) :parent_based, :probability_development, :trace_id_ratio_based, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -1127,7 +1144,8 @@ def self.from_hash(h) jaeger_remote_development: ExperimentalJaegerRemoteSampler.from_hash(h['jaeger_remote/development']), parent_based: ParentBasedSampler.from_hash(h['parent_based']), probability_development: ExperimentalProbabilitySampler.from_hash(h['probability/development']), - trace_id_ratio_based: TraceIdRatioBasedSampler.from_hash(h['trace_id_ratio_based']) + trace_id_ratio_based: TraceIdRatioBasedSampler.from_hash(h['trace_id_ratio_based']), + additional_properties: h.reject { |k, _| ['always_off', 'always_on', 'composite/development', 'jaeger_remote/development', 'parent_based', 'probability/development', 'trace_id_ratio_based'].include?(k) } ) end end @@ -1163,6 +1181,7 @@ def self.from_hash(h) :otlp_grpc, :otlp_file_development, :console, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -1172,7 +1191,8 @@ def self.from_hash(h) otlp_http: OtlpHttpExporter.from_hash(h['otlp_http']), otlp_grpc: OtlpGrpcExporter.from_hash(h['otlp_grpc']), otlp_file_development: ExperimentalOtlpFileExporter.from_hash(h['otlp_file/development']), - console: h.key?('console') + console: h.key?('console'), + additional_properties: h.reject { |k, _| ['otlp_http', 'otlp_grpc', 'otlp_file/development', 'console'].include?(k) } ) end end @@ -1203,6 +1223,7 @@ def self.from_hash(h) SpanProcessor = Struct.new( :batch, :simple, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -1210,7 +1231,8 @@ def self.from_hash(h) new( batch: BatchSpanProcessor.from_hash(h['batch']), - simple: SimpleSpanProcessor.from_hash(h['simple']) + simple: SimpleSpanProcessor.from_hash(h['simple']), + additional_properties: h.reject { |k, _| ['batch', 'simple'].include?(k) } ) end end @@ -1222,6 +1244,7 @@ def self.from_hash(h) :b3multi, :jaeger, :ottrace, + :additional_properties, keyword_init: true ) do def self.from_hash(h) @@ -1233,7 +1256,8 @@ def self.from_hash(h) b3: h.key?('b3'), b3multi: h.key?('b3multi'), jaeger: h.key?('jaeger'), - ottrace: h.key?('ottrace') + ottrace: h.key?('ottrace'), + additional_properties: h.reject { |k, _| ['tracecontext', 'baggage', 'b3', 'b3multi', 'jaeger', 'ottrace'].include?(k) } ) end end diff --git a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb index 4e80b7bf36..e4a73a7d2f 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb @@ -23,15 +23,17 @@ def configure_propagation(propagator_cfg) # Extracts an ordered, deduplicated list of propagator name strings from # the config. Names from +composite+ come first, then any additional names # from +composite_list+ that were not already included. - # - # +composite+ is an array of TextMapPropagator structs whose set presence - # flags (e.g. tracecontext:, baggage:) identify each propagator. def extract_propagator_names(cfg) propagators = [] Array(cfg.composite).each do |entry| next unless entry - entry.members.each { |m| propagators << m.to_s if entry[m] } + entry.members.each do |m| + next if m == :additional_properties + + propagators << m.to_s if entry[m] + end + (entry.additional_properties || {}).each_key { |k| propagators << k.to_s } end propagators += cfg.composite_list.split(',').map(&:strip) if cfg.composite_list.is_a?(String) propagators.uniq diff --git a/otelconfig/lib/opentelemetry/otelconfig/resource.rb b/otelconfig/lib/opentelemetry/otelconfig/resource.rb index 9913d90bea..1d18ed8c71 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/resource.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/resource.rb @@ -73,8 +73,6 @@ def build_detected_attributes(detection_cfg) 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) } @@ -88,7 +86,12 @@ def detector_names_from(detectors) Array(detectors).flat_map do |detector| next [] unless detector - detector.members.select { |m| detector[m] }.map(&:to_s) + names = detector.members.filter_map do |m| + next if m == :additional_properties + + m.to_s if detector[m] + end + names + Array(detector.additional_properties&.keys).map(&:to_s) end end diff --git a/otelconfig/lib/tasks/generate_constants.rake b/otelconfig/lib/tasks/generate_constants.rake index 511e6d1921..59ca2a87a2 100644 --- a/otelconfig/lib/tasks/generate_constants.rake +++ b/otelconfig/lib/tasks/generate_constants.rake @@ -25,13 +25,8 @@ SCHEMA_VERSION = ENV.fetch('SCHEMA_VERSION', 'v1.0.0-rc.3') SCHEMA_TARBALL = "https://api.github.com/repos/open-telemetry/opentelemetry-configuration/tarball/#{SCHEMA_VERSION}" OUTPUT_FILE = File.expand_path('../opentelemetry/constants/generated_constants.rb', __dir__) -# --------------------------------------------------------------------------- -# Pure-Ruby tarball downloader/extractor (no curl/wget/tar dependency) -# --------------------------------------------------------------------------- +# Ruby tarball downloader module SchemaDownloader - # Downloads the gzip tarball at +url+ and extracts every file under a - # top-level +schema/+ directory into +dest_dir+, flattening the leading - # "-/" path component that GitHub tarballs include. def self.fetch_schema(url, dest_dir) FileUtils.mkdir_p(dest_dir) @@ -53,10 +48,8 @@ module SchemaDownloader end end -# --------------------------------------------------------------------------- -# Schema loader: merges the split schema files into one flat $defs table and -# resolves the file-level $ref aliases (e.g. `Resource: { $ref: resource.yaml }`). -# --------------------------------------------------------------------------- +# merges the split schema files into one flat $defs table and +# resolves the file-level $ref aliases module SchemaReader ROOT_FILE = 'opentelemetry_configuration.yaml' @@ -95,20 +88,16 @@ module SchemaReader end end -# --------------------------------------------------------------------------- -# Code generator: schema $defs → Ruby Struct definitions (purely schema-driven) -# --------------------------------------------------------------------------- +# schema $defs → ruby struct definitions in constants/generated_constants.rb module ConstantsGenerator module_function - # Ruby Struct members must be valid identifiers. Schema keys like # "detection/development" become "detection_development". def field_name(key) key.gsub(%r{[/.\-]}, '_').gsub(/[^a-zA-Z0-9_]/, '_') end - # Extracts the definition name from a $ref string, or nil. - # Handles "#/$defs/Foo" and "common.yaml#/$defs/Foo". + # extracts the definition name from a $ref string def ref_name(schema) return nil unless schema.is_a?(Hash) @@ -124,9 +113,6 @@ module ConstantsGenerator body.is_a?(Hash) && body['properties'].is_a?(Hash) && !body['properties'].empty? end - # A "marker" is an empty object (no properties, no enum, additionalProperties - # not a map) — its presence is meaningful, its value is nil/{} (e.g. console:, - # always_on:, tracecontext:). These map to a presence boolean. def marker?(defs, name) body = defs[name] return false unless body.is_a?(Hash) @@ -142,7 +128,7 @@ module ConstantsGenerator Array(prop['type']).include?('array') || prop.key?('items') end - # Ruby expression that builds the value for one property given local var `h`. + # builds the value for one property given local var `h`. def value_expr(prop, defs, key) direct = ref_name(prop) @@ -164,11 +150,21 @@ module ConstantsGenerator def render_struct(name, body, defs) props = body['properties'] - members = props.keys.map { |k| ":#{field_name(k)}" }.join(",\n ") + member_names = props.keys.map { |k| field_name(k) } + + # capture them so non-schema plugins (e.g. additionalProperties) + capture_additional = body['additionalProperties'] != false + member_names << 'additional_properties' if capture_additional + + members = member_names.map { |m| ":#{m}" }.join(",\n ") assignments = props.map do |key, prop| " #{field_name(key)}: #{value_expr(prop, defs, key)}" - end.join(",\n") + end + if capture_additional + known = props.keys.map { |k| "'#{k}'" }.join(', ') + assignments << " additional_properties: h.reject { |k, _| [#{known}].include?(k) }" + end <<~RUBY #{name} = Struct.new( @@ -179,7 +175,7 @@ module ConstantsGenerator return nil unless h.is_a?(Hash) new( - #{assignments} + #{assignments.join(",\n")} ) end end @@ -209,9 +205,6 @@ module ConstantsGenerator end end -# --------------------------------------------------------------------------- -# Rake tasks -# --------------------------------------------------------------------------- namespace :generate do desc "Download OTel configuration schema (#{SCHEMA_VERSION}) and regenerate lib/opentelemetry/constants/generated_constants.rb" task :constants do From 8208d388f8b0f7a8994d9e4349eb12241295a4cd Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 15 Jun 2026 14:13:15 -0400 Subject: [PATCH 09/10] lint --- otelconfig/.rubocop.yml | 9 +++++++++ .../opentelemetry/constants/generated_constants.rb | 11 +++++------ .../lib/opentelemetry/otelconfig/propagation.rb | 2 +- otelconfig/lib/opentelemetry/otelconfig/resource.rb | 2 ++ otelconfig/lib/tasks/generate_constants.rake | 11 +++++++++-- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/otelconfig/.rubocop.yml b/otelconfig/.rubocop.yml index 41f6eb369f..7e066c93b3 100644 --- a/otelconfig/.rubocop.yml +++ b/otelconfig/.rubocop.yml @@ -14,3 +14,12 @@ Metrics/ClassLength: Enabled: false Metrics/ModuleLength: Max: 150 +Style/DocumentationMethod: + Exclude: + - lib/opentelemetry/constants/generated_constants.rb +Naming/MethodParameterName: + Exclude: + - lib/opentelemetry/constants/generated_constants.rb +Lint/StructNewOverride: + Exclude: + - lib/opentelemetry/constants/generated_constants.rb diff --git a/otelconfig/lib/opentelemetry/constants/generated_constants.rb b/otelconfig/lib/opentelemetry/constants/generated_constants.rb index 55bd03e701..3801e7996e 100644 --- a/otelconfig/lib/opentelemetry/constants/generated_constants.rb +++ b/otelconfig/lib/opentelemetry/constants/generated_constants.rb @@ -8,7 +8,6 @@ # Structs: 74 (one per object definition in the schema) # To regenerate: bundle exec rake generate:constants - Aggregation = Struct.new( :default, :drop, @@ -275,7 +274,7 @@ def self.from_hash(h) parent_threshold: ExperimentalComposableParentThresholdSampler.from_hash(h['parent_threshold']), probability: ExperimentalComposableProbabilitySampler.from_hash(h['probability']), rule_based: ExperimentalComposableRuleBasedSampler.from_hash(h['rule_based']), - additional_properties: h.reject { |k, _| ['always_off', 'always_on', 'parent_threshold', 'probability', 'rule_based'].include?(k) } + additional_properties: h.reject { |k, _| %w[always_off always_on parent_threshold probability rule_based].include?(k) } ) end end @@ -607,7 +606,7 @@ def self.from_hash(h) host: h.key?('host'), process: h.key?('process'), service: h.key?('service'), - additional_properties: h.reject { |k, _| ['container', 'host', 'process', 'service'].include?(k) } + additional_properties: h.reject { |k, _| %w[container host process service].include?(k) } ) end end @@ -769,7 +768,7 @@ def self.from_hash(h) new( batch: BatchLogRecordProcessor.from_hash(h['batch']), simple: SimpleLogRecordProcessor.from_hash(h['simple']), - additional_properties: h.reject { |k, _| ['batch', 'simple'].include?(k) } + additional_properties: h.reject { |k, _| %w[batch simple].include?(k) } ) end end @@ -1232,7 +1231,7 @@ def self.from_hash(h) new( batch: BatchSpanProcessor.from_hash(h['batch']), simple: SimpleSpanProcessor.from_hash(h['simple']), - additional_properties: h.reject { |k, _| ['batch', 'simple'].include?(k) } + additional_properties: h.reject { |k, _| %w[batch simple].include?(k) } ) end end @@ -1257,7 +1256,7 @@ def self.from_hash(h) b3multi: h.key?('b3multi'), jaeger: h.key?('jaeger'), ottrace: h.key?('ottrace'), - additional_properties: h.reject { |k, _| ['tracecontext', 'baggage', 'b3', 'b3multi', 'jaeger', 'ottrace'].include?(k) } + additional_properties: h.reject { |k, _| %w[tracecontext baggage b3 b3multi jaeger ottrace].include?(k) } ) end end diff --git a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb index e4a73a7d2f..2eedb38f5b 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/propagation.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/propagation.rb @@ -17,7 +17,7 @@ def configure_propagation(propagator_cfg) composite_propagators = propagators.filter_map { |name| resolve_propagator(name) } return if composite_propagators.empty? - return OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators(composite_propagators) + OpenTelemetry::Context::Propagation::CompositeTextMapPropagator.compose_propagators(composite_propagators) end # Extracts an ordered, deduplicated list of propagator name strings from diff --git a/otelconfig/lib/opentelemetry/otelconfig/resource.rb b/otelconfig/lib/opentelemetry/otelconfig/resource.rb index 1d18ed8c71..9f2cc7802d 100644 --- a/otelconfig/lib/opentelemetry/otelconfig/resource.rb +++ b/otelconfig/lib/opentelemetry/otelconfig/resource.rb @@ -73,6 +73,8 @@ def build_detected_attributes(detection_cfg) 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) } diff --git a/otelconfig/lib/tasks/generate_constants.rake b/otelconfig/lib/tasks/generate_constants.rake index 59ca2a87a2..13e560251d 100644 --- a/otelconfig/lib/tasks/generate_constants.rake +++ b/otelconfig/lib/tasks/generate_constants.rake @@ -22,11 +22,12 @@ require 'yaml' require 'rubygems/package' SCHEMA_VERSION = ENV.fetch('SCHEMA_VERSION', 'v1.0.0-rc.3') -SCHEMA_TARBALL = "https://api.github.com/repos/open-telemetry/opentelemetry-configuration/tarball/#{SCHEMA_VERSION}" +SCHEMA_TARBALL = "https://api.github.com/repos/open-telemetry/opentelemetry-configuration/tarball/#{SCHEMA_VERSION}".freeze OUTPUT_FILE = File.expand_path('../opentelemetry/constants/generated_constants.rb', __dir__) # Ruby tarball downloader module SchemaDownloader + # Downloads the gzip tarball from +url+ and extracts +schema/*.yaml+ files into +dest_dir+. def self.fetch_schema(url, dest_dir) FileUtils.mkdir_p(dest_dir) @@ -53,6 +54,7 @@ end module SchemaReader ROOT_FILE = 'opentelemetry_configuration.yaml' + # Loads and returns the parsed YAML document at +path+. def self.load_yaml(path) YAML.safe_load(File.read(path)) end @@ -94,7 +96,7 @@ module ConstantsGenerator # "detection/development" become "detection_development". def field_name(key) - key.gsub(%r{[/.\-]}, '_').gsub(/[^a-zA-Z0-9_]/, '_') + key.gsub(%r{[/.-]}, '_').gsub(/[^a-zA-Z0-9_]/, '_') end # extracts the definition name from a $ref string @@ -113,6 +115,7 @@ module ConstantsGenerator body.is_a?(Hash) && body['properties'].is_a?(Hash) && !body['properties'].empty? end + # Returns true when +name+ is a presence-only marker object (no properties, no enum). def marker?(defs, name) body = defs[name] return false unless body.is_a?(Hash) @@ -124,6 +127,7 @@ module ConstantsGenerator object_type && (add == false || add.nil?) end + # Returns true when +prop+ describes an array-typed schema property. def array_schema?(prop) Array(prop['type']).include?('array') || prop.key?('items') end @@ -148,6 +152,7 @@ module ConstantsGenerator end end + # Renders a Ruby Struct definition string for the given schema object +body+. def render_struct(name, body, defs) props = body['properties'] member_names = props.keys.map { |k| field_name(k) } @@ -182,6 +187,7 @@ module ConstantsGenerator RUBY end + # Generates the full Ruby source for all object struct definitions in +defs+. def generate(defs) names = defs.keys.select { |n| struct?(defs, n) }.sort parts = [header(names.size)] @@ -189,6 +195,7 @@ module ConstantsGenerator parts.join("\n") end + # Returns the frozen-string-literal header comment for the generated file. def header(count) <<~RUBY # frozen_string_literal: true From e6065b5a698d4bdcab00db6b43a2ef47df9320d1 Mon Sep 17 00:00:00 2001 From: xuan-cao-swi Date: Mon, 15 Jun 2026 14:17:25 -0400 Subject: [PATCH 10/10] readme --- otelconfig/README.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/otelconfig/README.md b/otelconfig/README.md index 54c40ca4ea..0d77fcb3e6 100644 --- a/otelconfig/README.md +++ b/otelconfig/README.md @@ -30,21 +30,34 @@ Or, if you use [bundler][bundler-home], include `opentelemetry-otelconfig` in yo ### 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. +Set `OTEL_CONFIG_FILE` to the path of your YAML config file. Call `OpenTelemetry::OtelConfig.configure` early in your application; it returns a `RubySDK` value that you wire into the global OpenTelemetry state yourself. ```sh OTEL_CONFIG_FILE=/path/to/otel-config.yaml bundle exec ruby app.rb ``` ```ruby +require 'opentelemetry-sdk' require 'opentelemetry-otelconfig' +sdk = OpenTelemetry::OtelConfig.configure +OpenTelemetry.tracer_provider = sdk.tracer_provider +OpenTelemetry.propagation = sdk.propagator if sdk&.propagator + tracer = OpenTelemetry.tracer_provider.tracer('my_app', '1.0.0') tracer.in_span('my-operation') do |span| span.set_attribute('key', 'value') end ``` +If you have a config file path at hand, call `configure_from_file` instead: + +```ruby +sdk = OpenTelemetry::OtelConfig.configure_from_file('/path/to/otel-config.yaml') +OpenTelemetry.tracer_provider = sdk.tracer_provider +OpenTelemetry.propagation = sdk.propagator if sdk&.propagator +``` + ## YAML configuration reference See full configuration reference in [declarative-configuration](https://opentelemetry.io/docs/languages/sdk-configuration/declarative-configuration/) @@ -114,15 +127,15 @@ propagator: composite_list: "tracecontext,baggage" ``` -Supported propagator names: `tracecontext`, `baggage`, `b3`, `b3multi`, `jaeger`, `xray`. +Supported propagator names: `tracecontext`, `baggage`, `b3`, `b3multi`, `jaeger`, `ottrace`, `xray`, `google_cloud_trace_context`. ### 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. +The `instrumentation/development` key configures auto-instrumentation. The `ruby:` sub-key maps snake_case library names to option hashes. ```yaml instrumentation/development: - general: + ruby: net_http: untraced_hosts: - localhost