Skip to main content
ImmutableLog logo
Back
SinatraRuby

Sinatra Integration

Integrate ImmutableLog into any Sinatra application with the `before`/`after` filters and the `error` block. It reuses the `ImmutableLog` client (Net::HTTP) and sends the event in a fire-and-forget Thread.

Reuse the client

Sinatra reuses the `ImmutableLog` class (Net::HTTP) from the main Ruby page — including `sanitize_trail`, timezone headers, and the Idempotency-Key. Copy it from there; here we only show the filters.

before / after / error filters

Register the filters in your Sinatra class. `before` captures the start; `after` classifies and sends the event; `error` captures unhandled exceptions.

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

How it works

before

Records request_id and start timestamp

after

Classifies the status and sends the event in a Thread

error

Captures unhandled exceptions as an error event

Custom event and trail

Inside a route, set the `@imtbl_event_name` and `@imtbl_trail` instance variables. The `after` filter runs in the same request scope and reads these values.

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

Auto default: http.METHOD.path.

Immutable trail (immutable_trail)

A trail groups related events into a single auditable timeline. Set `@imtbl_trail` in a route, or propagate it across services with the `X-Imtbl-Trail` header. `send_event` normalizes the value with `sanitize_trail` before sending.

The trail cannot be empty, exceed 256 characters, or contain `:` — violations return `400 invalid_immutable_trail`. `sanitize_trail` fixes the value before sending.

Response and status codes

The ingestion API is asynchronous. Success returns 202 (new) or 200 (idempotent duplicate). Since sending runs in a Thread, these codes matter mostly if you send events manually.

StatusMeaning
202Event accepted and queued (new)
200Idempotency-Key already exists — duplicate: true
400Missing/empty Idempotency-Key or invalid_immutable_trail
403Inactive/expired subscription, scope or retention
413payload_too_large (server limit: 16KB)
429monthly_limit_exceeded — do not retry
503mempool_full — transient, retryable

This documentation reflects the current integration behavior. For questions or advanced integrations, contact the support team.