From ce5976f3284606c8226ba4c75b87792f11574644 Mon Sep 17 00:00:00 2001 From: Neel Shah Date: Fri, 6 Mar 2026 13:40:17 +0100 Subject: [PATCH] feat(otlp): Add collector_url option to OTLP integration Allow users with their own OpenTelemetry collector to pass a custom collector URL via config.otlp.collector_url. When set, traces are sent to the specified endpoint without Sentry auth headers. When not set, the existing behavior of deriving the endpoint from the DSN is preserved. Co-Authored-By: Claude Opus 4.6 --- sentry-opentelemetry/Gemfile | 1 + .../lib/sentry/opentelemetry/configuration.rb | 2 + .../lib/sentry/opentelemetry/otlp_setup.rb | 21 +++--- .../opentelemetry/configuration_spec.rb | 6 ++ .../sentry/opentelemetry/otlp_setup_spec.rb | 71 +++++++++++++++++-- 5 files changed, 89 insertions(+), 12 deletions(-) diff --git a/sentry-opentelemetry/Gemfile b/sentry-opentelemetry/Gemfile index 4dde72bde..576d1ab73 100644 --- a/sentry-opentelemetry/Gemfile +++ b/sentry-opentelemetry/Gemfile @@ -9,6 +9,7 @@ eval_gemfile "../Gemfile.dev" gemspec gem "opentelemetry-sdk" +gem "opentelemetry-exporter-otlp" unless RUBY_PLATFORM == "java" gem "opentelemetry-instrumentation-rails" gem "sentry-ruby", path: "../sentry-ruby" diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/configuration.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/configuration.rb index dd99a2e0d..4ee68a529 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/configuration.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/configuration.rb @@ -21,11 +21,13 @@ module OTLP class Configuration attr_accessor :enabled attr_accessor :setup_otlp_traces_exporter + attr_accessor :collector_url attr_accessor :setup_propagator def initialize @enabled = false @setup_otlp_traces_exporter = true + @collector_url = nil @setup_propagator = true end end diff --git a/sentry-opentelemetry/lib/sentry/opentelemetry/otlp_setup.rb b/sentry-opentelemetry/lib/sentry/opentelemetry/otlp_setup.rb index 843ca8a00..a49af3b64 100644 --- a/sentry-opentelemetry/lib/sentry/opentelemetry/otlp_setup.rb +++ b/sentry-opentelemetry/lib/sentry/opentelemetry/otlp_setup.rb @@ -11,6 +11,7 @@ module OTLPSetup class << self def setup(config) @dsn = config.dsn + @collector_url = config.otlp.collector_url @sdk_logger = config.sdk_logger log_debug("[OTLP] Setting up OTLP integration") @@ -39,7 +40,7 @@ def setup_external_propagation_context end def setup_otlp_exporter - return unless @dsn + return unless @dsn || @collector_url log_debug("[OTLP] Setting up OTLP exporter") @@ -51,15 +52,19 @@ def setup_otlp_exporter return end - endpoint = "#{@dsn.server}#{@dsn.otlp_traces_endpoint}" - auth_header = @dsn.generate_auth_header(client: USER_AGENT) + exporter = if @collector_url + endpoint = @collector_url + log_debug("[OTLP] Sending traces to collector at #{endpoint}") - log_debug("[OTLP] Sending traces to #{endpoint}") + ::OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: endpoint) + else + endpoint = "#{@dsn.server}#{@dsn.otlp_traces_endpoint}" + auth_header = @dsn.generate_auth_header(client: USER_AGENT) + headers = { "X-Sentry-Auth" => auth_header } + log_debug("[OTLP] Sending traces to #{endpoint}") - exporter = ::OpenTelemetry::Exporter::OTLP::Exporter.new( - endpoint: endpoint, - headers: { "X-Sentry-Auth" => auth_header } - ) + ::OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: endpoint, headers: headers) + end span_processor = ::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter) ::OpenTelemetry.tracer_provider.add_span_processor(span_processor) diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/configuration_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/configuration_spec.rb index 292be5080..22f748423 100644 --- a/sentry-opentelemetry/spec/sentry/opentelemetry/configuration_spec.rb +++ b/sentry-opentelemetry/spec/sentry/opentelemetry/configuration_spec.rb @@ -7,6 +7,7 @@ it "sets default values" do expect(subject.enabled).to eq(false) expect(subject.setup_otlp_traces_exporter).to eq(true) + expect(subject.collector_url).to be_nil expect(subject.setup_propagator).to eq(true) end end @@ -17,6 +18,11 @@ expect(subject.enabled).to eq(true) end + it "allows setting collector_url" do + subject.collector_url = "http://localhost:4318/v1/traces" + expect(subject.collector_url).to eq("http://localhost:4318/v1/traces") + end + it "allows setting setup_otlp_traces_exporter" do subject.setup_otlp_traces_exporter = false expect(subject.setup_otlp_traces_exporter).to eq(false) diff --git a/sentry-opentelemetry/spec/sentry/opentelemetry/otlp_setup_spec.rb b/sentry-opentelemetry/spec/sentry/opentelemetry/otlp_setup_spec.rb index 06d5236e6..5f7dd0ce1 100644 --- a/sentry-opentelemetry/spec/sentry/opentelemetry/otlp_setup_spec.rb +++ b/sentry-opentelemetry/spec/sentry/opentelemetry/otlp_setup_spec.rb @@ -7,6 +7,10 @@ perform_otel_setup end + def span_processors + ::OpenTelemetry.tracer_provider.instance_variable_get(:@span_processors) + end + describe '.setup' do context 'with setup_propagator enabled' do before do @@ -23,14 +27,57 @@ end end - context 'with setup_otlp_traces_exporter enabled' do - before do - perform_basic_setup do |config| - config.otlp.enabled = true + context 'with setup_otlp_traces_exporter enabled', unless: RUBY_PLATFORM == "java" do + context 'without collector_url (default DSN-based endpoint)' do + before do + perform_basic_setup do |config| + config.otlp.enabled = true + config.otlp.setup_otlp_traces_exporter = true + end + end + + it 'creates the exporter with the DSN endpoint and auth headers' do + dsn = Sentry.configuration.dsn + expected_endpoint = "#{dsn.server}#{dsn.otlp_traces_endpoint}" + + expect(::OpenTelemetry::Exporter::OTLP::Exporter).to receive(:new) + .with(endpoint: expected_endpoint, headers: hash_including("X-Sentry-Auth")) + .and_call_original + + described_class.setup(Sentry.configuration) + + expect(span_processors.last).to be_a(::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor) + end + end + + context 'with collector_url' do + let(:collector_url) { "http://localhost:4318/v1/traces" } + + before do + perform_basic_setup do |config| + config.otlp.enabled = true + config.otlp.setup_otlp_traces_exporter = true + config.otlp.collector_url = collector_url + end + end + + it 'creates the exporter with the collector endpoint' do + expect(::OpenTelemetry::Exporter::OTLP::Exporter).to receive(:new) + .with(endpoint: collector_url) + .and_call_original + + described_class.setup(Sentry.configuration) + + expect(span_processors.last).to be_a(::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor) end end it 'logs a warning when opentelemetry-exporter-otlp is not installed' do + perform_basic_setup do |config| + config.otlp.enabled = true + config.otlp.setup_otlp_traces_exporter = true + end + allow_any_instance_of(Object).to receive(:require).with("opentelemetry/exporter/otlp").and_raise(LoadError) expect(Sentry.configuration.sdk_logger).to receive(:warn).with(/opentelemetry-exporter-otlp gem is not installed/) @@ -38,6 +85,22 @@ end end + context 'with setup_otlp_traces_exporter disabled' do + before do + perform_basic_setup do |config| + config.otlp.enabled = true + config.otlp.setup_otlp_traces_exporter = false + end + end + + it 'does not add a span processor' do + processors_before = span_processors.length + described_class.setup(Sentry.configuration) + + expect(span_processors.length).to eq(processors_before) + end + end + context 'with external propagation context' do before do perform_basic_setup do |config|