Ruby

Instrumentação Ruby on Rails com Elven Observability


Sumário

  • Visão geral
  • Pré-requisitos
  • Exemplo de referência
  • Configuração de variáveis de ambiente
  • Bootstrap OpenTelemetry
  • Instrumentações automáticas
  • Span manual no controller
  • Spans manuais por etapa no service object
  • Chamada HTTP de saída
  • Captura de erros em spans
  • Estrutura de código relevante
  • Como rodar localmente
  • Como validar no collector da Elven
  • Convenções recomendadas para spans manuais
  • Como adaptar para a sua aplicação
  • Testes
  • Troubleshooting
  • Checklist de deploy

Visão geral

A instrumentação Ruby on Rails da Elven usa o SDK oficial do OpenTelemetry para Ruby com exportação OTLP HTTP. Ela cobre instrumentação automática de HTTP server (Rails), banco de dados (ActiveRecord) e HTTP client (Net::HTTP), além de instrumentação manual de spans por etapa de negócio.

O Collector OTLP fica sempre no ambiente do cliente. A aplicação envia traces para esse Collector local/do cliente, e o Collector encaminha os dados para Tempo conforme a arquitetura contratada.

  • Grafana Tempo para traces
  • Grafana para consulta, correlação, painéis e alertas

Esta integração cobre somente traces. Métricas e logs ficam fora do escopo desta instrumentação.

Componente O que faz
opentelemetry-api API pública do OpenTelemetry para Ruby.
opentelemetry-sdk SDK completo com TracerProvider e BatchSpanProcessor.
opentelemetry-exporter-otlp Exporter OTLP HTTP para envio de traces ao collector.
opentelemetry-instrumentation-rails Instrumentação automática do Rails (HTTP server, ActiveRecord).
opentelemetry-instrumentation-net_http Instrumentação automática de chamadas HTTP de saída via Net::HTTP.

Pré-requisitos

  • Ruby 3.3.x instalado
  • Rails 8.1.2
  • Docker Desktop ou Docker Engine funcionando
  • Acesso de rede ao collector da Elven
  • Collector OTLP HTTP aceitando POST /v1/traces

Baseline técnico

Item Versão
Ruby 3.3.x
Rails 8.1.2
pg 1.6.3
opentelemetry-api 1.7.0
opentelemetry-sdk 1.10.0
opentelemetry-exporter-otlp 0.31.1
opentelemetry-instrumentation-rails 0.39.1
opentelemetry-instrumentation-net_http 0.27.0

Checklist do collector

Antes de subir a aplicação, confirme:

  1. O endpoint de traces está correto
  2. O header de autenticação está definido (se necessário)
  3. O collector aceita http/protobuf

Atenção: se o collector exigir token, o valor em OTEL_EXPORTER_OTLP_HEADERS deve estar URL-encoded. Exemplo:

OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20SEU_TOKEN


Exemplo de referência

A Elven disponibiliza uma aplicação demo completa em Ruby on Rails que demonstra todos os conceitos desta documentação:

👉 elven-observability/ruby-otel-app


Configuração de variáveis de ambiente

Copie o arquivo de exemplo e preencha com os dados do seu collector:

cp .env.example .env

Configuração mínima funcional:

OTEL_SERVICE_NAME=ruby-rails-traces-demo
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local,service.version=1.0.0
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=none
OTEL_LOGS_EXPORTER=none
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://SEU-COLLECTOR:4318/v1/traces
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20SEU_TOKEN

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
DATABASE_URL=postgres://postgres:postgres@localhost:5432/ruby_otel_app_development
DATABASE_TEST_URL=postgres://postgres:postgres@localhost:5432/ruby_otel_app_test

Variáveis de identidade do serviço

Variável Descrição Default
OTEL_SERVICE_NAME Nome do serviço. Deve ser estável e único por aplicação. ruby-rails-traces-demo
OTEL_RESOURCE_ATTRIBUTES Atributos extras no formato key=value,key2=value2.
OTEL_DEPLOYMENT_ENVIRONMENT Ambiente: local, staging, production. Lido diretamente no initializer. Rails.env

Variáveis de export OTLP

Variável Descrição Default
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT Endpoint específico de traces. http://localhost:4318/v1/traces
OTEL_EXPORTER_OTLP_HEADERS Headers em key=value. Para tokens, use URL-encoding.

Variáveis de sinais

Variável Descrição Default
OTEL_TRACES_EXPORTER Exporter de traces: otlp ou none. otlp
OTEL_METRICS_EXPORTER Desabilitado nesta integração. none
OTEL_LOGS_EXPORTER Desabilitado nesta integração. none

Variáveis de banco

Variável Descrição
POSTGRES_HOST Host do PostgreSQL.
POSTGRES_PORT Porta do PostgreSQL. Default: 5432.
POSTGRES_USER Usuário do banco.
POSTGRES_PASSWORD Senha do banco.
DATABASE_URL URL completa de conexão para desenvolvimento.
DATABASE_TEST_URL URL completa de conexão para testes.

Bootstrap OpenTelemetry

O initializer do OpenTelemetry deve ser criado em config/initializers/opentelemetry.rb. Ele é responsável por configurar o SDK, o exporter OTLP, o BatchSpanProcessor e ativar as instrumentações automáticas.

# config/initializers/opentelemetry.rb
require "opentelemetry/sdk"
require "opentelemetry/exporter/otlp"
require "opentelemetry/instrumentation/rails"
require "opentelemetry/instrumentation/net/http"

otel_headers = ENV["OTEL_EXPORTER_OTLP_HEADERS"]
otel_headers = {} if otel_headers.nil? || otel_headers.strip.empty?

exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(
  endpoint: ENV.fetch("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "http://localhost:4318/v1/traces"),
  headers: otel_headers
)

resource = OpenTelemetry::SDK::Resources::Resource.create(
  "deployment.environment" => ENV.fetch("OTEL_DEPLOYMENT_ENVIRONMENT", Rails.env.to_s)
)

OpenTelemetry::SDK.configure do |c|
  c.service_name = ENV.fetch("OTEL_SERVICE_NAME", "ruby-rails-traces-demo")
  c.resource = resource
  c.add_span_processor(OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(exporter))

  c.use "OpenTelemetry::Instrumentation::Rails"
  c.use "OpenTelemetry::Instrumentation::Net::HTTP"
end

Com esse setup:

  • O SDK inicializa antes do primeiro request Rails
  • O BatchSpanProcessor envia spans em lote ao collector
  • As instrumentações automáticas de Rails e Net::HTTP são ativadas explicitamente

Instrumentações automáticas

Com o initializer configurado, as seguintes instrumentações passam a funcionar sem nenhuma alteração no código da aplicação:

Instrumentação O que gera
OpenTelemetry::Instrumentation::Rails Span inbound HTTP server para cada request Rails. Span de banco (ActiveRecord) para cada query.
OpenTelemetry::Instrumentation::Net::HTTP Span outbound HTTP client para cada chamada via Net::HTTP.

Para adicionar outras instrumentações disponíveis no opentelemetry-ruby-contrib, basta incluir a gem correspondente e ativar com c.use no initializer. Exemplos:

c.use "OpenTelemetry::Instrumentation::Faraday"
c.use "OpenTelemetry::Instrumentation::Sidekiq"
c.use "OpenTelemetry::Instrumentation::Redis"

Span manual no controller

Defina um tracer por arquivo e envolva o bloco de negócio principal com in_span. Sempre capture o trace_id para retornar na resposta.

# app/controllers/checkout_controller.rb
TRACER = OpenTelemetry.tracer_provider.tracer("checkout-controller", "1.0.0")

def create
  TRACER.in_span("checkout.request") do |span|
    order = CheckoutService.new(
      customer_id: checkout_params.fetch(:customer_id),
      amount_cents: checkout_params.fetch(:amount_cents),
      force_error: checkout_params.fetch(:force_error),
      internal_url: internal_ping_url
    ).call

    render json: {
      order_id: order.id,
      status: order.status,
      trace_id: current_trace_id
    }, status: :ok
  rescue ActionController::ParameterMissing => e
    span.record_exception(e)
    span.status = OpenTelemetry::Trace::Status.error(e.message)
    render json: { error: e.message, trace_id: current_trace_id }, status: :unprocessable_entity
  rescue CheckoutService::CheckoutError => e
    span.record_exception(e)
    span.status = OpenTelemetry::Trace::Status.error(e.message)
    render json: { error: e.message, trace_id: current_trace_id }, status: :internal_server_error
  end
end

O trace_id retornado na resposta é o caminho mais rápido para localizar o trace completo no backend da Elven.


Spans manuais por etapa no service object

Crie um helper with_span no service object para centralizar a criação de spans e o tratamento de erros:

# app/services/checkout_service.rb
TRACER = OpenTelemetry.tracer_provider.tracer("checkout-service", "1.0.0")

def call
  valid_amount = with_span("checkout.validate_input") { validate_input! }
  order        = with_span("checkout.persist_order")  { persist_order!(valid_amount) }
                 with_span("checkout.call_internal_service") { call_internal_service! }
                 with_span("checkout.finalize") { finalize_order!(order) }
  order
end

def with_span(name, attributes = {})
  TRACER.in_span(name, attributes: attributes) do |span|
    yield
  rescue StandardError => e
    span.record_exception(e)
    span.status = OpenTelemetry::Trace::Status.error(e.message)
    raise
  end
end

Exemplos de spans manuais recomendados:

  • checkout.validate_input
  • checkout.persist_order
  • checkout.call_internal_service
  • checkout.finalize

Chamada HTTP de saída

Use Net::HTTP normalmente. Com a instrumentação Net::HTTP ativa no initializer, cada chamada gera automaticamente um span outbound correlacionado ao trace da request de origem:

def call_internal_service!
  uri = URI.parse(@internal_url)
  response = Net::HTTP.get_response(uri)
  return response if response.code.to_i.between?(200, 299)

  raise CheckoutError, "internal ping failed with status #{response.code}"
end

O que isso gera automaticamente:

  • Span outbound HTTP com método, URL e status code
  • Correlação automática com o span pai via propagação de contexto

Captura de erros em spans

Em qualquer bloco de span, registre exceções e defina o status de erro explicitamente:

span.record_exception(e)
span.status = OpenTelemetry::Trace::Status.error(e.message)

Isso garante que:

  • A exceção aparece como evento dentro do span no Tempo
  • O span é marcado com status ERROR, visível na árvore de traces
  • O trace_id da resposta pode ser usado para localizar o trace com erro

Estrutura de código relevante

ruby-otel-app/
├── config/
│   └── initializers/
│       └── opentelemetry.rb       # Bootstrap do SDK, exporter e instrumentações
├── app/
│   ├── controllers/
│   │   ├── checkout_controller.rb # Span manual checkout.request + captura de erro
│   │   ├── health_controller.rb   # GET /health
│   │   └── internal_controller.rb # GET /internal/ping
│   ├── services/
│   │   └── checkout_service.rb    # Spans manuais por etapa de negócio
│   └── models/
│       └── order.rb               # Persistência — gera spans de DB automaticamente
├── db/
│   └── migrate/
│       └── *_create_orders.rb
├── config/
│   └── routes.rb
├── Gemfile
├── docker-compose.yml
└── .env.example

Como rodar localmente

1. Preparar variáveis

cp .env.example .env
# Edite o .env e preencha os endpoints do collector

2. Subir PostgreSQL

docker compose up -d

Se a porta 5432 já estiver em uso:

POSTGRES_PORT=5433 docker compose up -d

3. Instalar dependências

bundle install

4. Exportar variáveis do .env

set -a
source .env
set +a

5. Preparar o banco

bin/rails db:prepare

6. Subir a aplicação

bin/rails s

7. Validar saúde

curl -s http://localhost:3000/health

8. Gerar traces de demonstração

Fluxo saudável:

curl -s -X POST http://localhost:3000/checkout <br>
  -H "Content-Type: application/json" <br>
  -d '{"customer_id":"cust-001","amount_cents":1500}'

Fluxo com erro forçado:

curl -s -X POST http://localhost:3000/checkout <br>
  -H "Content-Type: application/json" <br>
  -d '{"customer_id":"cust-002","amount_cents":2500,"force_error":true}'

Resposta esperada (fluxo saudável):

{
  "order_id": "a1b2c3d4-...",
  "status": "completed",
  "trace_id": "2b1f3a7b5c9049e18d3cba0f5a1f9c31"
}

Use o trace_id retornado para localizar o trace completo no backend da Elven.


Como validar no collector da Elven

Após chamar o endpoint, valide no backend nesta ordem:

  1. Filtre por service.name = ruby-rails-traces-demo (ou o nome configurado em OTEL_SERVICE_NAME)
  2. Pesquise pelo trace_id retornado pela API
  3. Confirme a árvore de spans
  4. Confirme o status de erro nos fluxos com force_error=true

Árvore de spans esperada (fluxo saudável)

  1. Span inbound Rails POST /checkout
  2. Span manual checkout.request
  3. Span manual checkout.validate_input
  4. Span manual checkout.persist_order
  5. Span ActiveRecord do INSERT em orders
  6. Span manual checkout.call_internal_service
  7. Span Net::HTTP outbound para /internal/ping
  8. Span manual checkout.finalize

No fluxo com erro

  • Span checkout.finalize com status ERROR
  • Exceção registrada como evento dentro do span
  • Mesmo trace_id disponível na resposta da API

Convenções recomendadas para spans manuais

  1. Nomeie spans com domínio e verbo no formato dominio.acao — ex.: checkout.persist_order, payment.authorize
  2. Evite spans excessivos: instrumente apenas etapas que agregam diagnóstico real
  3. Adicione atributos úteis para investigação, sem expor dados sensíveis
  4. Não coloque PII (CPF, e-mail, token, senha) em atributos de span
  5. Em blocos de erro, sempre use record_exception + status=error
  6. Retorne trace_id em respostas de erro e sucesso para acelerar troubleshooting

Como adaptar para a sua aplicação

Se a aplicação já é Rails

  1. Adicione as gems ao Gemfile e rode bundle install
  2. Crie o initializer em config/initializers/opentelemetry.rb
  3. Ative as instrumentações da sua stack com c.use
  4. Defina as variáveis de ambiente do collector
  5. Envolva etapas críticas de negócio em spans manuais com TRACER.in_span

Se a aplicação usa outros HTTP clients

Substitua opentelemetry-instrumentation-net_http pela gem correspondente:

# Faraday
gem "opentelemetry-instrumentation-faraday"
c.use "OpenTelemetry::Instrumentation::Faraday"

# HTTPrb
gem "opentelemetry-instrumentation-http"
c.use "OpenTelemetry::Instrumentation::HTTP"

Se a aplicação usa workers assíncronos

# Sidekiq
gem "opentelemetry-instrumentation-sidekiq"
c.use "OpenTelemetry::Instrumentation::Sidekiq"

Se a aplicação usa Redis

gem "opentelemetry-instrumentation-redis"
c.use "OpenTelemetry::Instrumentation::Redis"

Testes

bin/rails test

Cobrem:

  • Health check (GET /health)
  • Endpoint interno (GET /internal/ping)
  • Checkout com sucesso (persistência + chamada HTTP de saída)
  • Checkout com erro forçado (500 + trace_id na resposta)

Troubleshooting

Não aparecem traces no collector

  1. Confirme OTEL_EXPORTER_OTLP_TRACES_ENDPOINT está correto e acessível
  2. Confirme que OTEL_EXPORTER_OTLP_HEADERS contém o token correto com URL-encoding
  3. Confirme OTEL_TRACES_EXPORTER=otlp
  4. Verifique os logs da app ao subir — as instrumentações devem aparecer como instaladas
  5. Gere uma requisição nova e procure pelo trace_id retornado no JSON

O initializer não é carregado

Confirme que o arquivo está em config/initializers/opentelemetry.rb. O Rails carrega todos os arquivos desse diretório automaticamente no boot.

Spans de banco não aparecem

Confirme que OpenTelemetry::Instrumentation::Rails está ativado no initializer. A instrumentação do Rails já inclui ActiveRecord automaticamente.

Spans de HTTP client não aparecem

Confirme que OpenTelemetry::Instrumentation::Net::HTTP está ativado no initializer e que a chamada HTTP usa Net::HTTP (ou a gem instrumentada correspondente).

Token com caracteres especiais não funciona

Use URL-encoding no valor do header:

OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20SEU_TOKEN

Referências


Checklist de deploy

  • [ ] Ruby 3.3.x instalado no ambiente de build
  • [ ] Gems opentelemetry-api, opentelemetry-sdk, opentelemetry-exporter-otlp e instrumentações adicionadas ao Gemfile
  • [ ] Initializer criado em config/initializers/opentelemetry.rb
  • [ ] OTEL_SERVICE_NAME definido
  • [ ] OTEL_RESOURCE_ATTRIBUTES com deployment.environment e service.version
  • [ ] OTEL_EXPORTER_OTLP_TRACES_ENDPOINT apontando para o collector correto
  • [ ] OTEL_EXPORTER_OTLP_HEADERS com token URL-encoded (se necessário)
  • [ ] OTEL_TRACES_EXPORTER=otlp
  • [ ] OTEL_METRICS_EXPORTER=none
  • [ ] OTEL_LOGS_EXPORTER=none
  • [ ] Instrumentações automáticas ativadas com c.use para toda a stack usada
  • [ ] Spans manuais criados para etapas críticas de negócio
  • [ ] record_exception e status=error usados em todos os blocos de rescue
  • [ ] trace_id retornado nas respostas da API
  • [ ] Traces aparecem no Tempo com service.name correto
  • [ ] Árvore de spans reflete o fluxo real da aplicação