Observability for Ruby with OpenTelemetry

Khimananda Oli 7 min read Programming and Languages
Observability for Ruby with OpenTelemetry

By Khimananda Oli | Last reviewed: August 2026

Ruby applications often become opaque black boxes once they leave localhost, making debugging latency spikes or silent failures nearly impossible without proper tooling. Implementing observability for Ruby with OpenTelemetry solves this by standardizing how your application emits traces, metrics, and logs across any backend. This guide walks you through a production-grade setup that captures meaningful signal without degrading request performance or bloating your dependency tree.

Ruby ApplicationRails / Sinatra AppOTel SDK + APIAuto-InstrumentationOTLP / gRPCCollector (Optional)Batch ProcessorFilter / Redact PIIMulti-Backend FanoutObservability BackendTempo / Jaeger (Traces)Prometheus (Metrics)Loki / ELK (Logs)
High-level architecture for observability for Ruby with OpenTelemetry showing data flow from application through optional collector to backends

How do you configure observability for Ruby with OpenTelemetry in Rails?

The fastest path to observability for Ruby with OpenTelemetry in a Rails application relies on the official auto-instrumentation gems. These libraries hook into Rack, ActiveRecord, ActionMailer, and Sidekiq without requiring you to modify controller code. Before starting, ensure you understand the broader context by reading OpenTelemetry: the observability standard, which explains why vendor-neutral instrumentation matters for long-term maintainability.

Install the required gems

Add the core SDK and the specific instrumentations your stack requires to your Gemfile. Avoid installing every available instrumentation gem; each adds memory overhead and potential conflict surfaces.

# Gemfile
gem 'opentelemetry-sdk', '~> 1.5'
gem 'opentelemetry-exporter-otlp', '~> 0.29'
gem 'opentelemetry-instrumentation-rails', '~> 0.35'
gem 'opentelemetry-instrumentation-active_record', '~> 0.8'
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25' if defined?(Sidekiq)

Initialize the SDK safely

Create a dedicated initializer rather than embedding configuration in application.rb. This keeps telemetry concerns isolated and makes it trivial to disable in test environments. The following configuration uses environment variables for all sensitive values, preventing hardcoded endpoints from leaking into version control.

# config/initializers/opentelemetry.rb
return if Rails.env.test?

require 'opentelemetry/sdk'
require 'opentelemetry/exporter/otlp'
require 'opentelemetry/instrumentation/all'

OpenTelemetry::SDK.configure do |c|
  c.service_name = ENV.fetch('OTEL_SERVICE_NAME', 'my-rails-app')
  c.service_version = ENV.fetch('APP_VERSION', 'unknown')

  # Use batch processor in production to reduce network calls
  c.use_all(
    enabled_instrumentations: %w[
      OpenTelemetry::Instrumentation::Rack
      OpenTelemetry::Instrumentation::Rails
      OpenTelemetry::Instrumentation::ActiveRecord
      OpenTelemetry::Instrumentation::Sidekiq
    ]
  )

  # Explicit OTLP exporter with timeout safeguards
  c.add_span_processor(
    OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
      OpenTelemetry::Exporter::OTLP::Exporter.new(
        endpoint: ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', 'http://localhost:4318'),
        headers: { 'Authorization' => ENV['OTEL_AUTH_HEADER'] }.compact
      ),
      max_queue_size: 2048,
      schedule_delay: 5,
      export_timeout: 10
    )
  )
end

A common mistake in 2026 is still using the synchronous SimpleSpanProcessor in production. This blocks the request thread on every span export and will destroy your p99 latency under load. Always use BatchSpanProcessor outside of local development.

When should you add manual instrumentation to Ruby traces?

Auto-instrumentation covers framework boundaries, but it cannot understand your business logic. You need manual spans when debugging requires visibility inside service objects, external API wrappers, or complex query builders. Refer to instrument an app with OpenTelemetry for language-agnostic principles before applying these Ruby-specific patterns.

Wrapping service objects and business logic

Use the tracer from the global provider to create child spans. Always set attributes that aid filtering later; a span named "process_payment" is useless without knowing the payment gateway or amount tier.

class PaymentService
  TRACER = OpenTelemetry.tracer_provider.tracer('payment-service', '1.0.0')

  def call(order)
    TRACER.in_span('payment.process', attributes: {
      'payment.gateway' => order.gateway,
      'payment.amount_tier' => order.amount > 1000 ? 'high' : 'standard',
      'order.id' => order.id
    }) do |span|
      result = gateway.charge(order)
      span.set_attribute('payment.success', result.success?)

      unless result.success?
        span.status = OpenTelemetry::Trace::Status.error(result.error_message)
        span.record_exception(result.exception)
      end

      result
    end
  end
end

Propagating context across background jobs

If you use Sidekiq or Resque, verify that trace context propagates correctly. The Sidekiq instrumentation handles this automatically, but custom job wrappers or middleware can break the chain. Test this explicitly by triggering a job from a traced HTTP request and confirming the trace ID matches in your backend. Missing propagation is the single most frequent cause of "orphaned" spans in Ruby microservices.

HTTP ClientRails ServerRedis QueueSidekiq WorkerPOST /orders (trace-id: abc123)OrdersController#createOrder.create (AR)ENQUEUE (trace-id: abc123)DEQUEUE (trace-id: abc123)OrderConfirmationJobActionMailer.deliver
Trace context propagation sequence from HTTP request through Rails to Sidekiq preserving the same trace-id across async boundaries

Which OpenTelemetry exporter should Ruby applications use in production?

Choosing the right exporter determines whether your telemetry survives traffic spikes and network hiccups. While the console exporter is invaluable during development, production systems require a resilient transport layer.

ExporterProtocolBest ForProduction Safe?Key Trade-off
OTLP/gRPCBinary protobufHigh-throughput services, collector-directYesRequires gRPC-compatible LB; harder to debug raw payloads
OTLP/HTTPJSON/protobuf over HTTPSServerless, edge, corporate proxiesYesSlightly higher payload size vs gRPC; universally supported
ConsoleSTDOUTLocal dev, CI smoke testsNoBlocks I/O; pollutes logs; never enable in prod
Jaeger ThriftThrift over UDP/HTTPLegacy Jaeger-only shopsDeprecatedMigrate to OTLP; no new features post-2024

In practice, OTLP/HTTP is the safest default for Ruby applications in 2026. It traverses corporate firewalls and reverse proxies that strip gRPC headers, and the Ruby OTLP HTTP exporter has matured significantly. Only choose gRPC if you have verified your entire network path supports HTTP/2 and you are sending more than 10,000 spans per second.

How do you avoid performance pitfalls with Ruby OpenTelemetry?

Instrumentation is not free. I have seen Ruby applications lose 15% throughput after enabling OpenTelemetry due to misconfiguration. Follow these guardrails to keep overhead below 2%.

  • Always sample in high-traffic services. Head-based sampling at 10–20% is sufficient for most web workloads. Use OpenTelemetry::SDK::Trace::Samplers::TraceIdRatioBased.new(0.1) in your SDK configuration. Parent-based sampling ensures you either capture the full trace or none of it, avoiding broken partial traces.
  • Set explicit queue limits. The default batch processor queue can grow unbounded during backend outages. Cap max_queue_size at 2048–4096 and accept dropped spans over OOM kills. Dropped spans are recoverable via metrics; crashed pods are not.
  • Redact sensitive attributes before export. Never rely on backend-side filtering for PII. Use attribute processors in the SDK or Collector to mask emails, tokens, and payment data at the source. This is non-negotiable for SOC 2 or ISO 27001 compliance.
  • Disable unused instrumentations. Each loaded instrumentation hooks into Ruby's method dispatch. If you do not use Redis, Elasticsearch, or Faraday, do not load their gems. Audit your enabled_instrumentations list quarterly.
  • Monitor the SDK itself. Export otel.sdk.exported.spans and otel.sdk.dropped.spans as Prometheus metrics. A rising drop rate signals queue saturation or backend issues before users complain. See Prometheus metrics monitoring fundamentals for setting up these alerts.
✓ Production-Safe ConfigurationBatchSpanProcessor (queue: 2048, delay: 5s)ParentBased + TraceIdRatio (10%) SamplerOTLP/HTTP Exporter with 10s TimeoutAttribute Processor: Redact PII at SourceSDK Metrics: Exported + Dropped Span CountersOnly Required Instrumentations Loaded✗ Common Anti-PatternsSimpleSpanProcessor (blocks request thread)AlwaysOn Sampler (100% traffic exported)Console Exporter Enabled in ProductionRaw User Email / Token in Span AttributesNo SDK Self-Monitoring (blind to drops)All Instrumentation Gems Loaded by Default
Side-by-side comparison of production-safe versus risky OpenTelemetry configurations for Ruby applications

Start shipping reliable Ruby telemetry today

Implementing observability for Ruby with OpenTelemetry is a one-time investment that pays dividends across every future incident, capacity plan, and compliance audit. Start with auto-instrumentation and OTLP/HTTP, add manual spans only where business logic demands it, and enforce sampling and redaction from day one. Your future self debugging a 3 AM latency spike will thank you. If your team needs help designing a compliant, performant telemetry pipeline for Ruby or any other stack, reach out to discuss your observability architecture.

Frequently Asked Questions

Add opentelemetry-sdk and opentelemetry-exporter-otlp to your Gemfile, then run bundle install. Configure the SDK in an initializer with your endpoint and service name before application boot.

Yes. The opentelemetry-instrumentation-rails gem automatically captures requests, Active Record queries, and background jobs without manual code changes when loaded via Bundler.require.

Typically under two percent CPU overhead in production. Sampling reduces this further by exporting only a fraction of traces while maintaining statistical accuracy for latency analysis.

Yes. Use the OTLP exporter with vendor-specific endpoints. Both platforms accept standard OpenTelemetry protocol over HTTP or gRPC without proprietary agents in 2026.

Call OpenTelemetry::Trace.current_span.set_attribute inside any method. Use semantic conventions for keys like user.id or order.total to ensure backend compatibility.

Yes, if you exclude PHI from span attributes. Configure attribute filters in the SDK to redact sensitive fields before export and use encrypted OTLP endpoints.

Ensure opentelemetry-instrumentation-active_record is loaded after ActiveRecord initializes. Verify the gem version matches your Rails release and check logs for instrumentation warnings.

The W3C Trace Context header propagates automatically via Net::HTTP and Faraday instrumentations. Ensure all services use compatible OpenTelemetry versions to maintain trace continuity.

Use parent-based trace ID ratio sampling at one percent for production. This preserves complete traces for sampled requests while dropping unsampled ones early to reduce memory pressure.

Yes. Inject trace_id and span_id into your logger context using the opentelemetry-instrumentation-logging gem. Configure your log aggregator to parse these fields for linking.

Enable OTEL_LOG_LEVEL=debug temporarily. Check for uninitialized instrumentations, mismatched gem versions, or exporter connection failures in startup logs.

It replaces the agent layer but not the backend. You still need a visualization platform like Grafana Tempo or Honeycomb to query and display the exported telemetry data.

Ruby 3.2 or later is recommended. Older versions lack fiber scheduler hooks needed for async context propagation and may cause incorrect span nesting.

Define allowlists for dynamic attributes like user IDs or URLs. Unbounded values cause backend indexing costs to spike and degrade query performance significantly.

Yes. Pass enabled_instrumentations array to the SDK configuration listing only desired gems. This prevents unnecessary overhead from unused libraries like Redis or Sidekiq.