Pular para o conteúdo principal
ImmutableLog logo
Voltar
SinatraRuby

Integração com Sinatra

Integre o ImmutableLog em qualquer aplicação Sinatra com os filtros `before`/`after` e o bloco `error`. Reutiliza o cliente `ImmutableLog` (Net::HTTP) e envia o evento em uma Thread fire-and-forget.

Reaproveite o cliente

O Sinatra reutiliza a classe `ImmutableLog` (Net::HTTP) da página principal de Ruby — incluindo `sanitize_trail`, headers de timezone e a Idempotency-Key. Copie-a de lá; aqui mostramos apenas os filtros.

Filtros before / after / error

Registre os filtros na sua classe Sinatra. `before` captura o início; `after` classifica e envia o evento; `error` captura exceções não tratadas.

ruby
# app.rb
require "sinatra/base"
require "securerandom"
require_relative "immutable_log" # a classe ImmutableLog da página Ruby

class App < Sinatra::Base
  SKIP = ["/health", "/healthz"].freeze

  configure do
    set :imtbl, ImmutableLog.new(
      api_key: ENV["IMTBL_API_KEY"],
      service: "sinatra-service",
      env: ENV.fetch("RACK_ENV", "production"),
    )
  end

  before do
    @imtbl_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    @imtbl_request_id = request.env["HTTP_X_REQUEST_ID"] || SecureRandom.uuid
  end

  after do
    next if SKIP.include?(request.path_info)

    latency_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @imtbl_started) * 1000).to_i
    status = response.status
    kind = if status >= 400 then "error"
           elsif status >= 300 then "info"
           elsif status >= 200 then "success"
           else "info" end

    event_name = @imtbl_event_name || "http.#{request.request_method}.#{request.path_info}"
    trail = @imtbl_trail || request.env["HTTP_X_IMTBL_TRAIL"]

    payload = {
      "id" => SecureRandom.uuid,
      "kind" => kind,
      "message" => "#{request.request_method} #{request.path_info} -> #{status}",
      "context" => { "ip" => request.ip, "user_agent" => request.user_agent || "unknown" },
      "request" => { "request_id" => @imtbl_request_id,
                     "method" => request.request_method, "path" => request.path_info },
      "metrics" => { "latency_ms" => latency_ms, "status_code" => status },
      "severity" => kind == "error" ? "high" : "low",
    }

    # fire-and-forget — nao bloqueia a resposta.
    Thread.new do
      settings.imtbl.send_event(
        event_name: event_name, kind: kind, payload: payload, immutable_trail: trail,
      )
    rescue StandardError
      # Never let audit logging break the application.
    end
  end

  error do
    err = env["sinatra.error"]
    Thread.new do
      settings.imtbl.send_event(
        event_name: @imtbl_event_name || "http.#{request.request_method}.#{request.path_info}",
        kind: "error",
        payload: {
          "id" => SecureRandom.uuid, "kind" => "error",
          "message" => "#{request.request_method} #{request.path_info} failed: #{err&.class}",
          "error" => { "status_code" => 500, "retryable" => true,
                       "exception" => err&.class&.name,
                       "exception_message" => err&.message.to_s[0, 500] },
        },
        immutable_trail: @imtbl_trail,
      )
    rescue StandardError
    end
    "Internal Server Error"
  end
end

Como funciona

before

Registra request_id e timestamp de início

after

Classifica o status e envia o evento em uma Thread

error

Captura exceções não tratadas como evento de erro

Evento e trilha customizados

Dentro de uma rota, defina as variáveis de instância `@imtbl_event_name` e `@imtbl_trail`. O filtro `after` roda no mesmo escopo da requisição e lê esses valores.

ruby
class App < Sinatra::Base
  post "/payments" do
    # Sobrescreve o nome do evento e agrupa numa trilha auditavel.
    @imtbl_event_name = "payment.created"
    @imtbl_trail = "order-7782"

    # ... lógica de negócio / business logic ...
    json ok: true
  end

  post "/flows/:id/run" do
    @imtbl_trail = "flow-#{params[:id]}"
    json status: "ok"
  end
end

Padrão automático: http.METHOD.path.

Trilha imutável (immutable_trail)

A trilha agrupa eventos relacionados em uma mesma linha do tempo auditável. Defina `@imtbl_trail` em uma rota, ou propague entre serviços com o header `X-Imtbl-Trail`. O `send_event` normaliza o valor com `sanitize_trail` antes do envio.

A trilha não pode ser vazia, exceder 256 caracteres ou conter `:` — violações retornam `400 invalid_immutable_trail`. O `sanitize_trail` corrige o valor antes do envio.

Resposta e códigos de status

A API de ingestão é assíncrona. Sucesso retorna 202 (novo) ou 200 (duplicata idempotente). Como o envio roda em uma Thread, esses códigos importam principalmente se você enviar eventos manualmente.

StatusSignificado
202Evento aceito e enfileirado (novo)
200Idempotency-Key já existente — duplicate: true
400Idempotency-Key ausente/vazia ou invalid_immutable_trail
403Assinatura inativa/expirada, escopo ou retenção
413payload_too_large (limite do servidor: 16KB)
429monthly_limit_exceeded — não fazer retry
503mempool_full — transitório, pode dar retry

Campos de meta

Além do payload, o objeto meta carrega os campos que o SIEM usa para normalizar (ECS) e enriquecer o evento. Todos os valores de meta são strings.

Campo em metaO que faz no SIEM
event_nameO rótulo do evento (ex.: http.GET.auth-me-user, Pagamento erro) — o identificador que aparece na listagem. Vira event.action e deriva event.category (ex.: nome com loginauthentication).
client_ipVira source.ip com precedência máxima e proveniência client_asserted. Ver a seção abaixo.
immutable_trailVira imtbl.immutable_trail — agrupa eventos relacionados para investigação (ex.: um pedido, um usuário).
typeSeveridade do evento: error / warning / info / success. Colore o badge na listagem. (event_type é aceito como sinônimo.)
service, env, request_idMetadados: indexados e consultáveis no painel (busca/filtros).

client_ip — IP do usuário final

O problema que resolve

Entre o navegador e o core há proxies/ALB. O IP que o core observa na conexão é o do salto anterior (o backend do cliente ou um proxy da AWS), não o do usuário. Sem ação, source.ip seria o IP do proxy — inútil para geo, threat e detecção por IP. (Exemplo real: o XFF chegava 44.192.13.3 (AWS) enquanto o usuário era 179.110.4.205.)

A solução

O backend do cliente é o único que enxerga o IP real do navegador — então ele encaminha esse IP em meta.client_ip ao chamar POST /v1/events.

Como o SIEM trata

  • Valida que é um IP (v4/v6). Um valor não-IP é ignorado — não derruba o evento.
  • Escreve em source.ip com precedência máxima (ver ordem abaixo).
  • Carimba imtbl.source_ip_origin = "client_asserted" — a UI mostra o selo “IP do usuário”.
  • Alimenta GeoIP, threat intel e regras de detecção por IP.
Precedência de source.ip:client_ipX-Forwarded-For (1º hop)X-Real-IPIP de conexão

Proveniência e confiança

client_ip é asserido pelo tenant — o backend do cliente afirma o IP, e é spoofável por quem controla aquele backend. Porém é auto-contido ao próprio tenant (não cruza a fronteira de outro tenant). Um hit de threat/geo sobre um client_asserted é uma alegação, não uma observação de borda.

Como obter o IP no seu backend

Pegue o primeiro IP do X-Forwarded-For (o mais próximo do usuário); no fallback, use o IP de conexão.

ruby
def client_ip(request)
  xff = request.get_header("HTTP_X_FORWARDED_FOR").to_s
  return xff.split(",").first.strip unless xff.empty? # primeiro hop
  request.ip
end

meta = { event_name: "user.login", client_ip: client_ip(request) }

Exemplo de payload

json
{
  "payload": "{\"user\":\"bob\",\"action\":\"login\"}",
  "meta": {
    "event_name": "user.login",
    "client_ip": "179.110.4.205",
    "immutable_trail": "user-bob"
  }
}

Nota de PII: client_ip é PII e vai selado no bloco imutável (permanente). Decida conscientemente entre evidência forense e “direito ao esquecimento” antes de enviá-lo.

Esta documentação reflete o comportamento atual da integração. Para dúvidas ou integrações avançadas, entre em contato com o time de suporte.