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 |
options.onMissingContext | 'throw' | 'warn' | 'skip' | What to do when there is no active trace context (default: 'warn') |
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 () => { // ... },);Best-effort by default
Section titled “Best-effort by default”Audit is observability — a missing trace context must never crash the business
logic it wraps. When there is no active trace context, withAudit no longer
throws into your handler. The onMissingContext option controls the behaviour:
'warn'(default): run the wrapped handler un-audited and log one warning per action.'skip': run un-audited, silently.'throw': previous fail-fast behaviour — opt in when telemetry is mandatory.
// Safe to drop into a production hot path: no surrounding trace() needed,// no try/catch required.await withAudit( { action: 'user.delete', resource: 'user', actorId: 'admin-42' }, async () => deleteUser(userId), { onMissingContext: 'skip' },);The same option is accepted by securityEvent() / withSecurity() (see
Security Observability).
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”- Security Observability —
securityEvent(), zero-code signals, and alertable security metrics from the same package - Request Logging — Structured context and event emission
- Trace Helpers — Automatic metadata serialization
- Core API —
trace(),span(), context propagation