Skip to content

Commit 27bfcba

Browse files
committed
Implement OTLP support
1 parent 299d0a3 commit 27bfcba

File tree

13 files changed

+461
-13
lines changed

13 files changed

+461
-13
lines changed

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
## Unreleased
2+
3+
### Features
4+
5+
- Add support for OTLP ingestion in `sentry-opentelemetry` ([#2853](https://github.com/getsentry/sentry-ruby/pull/2853))
6+
7+
Sentry now has first class [OTLP ingestion](https://docs.sentry.io/concepts/otlp/) capabilities.
8+
9+
```ruby
10+
Sentry.init do |config|
11+
## ...
12+
config.otlp.enabled = true
13+
end
14+
```
15+
16+
Under the hood, this will setup:
17+
- An `OpenTelemetry::Exporter` that will automatically set up the OTLP ingestion endpoint from your DSN
18+
- You can turn this off with `config.otlp.setup_otlp_traces_exporter = false` to setup your own exporter
19+
- An `OTLPPropagator` that ensures Distributed Tracing works
20+
- You can turn this off with `config.otlp.setup_propagator = false`
21+
- Trace/Span linking for all other Sentry events such as Errors, Logs, Crons and Metrics
22+
23+
If you were using the `SpanProcessor` before, we recommend migrating over to `config.otlp` since it's a much simpler setup.
24+
125
## 6.3.1
226

327
### Bug Fixes

sentry-opentelemetry/lib/sentry-opentelemetry.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66
require "sentry/opentelemetry/version"
77
require "sentry/opentelemetry/span_processor"
88
require "sentry/opentelemetry/propagator"
9+
require "sentry/opentelemetry/configuration"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# frozen_string_literal: true
2+
3+
require "sentry/opentelemetry/otlp_setup"
4+
5+
module Sentry
6+
class Configuration
7+
# OTLP related configuration.
8+
# @return [OTLP::Configuration]
9+
attr_reader :otlp
10+
11+
after(:initialize) do
12+
@otlp = OTLP::Configuration.new
13+
end
14+
15+
after(:configured) do
16+
Sentry::OpenTelemetry::OTLPSetup.setup(self) if otlp.enabled
17+
end
18+
end
19+
20+
module OTLP
21+
class Configuration
22+
attr_accessor :enabled
23+
attr_accessor :setup_otlp_traces_exporter
24+
attr_accessor :setup_propagator
25+
26+
def initialize
27+
@enabled = false
28+
@setup_otlp_traces_exporter = true
29+
@setup_propagator = true
30+
end
31+
end
32+
end
33+
end
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# frozen_string_literal: true
2+
3+
module Sentry
4+
module OpenTelemetry
5+
class OTLPPropagator < Propagator
6+
def inject(
7+
carrier,
8+
context: ::OpenTelemetry::Context.current,
9+
setter: ::OpenTelemetry::Context::Propagation.text_map_setter
10+
)
11+
span_context = ::OpenTelemetry::Trace.current_span(context).context
12+
return unless span_context.valid?
13+
14+
sampled = span_context.trace_flags.sampled? ? "1" : "0"
15+
sentry_trace = "#{span_context.hex_trace_id}-#{span_context.hex_span_id}-#{sampled}"
16+
setter.set(carrier, SENTRY_TRACE_HEADER_NAME, sentry_trace)
17+
18+
baggage = context[SENTRY_BAGGAGE_KEY]
19+
if baggage.is_a?(Sentry::Baggage)
20+
baggage_string = baggage.serialize
21+
setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_string) if baggage_string && !baggage_string.empty?
22+
end
23+
end
24+
end
25+
end
26+
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
#
4+
require "sentry/opentelemetry/otlp_propagator"
5+
6+
module Sentry
7+
module OpenTelemetry
8+
module OTLPSetup
9+
USER_AGENT = "sentry-ruby.otlp/#{Sentry::VERSION}"
10+
11+
class << self
12+
def setup(config)
13+
@dsn = config.dsn
14+
@sdk_logger = config.sdk_logger
15+
log_debug("[OTLP] Setting up OTLP integration")
16+
17+
setup_external_propagation_context
18+
setup_otlp_exporter if config.otlp.setup_otlp_traces_exporter
19+
setup_sentry_propagator if config.otlp.setup_propagator
20+
end
21+
22+
private
23+
24+
def log_debug(message)
25+
@sdk_logger&.debug(message)
26+
end
27+
28+
def log_warn(message)
29+
@sdk_logger&.warn(message)
30+
end
31+
32+
def setup_external_propagation_context
33+
log_debug("[OTLP] Setting up trace linking for all events")
34+
35+
Sentry.register_external_propagation_context do
36+
span_context = ::OpenTelemetry::Trace.current_span.context
37+
span_context.valid? ? [span_context.hex_trace_id, span_context.hex_span_id] : nil
38+
end
39+
end
40+
41+
def setup_otlp_exporter
42+
return unless @dsn
43+
44+
log_debug("[OTLP] Setting up OTLP exporter")
45+
46+
begin
47+
require "opentelemetry/exporter/otlp"
48+
rescue LoadError
49+
log_warn("[OTLP] opentelemetry-exporter-otlp gem is not installed. " \
50+
"Please add it to your Gemfile to use the OTLP exporter.")
51+
return
52+
end
53+
54+
endpoint = "#{@dsn.server}#{@dsn.otlp_traces_endpoint}"
55+
auth_header = @dsn.generate_auth_header(client: USER_AGENT)
56+
57+
log_debug("[OTLP] Sending traces to #{endpoint}")
58+
59+
exporter = ::OpenTelemetry::Exporter::OTLP::Exporter.new(
60+
endpoint: endpoint,
61+
headers: { "X-Sentry-Auth" => auth_header }
62+
)
63+
64+
span_processor = ::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
65+
::OpenTelemetry.tracer_provider.add_span_processor(span_processor)
66+
end
67+
68+
def setup_sentry_propagator
69+
log_debug("[OTLP] Setting up propagator for distributed tracing")
70+
::OpenTelemetry.propagation = OTLPPropagator.new
71+
end
72+
end
73+
end
74+
end
75+
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe Sentry::OTLP::Configuration do
4+
subject { described_class.new }
5+
6+
describe "#initialize" do
7+
it "sets default values" do
8+
expect(subject.enabled).to eq(false)
9+
expect(subject.setup_otlp_traces_exporter).to eq(true)
10+
expect(subject.setup_propagator).to eq(true)
11+
end
12+
end
13+
14+
describe "accessors" do
15+
it "allows setting enabled" do
16+
subject.enabled = true
17+
expect(subject.enabled).to eq(true)
18+
end
19+
20+
it "allows setting setup_otlp_traces_exporter" do
21+
subject.setup_otlp_traces_exporter = false
22+
expect(subject.setup_otlp_traces_exporter).to eq(false)
23+
end
24+
25+
it "allows setting setup_propagator" do
26+
subject.setup_propagator = false
27+
expect(subject.setup_propagator).to eq(false)
28+
end
29+
end
30+
end
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
RSpec.describe Sentry::OpenTelemetry::OTLPPropagator do
6+
let(:tracer) { ::OpenTelemetry.tracer_provider.tracer('sentry', '1.0') }
7+
8+
before do
9+
perform_basic_setup
10+
perform_otel_setup
11+
end
12+
13+
describe '#inject' do
14+
let(:carrier) { {} }
15+
16+
it 'noops with invalid span_context' do
17+
subject.inject(carrier)
18+
expect(carrier).to eq({})
19+
end
20+
21+
context 'with valid span' do
22+
it 'sets sentry-trace header on carrier' do
23+
span = tracer.start_root_span('test')
24+
ctx = ::OpenTelemetry::Trace.context_with_span(span)
25+
26+
subject.inject(carrier, context: ctx)
27+
28+
span_context = span.context
29+
expected_trace = "#{span_context.hex_trace_id}-#{span_context.hex_span_id}-1"
30+
expect(carrier['sentry-trace']).to eq(expected_trace)
31+
end
32+
33+
it 'sets sampled flag to 0 when not sampled' do
34+
span = tracer.start_root_span('test')
35+
ctx = ::OpenTelemetry::Trace.context_with_span(span)
36+
37+
allow(span.context.trace_flags).to receive(:sampled?).and_return(false)
38+
subject.inject(carrier, context: ctx)
39+
40+
span_context = span.context
41+
expected_trace = "#{span_context.hex_trace_id}-#{span_context.hex_span_id}-0"
42+
expect(carrier['sentry-trace']).to eq(expected_trace)
43+
end
44+
end
45+
46+
context 'with baggage in context' do
47+
it 'sets baggage header on carrier' do
48+
span = tracer.start_root_span('test')
49+
ctx = ::OpenTelemetry::Trace.context_with_span(span)
50+
51+
baggage = Sentry::Baggage.new({
52+
'trace_id' => 'abc123',
53+
'public_key' => 'key123'
54+
})
55+
ctx = ctx.set_value(described_class::SENTRY_BAGGAGE_KEY, baggage)
56+
57+
subject.inject(carrier, context: ctx)
58+
59+
expect(carrier['baggage']).to include('sentry-trace_id=abc123')
60+
expect(carrier['baggage']).to include('sentry-public_key=key123')
61+
end
62+
63+
it 'does not set baggage header when baggage is empty' do
64+
span = tracer.start_root_span('test')
65+
ctx = ::OpenTelemetry::Trace.context_with_span(span)
66+
67+
baggage = Sentry::Baggage.new({})
68+
ctx = ctx.set_value(described_class::SENTRY_BAGGAGE_KEY, baggage)
69+
70+
subject.inject(carrier, context: ctx)
71+
72+
expect(carrier['baggage']).to be_nil
73+
end
74+
end
75+
end
76+
77+
describe '#extract' do
78+
let(:ctx) { ::OpenTelemetry::Context.empty }
79+
80+
it 'returns unchanged context without sentry-trace' do
81+
carrier = {}
82+
updated_ctx = subject.extract(carrier, context: ctx)
83+
expect(updated_ctx).to eq(ctx)
84+
end
85+
86+
it 'returns unchanged context with invalid sentry-trace' do
87+
carrier = { 'sentry-trace' => '000-000-0' }
88+
updated_ctx = subject.extract(carrier, context: ctx)
89+
expect(updated_ctx).to eq(ctx)
90+
end
91+
92+
context 'with valid sentry-trace header' do
93+
let(:carrier) do
94+
{ 'sentry-trace' => 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1' }
95+
end
96+
97+
it 'returns context with sentry-trace data' do
98+
updated_ctx = subject.extract(carrier, context: ctx)
99+
100+
sentry_trace_data = updated_ctx[described_class::SENTRY_TRACE_KEY]
101+
expect(sentry_trace_data).not_to be_nil
102+
103+
trace_id, parent_span_id, parent_sampled = sentry_trace_data
104+
expect(trace_id).to eq('d4cda95b652f4a1592b449d5929fda1b')
105+
expect(parent_span_id).to eq('6e0c63257de34c92')
106+
expect(parent_sampled).to eq(true)
107+
end
108+
109+
it 'returns context with correct span_context' do
110+
updated_ctx = subject.extract(carrier, context: ctx)
111+
112+
span_context = ::OpenTelemetry::Trace.current_span(updated_ctx).context
113+
expect(span_context.valid?).to eq(true)
114+
expect(span_context.hex_trace_id).to eq('d4cda95b652f4a1592b449d5929fda1b')
115+
expect(span_context.hex_span_id).to eq('6e0c63257de34c92')
116+
expect(span_context.remote?).to eq(true)
117+
end
118+
end
119+
120+
context 'with sentry-trace and baggage headers' do
121+
let(:carrier) do
122+
{
123+
'sentry-trace' => 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1',
124+
'baggage' => 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b, sentry-public_key=key123'
125+
}
126+
end
127+
128+
it 'returns context with baggage' do
129+
updated_ctx = subject.extract(carrier, context: ctx)
130+
131+
baggage = updated_ctx[described_class::SENTRY_BAGGAGE_KEY]
132+
expect(baggage).to be_a(Sentry::Baggage)
133+
expect(baggage.mutable).to eq(false)
134+
expect(baggage.items['trace_id']).to eq('d4cda95b652f4a1592b449d5929fda1b')
135+
expect(baggage.items['public_key']).to eq('key123')
136+
end
137+
end
138+
end
139+
140+
describe '#fields' do
141+
it 'returns header names' do
142+
expect(subject.fields).to eq(['sentry-trace', 'baggage'])
143+
end
144+
end
145+
end

0 commit comments

Comments
 (0)