This document describes the TalentTrust Backend's structured logging implementation using Pino, including the JSON field schema, redaction rules, and correlation ID propagation.
The backend uses Pino as its structured logging library. Pino provides high-performance JSON logging with built-in redaction capabilities and child logger support for request correlation.
The logger is configured in src/logger.ts with the following settings:
- Format: Newline-delimited JSON (NDJSON)
- Levels:
trace,debug,info,warn,error,fatal - Default Level:
infoin production,debugin development - Pretty Printing: Enabled in development via
pino-pretty - Redaction: Automatic redaction of sensitive fields with
[REDACTED]
Every log record contains the following base fields:
| Field | Type | Description |
|---|---|---|
level |
string | Log level (trace, debug, info, warn, error, fatal) |
time |
number | Unix timestamp in milliseconds (Pino default) |
message |
string | Human-readable log message |
service |
string | Constant value: "talenttrust-backend" |
pid |
number | Process ID |
hostname |
string | Hostname (from HOSTNAME env var or "unknown") |
requestId |
string (optional) | Per-request UUID for tracing |
correlationId |
string (optional) | Caller-supplied trace ID for distributed tracing |
err |
object (optional) | Error object with type, message, and stack (non-production) |
Any additional fields passed to the logger are merged into the log record. For example:
{
"level": "info",
"time": 1714377600000,
"message": "User logged in",
"service": "talenttrust-backend",
"pid": 12345,
"hostname": "web-01",
"userId": "user-123",
"action": "login",
"ip": "192.168.1.1"
}Sensitive data is automatically redacted using Pino's redaction feature. The following field patterns are redacted with [REDACTED]:
password,passwd,pwdsecret,secretstoken,tokens,jwt,bearerauthorization,authapikey,api_key,apikey_secretaccess_token,refresh_tokenclient_secret,client_id
email,email_addressssn,social_security_numbercredit_card,cc_number,cvvbank_account,routing_numberphone,phone_number,mobileaddress,street_address
privatekey,private_key,privateKeypublickey,public_key,publicKeymnemonic,seed,seed_phrasewallet,wallet_private_key
cookie,cookies,sessionsession_id,session_token
db_password,database_passwordconnection_string,conn_string
key,secret_key,passphrase
Redaction applies to:
- Top-level fields:
{ password: "secret" }→{ password: "[REDACTED]" } - Nested fields:
{ user: { email: "test@example.com" } }→{ user: { email: "[REDACTED]" } } - Deeply nested fields:
{ data: { user: { password: "secret" } } }→{ data: { user: { password: "[REDACTED]" } } }
logger.info('User registration', {
email: 'user@example.com',
password: 'hunter2',
name: 'John Doe'
});Output:
{
"level": "info",
"message": "User registration",
"email": "[REDACTED]",
"password": "[REDACTED]",
"name": "John Doe"
}HTTP requests are correlated using the requestIdMiddleware in src/middleware/requestId.ts:
-
Request ID Generation:
- If
X-Request-Idheader is present and valid, it is reused - Otherwise, a new UUID v4 is generated
- The ID is written back to the response as
X-Request-Id
- If
-
Correlation ID Propagation:
- If
X-Correlation-Idheader is present and valid, it is forwarded - The ID is written back to the response as
X-Correlation-Idwhen present
- If
-
Request-Scoped Logger:
- A child logger is attached to
res.locals.logwith the correlation context - All logs using this logger automatically include
requestIdandcorrelationId
- A child logger is attached to
The httpLoggerMiddleware in src/middleware/httpLogger.ts emits structured access logs for every HTTP request/response pair:
{
"level": "info",
"message": "http request",
"method": "GET",
"url": "/api/v1/contracts",
"statusCode": 200,
"durationMs": 45.234,
"userAgent": "Mozilla/5.0...",
"ip": "192.168.1.1",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"correlationId": "trace-abc-123"
}Background jobs support correlation IDs through the queue system:
-
Job Payload Structure:
- All job payloads include optional
correlationIdandrequestIdfields - See
src/queue/types.tsfor payload definitions
- All job payloads include optional
-
Job Enqueue:
await queueManager.addJob(JobType.EMAIL_NOTIFICATION, payload, { correlationId: req.headers['x-correlation-id'] as string, requestId: res.locals.requestId });
-
Job Processing:
- The queue manager extracts correlation IDs from the job payload
- A child logger is created with correlation context
- All logs within job processing include
correlationId,requestId, andjobType
import { logger } from './logger';
logger.info('Server started');
logger.error('Database connection failed', { error: err.message });
logger.debug('Processing request', { userId: '123' });import { createRequestLogger } from './logger';
// In middleware or route handler
const reqLogger = createRequestLogger(requestId, correlationId);
reqLogger.info('Processing user request', { userId: '123' });import { logger } from './logger';
const userLogger = logger.child({ userId: '123', action: 'profile_update' });
userLogger.info('Profile updated');
userLogger.error('Update failed', { error: err.message });import { logger } from './logger';
try {
await someOperation();
} catch (err) {
logger.error('Operation failed', { err });
}Redaction behavior is tested in src/logger.test.ts:
describe('Logger – sensitive key redaction', () => {
it('redacts "password" field', () => {
const log = new Logger();
log.info('sensitive', { password: 'secret' });
expect(cap.logs[0]!['password']).toBe('[REDACTED]');
});
it('redacts nested sensitive fields', () => {
const log = new Logger();
log.info('nested', { user: { email: 'test@example.com' } });
expect(cap.logs[0]!['user']['email']).toBe('[REDACTED]');
});
});Run the tests:
npm run test:ci -- --testPathPattern="logger"-
Never log sensitive data: The redaction rules are a safety net, but avoid passing sensitive data to log calls entirely.
-
Stack traces: Error stack traces are only included in non-production environments to avoid leaking internal file paths.
-
Header validation: External request/correlation IDs are validated against a strict allowlist pattern to prevent header injection attacks.
-
User-Agent truncation: User-Agent strings are truncated to 256 characters to prevent log injection attacks.
-
Query strings: Avoid placing sensitive data (tokens, passwords) in URL query strings, as the full URL is logged in access logs.
| Variable | Description | Default |
|---|---|---|
LOG_LEVEL |
Minimum log level to emit | info (production), debug (development) |
NODE_ENV |
Environment name (affects pretty printing) | - |
HOSTNAME |
Hostname for log records | "unknown" |
TRUST_PROXY |
Whether to trust X-Forwarded-For header for IP resolution |
"false" |
The project previously used Winston. Migration to Pino provides:
- Performance: Pino is significantly faster than Winston
- Redaction: Built-in redaction without external dependencies
- Child Loggers: Native support for request-scoped loggers
- JSON Schema: Consistent, queryable JSON output
If you encounter old winston imports, replace them with:
// Old
import winston from 'winston';
// New
import { logger } from './logger';Because logs are emitted as newline-delimited JSON, they can be easily queried using standard tools:
# Filter by level
cat logs/app.log | jq 'select(.level == "error")'
# Filter by correlation ID
cat logs/app.log | jq 'select(.correlationId == "trace-abc-123")'
# Extract specific fields
cat logs/app.log | jq '{level, message, correlationId}'# Search for error messages
grep '"level":"error"' logs/app.log
# Search by correlation ID
grep '"correlationId":"trace-abc-123"' logs/app.logThe JSON format is compatible with most log aggregation systems:
- Elasticsearch + Kibana
- Splunk
- Datadog
- CloudWatch Logs Insights
- Loki + Grafana
-
Use structured context: Pass relevant context as an object rather than string interpolation.
// Good logger.info('User login', { userId: '123', ip: '192.168.1.1' }); // Bad logger.info(`User login for user 123 from 192.168.1.1`);
-
Use appropriate log levels:
trace: Detailed debugging informationdebug: Debugging information for developersinfo: Normal operational eventswarn: Warning conditions that don't stop operationerror: Error conditions that affect operationfatal: Critical errors that require immediate attention
-
Propagate correlation IDs: Always include
correlationIdwhen enqueueing jobs or making external service calls. -
Use child loggers: Create child loggers for request-specific context to avoid repeating fields.
-
Test redaction: Ensure sensitive fields are properly redacted by running the test suite.