| name | wide-events-logging |
| description | Implement observability using the Wide Events (Canonical Log Lines) pattern. Instead of scattering logs throughout a request, accumulate high-cardinality context and emit a single, highly-dimensional structured event per service boundary.
|
Wide Events Logging (Canonical Log Lines)
Logging should not be a "debugging diary" where you sprinkle console.log("doing X") across your codebase. This creates log chaos: thousands of fragmented strings that are impossible to correlate or query effectively during an incident.
Instead, when instrumenting applications, implement Wide Events (also known as Canonical Log Lines).
Core Philosophy
- One Request, One Log Line: Emit exactly one comprehensive structured event per request, per service.
- Accumulate Context: Initialize an event object at the start of a request (usually in middleware), pass it down (or attach to context), enrich it with business data as the request executes, and log it in a
finally block or at the network boundary.
- High Cardinality: Strongly prefer adding attributes with millions of possible values (e.g.,
user_id, request_id, cart_id) because these are the most valuable fields for pinpointing specific failures.
- High Dimensionality: Don't just log HTTP status and duration. Log feature flags, user subscription tiers, database query counts, attempt numbers, and precise decline codes.
1. Building the Wide Event
Anti-Pattern: The Debugging Diary
app.post('/checkout', async (req, res) => {
logger.info(`Request received for user ${req.user.id}`);
const cart = await getCart(req.user.id);
logger.debug(`Loaded cart with ${cart.items.length} items`);
try {
await processPayment(cart);
logger.info("Payment successful");
res.json({ success: true });
} catch (e) {
logger.error("Payment failed", e);
res.status(500).send("Error");
}
});
Pattern: The Type-Safe Wide Event
interface WideEvent {
request_id: string;
timestamp: string;
method: string;
path: string;
service?: string;
deployment_id?: string;
status_code?: number;
outcome?: 'success' | 'error';
duration_ms?: number;
error?: {
type: string;
message: string;
code?: string;
retriable: boolean;
stripe_decline_code?: string;
};
user?: {
id: string;
subscription: string;
lifetime_value_cents: number;
};
feature_flags?: Record<string, boolean>;
cart?: {
item_count: number;
total_cents: number;
};
payment?: {
provider: string;
latency_ms: number;
attempt: number;
};
}
export async function wideEventMiddleware(ctx, next) {
const startTime = Date.now();
const event: Partial<WideEvent> = {
request_id: ctx.get('requestId'),
timestamp: new Date().toISOString(),
method: ctx.req.method,
path: ctx.req.path,
service: process.env.SERVICE_NAME,
deployment_id: process.env.DEPLOYMENT_ID,
};
ctx.set('wideEvent', event);
try {
await next();
event.status_code = ctx.res.status;
event.outcome = 'success';
} catch (error) {
event.status_code = error.status || 500;
event.outcome = 'error';
event.error = {
type: error.name,
message: error.message,
code: error.code,
retriable: error.retriable ?? false,
};
throw error;
} finally {
event.duration_ms = Date.now() - startTime;
logger.info(event);
}
}
2. Enriching with Business Context
As the request travels through your application, continually attach business context to the active event. By the time the event is emitted, it should answer exactly who did what, under what conditions, and why it failed.
app.post('/checkout', async (ctx) => {
const event = ctx.get('wideEvent') as Partial<WideEvent>;
const user = ctx.get('user');
event.user = {
id: user.id,
subscription: user.plan,
lifetime_value_cents: user.ltv,
};
event.feature_flags = user.flags;
const cart = await getCart(user.id);
event.cart = {
item_count: cart.items.length,
total_cents: cart.total,
};
const payment = await processPayment(cart, user);
event.payment = {
provider: payment.provider,
latency_ms: payment.latencyMs,
attempt: payment.attemptNumber,
};
if (payment.error) {
event.error = {
type: 'PaymentError',
stripe_decline_code: payment.error.declineCode,
};
}
return ctx.json({ orderId: payment.orderId });
});
3. Intelligent Tail Sampling
If logging wide events becomes too expensive at scale, do not use random sampling (e.g., arbitrarily dropping 95% of logs). Random sampling drops the specific errors you need to investigate.
Instead, implement Tail Sampling: Make the decision to keep or drop the event after the request completes.
function shouldSample(event: WideEvent): boolean {
if (event.status_code >= 500 || event.outcome === 'error') return true;
if (event.duration_ms > 2000) return true;
if (event.user?.subscription === 'enterprise') return true;
if (event.feature_flags?.new_checkout_flow) return true;
return Math.random() < 0.05;
}
Summary Checklist