OpenTelemetry Trace Context Propagation [Ruby]
This guide covers Ruby-specific implementation of context propagation. For a comprehensive overview of context propagation concepts, W3C TraceContext, propagators, and troubleshooting, see the OpenTelemetry Context Propagation guide.
Automatic propagation
OpenTelemetry Ruby handles traceparent headers automatically in most scenarios. When using auto-instrumentation libraries, HTTP client libraries automatically inject traceparent headers into outgoing requests, and server libraries automatically extract them from incoming requests.
Auto-instrumentation
# config/initializers/opentelemetry.rb
require 'opentelemetry/sdk'
require 'opentelemetry/instrumentation/all'
OpenTelemetry::SDK.configure do |c|
c.service_name = 'my-ruby-service'
c.use_all() # enables all instrumentation including propagation
end
Auto-instrumentation packages for HTTP clients (Net::HTTP, Faraday, RestClient) and web frameworks (Rails, Sinatra, Rack) automatically inject W3C tracecontext headers to outgoing HTTP requests and extract them from incoming requests.
Manual propagation
When automatic instrumentation is not available, you can manually handle traceparent headers using OpenTelemetry's propagation API.
Extracting context
require 'opentelemetry'
def handle_incoming_request(request_headers)
# Extract context from incoming request headers
# Headers should be in hash format: {"traceparent" => "00-..."}
context = OpenTelemetry.propagation.extract(request_headers)
# Create a new span with the extracted parent context
tracer = OpenTelemetry.tracer_provider.tracer('my_service', '1.0.0')
tracer.in_span('handle-request', kind: :server) do |span|
# Set the extracted context as parent
OpenTelemetry::Trace.with_span(span, context) do
# Your business logic here
process_request(request_headers)
end
end
end
# Rails controller example
class ApiController < ApplicationController
def show
# Extract context from request headers
headers = request.headers.to_h.transform_keys(&:downcase)
context = OpenTelemetry.propagation.extract(headers)
tracer = OpenTelemetry.tracer_provider.tracer('rails_controller', '1.0.0')
tracer.in_span('api#show', kind: :server) do |span|
OpenTelemetry::Trace.with_span(span, context) do
# Your controller logic
render json: { message: 'success' }
end
end
end
end
Injecting context
require 'opentelemetry'
require 'net/http'
def make_http_request(url)
# Create carrier hash for headers
headers = {}
# Inject current trace context into headers
OpenTelemetry.propagation.inject(headers)
# Make HTTP request with injected headers
uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Get.new(uri)
# Add propagation headers
headers.each { |key, value| request[key] = value }
response = http.request(request)
response
end
# Faraday client example
class HttpClient
def initialize
@tracer = OpenTelemetry.tracer_provider.tracer('http_client', '1.0.0')
end
def get(url)
@tracer.in_span('http-get', kind: :client) do |span|
headers = {}
OpenTelemetry.propagation.inject(headers)
response = Faraday.get(url, nil, headers)
# Set span attributes based on response
span.set_attribute('http.status_code', response.status) if span.recording?
response
end
end
end
Debugging propagation
Logging context
Log incoming traceparent headers and current span context for debugging:
require 'logger'
class PropagationDebugger
def initialize
@logger = Logger.new(STDOUT)
end
def log_trace_context(request_headers)
# Log incoming traceparent header
traceparent = request_headers['traceparent']
if traceparent
@logger.info("Incoming traceparent: #{traceparent}")
else
@logger.info('No traceparent header found')
end
# Log current span context
current_span = OpenTelemetry::Trace.current_span
if current_span && current_span.context.valid?
context = current_span.context
@logger.info("Current trace context - TraceId: #{context.hex_trace_id}, " \
"SpanId: #{context.hex_span_id}, " \
"Sampled: #{context.trace_flags.sampled?}")
else
@logger.info('No valid span context found')
end
end
end
# Usage in Rails middleware
class PropagationLoggingMiddleware
def initialize(app)
@app = app
@debugger = PropagationDebugger.new
end
def call(env)
request = Rack::Request.new(env)
headers = extract_headers(env)
@debugger.log_trace_context(headers)
@app.call(env)
end
private
def extract_headers(env)
headers = {}
env.each do |key, value|
if key.start_with?('HTTP_')
header_name = key[5..-1].downcase.tr('_', '-')
headers[header_name] = value
end
end
headers
end
end
# Add to Rails application
# config/application.rb
config.middleware.use PropagationLoggingMiddleware
Validating format
Validate traceparent headers to ensure they follow the W3C specification:
class TraceparentValidator
TRACEPARENT_PATTERN = /\A00-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}\z/
def self.valid_traceparent?(traceparent)
return false if traceparent.nil? || traceparent.empty?
TRACEPARENT_PATTERN.match?(traceparent)
end
def self.parse_traceparent(traceparent)
raise ArgumentError, "Invalid traceparent format: #{traceparent}" unless valid_traceparent?(traceparent)
parts = traceparent.split('-')
{
version: parts[0],
trace_id: parts[1],
span_id: parts[2],
flags: parts[3],
sampled: parts[3] == '01'
}
end
end
# Usage
validator = TraceparentValidator
traceparent = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01'
if validator.valid_traceparent?(traceparent)
parsed = validator.parse_traceparent(traceparent)
puts "Valid traceparent: #{parsed}"
else
puts 'Invalid traceparent format'
end
Getting trace info
Access current trace context information:
def get_trace_info
current_span = OpenTelemetry::Trace.current_span
return { error: 'No valid span context available' } unless current_span&.context&.valid?
context = current_span.context
{
trace_id: context.hex_trace_id,
span_id: context.hex_span_id,
sampled: context.trace_flags.sampled?.to_s,
remote: context.remote?.to_s,
trace_state: context.trace_state.to_s
}
end
# Usage in your application
def my_handler
trace_info = get_trace_info
puts "Current trace info: #{trace_info}"
# Include trace info in logs
Rails.logger.info('Processing request', trace_info) if defined?(Rails)
end
# Rack middleware to add trace info to response headers
class TraceInfoMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
# Add trace info to response headers for debugging
trace_info = get_trace_info
unless trace_info[:error]
headers['X-Trace-Id'] = trace_info[:trace_id]
headers['X-Span-Id'] = trace_info[:span_id]
end
[status, headers, body]
end
end
Custom propagators
You can create custom propagators for non-standard trace context formats:
class CustomPropagator
def extract(carrier, context = nil)
# Extract custom headers
custom_trace_id = carrier['x-custom-trace-id']
custom_span_id = carrier['x-custom-span-id']
return context || OpenTelemetry::Context.empty unless custom_trace_id && custom_span_id
begin
# Convert custom format to OpenTelemetry format
trace_id = custom_trace_id.to_i(16)
span_id = custom_span_id.to_i(16)
span_context = OpenTelemetry::Trace::SpanContext.new(
trace_id: trace_id,
span_id: span_id,
remote: true,
trace_flags: OpenTelemetry::Trace::TraceFlags.new(0x01) # Sampled
)
span = OpenTelemetry::Trace::Span.new(span_context)
OpenTelemetry::Trace.context_with_span(span, parent_context: context)
rescue StandardError
# Invalid format, return original context
context || OpenTelemetry::Context.empty
end
end
def inject(carrier, context = nil)
span = OpenTelemetry::Trace.current_span(context)
return unless span&.context&.valid?
span_context = span.context
carrier['x-custom-trace-id'] = span_context.hex_trace_id
carrier['x-custom-span-id'] = span_context.hex_span_id
end
def fields
%w[x-custom-trace-id x-custom-span-id]
end
end
# Register custom propagator
OpenTelemetry.propagation = OpenTelemetry::Propagation::CompositePropagator.new([
OpenTelemetry::Trace::Propagation::TraceContext.new,
CustomPropagator.new
])
Configuration
Environment variables
Configure propagation format using environment variables:
# Default: W3C TraceContext and Baggage
export OTEL_PROPAGATORS="tracecontext,baggage"
# B3 format (requires opentelemetry-propagator-b3 gem)
export OTEL_PROPAGATORS="b3"
# Multiple formats
export OTEL_PROPAGATORS="tracecontext,b3,baggage"
Install B3 propagator if needed:
gem install opentelemetry-propagator-b3
# Gemfile
gem 'opentelemetry-propagator-b3'
Programmatic configuration
require 'opentelemetry/propagation'
require 'opentelemetry/trace/propagation/trace_context'
require 'opentelemetry/baggage/propagation'
# Set custom propagator combination
OpenTelemetry.propagation = OpenTelemetry::Propagation::CompositePropagator.new([
OpenTelemetry::Trace::Propagation::TraceContext.new,
OpenTelemetry::Baggage::Propagation.new
])
# Or configure in SDK setup
OpenTelemetry::SDK.configure do |c|
c.service_name = 'my-service'
# Propagation is configured automatically based on OTEL_PROPAGATORS
# or you can set it explicitly here if needed
end
Production considerations
Performance
Propagation has minimal performance impact:
- Header extraction/injection is fast (< 1ms)
- Context objects are lightweight
- No network calls during propagation
- Thread-safe operations
Security
Be aware of potential security implications:
class SecurePropagationMiddleware
def initialize(app)
@app = app
@allowed_origins = ENV['ALLOWED_TRACE_ORIGINS']&.split(',') || []
end
def call(env)
request = Rack::Request.new(env)
# Only extract trace context from trusted origins
if trusted_origin?(request)
headers = extract_headers(env)
context = OpenTelemetry.propagation.extract(headers)
# Continue with extracted context
else
# Start new trace for untrusted origins
Rails.logger.warn("Ignoring trace context from untrusted origin: #{request.env['HTTP_ORIGIN']}")
end
@app.call(env)
end
private
def trusted_origin?(request)
origin = request.env['HTTP_ORIGIN']
return true if origin.nil? # Same-origin requests
@allowed_origins.include?(origin)
end
def extract_headers(env)
# Extract headers logic
end
end
Monitoring propagation
Monitor propagation health in production:
class PropagationMonitor
def initialize
@stats = Hash.new(0)
@start_time = Time.now
@mutex = Mutex.new
end
def record_request(has_parent:, is_sampled:)
@mutex.synchronize do
@stats[:total_requests] += 1
@stats[:requests_with_parent] += 1 if has_parent
@stats[:sampled_requests] += 1 if is_sampled
end
end
def get_stats
@mutex.synchronize do
elapsed = Time.now - @start_time
total = @stats[:total_requests]
{
total_requests: total,
requests_with_parent: @stats[:requests_with_parent],
propagation_rate: total > 0 ? @stats[:requests_with_parent].to_f / total : 0,
sampling_rate: total > 0 ? @stats[:sampled_requests].to_f / total : 0,
elapsed_seconds: elapsed
}
end
end
end
# Usage in request handler
class MonitoredPropagationMiddleware
def initialize(app)
@app = app
@monitor = PropagationMonitor.new
end
def call(env)
current_span = OpenTelemetry::Trace.current_span
has_parent = current_span&.context&.remote? || false
is_sampled = current_span&.context&.trace_flags&.sampled? || false
@monitor.record_request(has_parent: has_parent, is_sampled: is_sampled)
# Log stats periodically
stats = @monitor.get_stats
if stats[:total_requests] % 1000 == 0
Rails.logger.info("Propagation stats: #{stats}")
end
@app.call(env)
end
end
Rails integration
Complete Rails integration example:
# config/initializers/opentelemetry.rb
require 'opentelemetry/sdk'
require 'opentelemetry/instrumentation/all'
OpenTelemetry::SDK.configure do |c|
c.service_name = Rails.application.class.module_parent_name.downcase
c.service_version = ENV['APP_VERSION'] || '1.0.0'
c.resource = OpenTelemetry::SDK::Resources::Resource.create({
'deployment.environment' => Rails.env
})
c.use_all() # This enables automatic propagation
end
# config/application.rb - Add middleware for debugging if needed
if Rails.env.development?
config.middleware.use PropagationLoggingMiddleware
end
if Rails.env.production?
config.middleware.use MonitoredPropagationMiddleware
end
Background jobs
Propagate context to background jobs:
# Sidekiq job with context propagation
class ProcessOrderJob
include Sidekiq::Job
def perform(order_id, trace_context = nil)
if trace_context
# Restore trace context in background job
context = OpenTelemetry.propagation.extract(trace_context)
OpenTelemetry::Context.with_current(context) do
process_order_with_tracing(order_id)
end
else
process_order_with_tracing(order_id)
end
end
private
def process_order_with_tracing(order_id)
tracer = OpenTelemetry.tracer_provider.tracer('background_jobs', '1.0.0')
tracer.in_span('process-order', kind: :consumer) do |span|
span.set_attribute('order.id', order_id) if span.recording?
# Process order logic
order = Order.find(order_id)
order.process!
end
end
end
# Enqueue job with trace context
class OrdersController < ApplicationController
def create
tracer = OpenTelemetry.tracer_provider.tracer('orders_controller', '1.0.0')
tracer.in_span('create-order') do |span|
order = Order.create!(order_params)
# Extract current trace context for background job
trace_context = {}
OpenTelemetry.propagation.inject(trace_context)
# Enqueue job with trace context
ProcessOrderJob.perform_async(order.id, trace_context)
render json: order
end
end
end