Audit Logging
autotel-audit provides audit-focused helpers for structured compliance logging with automatic outcome tagging and sampling bypass for critical events.
Installation
Section titled “Installation”npm install autotel-auditBasic Usage
Section titled “Basic Usage”Wrap sensitive operations with automatic audit metadata and outcome tracking:
import { trace } from 'autotel';import { withAudit } from 'autotel-audit';
export const deleteUser = trace(async (userId: string) => { return withAudit( { action: 'user.delete', resource: 'user', actorId: 'admin-42', }, async (_ctx, log) => { await db.users.delete(userId); log.info('User deleted successfully'); return { ok: true }; }, { emitNow: true }, );});Setup in Frameworks
Section titled “Setup in Frameworks”Express / Fastify
Section titled “Express / Fastify”import { trace } from 'autotel';import { withAudit } from 'autotel-audit';
app.delete('/users/:id', trace(async (req, res) => { await withAudit( { action: 'user.delete', resource: 'user', actorId: req.user.id, userId: req.params.id, }, async () => { await deleteUser(req.params.id); }, { emitNow: true }, );
res.json({ ok: true });}));NestJS
Section titled “NestJS”import { Controller, Delete, Param } from '@nestjs/common';import { trace } from 'autotel';import { withAudit } from 'autotel-audit';
@Controller('users')export class UsersController { @Delete(':id') async deleteUser(@Param('id') id: string) { return withAudit( { action: 'user.delete', resource: 'user', actorId: 'admin-123', }, async (ctx, log) => { await this.usersService.delete(id); log.info('User deleted'); return { ok: true }; }, { emitNow: true }, ); }}Next.js / TanStack Start
Section titled “Next.js / TanStack Start”import { trace } from 'autotel';import { withAudit } from 'autotel-audit';
export async function DELETE(req: Request, { params }: { params: { id: string } }) { return withAudit( { action: 'user.delete', resource: 'user', actorId: req.user?.id || 'anonymous', }, async () => { await db.users.delete(params.id); }, { emitNow: true }, );}API Reference
Section titled “API Reference”withAudit<T>(metadata, fn, options?)
Section titled “withAudit<T>(metadata, fn, options?)”Wraps an async operation with audit context and automatic outcome tagging.
Parameters:
| Name | Type | Description |
|------|------|-------------|
| metadata | AuditMetadata | Audit event metadata (action, resource, actor, etc.) |
| fn | (ctx, logger) => Promise<T> | Async function receiving audit context and request logger |
| options.emitNow | boolean | Immediately emit the audit event (default: false) |
| options.forceKeep | boolean | Force event through tail-sampling (default: true) |
| options.ctx | AuditContext | Custom audit context (auto-resolved if omitted) |
| options.logger | RequestLogger | Override request logger instance |
Returns: Promise<T> — Result from the wrapped function.
Outcome Tagging:
- ✅ Success: Automatically tagged with
outcome: 'success'(unless explicitly set) - ❌ Failure: Automatically tagged with
outcome: 'failure'and logged as error
// Explicit outcome overrideawait withAudit( { action: 'payment.process', outcome: 'partial' }, async () => { // ... },);setAuditAttributes(metadata, ctx?)
Section titled “setAuditAttributes(metadata, ctx?)”Write audit metadata as normalized audit.* span attributes without wrapping an operation.
import { setAuditAttributes } from 'autotel-audit';
setAuditAttributes({ action: 'config.update', resource: 'settings', actorId: 'user-123',});// Sets: audit.action, audit.resource, audit.actorId, autotel.audit=trueforceKeepAuditEvent(ctx?)
Section titled “forceKeepAuditEvent(ctx?)”Mark the active trace to bypass tail-drop sampling. Called automatically by withAudit unless disabled.
import { forceKeepAuditEvent } from 'autotel-audit';
if (request.isPrivileged()) { forceKeepAuditEvent();}Audit Metadata Structure
Section titled “Audit Metadata Structure”interface AuditMetadata { action: string; // Required: e.g., 'user.delete', 'payment.process' resource?: string; // What was modified: 'user', 'account', 'transaction' actorId?: string; // Who performed the action category?: string; // Optional grouping: 'admin', 'compliance', 'security' outcome?: 'success' | 'failure' | string; // Auto-set if omitted [key: string]: unknown; // Custom fields (auto-serialized)}Common Patterns
Section titled “Common Patterns”Critical Operations (Force Keep)
Section titled “Critical Operations (Force Keep)”Always keep sensitive operations in sampling:
await withAudit( { action: 'secret.access', resource: 'api-key', actorId: user.id, environment: 'production', }, async () => { // Fetch secret }, { emitNow: true, forceKeep: true },);Conditional Audit Emission
Section titled “Conditional Audit Emission”Emit only on errors or special conditions:
try { await withAudit( { action: 'data.export', resource: 'report' }, async () => { return await generateReport(); }, { emitNow: false }, // Default sampling applies );} catch (error) { // Error is auto-logged with outcome: failure // forceKeep is enabled by default for errors throw error;}Bulk Operations with Audit Trail
Section titled “Bulk Operations with Audit Trail”await withAudit( { action: 'users.deactivate', resource: 'user', actorId: 'admin-123', count: userIds.length, reason: 'inactivity', }, async (ctx, log) => { for (const id of userIds) { await deactivateUser(id); } log.info(`Deactivated ${userIds.length} users`); }, { emitNow: true },);Nested Audit Context
Section titled “Nested Audit Context”export const processPayment = trace(async (payment) => { return withAudit( { action: 'payment.process', resource: 'transaction', actorId: payment.merchant, amount: payment.amount, currency: payment.currency, }, async (ctx, log) => { const charge = await stripe.charges.create({ amount: payment.amount, currency: payment.currency, });
log.info('Payment processed', { chargeId: charge.id });
// Nested audit for refund capability setAuditAttributes({ chargeId: charge.id, stripeId: charge.id, });
return charge; }, { emitNow: true }, );});Compliance & Sampling
Section titled “Compliance & Sampling”Why Force-Keep?
Section titled “Why Force-Keep?”Tail-sampling decisions happen after spans complete. Critical audit trails must bypass statistical sampling:
forceKeepAuditEvent()marks spans for keeper-worthy treatment- Ensures compliance events aren't dropped by sampling rules
- Enabled by default in
withAudit()
// Safe default: force-keep enabled for all audit eventsawait withAudit(metadata, fn);
// Disable for high-volume audit streams with separate retentionawait withAudit(metadata, fn, { forceKeep: false });Backend Integration
Section titled “Backend Integration”Audit attributes integrate with any OTLP-compatible backend:
Attributes exported:
audit.action— The audited actionaudit.resource— The affected resourceaudit.actorId— The actor performing the actionaudit.outcome—'success'or'failure'audit.*— All custom metadata fieldsautotel.audit— Alwaystrue(for filtering)
Query examples (Datadog):
@autotel.audit:true @audit.action:user.delete @audit.outcome:failureAlerts:
// Create alert for all failed admin operations@audit.category:admin @audit.outcome:failureType Safety
Section titled “Type Safety”Define audit schemas per operation for IDE autocomplete:
import type { AuditMetadata } from 'autotel-audit';
type UserDeleteAudit = AuditMetadata & { action: 'user.delete'; resource: 'user'; actorId: string; reason?: string;};
type PaymentProcessAudit = AuditMetadata & { action: 'payment.process'; resource: 'transaction'; amount: number; currency: string; actorId: string;};
// IDE will autocomplete these fieldsawait withAudit<UserDeleteAudit>(metadata, fn);Error Handling
Section titled “Error Handling”Errors inside withAudit are automatically tagged with outcome: 'failure':
await withAudit( { action: 'account.suspend' }, async () => { await checkCompliance(); // May throw await suspendAccount(); }, { emitNow: true },);// On error:// - outcome: 'failure' is set// - Error is logged via logger.error()// - Exception is re-thrown// - Event is emitted (if emitNow: true)Performance Considerations
Section titled “Performance Considerations”withAuditadds minimal overhead (attribute serialization only)- Type conversion handles circular references gracefully
emitNow: truesends events immediately; omit for batch emission- Default
forceKeep: trueensures compliance but may override sampling decisions
See Also
Section titled “See Also”- Request Logging — Structured context and event emission
- Trace Helpers — Automatic metadata serialization
- Core API —
trace(),span(), context propagation