Leveled JSON logger with child loggers, key redaction (deep), and pluggable async sink. Outputs ISO timestamps, filters by level, and propagates context through child loggers.
npm install @ferrow/structured-loggerimport { StructuredLogger } from 'structured-logger';
const logger = new StructuredLogger({
level: 'info',
redactKeys: ['password', 'token'],
});
logger.info('user_login', { userId: 123, email: 'user@example.com' });
// Create a child logger with bound context
const requestLogger = logger.child({ requestId: 'req-456' });
requestLogger.info('request_start', { method: 'GET', path: '/api/users' });Create a logger.
Options:
level(LogLevel, default: 'info'): Minimum level to output (debug, info, warn, error)redactKeys(string[], default: []): Keys to redact (case-insensitive, deep)sink(LogSink, default: stdout line-JSON): Custom sink function
Log at debug level (lowest).
Log at info level.
Log at warn level.
Log at error level. If an Error object is passed, message and stack are extracted.
try {
throw new Error('Connection failed');
} catch (err) {
logger.error('database_error', err, { attempt: 1 });
// Outputs: { timestamp, level: 'error', message: 'database_error', context: { attempt: 1, message: '...', stack: '...' } }
}Create a child logger with bound context. All logs from the child include the bound fields.
const logger = new StructuredLogger();
const child = logger.child({ userId: 123, sessionId: 'abc' });
child.info('action_taken'); // Includes userId and sessionId in contextEach entry is JSON-serialized on a single line:
{
"timestamp": "2026-08-12T01:55:00.123Z",
"level": "info",
"message": "request_complete",
"context": {
"method": "GET",
"statusCode": 200,
"durationMs": 45
}
}Fields:
timestamp(ISO string): When the log was createdlevel(string): debug, info, warn, errormessage(string): Log messagecontext(object, optional): Additional fields from both bound and call-time context
Keys are redacted (case-insensitive) to [REDACTED] throughout the entire context object, including nested objects and arrays:
const logger = new StructuredLogger({
redactKeys: ['password', 'token', 'secret'],
});
logger.info('user_login', {
email: 'user@example.com',
password: 'secret123',
config: { token: 'abc-xyz' },
tags: ['admin', 'password'],
});
// Outputs (redacted fields):
// {
// "email": "user@example.com",
// "password": "[REDACTED]",
// "config": { "token": "[REDACTED]" },
// "tags": ["admin", "password"] // Note: array values not redacted (keys only)
// }Only logs at or above the configured level are emitted:
const logger = new StructuredLogger({ level: 'warn' });
logger.debug('debug_message'); // Not emitted
logger.info('info_message'); // Not emitted
logger.warn('warn_message'); // Emitted
logger.error('error_message'); // EmittedReplace the default stdout sink with a custom one:
const entries = [];
const logger = new StructuredLogger({
sink: (entry) => {
entries.push(entry);
},
});
logger.info('test');
console.log(entries[0]); // { timestamp: '...', level: 'info', message: 'test' }Sinks can be async:
const logger = new StructuredLogger({
sink: async (entry) => {
await sendToLoggingService(entry);
},
});Sink errors are silently ignored to prevent logger crashes.
const logger = new StructuredLogger({ level: 'debug' });
// Parent
logger.info('app_start');
// Child with request context
const reqLogger = logger.child({ requestId: 'req-123', userId: 'user-456' });
reqLogger.debug('request_received', { method: 'POST', path: '/api/submit' });
reqLogger.info('validation_passed');
// Grandchild adds more context
const dbLogger = reqLogger.child({ database: 'orders' });
dbLogger.info('query_executed', { rows: 42 });const logger = new StructuredLogger({
redactKeys: ['apiKey', 'password'],
});
logger.info('auth_attempt', {
user: 'alice',
credentials: {
password: 'secret',
apiKey: 'sk-123-abc',
},
metadata: {
ip: '192.168.1.1',
},
});
// Output:
// {
// "user": "alice",
// "credentials": {
// "password": "[REDACTED]",
// "apiKey": "[REDACTED]"
// },
// "metadata": { "ip": "192.168.1.1" }
// }- No structured field types; all values JSON-serialized as-is.
- Circular references in context objects will cause JSON.stringify to fail; ensure context is acyclic.
- Sink errors are silently ignored; implement your own error handling in custom sinks.
- No automatic performance metrics (duration, memory, etc.); pass these as context fields.
- No built-in filtering by context fields; implement in custom sink if needed.
Sponsored by Ferrow
Part of the ferrow-toolkit collection · Sponsored by Ferrow