Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
## Unreleased

### Features

- Add support for OTLP ingestion in `sentry-opentelemetry` ([#2853](https://github.com/getsentry/sentry-ruby/pull/2853))

Sentry now has first class [OTLP ingestion](https://docs.sentry.io/concepts/otlp/) capabilities.

```ruby
Sentry.init do |config|
## ...
config.otlp.enabled = true
end
```

Under the hood, this will setup:
- An `OpenTelemetry::Exporter` that will automatically set up the OTLP ingestion endpoint from your DSN
- You can turn this off with `config.otlp.setup_otlp_traces_exporter = false` to setup your own exporter
- An `OTLPPropagator` that ensures Distributed Tracing works
- You can turn this off with `config.otlp.setup_propagator = false`
- Trace/Span linking for all other Sentry events such as Errors, Logs, Crons and Metrics

If you were using the `SpanProcessor` before, we recommend migrating over to `config.otlp` since it's a much simpler setup.

## 6.3.1

### Bug Fixes
Expand Down
1 change: 1 addition & 0 deletions sentry-opentelemetry/lib/sentry-opentelemetry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
require "sentry/opentelemetry/version"
require "sentry/opentelemetry/span_processor"
require "sentry/opentelemetry/propagator"
require "sentry/opentelemetry/configuration"
33 changes: 33 additions & 0 deletions sentry-opentelemetry/lib/sentry/opentelemetry/configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

require "sentry/opentelemetry/otlp_setup"

module Sentry
class Configuration
# OTLP related configuration.
# @return [OTLP::Configuration]
attr_reader :otlp

after(:initialize) do
@otlp = OTLP::Configuration.new
end

after(:configured) do
Sentry::OpenTelemetry::OTLPSetup.setup(self) if otlp.enabled
end
end

module OTLP
class Configuration
attr_accessor :enabled
attr_accessor :setup_otlp_traces_exporter
attr_accessor :setup_propagator

def initialize
@enabled = false
@setup_otlp_traces_exporter = true
@setup_propagator = true
end
end
end
end
31 changes: 31 additions & 0 deletions sentry-opentelemetry/lib/sentry/opentelemetry/otlp_propagator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# frozen_string_literal: true

module Sentry
module OpenTelemetry
class OTLPPropagator < Propagator
def inject(
carrier,
context: ::OpenTelemetry::Context.current,
setter: ::OpenTelemetry::Context::Propagation.text_map_setter
)
span_context = ::OpenTelemetry::Trace.current_span(context).context
return unless span_context.valid?

setter.set(carrier, SENTRY_TRACE_HEADER_NAME, to_sentry_trace(span_context))

baggage = context[SENTRY_BAGGAGE_KEY]
if baggage.is_a?(Sentry::Baggage)
baggage_string = baggage.serialize
setter.set(carrier, BAGGAGE_HEADER_NAME, baggage_string) unless baggage_string&.empty?
end
end

private

def to_sentry_trace(span_context)
sampled = span_context.trace_flags.sampled? ? "1" : "0"
"#{span_context.hex_trace_id}-#{span_context.hex_span_id}-#{sampled}"
end
end
end
end
75 changes: 75 additions & 0 deletions sentry-opentelemetry/lib/sentry/opentelemetry/otlp_setup.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

#
require "sentry/opentelemetry/otlp_propagator"

module Sentry
module OpenTelemetry
module OTLPSetup
USER_AGENT = "sentry-ruby.otlp/#{Sentry::VERSION}"

class << self
def setup(config)
@dsn = config.dsn
@sdk_logger = config.sdk_logger
log_debug("[OTLP] Setting up OTLP integration")

setup_external_propagation_context
setup_otlp_exporter if config.otlp.setup_otlp_traces_exporter
setup_sentry_propagator if config.otlp.setup_propagator
end

private

def log_debug(message)
@sdk_logger&.debug(message)
end

def log_warn(message)
@sdk_logger&.warn(message)
end

def setup_external_propagation_context
log_debug("[OTLP] Setting up trace linking for all events")

Sentry.register_external_propagation_context do
span_context = ::OpenTelemetry::Trace.current_span.context
span_context.valid? ? [span_context.hex_trace_id, span_context.hex_span_id] : nil
end
end

def setup_otlp_exporter
return unless @dsn

log_debug("[OTLP] Setting up OTLP exporter")

begin
require "opentelemetry/exporter/otlp"
rescue LoadError
log_warn("[OTLP] opentelemetry-exporter-otlp gem is not installed. " \
"Please add it to your Gemfile to use the OTLP exporter.")
return
end

endpoint = "#{@dsn.server}#{@dsn.otlp_traces_endpoint}"
auth_header = @dsn.generate_auth_header(client: USER_AGENT)

log_debug("[OTLP] Sending traces to #{endpoint}")

exporter = ::OpenTelemetry::Exporter::OTLP::Exporter.new(
endpoint: endpoint,
headers: { "X-Sentry-Auth" => auth_header }
)

span_processor = ::OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter)
::OpenTelemetry.tracer_provider.add_span_processor(span_processor)
end

def setup_sentry_propagator
log_debug("[OTLP] Setting up propagator for distributed tracing")
::OpenTelemetry.propagation = OTLPPropagator.new
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

RSpec.describe Sentry::OTLP::Configuration do
subject { described_class.new }

describe "#initialize" do
it "sets default values" do
expect(subject.enabled).to eq(false)
expect(subject.setup_otlp_traces_exporter).to eq(true)
expect(subject.setup_propagator).to eq(true)
end
end

describe "accessors" do
it "allows setting enabled" do
subject.enabled = true
expect(subject.enabled).to eq(true)
end

it "allows setting setup_otlp_traces_exporter" do
subject.setup_otlp_traces_exporter = false
expect(subject.setup_otlp_traces_exporter).to eq(false)
end

it "allows setting setup_propagator" do
subject.setup_propagator = false
expect(subject.setup_propagator).to eq(false)
end
end
end
145 changes: 145 additions & 0 deletions sentry-opentelemetry/spec/sentry/opentelemetry/otlp_propagator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Sentry::OpenTelemetry::OTLPPropagator do
let(:tracer) { ::OpenTelemetry.tracer_provider.tracer('sentry', '1.0') }

before do
perform_basic_setup
perform_otel_setup
end

describe '#inject' do
let(:carrier) { {} }

it 'noops with invalid span_context' do
subject.inject(carrier)
expect(carrier).to eq({})
end

context 'with valid span' do
it 'sets sentry-trace header on carrier' do
span = tracer.start_root_span('test')
ctx = ::OpenTelemetry::Trace.context_with_span(span)

subject.inject(carrier, context: ctx)

span_context = span.context
expected_trace = "#{span_context.hex_trace_id}-#{span_context.hex_span_id}-1"
expect(carrier['sentry-trace']).to eq(expected_trace)
end

it 'sets sampled flag to 0 when not sampled' do
span = tracer.start_root_span('test')
ctx = ::OpenTelemetry::Trace.context_with_span(span)

allow(span.context.trace_flags).to receive(:sampled?).and_return(false)
subject.inject(carrier, context: ctx)

span_context = span.context
expected_trace = "#{span_context.hex_trace_id}-#{span_context.hex_span_id}-0"
expect(carrier['sentry-trace']).to eq(expected_trace)
end
end

context 'with baggage in context' do
it 'sets baggage header on carrier' do
span = tracer.start_root_span('test')
ctx = ::OpenTelemetry::Trace.context_with_span(span)

baggage = Sentry::Baggage.new({
'trace_id' => 'abc123',
'public_key' => 'key123'
})
ctx = ctx.set_value(described_class::SENTRY_BAGGAGE_KEY, baggage)

subject.inject(carrier, context: ctx)

expect(carrier['baggage']).to include('sentry-trace_id=abc123')
expect(carrier['baggage']).to include('sentry-public_key=key123')
end

it 'does not set baggage header when baggage is empty' do
span = tracer.start_root_span('test')
ctx = ::OpenTelemetry::Trace.context_with_span(span)

baggage = Sentry::Baggage.new({})
ctx = ctx.set_value(described_class::SENTRY_BAGGAGE_KEY, baggage)

subject.inject(carrier, context: ctx)

expect(carrier['baggage']).to be_nil
end
end
end

describe '#extract' do
let(:ctx) { ::OpenTelemetry::Context.empty }

it 'returns unchanged context without sentry-trace' do
carrier = {}
updated_ctx = subject.extract(carrier, context: ctx)
expect(updated_ctx).to eq(ctx)
end

it 'returns unchanged context with invalid sentry-trace' do
carrier = { 'sentry-trace' => '000-000-0' }
updated_ctx = subject.extract(carrier, context: ctx)
expect(updated_ctx).to eq(ctx)
end

context 'with valid sentry-trace header' do
let(:carrier) do
{ 'sentry-trace' => 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1' }
end

it 'returns context with sentry-trace data' do
updated_ctx = subject.extract(carrier, context: ctx)

sentry_trace_data = updated_ctx[described_class::SENTRY_TRACE_KEY]
expect(sentry_trace_data).not_to be_nil

trace_id, parent_span_id, parent_sampled = sentry_trace_data
expect(trace_id).to eq('d4cda95b652f4a1592b449d5929fda1b')
expect(parent_span_id).to eq('6e0c63257de34c92')
expect(parent_sampled).to eq(true)
end

it 'returns context with correct span_context' do
updated_ctx = subject.extract(carrier, context: ctx)

span_context = ::OpenTelemetry::Trace.current_span(updated_ctx).context
expect(span_context.valid?).to eq(true)
expect(span_context.hex_trace_id).to eq('d4cda95b652f4a1592b449d5929fda1b')
expect(span_context.hex_span_id).to eq('6e0c63257de34c92')
expect(span_context.remote?).to eq(true)
end
end

context 'with sentry-trace and baggage headers' do
let(:carrier) do
{
'sentry-trace' => 'd4cda95b652f4a1592b449d5929fda1b-6e0c63257de34c92-1',
'baggage' => 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b, sentry-public_key=key123'
}
end

it 'returns context with baggage' do
updated_ctx = subject.extract(carrier, context: ctx)

baggage = updated_ctx[described_class::SENTRY_BAGGAGE_KEY]
expect(baggage).to be_a(Sentry::Baggage)
expect(baggage.mutable).to eq(false)
expect(baggage.items['trace_id']).to eq('d4cda95b652f4a1592b449d5929fda1b')
expect(baggage.items['public_key']).to eq('key123')
end
end
end

describe '#fields' do
it 'returns header names' do
expect(subject.fields).to eq(['sentry-trace', 'baggage'])
end
end
end
Loading
Loading