Integração com Laravel
Integre o ImmutableLog em qualquer aplicação Laravel com um middleware terminável. O método `terminate()` roda depois da resposta ser enviada ao cliente — ou seja, o envio é naturalmente fire-and-forget, sem adicionar latência.
Instalação
O middleware usa o Guzzle (já presente na maioria dos projetos Laravel) e o ramsey/uuid (incluso no framework).
# Guzzle já vem no Laravel; instale se necessário:
composer require guzzlehttp/guzzleCódigo do middleware
Salve em `app/Http/Middleware/ImmutableLogMiddleware.php`. O `handle()` registra timestamp e request_id; o `terminate()` monta o evento e envia após a resposta. Query params passam por `redact()`.
<?php
// app/Http/Middleware/ImmutableLogMiddleware.php
namespace App\Http\Middleware;
use Closure;
use GuzzleHttp\Client;
use Illuminate\Http\Request;
use Ramsey\Uuid\Uuid;
class ImmutableLogMiddleware
{
// Chaves cujos valores sao mascarados antes do envio (PII/segredos).
private const SENSITIVE = [
'password', 'pwd', 'senha', 'token', 'access_token', 'refresh_token', 'api_key',
'apikey', 'secret', 'client_secret', 'authorization', 'auth', 'credit_card',
'card_number', 'cvv', 'cpf', 'otp', 'code',
];
public function handle(Request $request, Closure $next)
{
$request->attributes->set(
'imtbl.request_id',
$request->header('X-Request-Id') ?? Uuid::uuid4()->toString()
);
$request->attributes->set('imtbl.started_at', microtime(true));
return $next($request);
}
// terminate() roda DEPOIS da resposta ser enviada — fire-and-forget natural.
public function terminate(Request $request, $response): void
{
try {
$apiKey = env('IMTBL_API_KEY');
if (empty($apiKey)) {
return;
}
if (in_array($request->getPathInfo(), ['/health', '/healthz', '/metrics'], true)) {
return;
}
$startedAt = $request->attributes->get('imtbl.started_at', microtime(true));
$latencyMs = (int) ((microtime(true) - $startedAt) * 1000);
$status = $response->getStatusCode();
$requestId = $request->attributes->get('imtbl.request_id');
$kind = $status >= 400 ? 'error'
: ($status >= 300 ? 'info' : ($status >= 200 ? 'success' : 'info'));
$eventName = $request->attributes->get('imtbl.event_name')
?? 'http.' . $request->method() . '.' . $request->path();
$context = ['ip' => $request->ip(), 'user_agent' => $request->userAgent() ?? 'unknown'];
if ($user = $request->user()) {
$context['user_id'] = $user->getAuthIdentifier();
$context['email'] = $user->email ?? null;
}
$payload = [
'id' => Uuid::uuid4()->toString(),
'kind' => $kind,
'message' => $request->method() . ' /' . $request->path() . ' -> ' . $status,
'timestamp' => gmdate('c'),
'context' => $context,
'request' => [
'request_id' => $requestId,
'method' => $request->method(),
'path' => '/' . $request->path(),
// query params passam por redact() — nunca enviar segredos em claro.
'query_params' => self::redact($request->query()) ?: null,
],
'metrics' => ['latency_ms' => $latencyMs, 'status_code' => $status],
'severity' => $kind === 'error' ? 'high' : 'low',
];
if ($kind === 'error') {
$payload['error'] = [
'status_code' => $status,
'retryable' => in_array($status, [408, 429, 500, 502, 503, 504], true),
];
}
// Trilha: attribute imtbl.trail -> header X-Imtbl-Trail.
$trail = self::sanitizeTrail(
$request->attributes->get('imtbl.trail') ?? $request->header('X-Imtbl-Trail')
);
// Todos os valores de meta precisam ser strings.
$meta = [
'type' => $kind,
'event_name' => $eventName,
'service' => env('IMTBL_SERVICE_NAME', 'laravel-service'),
'request_id' => $requestId,
'env' => app()->environment(),
];
if ($trail !== null) {
$meta['immutable_trail'] = $trail;
}
$headers = [
'Authorization' => "Bearer {$apiKey}",
'Content-Type' => 'application/json',
// Idempotency-Key e OBRIGATORIO (sem ele a API responde 400).
'Idempotency-Key' => "{$eventName}-{$requestId}",
'Request-Id' => $requestId,
'X-Client-TZ' => 'America/Sao_Paulo',
'X-Client-Offset-Minutes' => '-180',
];
if ($trail !== null) {
$headers['X-Imtbl-Trail'] = $trail;
}
(new Client(['timeout' => 5]))->post(
(env('IMTBL_URL') ?: 'https://api.immutablelog.com') . '/v1/events',
['headers' => $headers, 'json' => ['payload' => json_encode($payload), 'meta' => $meta]]
);
} catch (\Throwable $e) {
// Never let audit logging break the application.
}
}
private static function redact(array $data): array
{
foreach ($data as $k => $v) {
if (is_string($k) && in_array(strtolower($k), self::SENSITIVE, true)) {
$data[$k] = '***REDACTED***';
} elseif (is_array($v)) {
$data[$k] = self::redact($v);
}
}
return $data;
}
// Normaliza o immutable_trail (trim, nao-vazio, max 256, sem ':').
private static function sanitizeTrail(?string $value): ?string
{
if ($value === null) {
return null;
}
$v = trim($value);
if ($v === '') {
return null;
}
$v = str_replace(':', '-', $v);
if (mb_strlen($v) > 256) {
$v = mb_substr($v, 0, 256);
}
return $v;
}
}Registro do middleware
No Laravel 11+, registre no `bootstrap/app.php`. Em versões anteriores, adicione à propriedade `$middleware` do `app/Http/Kernel.php`.
<?php
// Laravel 11+ — bootstrap/app.php
use App\Http\Middleware\ImmutableLogMiddleware;
return Application::configure(basePath: dirname(__DIR__))
->withMiddleware(function (Middleware $middleware) {
// Global: captura todas as requisições.
$middleware->append(ImmutableLogMiddleware::class);
})
->create();
// Laravel <= 10 — app/Http/Kernel.php
// protected $middleware = [
// // ...
// \App\Http\Middleware\ImmutableLogMiddleware::class,
// ];Nunca coloque o token no código. Defina IMTBL_API_KEY no .env e garanta que o .env está no .gitignore.
Como funciona
handle()
Registra request_id e timestamp de início
next($request)
A aplicação processa e a resposta é enviada ao cliente
terminate()
Roda após a resposta — monta e envia o evento
Evento e trilha customizados
Defina os atributos `imtbl.event_name` e `imtbl.trail` na request dentro de qualquer controller. O `terminate()` lê esses valores ao montar o evento.
<?php
use Illuminate\Http\Request;
class PaymentController
{
public function store(Request $request)
{
// Sobrescreve o nome do evento e agrupa numa trilha auditavel.
$request->attributes->set('imtbl.event_name', 'payment.created');
$request->attributes->set('imtbl.trail', 'order-7782');
// ... lógica de negócio / business logic ...
return response()->json(['ok' => true]);
}
}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 o atributo `imtbl.trail` na request, ou propague entre serviços com o header `X-Imtbl-Trail`. O valor passa por `sanitizeTrail` antes do envio.
Route::post('/flows/{id}/run', function (Request $request, string $id) {
$request->attributes->set('imtbl.trail', "flow-{$id}");
// ... business logic ...
return response()->json(['status' => 'ok']);
});A trilha não pode ser vazia, exceder 256 caracteres ou conter `:` — violações retornam `400 invalid_immutable_trail`. O `sanitizeTrail` 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 middleware roda em terminate(), esses códigos importam principalmente se você enviar eventos manualmente.
| Status | Significado |
|---|---|
| 202 | Evento aceito e enfileirado (novo) |
| 200 | Idempotency-Key já existente — duplicate: true |
| 400 | Idempotency-Key ausente/vazia ou invalid_immutable_trail |
| 403 | Assinatura inativa/expirada, escopo ou retenção |
| 413 | payload_too_large (limite do servidor: 16KB) |
| 429 | monthly_limit_exceeded — não fazer retry |
| 503 | mempool_full — transitório, pode dar retry |
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.
