Node.js
Guia completo de integração com o ImmutableLog em Node.js e TypeScript. Escolha a abordagem que melhor se encaixa no seu projeto: middleware/plugin automático para frameworks web, integração direta com `fetch`, ou wrappers para instrumentar funções específicas.
Middlewares para frameworks
A forma mais rápida de integrar o ImmutableLog é via middleware ou plugin nativo do framework. Toda requisição HTTP é capturada automaticamente — erros, sucessos, latência e contexto — sem alterar nenhuma rota. Clique no framework para ver o código completo pronto para usar.
Middleware function com `app.use()`. Captura via `res.on('finish')` e `res.on('error')`. Zero dependências extras.
import { immutableLogMiddleware } from './middleware/immutablelog';
app.use(immutableLogMiddleware({
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'express-service',
}));Ver documentação completa →
Plugin com `fastify-plugin` e hooks `onRequest`, `onResponse`, `onError`. Suporta TypeScript type augmentation.
import immutableLogPlugin from './plugins/immutablelog';
await fastify.register(immutableLogPlugin, {
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'fastify-service',
});Ver documentação completa →
Interceptor com RxJS `tap`/`catchError`. Suporta registro global via `APP_INTERCEPTOR`, por módulo ou por controller.
// app.module.ts
ImmutableLogModule.forRoot({
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'nestjs-service',
})Ver documentação completa →
Integração com fetch
Use a API nativa `fetch` do Node.js 18+ para enviar eventos diretamente ao ImmutableLog sem depender de um framework web. Ideal para workers, scripts, jobs e qualquer código Node.js que precise registrar eventos no ledger.
Função helper sendEvent
Crie uma função utilitária reutilizável para encapsular a lógica de envio. Totalmente tipada em TypeScript — serializa o payload, gera os headers obrigatórios e faz o POST ao endpoint de ingestão.
// src/lib/immutablelog.ts
import crypto from 'crypto';
const IMTBL_API_KEY = process.env.IMTBL_API_KEY ?? '';
const IMTBL_URL = process.env.IMTBL_URL ?? 'https://api.immutablelog.com';
export interface SendEventOptions {
eventName: string;
payload: Record<string, unknown>;
kind?: 'success' | 'error' | 'info' | 'warning';
service?: string;
env?: string;
}
export async function sendEvent(opts: SendEventOptions): Promise<{ tx_id: string; payload_hash: string }> {
const { eventName, payload, kind = 'info', service = 'node-service', env = 'production' } = opts;
const requestId = crypto.randomUUID();
const payloadStr = JSON.stringify({
...payload,
timestamp: new Date().toISOString(),
});
const event = {
payload: payloadStr,
meta: {
type: kind,
event_name: eventName,
service,
request_id: requestId,
env,
},
};
const res = await fetch(`${IMTBL_URL}/v1/events`, {
method: 'POST',
headers: {
Authorization: `Bearer ${IMTBL_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `${eventName}-${requestId}`,
'Request-Id': requestId,
},
body: JSON.stringify(event),
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error(`ImmutableLog error: ${res.status}`);
return res.json() as Promise<{ tx_id: string; payload_hash: string }>;
}
// Uso / Usage
await sendEvent({
eventName: 'payment.approved',
payload: { paymentId: 'pay_abc123', amount: 299.90, currency: 'BRL' },
kind: 'success',
service: 'payments-service',
});Retry com backoff exponencial
Para produção com alta disponibilidade, adicione retry automático com backoff exponencial. O ImmutableLog retorna `202` em sucesso e `429` quando o limite mensal é atingido — nunca faça retry em 429.
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function sendEventWithRetry(
opts: SendEventOptions,
maxRetries = 3,
baseDelay = 500,
): Promise<{ tx_id: string } | null> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await sendEvent(opts);
} catch (err) {
const status = (err as { status?: number }).status;
// 429 = limite mensal — nao fazer retry / monthly limit — do not retry
if (status === 429) {
console.warn('[ImmutableLog] Monthly limit reached, skipping retry.');
return null;
}
if (attempt === maxRetries - 1) {
console.error('[ImmutableLog] Max retries reached:', err);
return null;
}
const delay = baseDelay * 2 ** attempt; // 500ms, 1s, 2s
console.warn(`[ImmutableLog] Retry ${attempt + 1}/${maxRetries} in ${delay}ms`);
await sleep(delay);
}
}
return null;
}Envio em lote (batch)
Em workers de alta frequência, acumule eventos e envie em paralelo usando `Promise.allSettled`. Cada evento ainda é enviado individualmente — use `Promise.allSettled` para paralelizar sem falhar no conjunto.
async function sendEventsBatch(events: SendEventOptions[]): Promise<void> {
const results = await Promise.allSettled(events.map((ev) => sendEvent(ev)));
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`[ImmutableLog] Batch item ${i} failed:`, result.reason);
}
});
}
// Uso / Usage
await sendEventsBatch([
{ eventName: 'order.created', payload: { orderId: 'ord_1' }, kind: 'success' },
{ eventName: 'order.created', payload: { orderId: 'ord_2' }, kind: 'success' },
{ eventName: 'payment.failed', payload: { orderId: 'ord_3' }, kind: 'error' },
]);Wrappers de função
Wrappers permitem instrumentar funções específicas sem modificar sua lógica interna. Ideal para marcar operações críticas de negócio (ex: processar pagamento, criar usuário) onde você quer garantir rastreabilidade independente do framework usado.
Wrapper síncrono / async
Um único wrapper detecta automaticamente se a função é assíncrona via `fn.constructor.name`. O evento é enviado no bloco `finally` — garantindo o registro mesmo quando a função lança uma exceção.
function withAudit<T extends (...args: unknown[]) => unknown>(
fn: T,
eventName?: string,
kind: 'success' | 'info' = 'success',
service = 'node-service',
): T {
const isAsync = fn.constructor.name === 'AsyncFunction';
const name = eventName ?? `fn.${fn.name}`;
const handle = (startedAt: number, err: Error | null) => {
const latencyMs = Date.now() - startedAt;
const actualKind = err ? 'error' : kind;
const payload: Record<string, unknown> = {
id: crypto.randomUUID(),
kind: actualKind,
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
};
// fire-and-forget
sendEvent({ eventName: name, payload, kind: actualKind, service }).catch(() => {});
};
if (isAsync) {
return (async (...args: unknown[]) => {
const startedAt = Date.now();
try { return await (fn as (...a: unknown[]) => Promise<unknown>)(...args); }
catch (err) { handle(startedAt, err as Error); throw err; }
finally { handle(startedAt, null); }
}) as unknown as T;
}
return ((...args: unknown[]) => {
const startedAt = Date.now();
try { return fn(...args); }
catch (err) { handle(startedAt, err as Error); throw err; }
finally { handle(startedAt, null); }
}) as unknown as T;
}
// Uso / Usage
const processPayment = withAudit(
async (paymentId: string) => {
// ... logica de negocio / business logic ...
return { status: 'approved' };
},
'payment.process',
'success',
'payments-service',
);
await processPayment('pay_abc123');Classe AuditClient (configurável)
Para projetos maiores, use uma classe para centralizar a configuração. Instancie uma vez na inicialização e use `audit.wrap()` em qualquer função sync ou async.
import crypto from 'crypto';
interface AuditClientOptions {
apiKey?: string;
apiUrl?: string;
service?: string;
env?: string;
}
class AuditClient {
private readonly apiKey: string;
private readonly apiUrl: string;
private readonly service: string;
private readonly env: string;
constructor(opts: AuditClientOptions = {}) {
this.apiKey = opts.apiKey ?? process.env.IMTBL_API_KEY ?? '';
this.apiUrl = opts.apiUrl ?? process.env.IMTBL_URL ?? 'https://api.immutablelog.com';
this.service = opts.service ?? 'node-service';
this.env = opts.env ?? 'production';
}
wrap<T extends (...args: unknown[]) => unknown>(
fn: T,
eventName?: string,
kind: 'success' | 'info' = 'success',
): T {
const name = eventName ?? `fn.${fn.name}`;
const isAsync = fn.constructor.name === 'AsyncFunction';
const emit = (startedAt: number, err: Error | null) => {
const payload = {
id: crypto.randomUUID(),
kind: err ? 'error' : kind,
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs: Date.now() - startedAt },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
};
this._send(name, payload, err ? 'error' : kind);
};
if (isAsync) {
return (async (...args: unknown[]) => {
const t = Date.now();
try { return await (fn as (...a: unknown[]) => Promise<unknown>)(...args); }
catch (e) { emit(t, e as Error); throw e; }
finally { emit(t, null); }
}) as unknown as T;
}
return ((...args: unknown[]) => {
const t = Date.now();
try { return fn(...args); }
catch (e) { emit(t, e as Error); throw e; }
finally { emit(t, null); }
}) as unknown as T;
}
private _send(eventName: string, payload: object, kind: string) {
const requestId = crypto.randomUUID();
fetch(`${this.apiUrl}/v1/events`, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': `${eventName}-${requestId}`,
'Request-Id': requestId,
},
body: JSON.stringify({
payload: JSON.stringify(payload),
meta: { type: kind, event_name: eventName, service: this.service, env: this.env },
}),
signal: AbortSignal.timeout(5000),
}).catch(() => {});
}
}
// Inicializar uma vez / Initialize once
const audit = new AuditClient({
service: 'payments-service',
env: 'production',
});
// Usar em qualquer funcao / Use on any function
const processPayment = audit.wrap(
async (id: string) => ({ status: 'approved', id }),
'payment.process',
);
const deleteUser = audit.wrap(
async (userId: number) => { /* ... */ },
'user.delete',
'info',
);Decorator TypeScript (experimental)
Se seu projeto usa TypeScript com `experimentalDecorators: true`, você pode usar o decorator `@AuditLog()` diretamente em métodos de classe. Funciona com métodos sync e async.
// tsconfig.json: "experimentalDecorators": true
function AuditLog(eventName?: string, kind: 'success' | 'info' = 'success') {
return function (
_target: object,
propertyKey: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const original = descriptor.value as (...args: unknown[]) => unknown;
const name = eventName ?? `method.${propertyKey}`;
const isAsync = original.constructor.name === 'AsyncFunction';
descriptor.value = isAsync
? async function (this: unknown, ...args: unknown[]) {
const startedAt = Date.now();
let err: Error | null = null;
try {
return await original.apply(this, args);
} catch (e) {
err = e as Error;
throw e;
} finally {
emitAudit(name, kind, startedAt, err);
}
}
: function (this: unknown, ...args: unknown[]) {
const startedAt = Date.now();
let err: Error | null = null;
try {
return original.apply(this, args);
} catch (e) {
err = e as Error;
throw e;
} finally {
emitAudit(name, kind, startedAt, err);
}
};
return descriptor;
};
}
function emitAudit(name: string, kind: string, startedAt: number, err: Error | null) {
const actualKind = err ? 'error' : kind;
sendEvent({
eventName: name,
payload: {
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs: Date.now() - startedAt },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
},
kind: actualKind as 'success' | 'error' | 'info',
}).catch(() => {});
}
// Uso em classe / Usage in class
class PaymentService {
@AuditLog('payment.process', 'success')
async processPayment(paymentId: string): Promise<{ status: string }> {
// ... logica de negocio / business logic ...
return { status: 'approved' };
}
@AuditLog('invoice.generate')
async generateInvoice(orderId: string): Promise<void> {
// ...
}
}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.
