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 .blank?
      request.env["iugu.current_account_id"]      = .id
      request.env["iugu.current_tier"]            = .tier if .respond_to?(:tier)
    rescue StandardError => e
      Rails.logger.warn("IuguTenantContext: #{e.class}: #{e.message}")
    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: .id, tier: .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):

  1. OpenTelemetry::Trace.current_span (Ruby ≥ 2.6 com otel-api) → source: "opentelemetry"
  2. Datadog::Tracing.active_trace (ddtrace) → source: "datadog"
  3. W3C traceparent header → source: "w3c"
  4. iugu legacy X-Request-Id / Rails action_dispatch.request_idsource: "request_id" (correlação só, span_id zerado)
  5. nil — Logger emite sem o bloco trace.*

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.XIuguLogger.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.message}")

Depois (kv-args + schema):

IuguLogger.event("user.processing.failed",
                 severity: :error,
                 iugu:     { user_id: user.id },
                 error:    { type: e.class.name, message: e.message })

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-awaresource/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-correlationIuguLogger.event herda o trace.* do request/job (Buffer > OTEL ambiente); trace: explícito vence Logger#inject_trace_context

Distribuição

Publicada na RubyGems.orggem "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_controller v0.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.