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.
Middlewares
Integração automática via middleware — toda requisição HTTP é capturada sem modificar nenhum controller. Clique no framework para ver o código completo.
Middleware terminável com handle() + terminate(). O terminate() roda após a resposta ser enviada — fire-and-forget natural.
public function terminate($request, $response): void
{
$this->emit($request, $response);
// roda DEPOIS da resposta enviada
}Ver documentação completa →
EventSubscriber em kernel.response/kernel.exception + kernel.terminate para enviar após a resposta, com o HttpClient do Symfony.
public static function getSubscribedEvents(): array
{
return [
KernelEvents::TERMINATE => 'onTerminate',
KernelEvents::EXCEPTION => 'onException',
];
}Ver documentação completa →
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
// 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).
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
// 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 meta | O que faz no SIEM |
|---|---|
event_name | O 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 login → authentication). |
client_ip | Vira source.ip com precedência máxima e proveniência client_asserted. Ver a seção abaixo. |
immutable_trail | Vira imtbl.immutable_trail — agrupa eventos relacionados para investigação (ex.: um pedido, um usuário). |
type | Severidade do evento: error / warning / info / success. Colore o badge na listagem. (event_type é aceito como sinônimo.) |
service, env, request_id | Metadados: 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.ipcom 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.
client_ipX-Forwarded-For (1º hop)X-Real-IPIP de conexãoProveniê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.
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
{
"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.
