iugu_logger
SDK Ruby do iugu Logging Standard (ILS) — schema canônico, PII detection, trace context, registry de eventos, paridade
rails_semantic_logger. Funciona de Ruby 2.4 (platform) até 3.x (Rails 8).
SDK Ruby que implementa o iugu Logging Standard (ILS): logging estruturado JSON, schema canônico de eventos, detecção de PII e correlação de trace context.
Status: v0.9.0 — em produção
Validado em produção em 2026-05-13 com tráfego real. Componentes:
| Componente | Versão | Estado |
|---|---|---|
IuguLogger.configure DSL + Logger.event core |
0.1 | ✅ |
Custom :note severity (entre :info e :warn) |
0.1 | ✅ |
IuguLogger::Pii — 3-layer scanner + BR regex + SAFE_PATTERNS |
0.2 | ✅ |
IuguLogger::Schema::Validator — registry, :off/:warn/:strict + Levenshtein |
0.3 | ✅ |
IuguLogger::TraceContext — OTEL/Datadog/W3C/X-Request-Id chain |
0.3.1 | ✅ |
IuguLogger::TenantContext — Rack env extractor |
0.3.1 | ✅ |
IuguLogger::Buffer (thread-local) + RequestLogger Rack middleware |
0.4.0 | ✅ |
IuguLogger::JobLogger — Sidekiq + ActiveJob middlewares |
0.4.1 | ✅ |
IuguLogger::Railtie — auto-config Rails + Sidekiq + ActionController runtime subscriber |
0.5 → 0.8.1 | ✅ |
| Install generator + smoke rake task | 0.6.0 | ✅ |
PII timing fix (re-read tenant after @app.call) |
0.6.3 | ✅ |
Data-completeness-first PII defaults (:detect_only para CPF/CNPJ/phone/email/address) |
0.7.0 | ✅ |
RequestLogger enrichment (params, format, status_message, rails.*) |
0.8.0 | ✅ |
| CC Luhn check + format/runtime fallbacks | 0.8.1 | ✅ |
max_log_size_kb truncation + conformance runner |
0.9.0 | ✅ |
Single-line JSON default em todo ambiente (IUGU_LOG_PRETTY ativa pretty) |
unreleased | ✅ |
Trace enrichment OTEL-aware (source, sampled, otel diag, parent_id backfill) |
unreleased | ✅ |
service.instance opt-in (emit_service_instance, default off) |
unreleased | ✅ |
Trace auto-correlation em event() (herda do Buffer; fallback OTEL ambiente) |
unreleased | ✅ |
| Conformance suite cross-language passing + v1.0 publish | 1.0 | ⏳ |
~197 RSpec examples passing em Ruby 2.4 / 2.7 / 3.2 matrix.
Integração em 3 passos
1. Adicionar a gem ao Gemfile
gem "iugu_logger", "~> 0.9"
Depois rode bundle install. A gem é pública na RubyGems.org — sem source customizado, autenticação ou secret de build.
Defina GIT_SHA no build/deploy (service.version dos eventos): no Dockerfile via ARG GIT_SHA + ENV GIT_SHA=${GIT_SHA}, ou via env no Deployment.
2. Gerar initializer + populate Rack tenant + smoke
rails g iugu_logger:install
bundle exec rake iugu_logger:smoke
Pra popular iugu.account_id nos eventos canônicos, adicionar no ApplicationController (ou Api::BaseController):
include Api::IuguTenantContext # popula request.env["iugu.current_account_id"] após :authenticate
Exemplo do concern (que vive no app, não no SDK):
# app/controllers/concerns/api/iugu_tenant_context.rb
module Api
module IuguTenantContext
extend ActiveSupport::Concern
included do
before_action :populate_iugu_tenant_context
end
private
def populate_iugu_tenant_context
return if current_account.blank?
request.env["iugu.current_account_id"] = current_account.id
request.env["iugu.current_tier"] = current_account.tier if current_account.respond_to?(:tier)
rescue StandardError => e
Rails.logger.warn("IuguTenantContext: #{e.class}: #{e.}")
end
end
end
O canonical event (http.request.completed) — exemplo real de prod
{
"@timestamp": "2026-05-13T15:03:13.835646Z",
"log.level": "info",
"event.kind": "event",
"event.action": "http.request.completed",
"message": "PATCH /api/v1/payments 200 [39ms]",
"service": {
"name": "payments-api",
"version": "1db43d1c238229c550a68d44c3be6892fc11d398", // build SHA
"environment": "production"
// "instance" (= pod name) é opt-in: por padrão NÃO é emitido, pois o
// Alloy já anexa o label `pod_name`. Habilite com
// c.emit_service_instance = true em contextos sem coletor (dev/local).
},
"trace": {
"id": "6a0492b10000000027ed5c23af8b1ff0",
"span_id": "77c72a8787354d51",
"parent_id": null, // span do chamador upstream; null = trace nasceu aqui
"source": "opentelemetry", // opentelemetry | datadog | w3c | request_id
"sampled": true // decisão de sampling (quando a origem expõe)
},
"iugu": {
"account_id": "1Huj2DcSjtUnVPQbAGqwpR" // tenant
},
"request": {
"id": "3ba30263-0786-4fa7-ad3a-5a34a5012d69",
"method": "PATCH",
"path": "/api/v1/payments",
"source": "api",
"format": "json", // v0.8.0
"params": { /* payload completo, Rails filter_parameters aplicado */ } // v0.8.0
},
"rails": { // v0.8.0
"controller": "Api::V1::PaymentsController",
"action": "update",
"view_runtime_ms": 0.21, // v0.8.1 (via process_action subscriber)
"db_runtime_ms": 4.50 // v0.8.1
},
"http": {
"status_code": 200,
"status_message": "OK", // v0.8.0
"duration_ms": 39
},
"pii": {
"scanned": true,
"detected": ["cnpj", "phone", "email"], // detection-only por default
"redacted": 0 // só CC + credenciais redactam
}
}
Paridade com rails_semantic_logger legacy (Completed #...) + valor adicionado iugu (event.action, service.version, service.instance, iugu.account_id, pii.detected, trace.id correlation).
API
Emissão direta de evento
IuguLogger.event("fraud.feedzai.payment.created",
message: "feedzai payment payload sent",
iugu: { account_id: account.id, tier: account.tier },
fraud: { feedzai_environment: "prod", lifecycle_id: "PIXUGU:..." })
Trace auto-correlation: eventos emitidos durante um request/job herdam
automaticamente o trace.* (mesmo trace_id do http.request.completed /
log consolidado), sem você passar nada. A resolução é Buffer (preenchido pelo
RequestLogger) → span ambiente do OpenTelemetry/Datadog (cobre jobs/scripts).
Passar trace: explicitamente sobrescreve. Fora de qualquer contexto de
trace, o bloco é omitido.
Severity (incluindo :note custom)
IuguLogger.event("pix.out.timeout",
severity: :error,
message: "JDPI timeout after 30s",
pix: { end_to_end_id: e2e })
Levels: :trace, :debug, :info, :note (custom — entre info e warn), :warn, :error, :fatal.
Buffer thread-local (in-app logs durante request)
IuguLogger::Buffer.current.push(severity: :info, message: "validating pix request")
IuguLogger::Buffer.current.push(severity: :note, message: "pix accepted by jdpi")
RequestLogger drena no fim da request → logs: [...] no evento http.request.completed. 1 log/request consolidado.
ActiveJob
class ApplicationJob < ActiveJob::Base
include IuguLogger::JobLogger::ActiveJob
end
Cada execução de job emite activejob.job.completed (ou .failed) com request: { id: job_id, source: "activejob" }, labels: { job_class, queue, attempt }, duration_ms, logs: [...].
Sidekiq
Auto-registrado pelo Railtie quando Sidekiq é detectado no boot. Standalone:
Sidekiq.configure_server do |config|
config.server_middleware { |chain| chain.add IuguLogger::JobLogger::Sidekiq }
end
Configuração
Override via IuguLogger.configure
IuguLogger.configure do |c|
c.service_name = "payments-api" # auto-derived em Rails
c.service_version = ENV["GIT_SHA"] || "0.0.0"
c.service_environment = Rails.env
c.format = %w[1 true yes].include?(ENV["IUGU_LOG_PRETTY"].to_s.downcase) ? :pretty : :json
# ↑ single-line JSON em todo ambiente por padrão; IUGU_LOG_PRETTY=true ativa pretty local
c.event_action_validator = :warn # :off | :warn | :strict
c.event_action_registry = JSON.parse(File.read("dist/registry.json"))
# service.instance (= HOSTNAME/POD_NAME) — OFF por padrão. Em K8s + Alloy o
# label `pod_name` já cobre isso; habilite só em dev/local ou fora do K8s.
# c.emit_service_instance = true
end
Schema validator
| Mode | Comportamento |
|---|---|
:off (default sem registry) |
Sem validation, aceita qualquer event.action |
:warn |
Anota labels.schema_warning: "unknown_event_action", emite normal |
:strict |
Raise IuguLogger::UnknownEventAction (sugestões typo via Levenshtein) ou SchemaViolation (campos required ausentes) |
PII strategies — data-completeness-first (v0.7+)
| Tipo | Default | Comportamento |
|---|---|---|
cpf, cnpj, phone, email, address |
:detect_only |
Conteúdo preservado, pii.detected populado |
cc (cartão) — com validação Luhn (v0.8.1) |
:last4 |
**** **** **** 1234 (PCI-DSS) |
aws_key, bearer, url_with_creds |
:full_redact |
[AWS_KEY_REDACTED] (credenciais — nunca user data) |
account_id 32-hex (ILS-002) e schema canonical paths (trace.*, request.id, service.*) — SAFE_KEY_PATHS skipa scan inteiramente.
Override per-app (apps que precisam stricter, ex: external log export):
c.pii_redaction = IuguLogger::Pii::DEFAULT_STRATEGIES.merge(
cpf: :full_redact,
email: :full_redact
)
Trace context (chain de fallback)
IuguLogger::TraceContext.extract(rack_env: env) resolve a origem nesta ordem (primeira não-nil vence):
OpenTelemetry::Trace.current_span(Ruby ≥ 2.6 com otel-api) →source: "opentelemetry"Datadog::Tracing.active_trace(ddtrace) →source: "datadog"- W3C
traceparentheader →source: "w3c" - iugu legacy
X-Request-Id/ Railsaction_dispatch.request_id→source: "request_id"(correlação só,span_idzerado) nil— Logger emite sem o blocotrace.*
O bloco trace resultante carrega:
| Campo | Descrição |
|---|---|
id |
trace ID 32-hex — vem direto do OTEL quando ativo (o nome ECS já carrega o identificador OpenTelemetry) |
span_id |
span ID 16-hex do span local |
parent_id |
span do chamador upstream, backfilled do traceparent recebido quando o trace foi continuado. Presente = trace veio de outro serviço; null = trace nasceu nesta chamada |
source |
origem do contexto: opentelemetry / datadog / w3c / request_id |
sampled |
decisão de sampling (bit das trace-flags), quando a origem expõe. false = trace não será exportado ao backend, mas o log ainda correlaciona pelo id |
otel |
diagnóstico "not_configured" quando o SDK do OpenTelemetry está no bundle mas nunca foi inicializado (OpenTelemetry::SDK.configure não rodou). Ausente quando OTEL está ativo ou não está no bundle |
Migração Rails.logger.X → IuguLogger.event
Regra de coexistência (spec §10)
Não remover Rails.logger.X antes da gem v0.8+ estar validada em produção e o evento canônico carregar todos os dados. Pattern de migração rolling:
def submit_payment(params:)
enriched = enrich(params)
# Legacy — mantido durante rolling, ops/suporte/fraude dependem
Rails.logger.info("Payload enviado para a feedzai", enriched)
# Canônico — schema iugu, eventualmente substitui o legacy
IuguLogger.event("fraud.feedzai.payment.created",
message: "Payload enviado para a feedzai",
fraud: { feedzai_environment:, payload: enriched })
post_payment(enriched)
end
Após validação em prod (~dias), follow-up PR remove a linha legacy.
Exemplos antes/depois
Antes (string interpolation — antipattern):
Rails.logger.error("Erro processando #{user.id}: #{e.}")
Depois (kv-args + schema):
IuguLogger.event("user.processing.failed",
severity: :error,
iugu: { user_id: user.id },
error: { type: e.class.name, message: e. })
Decisões aplicadas
| ID | Decisão | Implementação |
|---|---|---|
| ILS-002 | iugu.account_id 32-hex preservado |
Pii::SAFE_PATTERNS + SAFE_KEY_PATHS |
| ILS-003 | Email :detect_only (deferido — agora generalizado em v0.7) |
Pii::DEFAULT_STRATEGIES |
| §13.7 | Ruby 2.4 first-class | gemspec.required_ruby_version >= 2.4 + CI matrix |
| v0.7 | Data-completeness-first — PII detection-only por default | Pii::DEFAULT_STRATEGIES |
| v0.8.0 | RequestLogger enrichment pra paridade com rails_semantic_logger |
request_logger.rb |
| v0.8.1 | CC Luhn check previne CNPJ false-positive | Pii::Scanner#luhn_valid? |
| v0.9.0 | max_log_size_kb truncation + conformance runner |
Configuration#max_log_size_kb + spec/conformance/ |
| unreleased | Single-line JSON default em todo ambiente (Loki/Alloy: 1 evento/linha; pretty via IUGU_LOG_PRETTY) |
Configuration#format |
| unreleased | Trace context OTEL-aware — source/sampled/otel diag + parent_id backfill do traceparent recebido |
trace_context.rb |
| unreleased | service.instance opt-in — evita duplicar o label pod_name que o Alloy já anexa |
Configuration#emit_service_instance |
| unreleased | Trace auto-correlation — IuguLogger.event herda o trace.* do request/job (Buffer > OTEL ambiente); trace: explícito vence |
Logger#inject_trace_context |
Distribuição
Publicada na RubyGems.org — gem "iugu_logger", sem autenticação. Versionamento segue SemVer e o CHANGELOG.md acompanha cada release.
Desenvolvimento
bundle install
bundle exec rspec
CI valida matriz Ruby 2.4 + 2.7 + 3.2 em todo PR.
Compatibilidade
- Ruby >= 2.4 (cobre platform Rails 4.2 até modernos)
- Rails opcional (Railtie auto-config quando detectado)
- ActionController::API e ActionController::Base (subscriber pra
process_action.action_controllerv0.8.1) - Sidekiq opcional (auto-registra)
- ActiveJob opcional (mixin opt-in)
- OpenTelemetry opcional (chain fallback)
Ownership
Mantido pela Engenharia de Plataforma da iugu — eng-plataforma@iugu.com.