# 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](https://github.com/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:

```bash
cp .env.example .env
```

Configuração mínima funcional:

```env
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.

```ruby
# 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](https://github.com/open-telemetry/opentelemetry-ruby-contrib), basta incluir a gem correspondente e ativar com `c.use` no initializer. Exemplos:

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

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

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

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

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

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

**2. Subir PostgreSQL**

```bash
docker compose up -d
```

Se a porta `5432` já estiver em uso:

```bash
POSTGRES_PORT=5433 docker compose up -d
```

**3. Instalar dependências**

```bash
bundle install
```

**4. Exportar variáveis do `.env`**

```bash
set -a
source .env
set +a
```

**5. Preparar o banco**

```bash
bin/rails db:prepare
```

**6. Subir a aplicação**

```bash
bin/rails s
```

**7. Validar saúde**

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

**8. Gerar traces de demonstração**

Fluxo saudável:

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

Fluxo com erro forçado:

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

Resposta esperada (fluxo saudável):

```json
{
  "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:

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

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

**Se a aplicação usa Redis**

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

***

#### Testes

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

```env
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20SEU_TOKEN
```

***

#### Referências

* [OpenTelemetry Ruby — Documentação oficial](https://opentelemetry.io/docs/languages/ruby/)
* [OpenTelemetry Ruby SDK](https://github.com/open-telemetry/opentelemetry-ruby)
* [OpenTelemetry Ruby Contrib](https://github.com/open-telemetry/opentelemetry-ruby-contrib)
* [ruby-otel-app — Repositório de referência](https://github.com/elven-observability/ruby-otel-app)

***

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.elven.works/elven-platform/elven-observability/integracao-e-instrumentacao/ruby.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
