# OpenTelemetry Logs for Ruby

> Collect Ruby application logs with OpenTelemetry and send structured logs to Uptrace with trace correlation.

![undefined](/devicon/ruby-original.svg)<partial path="otel-prereq-ruby">



</partial>

If you are not familiar with logs terminology like structured logging or log-trace correlation, read the introduction to [OpenTelemetry Logs](/opentelemetry/logs) first.

## Overview

OpenTelemetry provides two approaches for collecting logs in Ruby:

1. **Logs SDK**: Use the OpenTelemetry Logs SDK to emit structured logs directly to your observability backend.
2. **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

```ruby
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

```ruby
#!/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](https://github.com/uptrace/uptrace-ruby/tree/master/example/otlp-logs) 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:

```ruby
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:

```ruby
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:

```ruby
# 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:

```ruby
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:

```ruby
# 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

```ruby
# 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:

```ruby
# 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:

```ruby
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
```

## What's next?

- [Get started](/get/opentelemetry-ruby)
- [Learn about OpenTelemetry Ruby Tracing API](/get/opentelemetry-ruby/tracing)
- [Learn about OpenTelemetry Ruby Metrics API](/get/opentelemetry-ruby/metrics)
- [Learn about OpenTelemetry Ruby Resource detectors](/get/opentelemetry-ruby/resources)
