Nest.js backend now has production-grade request/response logging with:
- Correlation IDs — unique ID per request for distributed tracing
- Structured logging — JSON logs via Pino for machine parsing
- Sensitive data sanitization — automatic redaction of secrets, passwords, tokens
- Performance tracking — request duration logged on every response
- APM integration ready — correlation IDs forwarded to Sentry/DataDog/etc.
- Log retention — configurable via pino transports (file, S3, CloudWatch, etc.)
File: src/common/middleware/correlation-id.middleware.ts
- Runs first on every request
- Generates or accepts
X-Correlation-IDfrom client headers - Attaches to
req.correlationIdfor downstream use - Echoes back in response headers for client tracing
- Records request start time for duration calculation
Client Usage:
curl -H "X-Correlation-ID: abc123" http://localhost:3001/api/health
# Response includes: X-Correlation-ID: abc123File: src/common/interceptors/correlation-id.interceptor.ts
- Safety net to ensure ID exists even if middleware is bypassed
- Augments pino logger context so correlation ID appears in all logs
File: src/common/interceptors/request-logging.interceptor.ts
- Logs incoming request: method, URL, IP, user agent, user ID, wallet address (masked)
- Logs outgoing response: status code, duration, content length
- Logs errors: stack trace (server errors only), error name, message
- Skips noisy paths:
/api/health,/api/metrics,/favicon.ico - Uses pino for structured JSON logs
Log Format:
{
"msg": "→ POST /api/v2/auth/login",
"type": "REQUEST",
"correlationId": "a1b2c3d4-...",
"method": "POST",
"url": "/api/v2/auth/login",
"ip": "192.168.1.100",
"userAgent": "Mozilla/5.0...",
"timestamp": "2026-06-02T10:15:30.123Z"
}{
"msg": "← POST /api/v2/auth/login 200 (145ms)",
"type": "RESPONSE",
"correlationId": "a1b2c3d4-...",
"method": "POST",
"url": "/api/v2/auth/login",
"statusCode": 200,
"duration": 145,
"userId": "user_123",
"timestamp": "2026-06-02T10:15:30.268Z"
}File: src/common/services/log-sanitizer.service.ts
Automatically redacts:
- Headers:
Authorization,Cookie,Set-Cookie,X-API-Key,Proxy-Authorization - Body fields:
password,secret,token,privateKey,mnemonic,seedPhrase,apiKey,otp,pin,cvv,ssn - Stellar secret keys: Detects
S[A-Z2-7]{55}pattern →[STELLAR_SECRET_REDACTED] - Long values: Truncates to 500 chars →
...[truncated] - Wallet addresses: Masks to
GABCDE...XY01(first 6 + last 4)
Methods:
sanitizeHeaders(headers)— redact sensitive headerssanitizeBody(body)— deep redact sensitive fieldssanitizeUrl(url)— remove tokens from query stringsmaskAddress(address)— privacy-safe address logging
File: src/app.module.ts (LoggerModule.forRootAsync)
Features:
- Correlation ID injection — every log line includes
correlationIdviacustomProps - Redaction — auto-redacts
req.headers.authorization,req.body.password, etc. - Serializers — structured req/res/err objects
- Pretty printing in dev —
pino-prettywith custom format:[{correlationId}] {msg} - JSON logs in production — machine-parsable for log aggregation
File: src/common/filters/http-exception.filter.ts
Now includes correlationId in all error responses:
{
"success": false,
"statusCode": 404,
"correlationId": "a1b2c3d4-...",
"timestamp": "2026-06-02T10:15:30.456Z",
"path": "/api/v2/users/999",
"message": "User not found"
}Propagating Correlation IDs Across Services:
When making HTTP calls to other services (e.g., Horizon, Soroban RPC, internal microservices), forward the correlation ID:
import { Request } from 'express';
@Injectable()
export class MyService {
async callExternalApi(request: Request) {
const correlationId = (request as any).correlationId;
const response = await fetch('https://external-api.com/data', {
headers: {
'X-Correlation-ID': correlationId,
'Content-Type': 'application/json',
},
});
return response.json();
}
}APM Integration (Sentry, DataDog, New Relic):
Correlation IDs are automatically included in all pino logs. Configure your log shipper (Fluentd, Logstash, CloudWatch Logs, etc.) to parse correlationId and send to your APM:
# Example: Fluentd config to forward logs to DataDog
<source>
@type tail
path /var/log/nestera-api.log
format json
tag nestera.api
</source>
<match nestera.api>
@type datadog
api_key YOUR_DATADOG_API_KEY
service nestera-api
dd_source nestjs
dd_tags env:production,service:api
</match>Pino supports multiple transports for log shipping:
npm install pino-rollUpdate app.module.ts:
transport: {
target: 'pino-roll',
options: {
file: '/var/log/nestera-api.log',
frequency: 'daily',
mkdir: true,
size: '100m', // Rotate when file reaches 100MB
},
},npm install pino-cloudwatchtransport: {
target: 'pino-cloudwatch',
options: {
logGroupName: '/aws/lambda/nestera-api',
logStreamName: 'production',
awsRegion: 'us-east-1',
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
},Configure pino to write JSON to stdout, then ship via Fluentd/Logstash to:
- ElasticSearch (ELK stack)
- Splunk
- DataDog
- New Relic
- Sumo Logic
No pino config change needed — just pipe stdout to your log shipper.
Use pino-s3 for long-term archival:
npm install pino-s3transport: {
target: 'pino-s3',
options: {
bucket: 'nestera-logs',
region: 'us-east-1',
prefix: 'api-logs/',
},
},Add to .env:
# Logging Configuration
LOG_LEVEL=info # debug | info | warn | error
LOG_RETENTION_DAYS=30 # For file-based rotation
LOG_CLOUDWATCH_GROUP=/aws/nestera/api # For CloudWatch
LOG_S3_BUCKET=nestera-logs # For S3 archival
# APM Integration
SENTRY_DSN=https://... # Error tracking
DATADOG_API_KEY=... # APM/metrics
NEW_RELIC_LICENSE_KEY=... # APMimport { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable()
export class MyService {
constructor(@Inject(REQUEST) private readonly request: Request) {}
async doSomething() {
const correlationId = (this.request as any).correlationId;
console.log(`[${correlationId}] Processing request...`);
}
}import { Logger } from 'nestjs-pino';
@Injectable()
export class MyService {
constructor(private readonly logger: Logger) {}
async processPayment(amount: number) {
this.logger.log({
msg: 'Processing payment',
amount,
currency: 'USD',
});
// correlationId is automatically added by pino
}
}import { LogSanitizerService } from './common/services/log-sanitizer.service';
@Injectable()
export class MyService {
constructor(private readonly sanitizer: LogSanitizerService) {}
logUserData(user: any) {
const sanitized = this.sanitizer.sanitizeBody(user);
console.log('User data:', sanitized);
// password, secret, privateKey are [REDACTED]
}
}Check Correlation ID Propagation:
curl -i -H "X-Correlation-ID: test123" http://localhost:3001/api/health
# Response should include: x-correlation-id: test123Check Logs:
# Dev mode (pretty printed)
npm run start:dev
# Logs show:
# [test123] → GET /api/health
# [test123] ← GET /api/health 200 (5ms)Production Logs (JSON):
NODE_ENV=production npm start
# {"level":30,"correlationId":"test123","msg":"→ GET /api/health",...}Existing interceptors updated:
RequestLoggingInterceptor— now uses pino + sanitizerCorrelationIdInterceptor— simplified (middleware does heavy lifting)
New components:
LogSanitizerService— sensitive data redactionCorrelationIdMiddleware— upgraded to set start time
Breaking changes: None — all changes are additive or internal improvements.
- Negligible — correlation ID generation is ~1μs
- Log sanitization only runs if
LogSanitizerServiceis injected - Pino is one of the fastest Node.js loggers (benchmarked)
- Health check paths skipped from verbose logging
✅ Passwords never logged — redacted automatically
✅ Stellar secret keys never logged — detected & redacted
✅ Authorization headers redacted — JWT tokens not exposed
✅ Query string tokens redacted — ?token=xyz → ?token=[REDACTED]
✅ Wallet addresses masked — only first 6 + last 4 chars
✅ PII compliant — no SSN, card numbers, or sensitive fields logged
Correlation ID missing in logs?
- Check middleware is registered:
app.configure()inapp.module.ts - Verify pino
customPropsconfig includescorrelationId
Sensitive data still appearing?
- Check
LogSanitizerServiceis registered inCommonModule - Add new sensitive fields to
SENSITIVE_BODY_KEYSset
Logs not shipping to APM?
- Verify pino transport config matches APM requirements
- Check environment variables (API keys, region, etc.)
- Test with
console.logto ensure stdout is captured
The audit log entries stored in the database (audit_logs table) are subject to a configurable retention policy separate from the Pino log transport layer above.
| Variable | Default | Description |
|---|---|---|
audit.retentionDays |
90 |
How long (in days) audit log rows are kept in the primary database |
Configured via environment or config:
# .env
AUDIT_RETENTION_DAYS=90The AdminAuditLogsService.cleanupOldLogs() method deletes rows older than retentionDays:
- Called on-demand via
POST /admin/audit-logs/cleanup - Deletes all rows where
timestamp < NOW() - retentionDays - Returns the count of deleted rows
The AdminAuditLogsArchivalService runs daily at 01:00 UTC via a @Cron('0 1 * * *') scheduled task:
- Batch read — fetches logs older than retention in batches of 10,000 (configurable via
audit.archivalBatchSize) - Local file — writes each batch to a JSONL file (
audit-logs-{timestamp}.jsonl) - Compression — gzips the file (enabled by default via
audit.compression.enabled) - Cold storage — uploads to S3 Glacier when
audit.coldStorage.enabledis true (requiresaudit.coldStorage.s3Bucket,awsAccessKeyId,awsSecretAccessKey) - Deletion — removes the archived rows from the primary database
AUDIT_COLD_STORAGE_ENABLED=true
AUDIT_COLD_STORAGE_S3_BUCKET=nestera-audit-logs
AUDIT_COLD_STORAGE_REGION=us-east-1
AUDIT_COLD_STORAGE_AWS_ACCESS_KEY_ID=...
AUDIT_COLD_STORAGE_AWS_SECRET_ACCESS_KEY=...
AUDIT_COMPRESSION_ENABLED=true
AUDIT_ARCHIVE_PATH=/var/archive/audit-logs
AUDIT_ARCHIVAL_BATCH_SIZE=10000Query retention policy status:
GET /admin/audit-logs/retention-policy
# → { "retentionDays": 90, "configured": true }Query archival statistics:
GET /admin/audit-logs/stats
# → { totalLogsInDb, logsOlderThanRetention, retentionDays, estimatedStorageGb }- Add request/response size tracking
- Implement log sampling for high-traffic endpoints
- Add trace ID for multi-hop distributed tracing (OpenTelemetry)
- Integrate with Jaeger/Zipkin for full trace visualization
- Add performance budgets (alert if p95 > threshold)