Integração com Fastify
Integre o ImmutableLog em qualquer aplicação Fastify com um plugin que usa os hooks nativos do framework. Captura automaticamente todas as requisições com latência mínima — projetado para aplicações de alta performance.
Instalação
O plugin depende do `fastify-plugin` para garantir que os hooks sejam globais. O `fetch` nativo do Node.js 18+ é usado para envio não-bloqueante.
npm install fastify fastify-pluginCódigo do Plugin
Salve o código abaixo em `src/plugins/immutablelog.ts`. O plugin usa `fastify-plugin` para desencapsular os hooks e garantir que funcionem globalmente em toda a aplicação, incluindo rotas em sub-plugins.
// src/plugins/immutablelog.ts
import crypto from 'crypto';
import fp from 'fastify-plugin';
import {
FastifyInstance,
FastifyPluginOptions,
FastifyRequest,
FastifyReply,
} from 'fastify';
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;
s = JSON.stringify(p);
if (Buffer.byteLength(s, 'utf8') <= MAX_PAYLOAD_BYTES) return s;
return JSON.stringify({
id: (p as Record<string, unknown>).id,
kind: (p as Record<string, unknown>).kind,
message: String((p as Record<string, unknown>).message ?? 'event').slice(0, 500),
timestamp: (p as Record<string, unknown>).timestamp,
});
}
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 ImmutableLogPluginOptions extends FastifyPluginOptions {
apiKey: string;
apiUrl?: string;
serviceName?: string;
env?: string;
skipPaths?: string[];
}
async function immutableLogPlugin(
fastify: FastifyInstance,
opts: ImmutableLogPluginOptions,
) {
const {
apiKey,
apiUrl = 'https://api.immutablelog.com',
serviceName = 'fastify-service',
env = 'production',
skipPaths = ['/health', '/healthz'],
} = opts;
const skip = new Set(skipPaths);
fastify.addHook('onRequest', async (request: FastifyRequest) => {
if (skip.has(request.url)) return;
(request as any).startedAt = Date.now();
(request as any).requestId =
(request.headers['x-request-id'] as string) || crypto.randomUUID();
});
fastify.addHook('onResponse', async (request: FastifyRequest, reply: FastifyReply) => {
if (skip.has(request.url) || !(request as any).startedAt) return;
emit(request, reply.statusCode, null);
});
fastify.addHook('onError', async (request: FastifyRequest, reply: FastifyReply, error: Error) => {
if (!(request as any).startedAt) return;
emit(request, reply.statusCode || 500, error);
});
function emit(request: FastifyRequest, statusCode: number, err: Error | null) {
const startedAt: number = (request as any).startedAt;
const requestId: string = (request as any).requestId || crypto.randomUUID();
const latencyMs = Date.now() - startedAt;
const kind = severityFrom(statusCode, err);
const eventName =
(request as any).imtblEventName ??
`http.${request.method}.${(request as any).routeOptions?.url ?? request.url}`;
const payload: Record<string, unknown> = {
id: crypto.randomUUID(),
kind,
message: err
? `${request.method} ${request.url} failed: ${err.constructor.name}`
: kind === 'success'
? `${request.method} ${request.url} completed successfully`
: `${request.method} ${request.url} processed`,
timestamp: new Date().toISOString(),
context: {
ip: request.ip ?? 'unknown',
userAgent: request.headers['user-agent'] ?? 'unknown',
},
request: {
requestId,
method: request.method,
path: request.url,
queryParams: Object.keys(request.query as object).length ? request.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 }),
};
} else if (kind === 'success') {
payload.success = { statusCode, result: statusCode === 200 ? 'ok' : 'processed' };
} else {
payload.info = { statusCode, action: request.method.toLowerCase() };
}
const event = {
payload: clampPayload(payload),
meta: { type: kind, event_name: eventName, service: serviceName, request_id: requestId, env },
};
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) => fastify.log.warn(`[ImmutableLog] emit failed: ${e.message}`));
}
}
// fp() desencapsula o plugin para que os hooks sejam globais
// fp() unwraps the plugin so hooks are global
export default fp(immutableLogPlugin, { name: 'immutablelog-audit', fastify: '>=4.0.0' });Registro e Configuração
Registre o plugin com `fastify.register()` antes das suas rotas. As opções são tipadas e passadas diretamente no registro. Use variáveis de ambiente para as credenciais.
// src/server.ts
import Fastify from 'fastify';
import immutableLogPlugin from './plugins/immutablelog';
const fastify = Fastify({ logger: true });
await fastify.register(immutableLogPlugin, {
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: process.env.IMTBL_URL ?? 'https://api.immutablelog.com',
serviceName: process.env.IMTBL_SERVICE_NAME ?? 'fastify-service',
env: process.env.IMTBL_ENV ?? 'production',
skipPaths: ['/health', '/healthz', '/metrics'],
});
fastify.get('/users', async () => ({ users: [] }));
await fastify.listen({ port: 3000 });Nunca passe o token diretamente no código. Use variáveis de ambiente e garanta que `.env` esteja no `.gitignore`.
Como funciona
O plugin registra três hooks do Fastify. `onRequest` captura o timestamp de início e gera o `requestId`. `onResponse` calcula a latência e emite o evento de sucesso/info. `onError` captura exceções não tratadas e emite o evento de erro com detalhes da exceção.
onRequest
Registra startedAt e gera requestId unico
onResponse
Calcula latencia, emite evento via fetch fire-and-forget
onError
Captura excecoes, emite evento de erro com detalhes
O uso de `fastify-plugin` é essencial: sem ele, os hooks ficam encapsulados no escopo do plugin e não capturam rotas registradas fora dele.
Nome de evento customizado
Atribua `request.imtblEventName` dentro de qualquer handler ou hook `preHandler` para sobrescrever o nome do evento gerado automaticamente pelo plugin.
fastify.post('/checkout', async (request, reply) => {
(request as any).imtblEventName = 'payment.checkout.initiated';
return { status: 'ok' };
});Exclusão de rotas
Passe `skipPaths` nas opções do plugin. As rotas ignoradas não geram eventos no ledger, evitando que health checks e métricas poluam a auditoria.
await fastify.register(immutableLogPlugin, {
apiKey: process.env.IMTBL_API_KEY!,
skipPaths: ['/health', '/healthz', '/metrics', '/readyz'],
});Augmentation de tipos
Para evitar o uso de `any` ao acessar `request.imtblEventName` e `request.startedAt`, adicione a declaração de módulo abaixo ao seu projeto TypeScript.
// src/types/fastify.d.ts
import 'fastify';
declare module 'fastify' {
interface FastifyRequest {
startedAt?: number;
requestId?: string;
imtblEventName?: string;
}
}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.
