Pular para o conteúdo principal
ImmutableLog logo
VoltarPHP

PHP

Guia completo de integração com o ImmutableLog em PHP. Middleware automático para Laravel e Symfony — ou um cliente HTTP direto com Guzzle para workers, comandos e jobs de fila. Use o middleware terminável (Laravel) ou kernel.terminate (Symfony) para enviar depois da resposta.

Cliente HTTP direto (Guzzle)

Use a classe ImmutableLog para enviar eventos diretamente sem depender de um framework web. Ideal para comandos artisan/console, workers de fila e qualquer script PHP que precise registrar eventos.

php
<?php
// src/ImmutableLog.php
// composer require guzzlehttp/guzzle ramsey/uuid

use GuzzleHttp\Client;
use Ramsey\Uuid\Uuid;

final class ImmutableLog
{
    private Client $http;

    public function __construct(
        private string $apiKey,
        private string $service = 'php-service',
        private string $env = 'production',
        ?string $apiUrl = null,
    ) {
        $this->http = new Client([
            'base_uri' => $apiUrl ?? (getenv('IMTBL_URL') ?: 'https://api.immutablelog.com'),
            'timeout'  => 5,
        ]);
    }

    public function sendEvent(
        string $eventName,
        string $kind,                 // "success" | "info" | "error"
        array $payload,
        ?string $immutableTrail = null,
    ): void {
        $requestId = Uuid::uuid4()->toString();
        $trail = self::sanitizeTrail($immutableTrail);

        // Todos os valores de meta precisam ser strings.
        $meta = [
            'type'       => $kind,
            'event_name' => $eventName,
            'service'    => $this->service,
            'request_id' => $requestId,
            'env'        => $this->env,
        ];
        if ($trail !== null) {
            $meta['immutable_trail'] = $trail;
        }

        $event = [
            'payload' => json_encode($payload + ['timestamp' => gmdate('c')]),
            'meta'    => $meta,
        ];

        $headers = [
            'Authorization' => "Bearer {$this->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;
        }

        $this->http->post('/v1/events', [
            'headers' => $headers,
            'json'    => $event,
        ]);
    }

    // Normaliza o immutable_trail (trim, nao-vazio, max 256, sem ':').
    // O servidor responde 400 invalid_immutable_trail se a regra for violada.
    public 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;
    }
}

// Uso / Usage
$log = new ImmutableLog(getenv('IMTBL_API_KEY'), 'payments-service', 'production');
$log->sendEvent('payment.approved', 'success', [
    'payment_id' => 'pay_abc123',
    'amount'     => 299.90,
], immutableTrail: 'order-7782');

PHP é síncrono por requisição. Para não bloquear o usuário, envie eventos a partir de um job de fila (Laravel Queue / Symfony Messenger) ou de um middleware terminável.

Retry com backoff exponencial

Para produção de alta disponibilidade, adicione retry com backoff exponencial. O ImmutableLog retorna 202 em sucesso e 200 em duplicata idempotente — nunca faça retry em 429 (limite mensal atingido).

php
public function sendWithRetry(
    string $eventName,
    string $kind,
    array $payload,
    ?string $trail = null,
    int $maxAttempts = 3,
): void {
    $delayMs = 500;

    for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
        try {
            $this->sendEvent($eventName, $kind, $payload, $trail);
            return;
        } catch (\GuzzleHttp\Exception\RequestException $e) {
            $status = $e->getResponse()?->getStatusCode();

            // 429 = limite mensal — nao fazer retry.
            if ($status === 429) {
                return;
            }
            if ($attempt + 1 < $maxAttempts) {
                usleep($delayMs * 1000);
                $delayMs *= 2; // 500ms -> 1s -> 2s
            }
        }
    }
}

Hash de dados sensíveis

Nunca envie dados pessoais brutos (e-mail, CPF, IP) para o ImmutableLog. Use SHA-256 para gerar um digest determinístico — rastreável sem expor o dado original.

php
<?php
// Faz o hash de dados sensiveis antes de enviar — nunca logue PII bruto.
function sha256_hex(string $data): string
{
    return hash('sha256', $data);
}

// Uso no payload do evento:
$payload = [
    'user_id'    => 'usr_123',
    'email_hash' => sha256_hex('user@example.com'),
    'ip_hash'    => sha256_hex('192.168.1.1'),
];

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.

php
function client_ip(): ?string {
    $xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
    if ($xff !== '') {
        return trim(explode(',', $xff)[0]); // primeiro hop = IP do usuario
    }
    return $_SERVER['REMOTE_ADDR'] ?? null;
}

$meta = ['event_name' => 'user.login', 'client_ip' => client_ip()];

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 API. Para dúvidas ou integrações avançadas, entre em contato com o time de suporte.