OpenTelemetry Logs for Ruby
Make sure your exporter is configured before you start instrumenting code. Follow Getting started with OpenTelemetry Ruby or set up Direct OTLP Configuration first.
If you are not familiar with logs terminology like structured logging or log-trace correlation, read the introduction to OpenTelemetry Logs first.
Overview
OpenTelemetry provides two approaches for collecting logs in Ruby:
- Logs SDK: Use the OpenTelemetry Logs SDK to emit structured logs directly to your observability backend.
- Span events: Use span events to attach log-like messages to traces (simpler but less flexible).
The Logs SDK approach is recommended for production applications as it provides better correlation and filtering capabilities.
OpenTelemetry Logs SDK
The OpenTelemetry Ruby Logs SDK allows you to emit structured logs that are automatically correlated with traces.
Basic configuration
require 'opentelemetry/sdk'
require 'opentelemetry-logs-sdk'
require 'opentelemetry/exporter/otlp_logs'
# Configure log exporter
log_exporter = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(
endpoint: 'https://api.uptrace.dev/v1/logs',
headers: { 'uptrace-dsn': ENV['UPTRACE_DSN'] },
compression: 'gzip'
)
# Attach batch processor
processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(log_exporter)
OpenTelemetry.logger_provider.add_log_record_processor(processor)
# Ensure logs are flushed on shutdown
at_exit { OpenTelemetry.logger_provider.shutdown }
Complete example
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'rubygems'
require 'bundler/setup'
require 'uptrace'
require 'opentelemetry-logs-sdk'
require 'opentelemetry/exporter/otlp_logs'
# Configure OpenTelemetry with Uptrace
Uptrace.configure_opentelemetry(dsn: '') do |c|
c.service_name = 'myservice'
c.service_version = '1.0.0'
end
# Configure log exporter
log_exporter = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(
endpoint: 'https://api.uptrace.dev/v1/logs',
headers: { 'uptrace-dsn': ENV['UPTRACE_DSN'] },
compression: 'gzip'
)
processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(log_exporter)
OpenTelemetry.logger_provider.add_log_record_processor(processor)
at_exit do
OpenTelemetry.tracer_provider.shutdown
OpenTelemetry.logger_provider.shutdown
end
# Create a logger
LOGGER = OpenTelemetry.logger_provider.logger(name: 'myservice', version: '1.0.0')
# Create a tracer
TRACER = OpenTelemetry.tracer_provider.tracer('myservice', '1.0.0')
# Helper for structured logging
def log_info(message, attrs = {})
LOGGER.on_emit(
timestamp: Time.now.utc,
severity_text: 'INFO',
body: message,
attributes: attrs
)
end
def log_error(message, attrs = {})
LOGGER.on_emit(
timestamp: Time.now.utc,
severity_text: 'ERROR',
body: message,
attributes: attrs
)
end
# Start a traced operation with logs
TRACER.in_span('process-request', kind: :server) do |span|
# Logs are automatically correlated with the active trace
log_info('Processing request started', {
'user.id' => '12345',
'request.action' => 'login'
})
# Simulate some work
sleep(0.1)
log_info('Request processed successfully', {
'duration_ms' => 100
})
puts "Trace URL: #{Uptrace.trace_url(span)}"
end
See the GitHub example for a complete working example.
Log-trace correlation
When you emit a log within an active trace span, OpenTelemetry can automatically include:
- trace_id: Links log to the entire distributed trace
- span_id: Links log to the specific operation
- trace_flags: Indicates if the trace is sampled
This enables bidirectional navigation between logs and traces in your observability backend.
Manual correlation
If you need manual correlation, extract trace context from the current span:
def log_with_context(message, level: 'INFO', attrs: {})
current_span = OpenTelemetry::Trace.current_span
log_attrs = attrs.dup
if current_span&.context&.valid?
context = current_span.context
log_attrs['trace_id'] = context.hex_trace_id
log_attrs['span_id'] = context.hex_span_id
log_attrs['trace_flags'] = context.trace_flags.sampled? ? '01' : '00'
end
LOGGER.on_emit(
timestamp: Time.now.utc,
severity_text: level,
body: message,
attributes: log_attrs
)
end
# Usage within a span
TRACER.in_span('my-operation') do |span|
log_with_context('Operation started', attrs: { 'operation' => 'process' })
# ... do work ...
log_with_context('Operation completed')
end
Using span events for logs
For simpler use cases, you can use span events as log-like messages:
TRACER.in_span('process-order') do |span|
# Log-like event with attributes
span.add_event('log', attributes: {
'log.severity' => 'INFO',
'log.message' => 'Order processing started',
'order.id' => order_id
})
begin
process_order(order_id)
span.add_event('log', attributes: {
'log.severity' => 'INFO',
'log.message' => 'Order processed successfully'
})
rescue StandardError => e
span.add_event('log', attributes: {
'log.severity' => 'ERROR',
'log.message' => "Order processing failed: #{e.message}",
'error.type' => e.class.name
})
raise
end
end
Rails integration
For Rails applications, configure logging in an initializer:
# config/initializers/opentelemetry_logs.rb
require 'opentelemetry-logs-sdk'
require 'opentelemetry/exporter/otlp_logs'
Rails.application.config.after_initialize do
next unless ENV['UPTRACE_DSN']
log_exporter = OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(
endpoint: 'https://api.uptrace.dev/v1/logs',
headers: { 'uptrace-dsn': ENV['UPTRACE_DSN'] },
compression: 'gzip'
)
processor = OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(log_exporter)
OpenTelemetry.logger_provider.add_log_record_processor(processor)
end
at_exit { OpenTelemetry.logger_provider.shutdown }
Custom Rails logger with OTel
Create a custom logger that sends logs to both Rails and OpenTelemetry:
class OtelLogger < ActiveSupport::Logger
def initialize(*args)
super
@otel_logger = OpenTelemetry.logger_provider.logger(
name: Rails.application.class.module_parent_name.downcase,
version: ENV['APP_VERSION'] || '1.0.0'
)
end
def add(severity, message = nil, progname = nil, &block)
super
severity_text = case severity
when Logger::DEBUG then 'DEBUG'
when Logger::INFO then 'INFO'
when Logger::WARN then 'WARN'
when Logger::ERROR then 'ERROR'
when Logger::FATAL then 'FATAL'
else 'UNKNOWN'
end
log_message = message || (block_given? ? yield : progname)
@otel_logger.on_emit(
timestamp: Time.now.utc,
severity_text: severity_text,
body: log_message.to_s,
attributes: extract_trace_context
)
end
private
def extract_trace_context
attrs = {}
current_span = OpenTelemetry::Trace.current_span
if current_span&.context&.valid?
context = current_span.context
attrs['trace_id'] = context.hex_trace_id
attrs['span_id'] = context.hex_span_id
end
attrs
end
end
# config/environments/production.rb
Rails.application.configure do
config.logger = OtelLogger.new(STDOUT)
end
Best practices
Use structured attributes
Use key-value attributes for structured logging to enable filtering:
# Good: Structured attributes
log_info('Database query executed', {
'db.system' => 'postgresql',
'db.operation' => 'SELECT',
'db.table' => 'users',
'duration_ms' => 45,
'rows_returned' => 10
})
# Bad: Unstructured message
log_info("SELECT query on users table took 45ms and returned 10 rows")
Use appropriate severity levels
# DEBUG: Detailed information for debugging
log_debug('Cache lookup', { 'key' => cache_key, 'hit' => cache_hit })
# INFO: General operational information
log_info('Request processed', { 'status' => 200, 'path' => '/api/users' })
# WARN: Potentially harmful situations
log_warn('Rate limit approaching', { 'current' => 95, 'limit' => 100 })
# ERROR: Error events that might still allow the application to continue
log_error('Database connection failed', { 'retry_count' => 3 })
Avoid logging sensitive data
Never log passwords, tokens, or PII:
# Bad: Logging sensitive data
log_info('User login', { 'password' => password, 'email' => email })
# Good: Redact sensitive fields
log_info('User login', { 'user_id' => user_id, 'login_method' => 'password' })
Include contextual information
Add relevant context to help with debugging:
def process_payment(order_id, amount)
TRACER.in_span('process-payment') do |span|
log_info('Payment processing started', {
'order.id' => order_id,
'payment.amount' => amount,
'payment.currency' => 'USD'
})
begin
result = payment_gateway.charge(amount)
log_info('Payment successful', {
'order.id' => order_id,
'transaction.id' => result.transaction_id
})
result
rescue PaymentError => e
log_error('Payment failed', {
'order.id' => order_id,
'error.code' => e.code,
'error.message' => e.message
})
raise
end
end
end