Standard Log Structure
In a microservice architecture, a log entry without context is nearly useless. When a single user action triggers calls across four services, you need a way to stitch those scattered log lines back into a coherent story. This guide defines the structured logging standard that makes that possible.
Core Principles
Section titled “Core Principles”1. Always Use Structured Logging (JSON)
Section titled “1. Always Use Structured Logging (JSON)”Every log entry must be a JSON object — not a plain string. JSON logs can be ingested, parsed, and indexed by any observability platform (Datadog, Loki, ELK Stack, CloudWatch) without custom parsers.
// ❌ Unstructured — impossible to query reliably"User 123 updated record at 10:40:00"
// ✅ Structured — every field is queryable{ "level": "info", "user_id": "123", "action": "UPDATE_RECORD", "timestamp": "..." }2. Every Log Must Have Context
Section titled “2. Every Log Must Have Context”Knowing what happened is not enough. A log must also tell you where (which service), who (which user), and which request (correlation ID). Without these three anchors, debugging across services is guesswork.
3. Use Log Levels Correctly
Section titled “3. Use Log Levels Correctly”| Level | When to Use |
|---|---|
debug | Verbose internal state, useful during development only |
info | Normal operational events (request received, record created) |
warn | Unexpected but recoverable situations |
error | Failures that require attention |
fatal | System-level failures requiring immediate intervention |
💡 In production,
debuglogs should be disabled by default and toggled only during active investigation.
4. Use Correlation IDs and Trace IDs
Section titled “4. Use Correlation IDs and Trace IDs”Every inbound request should be assigned a correlation_id at the API gateway or the first service that receives it. This ID is then forwarded in all downstream calls and included in every log entry. With it, you can reconstruct the full request lifecycle across all services from a single search.
5. Never Log Sensitive Data
Section titled “5. Never Log Sensitive Data”The following must never appear in any log entry:
- Passwords or password hashes
- JWT tokens or API keys
- Personally Identifiable Information (PII) beyond what is operationally necessary
- Full credit card or payment data
- Session secrets
The Standard Log Schema
Section titled “The Standard Log Schema”Every log entry across all services must conform to this schema:
{ "timestamp": "2025-09-12T10:40:00.123Z", "level": "info", "message": "Resource record updated successfully", "service": { "name": "resource-management", "version": "1.2.0" }, "trace": { "trace_id": "abc-123-xyz-789", "span_id": "span-456", "correlation_id": "req-1a2b3c4d" }, "user": { "id": "usr-007", "role": "admin" }, "context": { "resource_id": "res-123456", "action": "UPDATE_RESOURCE", "ip_address": "203.0.113.x" }, "details": { "fields_updated": ["address", "phone_number"] }}Field Reference
Section titled “Field Reference”| Field | Required | Description |
|---|---|---|
timestamp | ✅ | ISO 8601 UTC timestamp of when the event occurred |
level | ✅ | Severity: debug, info, warn, error, or fatal |
message | ✅ | Human-readable summary of the event |
service.name | ✅ | The service or bounded context emitting the log (e.g., auth, orders-bc) |
service.version | ✅ | Service version — critical for correlating issues to a specific deployment |
trace.trace_id | ✅ | Unique ID for the distributed trace (spans the full request lifecycle) |
trace.span_id | Optional | ID of the current operation within the trace |
trace.correlation_id | ✅ | ID linking all logs from the same originating request |
user.id | If available | The authenticated user’s identifier |
user.role | If available | The user’s role at the time of the event |
context | ✅ | Domain-specific context: resource IDs, action names, etc. |
details | Optional | Additional structured data for deeper debugging |
🔑
service.nameis the most important field for multi-service deployments. It is the primary key for filtering logs by bounded context in any observability platform.
NestJS Implementation
Section titled “NestJS Implementation”Custom Logger Service
Section titled “Custom Logger Service”import { Injectable, LoggerService } from '@nestjs/common';
@Injectable()export class StructuredLoggerService implements LoggerService { private buildEntry(level: string, message: string, meta: Record<string, unknown>) { return JSON.stringify({ timestamp: new Date().toISOString(), level, message, service: { name: process.env.SERVICE_NAME, version: process.env.SERVICE_VERSION, }, ...meta, }); }
log(message: string, meta?: Record<string, unknown>) { console.log(this.buildEntry('info', message, meta ?? {})); }
error(message: string, meta?: Record<string, unknown>) { console.error(this.buildEntry('error', message, meta ?? {})); }
warn(message: string, meta?: Record<string, unknown>) { console.warn(this.buildEntry('warn', message, meta ?? {})); }}Logging with Correlation ID via Interceptor
Section titled “Logging with Correlation ID via Interceptor”@Injectable()export class LoggingInterceptor implements NestInterceptor { constructor(private readonly logger: StructuredLoggerService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> { const request = context.switchToHttp().getRequest(); const correlationId = request.headers['x-correlation-id'] ?? randomUUID();
this.logger.log('Incoming request', { trace: { correlation_id: correlationId }, context: { method: request.method, path: request.path, }, });
return next.handle(); }}Log Level Usage Guide
Section titled “Log Level Usage Guide”// ✅ INFO — normal operationsthis.logger.log('Order created successfully', { trace: { correlation_id }, context: { order_id: order.id, action: 'CREATE_ORDER' },});
// ✅ WARN — recoverable anomalythis.logger.warn('Payment gateway timeout — retrying', { trace: { correlation_id }, context: { attempt: retryCount, max_retries: 3 },});
// ✅ ERROR — operation failedthis.logger.error('Failed to process payment', { trace: { correlation_id }, context: { order_id: order.id, error_code: err.code }, // ❌ NEVER include: card numbers, tokens, raw error stack with secrets});
// ❌ WRONG — sensitive data in logthis.logger.log('User logged in', { context: { password: dto.password, token: jwtToken }, // NEVER DO THIS});Summary
Section titled “Summary”A well-implemented structured logging system transforms debugging from archaeology into search. The investment is low — a consistent schema and a single interceptor — but the payoff during an incident is enormous: instead of grep-ing through interleaved plain-text output from a dozen services, you run a single query by correlation_id and see the full picture in seconds.