Project

exis_ray

0.0
The project is in a healthy, maintained state
Gema que gestiona el contexto de request, logs y propagación de headers.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

 Project Readme

ExisRay

Capa de observabilidad y trazabilidad distribuida para microservicios Rails del ecosistema Wispro. Unifica tracing (AWS X-Ray compatible), logging JSON estructurado, propagación de contexto de negocio y reporte de errores en una sola gema.

Features

  • Distributed Tracing — Parseo, generación y propagación automática de headers X-Amzn-Trace-Id.
  • Structured JSON Logging — Logs HTTP, Sidekiq y Rake en formato JSON single-line con contexto inyectado.
  • Context Propagationuser_id, isp_id y correlation_id viajan automáticamente entre servicios.
  • Error Reporting — Wrapper de Sentry que enriquece cada evento con trace context e identidad de negocio.
  • Auto-instrumentación — HTTP, Sidekiq, ActiveResource y BugBunny se configuran automáticamente via Railtie.
  • Compatibilidad — Ruby >= 2.6, Rails 6, 7 y 8.

Cómo funciona

ExisRay opera en tres capas que se combinan automáticamente:

                          ┌─────────────────────────────────┐
                          │         App Host (Rails)         │
                          │  Current ← user_id, isp_id      │
                          │  Reporter ← Sentry context       │
                          └──────────────┬──────────────────┘
                                         │
              ┌──────────────────────────┼──────────────────────────┐
              │                          │                          │
     ┌────────▼────────┐      ┌─────────▼─────────┐     ┌─────────▼─────────┐
     │  HTTP Request    │      │  Sidekiq Job       │     │  BugBunny Msg     │
     │  HttpMiddleware  │      │  ServerMiddleware   │     │  ConsumerTracing  │
     └────────┬────────┘      └─────────┬─────────┘     └─────────┬─────────┘
              │                          │                          │
              └──────────────────────────┼──────────────────────────┘
                                         │
                                         ▼
                              ┌─────────────────────┐
                              │   ExisRay::Tracer    │
                              │  (CurrentAttributes) │
                              │                      │
                              │  root_id, trace_id,  │
                              │  source, self_id,    │
                              │  called_from,        │
                              │  total_time_so_far   │
                              └──────────┬──────────┘
                                         │
                         ┌───────────────┼───────────────┐
                         │               │               │
                         ▼               ▼               ▼
                   JsonFormatter    FaradayMiddleware  PublisherTracing
                   (logs + context) (HTTP saliente)   (RabbitMQ saliente)

Flujo de propagación:

  1. Un request/job/mensaje llega al servicio. El middleware correspondiente hidrata el Tracer con el header entrante (o genera un nuevo root_id si no trae uno).
  2. JsonFormatter inyecta automáticamente root_id, trace_id, source y el contexto de negocio (user_id, isp_id, correlation_id) en cada línea de log.
  3. Cuando el servicio llama a otro servicio (HTTP, Sidekiq, RabbitMQ), el middleware de salida genera un nuevo header con Tracer.generate_trace_header, que incluye el root_id original, el self_id del servicio actual, el CalledFrom y el tiempo acumulado.
  4. El servicio destino repite desde el paso 1. El root_id se mantiene constante a lo largo de toda la cadena.

Instalación

gem "exis_ray"

Quick Start

Configuración mínima para un servicio Rails nuevo:

# 1. Configurar ExisRay (config/initializers/exis_ray.rb)
ExisRay.configure do |config|
  config.log_format     = Rails.env.production? ? :json : :text
  config.current_class  = "Current"
  config.reporter_class = "Reporter"
end

# 2. Crear Current (app/models/current.rb)
class Current < ExisRay::Current
  # ExisRay provee: user_id, isp_id, correlation_id
  # Agregar atributos específicos de la app:
  attribute :permissions
end

# 3. Crear Reporter (app/models/reporter.rb)
class Reporter < ExisRay::Reporter
end

# 4. Hidratar el Current en cada request (app/controllers/application_controller.rb)
class ApplicationController < ActionController::Base
  before_action :set_context

  private

  def set_context
    Current.user = current_user if current_user
    Current.isp_id = request.headers["X-Isp-Id"]
  end
end

Con esto, ExisRay auto-instrumenta HTTP y Sidekiq (si está presente). Los logs en producción salen en JSON con trace context completo.

Configuración

# config/initializers/exis_ray.rb
ExisRay.configure do |config|
  # Header entrante (formato Rack). Default: "HTTP_X_AMZN_TRACE_ID"
  config.trace_header = "HTTP_X_AMZN_TRACE_ID"

  # Header saliente (formato HTTP). Default: "X-Amzn-Trace-Id"
  config.propagation_trace_header = "X-Amzn-Trace-Id"

  # Clases de la app host (strings para evitar problemas de autoloading)
  config.current_class  = "Current"    # hereda de ExisRay::Current
  config.reporter_class = "Reporter"   # hereda de ExisRay::Reporter

  # Formato de logs: :text (default) o :json
  config.log_format = Rails.env.production? ? :json : :text

  # Subclase de LogSubscriber para campos HTTP extra (opcional)
  # config.log_subscriber_class = "MyLogSubscriber"
end

Clases de la App Host

Current (contexto de negocio)

ExisRay::Current extiende ActiveSupport::CurrentAttributes y provee tres atributos base: user_id, isp_id y correlation_id. Al asignarlos, propaga automáticamente a PaperTrail (whodunnit), ActiveResource (headers) y Reporter (tags de Sentry).

# app/models/current.rb
class Current < ExisRay::Current
  # Atributos heredados: user_id, isp_id, correlation_id
  # Helpers heredados: user, user=, isp, isp=, user?, isp?, correlation_id?
  #
  # Agregar atributos específicos de la app:
  attribute :permissions
end

Helpers disponibles:

Método Descripción
Current.user = object Asigna user_id desde object.id
Current.user Lazy-loads ::User.find_by(id: user_id) (cacheado por request)
Current.isp = object Asigna isp_id desde object.id
Current.isp Lazy-loads ::Isp.find_by(id: isp_id) (cacheado por request)
Current.user? / Current.isp? Predicate: true si el ID no es nil
Current.correlation_id? Predicate: true si está presente (no vacío)

Reporter (reporte de errores)

ExisRay::Reporter es un wrapper de Sentry que enriquece automáticamente cada evento con el trace context del Tracer y el contexto de negocio del Current. Soporta Sentry SDK moderno y legacy (Raven/Session).

# app/models/reporter.rb
class Reporter < ExisRay::Reporter
  # Hook opcional para contexto adicional en Sentry
  def self.build_custom_context
    add_tags(plan: ExisRay.current_class.isp&.plan)
  end

  # Hook opcional para controlar qué datos del usuario se envían a Sentry.
  # Default: solo { id: current.user_id }
  def self.sentry_user_context(current)
    { id: current.user_id, email: current.user&.email }
  end
end

API pública:

# Reportar un mensaje
Reporter.report("algo inesperado", context: { order_id: 123 }, tags: { severity: "high" })

# Reportar una excepción
Reporter.exception(error, context: { order_id: 123 }, fingerprint: ["order-failure"])

# Acumular contexto durante el request (se envía con el próximo report/exception)
Reporter.add_context(order: { id: 123, total: 500 })
Reporter.add_tags(feature: "checkout")
Reporter.add_fingerprint("checkout-error")

Integraciones

HTTP (automático)

El Railtie inserta ExisRay::HttpMiddleware después de ActionDispatch::RequestId. Hidrata el Tracer con el header entrante y sincroniza el correlation_id en cada request. No requiere configuración.

Sidekiq (automático)

El Railtie registra client y server middleware. El trace context se propaga automáticamente entre enqueuer y worker. El JsonFormatter se aplica al logger de Sidekiq si json_logs? está activo. No requiere cambios en los workers.

BugBunny — Publisher (manual)

Agregar PublisherTracing al middleware stack del cliente:

# BugBunny::Client
client = BugBunny::Client.new(pool: pool) do |stack|
  stack.use ExisRay::BugBunny::PublisherTracing
end

# BugBunny::Resource (se hereda a subclases)
class ApplicationResource < BugBunny::Resource
  client_middleware do |stack|
    stack.use ExisRay::BugBunny::PublisherTracing
  end
end

BugBunny — Consumer (automático)

El Railtie registra ConsumerTracingMiddleware en el consumer middleware stack, más los hooks RPC (rpc_reply_headers y on_rpc_reply) para propagación completa en llamadas síncronas.

Faraday (manual)

conn = Faraday.new(url: "https://api.internal") do |f|
  f.use ExisRay::FaradayMiddleware
end

ActiveResource (automático)

El Railtie prepend ActiveResourceInstrumentation a ActiveResource::Base. Inyecta el propagation_trace_header en cada request saliente.

TaskMonitor (Rake/Cron)

Genera un contexto de trazabilidad para tareas que no tienen request HTTP entrante:

task generate_invoices: :environment do
  ExisRay::TaskMonitor.run("billing:generate_invoices") do
    InvoiceService.process_all
  end
end

TaskMonitor genera un root_id nuevo, configura Reporter y Current, loguea task_started/task_finished con duration_s y outcome, y limpia el contexto al finalizar. Si el bloque lanza una excepción, la registra como outcome=failed con exception.type, exception.message y exception.stacktrace (OTel) y la re-lanza.

JSON Logging

Con log_format: :json, ExisRay::JsonFormatter reemplaza el formatter de Rails y emite cada línea como JSON single-line con contexto inyectado automáticamente:

{"time":"2026-04-01T14:30:00Z","level":"INFO","severity_number":9,"service":"wispro_agent","service_version":"1.2.3","deployment_environment":"production","root_id":"1-65f...abc","trace_id":"Root=1-65f...;Self=...","source":"http","user_id":42,"isp_id":10,"component":"exis_ray","event":"http_request","method":"GET","path":"/api/v1/users","http_route":"/api/v1/users","http_status":200,"duration_s":0.0452,"user_agent_original":"Mozilla/5.0","server_address":"api.example.com"}

Los mensajes con formato key=value se parsean y elevan al root del JSON. Los valores numéricos se castean automáticamente:

Rails.logger.info "component=billing event=invoice_created invoice_id=123 total=45.50"
# => {"time":"...","level":"INFO","service":"...","root_id":"...","component":"billing","event":"invoice_created","invoice_id":123,"total":45.5}

Los mensajes tipo Hash (usados internamente por LogSubscriber) se mergean directamente. Los mensajes de texto libre se asignan a la clave body:

Rails.logger.info "Algo pasó sin formato KV"
# => {"time":"...","level":"INFO","service":"...","body":"Algo pasó sin formato KV"}

En modo :text, ExisRay inyecta el trace_id o root_id como tag de Rails (config.log_tags) y no modifica el formatter.

Campos auto-inyectados

JsonFormatter inyecta estos campos automáticamente en cada línea. Nunca los incluyas manualmente en tus logs:

Campo Condición
time Siempre (UTC ISO 8601)
level Siempre
severity_number Siempre (OTel SeverityNumber: DEBUG=5, INFO=9, WARN=13, ERROR=17, FATAL=21)
service Siempre (nombre de la app Rails en snake_case)
service_version Siempre (lee de config.version o config.x.version)
deployment_environment Siempre (lee de Rails.env)
root_id Cuando hay trace context activo
trace_id Cuando hay trace context activo
source Cuando hay trace context activo (http, sidekiq, task, system)
correlation_id Cuando Current.correlation_id está presente
user_id Cuando Current.user_id no es nil
isp_id Cuando Current.isp_id no es nil
sidekiq_job Solo en procesos Sidekiq
task Solo en procesos TaskMonitor
tags Solo si hay Rails tagged logging activo

LogSubscriber inyecta además estos campos en los logs de requests HTTP:

Campo Descripción
http_status Código HTTP (Integer). Antes status, renombrado en v0.6.0
http_route Template de ruta (ej: /users/:id). Resolución via route.defaults
user_agent_original Header User-Agent del request
server_address Hostname sin puerto del header Host
exception.type Clase de la excepción (cuando el request falla)
exception.message Mensaje de la excepción
exception.stacktrace Primeras 20 líneas del backtrace

Filtrado de claves sensibles

Las claves que matcheen /password|pass|passwd|secret|token|api_key|auth/i se reemplazan automáticamente por [FILTERED], tanto en strings KV como en Hashes (incluyendo anidados).

LogSubscriber custom

Para inyectar campos extra en los logs de requests HTTP, crear una subclase de ExisRay::LogSubscriber:

# app/subscribers/my_log_subscriber.rb
class MyLogSubscriber < ExisRay::LogSubscriber
  def self.extra_fields(event)
    { user_agent: event.payload[:headers]["HTTP_USER_AGENT"] }
  end
end

# config/initializers/exis_ray.rb
ExisRay.configure do |config|
  config.log_subscriber_class = "MyLogSubscriber"
end

Referencia del Tracer

ExisRay::Tracer es el componente central. Extiende ActiveSupport::CurrentAttributes para thread-safety y se resetea automáticamente al final de cada request/job.

Atributos

Atributo Tipo Descripción
trace_id String Header completo parseado (Root=...;Self=...;CalledFrom=...)
root_id String ID raíz, constante a lo largo de toda la cadena de servicios
self_id String ID del span del servicio que generó el header
called_from String Nombre del servicio que envió el request
total_time_so_far Integer Tiempo acumulado en ms desde el inicio de la cadena
source String Entrypoint: http, sidekiq, task, system
request_id String UUID del request (Rails ActionDispatch::RequestId)
created_at Float Timestamp monotónico del inicio del contexto
sidekiq_job String Nombre del job Sidekiq (solo en workers)
task String Nombre de la tarea (solo en TaskMonitor)

Métodos públicos

# Hidratar el Tracer (usado internamente por los middlewares)
ExisRay::Tracer.hydrate(trace_id: header_string, source: "http")

# Generar header de propagación para el siguiente servicio
ExisRay::Tracer.generate_trace_header
# => "Root=1-abc123-...;Self=1-def456-...;CalledFrom=wispro_agent;TotalTimeSoFar=42ms"

# Duración del request actual
ExisRay::Tracer.current_duration_s   # => 0.0452 (Float, segundos)
ExisRay::Tracer.current_duration_ms  # => 45 (Integer, milisegundos)

# Formatear duración human-readable
ExisRay::Tracer.format_duration(0.007)   # => "7.0ms"
ExisRay::Tracer.format_duration(1.25)    # => "1.25s"
ExisRay::Tracer.format_duration(125.0)   # => "2 minutes 5 seconds"

# Nombre del servicio (snake_case del module parent de la app Rails)
ExisRay::Tracer.service_name  # => "wispro_agent"

# Correlation ID compuesto
ExisRay::Tracer.correlation_id  # => "wispro_agent;1-65f...abc"

# Sincronizar correlation_id al Current configurado
ExisRay.sync_correlation_id

Licencia

Disponible como open source bajo los términos de la MIT License.