Integração com Express
Integre o ImmutableLog em qualquer aplicação Express com um middleware que captura automaticamente todas as requisições HTTP. Usa a API nativa `fetch` do Node.js 18+ para envio não-bloqueante.
Instalação
Nenhuma dependência extra além do Express. A integração usa o `fetch` nativo do Node.js 18+. Para versões anteriores, instale `node-fetch`.
npm install express
# Node.js < 18 only:
npm install node-fetchCódigo do Middleware
Salve o código abaixo em `src/middleware/immutablelog.ts`. O middleware usa `res.on('finish')` para capturar o status final da resposta após o handler completar, e `res.on('error')` para capturar erros de stream.
// src/middleware/immutablelog.ts
import crypto from 'crypto';
import { Request, Response, NextFunction } from 'express';
const MAX_PAYLOAD_BYTES = 12_000;
function clampPayload(payload: object): string {
let s = JSON.stringify(payload);
if (Buffer.byteLength(s, 'utf8') <= MAX_PAYLOAD_BYTES) return s;
const p = { ...(payload as Record<string, unknown>) };
delete p.error;
delete p.requestBody;
s = JSON.stringify(p);
if (Buffer.byteLength(s, 'utf8') <= MAX_PAYLOAD_BYTES) return s;
return JSON.stringify({
id: (payload as Record<string, unknown>).id,
kind: (payload as Record<string, unknown>).kind,
message: String((payload as Record<string, unknown>).message ?? 'event').slice(0, 500),
timestamp: (payload as Record<string, unknown>).timestamp,
});
}
function hashBody(body: string | Buffer): string {
const buf = Buffer.isBuffer(body) ? body : Buffer.from(body);
return crypto.createHash('sha256').update(buf).digest('hex');
}
function severityFrom(status: number | null, err: Error | null): string {
if (err) return 'error';
if (!status) return 'info';
if (status >= 400) return 'error';
if (status >= 300) return 'info';
if (status >= 200) return 'success';
return 'info';
}
export interface ImmutableLogOptions {
apiKey: string;
apiUrl?: string;
serviceName?: string;
env?: string;
skipPaths?: string[];
}
export function immutableLogMiddleware(opts: ImmutableLogOptions) {
const {
apiKey,
apiUrl = 'https://api.immutablelog.com',
serviceName = 'express-service',
env = 'production',
skipPaths = ['/health', '/healthz'],
} = opts;
const skip = new Set(skipPaths);
return (req: Request, res: Response, next: NextFunction): void => {
if (skip.has(req.path)) return next();
const startedAt = Date.now();
const requestId =
(req.headers['x-request-id'] as string) || crypto.randomUUID();
(req as Request & { requestId: string }).requestId = requestId;
const rawBody: string =
(req as Request & { rawBody?: string }).rawBody ?? '';
const emit = (err?: Error): void => {
const latencyMs = Date.now() - startedAt;
const statusCode = err ? 500 : res.statusCode;
const kind = severityFrom(statusCode, err ?? null);
const eventName =
(req as Request & { imtblEventName?: string }).imtblEventName ??
`http.${req.method}.${(req.route?.path as string | undefined) ?? req.path}`;
const payload: Record<string, unknown> = {
id: crypto.randomUUID(),
kind,
message: err
? `${req.method} ${req.path} failed: ${err.constructor.name}`
: kind === 'success'
? `${req.method} ${req.path} completed successfully`
: `${req.method} ${req.path} processed`,
timestamp: new Date().toISOString(),
context: {
ip: req.ip ?? req.socket?.remoteAddress ?? 'unknown',
userAgent: req.headers['user-agent'] ?? 'unknown',
},
request: {
requestId,
method: req.method,
path: req.path,
queryParams: Object.keys(req.query).length ? req.query : null,
},
metrics: { latencyMs, statusCode },
severity: kind === 'error' ? 'high' : 'low',
};
if (kind === 'error') {
payload.error = {
statusCode,
retryable: [408, 429, 500, 502, 503, 504].includes(statusCode),
...(err && {
exception: err.constructor.name,
exceptionMessage: err.message,
}),
...(rawBody && { requestBodyHash: hashBody(rawBody) }),
};
} else if (kind === 'success') {
payload.success = {
statusCode,
result: statusCode === 200 ? 'ok' : 'processed',
};
} else {
payload.info = { statusCode, action: req.method.toLowerCase() };
}
const event = {
payload: clampPayload(payload),
meta: {
type: kind,
event_name: eventName,
service: serviceName,
request_id: requestId,
env,
},
};
// fire-and-forget — nunca bloqueia a resposta / never blocks the response
fetch(`${apiUrl}/v1/events`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': `${eventName}-${requestId}`,
'Request-Id': requestId,
},
body: JSON.stringify(event),
signal: AbortSignal.timeout(5000),
}).catch((e: Error) =>
console.warn('[ImmutableLog] emit failed:', e.message),
);
res.setHeader('x-request-id', requestId);
};
res.on('finish', () => emit());
res.on('error', (err: Error) => emit(err));
next();
};
}Registro e Configuração
Registre o middleware globalmente com `app.use()` antes das suas rotas. Configure via variáveis de ambiente para manter as credenciais fora do código.
// src/app.ts
import express from 'express';
import { immutableLogMiddleware } from './middleware/immutablelog';
const app = express();
app.use(express.json());
// Registrar antes das rotas / Register before routes
app.use(
immutableLogMiddleware({
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: process.env.IMTBL_URL ?? 'https://api.immutablelog.com',
serviceName: process.env.IMTBL_SERVICE_NAME ?? 'express-service',
env: process.env.IMTBL_ENV ?? 'production',
skipPaths: ['/health', '/healthz', '/metrics'],
}),
);
app.get('/users', (req, res) => res.json({ users: [] }));
app.listen(3000);Nunca passe o token diretamente no código. Use variáveis de ambiente (.env) e garanta que o arquivo `.env` esteja no `.gitignore`.
Como funciona
O middleware registra o timestamp de início na entrada, escuta `res.on('finish')` para capturar o status final e calcula a latência. O envio ao ImmutableLog é feito via `fetch` fire-and-forget — nunca bloqueia a resposta para o cliente.
entry
Timestamp registrado, request_id gerado ou herdado do header
res.on('finish')
Latencia calculada, evento emitido via fetch fire-and-forget
res.on('error')
Erros de stream capturados e emitidos como evento de erro
O header x-request-id é injetado na resposta para rastreabilidade end-to-end entre sistemas.
Nome de evento customizado
Atribua `req.imtblEventName` em qualquer handler ou middleware anterior para sobrescrever o nome do evento gerado automaticamente.
app.post('/checkout', (req, res) => {
// Sobrescreve o nome do evento / Override the event name
(req as any).imtblEventName = 'payment.checkout.initiated';
// ... logica de negocio / business logic ...
res.json({ status: 'ok' });
});Padrao automatico: http.METHOD.route_path (ex: http.POST./checkout).
Exclusão de rotas
Passe `skipPaths` nas opções para ignorar rotas específicas como health checks e métricas.
app.use(
immutableLogMiddleware({
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
skipPaths: ['/health', '/healthz', '/metrics', '/readyz'],
}),
);Enriquecimento em erros
Em eventos de erro (4xx/5xx), o middleware inclui o hash SHA-256 do body da requisição, flag `retryable` e, quando disponíveis, os detalhes da exceção.
// Error handler do Express — capturado automaticamente pelo middleware
// Express error handler — automatically captured by the middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({ error: err.message });
});error.requestBodyHash
SHA-256 do body (auditoria sem expor dados)
error.retryable
true para: 408, 429, 500, 502, 503, 504
error.exception
Nome da classe do erro (se disponivel)
error.exceptionMessage
Mensagem do erro (se disponivel)
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.
