A production-ready, fully structured Node.js logging system built on Winston.
Separate log files per type · 9 custom levels · Sensitive field redaction · Correlation IDs · Express middleware included
- Features
- Project Structure
- Quick Start
- Log Levels
- Log Files
- Configuration
- API Reference
- Express Integration
- Log Output Examples
- Security & Redaction
- Environment Guide
- Contributing
- License
| Feature | Details |
|---|---|
| 9 custom log levels | fatal error security warn audit info http debug trace |
| Separated log files | Each log type routes to its own file automatically |
| Sensitive field redaction | Passwords, tokens, secrets never reach disk |
| Correlation IDs | Tie all logs from one request together across modules |
| Child loggers | Scoped per module — [auth], [database], [http] |
| Auto-rotating files | Daily rotation with configurable size limit and retention |
| Express middleware | Drop-in requestLogger and errorLogger |
| Performance timer | startTimer() / stop() for measuring operation duration |
| Dev vs prod formats | Colorized readable output in dev, clean JSON in prod |
| Exception handling | Uncaught exceptions and unhandled rejections auto-captured |
winston-logger/
├── src/
│ ├── logger/
│ │ ├── index.js # Main export — logger + all helpers
│ │ ├── levels.js # Custom log level definitions
│ │ ├── formats.js # prodFormat, devFormat, auditFormat
│ │ └── transports.js # File + console transport configs
│ ├── app.js # Demo — runs all features
│ └── middleware.js # Express request/error logging middleware
├── logs/ # Auto-created at runtime (git-ignored)
├── .env # Environment config (copy from .env.example)
├── .env.example # Template — safe to commit
├── .gitignore
└── package.json
# 1. Clone
git clone https://github.com/HaronKhalid/winston-logger.git
cd winston-logger
# 2. Install
npm install
# 3. Configure
cp .env.example .env
# Edit .env to set SERVICE_NAME, LOG_LEVEL, etc.
# 4. Run the demo
npm run devAfter running, check the logs/ folder — each log type is in its own file.
Custom 9-level scale, ordered from most to least critical:
| Level | Priority | Color | When to use |
|---|---|---|---|
fatal |
0 | 🔴 Bold Red | System crash — unrecoverable, immediate shutdown |
error |
1 | 🔴 Red | Operation failed — investigate promptly |
security |
2 | 🟣 Magenta | Auth failure, access violation, anomaly detected |
warn |
3 | 🟡 Yellow | Unexpected but recovered — watch for trends |
audit |
4 | 🔵 Cyan | Compliance trail — who did what and when |
info |
5 | 🟢 Green | Normal business events, milestones |
http |
6 | 🔵 Blue | HTTP request/response cycle |
debug |
7 | ⚪ White | Developer diagnostics — dev only |
trace |
8 | ⬜ Grey | Finest detail — function entry/exit, loops |
Winston's level filter is inclusive: setting
LOG_LEVEL=infocapturesfatalthroughinfo, excludinghttp,debug, andtrace.
| File | Contents | Retention |
|---|---|---|
combined-YYYY-MM-DD.log |
Every log entry (all levels) | 30 days, rotates daily |
error.log |
fatal and error only |
Permanent (manual cleanup) |
security.log |
security and audit only |
Permanent — compliance critical |
http.log |
http level only |
30 days, rotates daily |
debug.log |
debug and trace only |
Dev environment only |
exceptions.log |
Uncaught exceptions | Permanent |
rejections.log |
Unhandled Promise rejections | Permanent |
Copy .env.example to .env and adjust:
# Environment
NODE_ENV=development # development | production | test
# Logger
LOG_LEVEL=debug # trace | debug | info | http | audit | warn | security | error | fatal
SERVICE_NAME=my-app # appears in every log entry
SERVICE_VERSION=1.0.0
# File paths & rotation
LOG_DIR=logs
LOG_MAX_SIZE=20m # rotate file after this size
LOG_MAX_FILES=30d # delete files older than this
# Console
LOG_CONSOLE_SILENT=false # set true to suppress all console output| Environment | Default level | Console | Debug file |
|---|---|---|---|
development |
debug |
✅ Colorized | ✅ Created |
production |
info |
✅ JSON | ❌ Skipped |
test |
debug |
❌ Silent | ✅ Created |
const {
logger, // root logger
authLogger, // child: { module: 'auth' }
dbLogger, // child: { module: 'database' }
httpLogger, // child: { module: 'http' }
auditLogger, // child: { module: 'audit' }
cacheLogger, // child: { module: 'cache' }
jobLogger, // child: { module: 'jobs' }
createRequestLogger, // factory: request-scoped child with correlationId
logRequest, // helper: log HTTP request/response
logSecurity, // helper: log security events → security.log
logAudit, // helper: log audit trail → security.log
logError, // helper: log errors with full stack context
startTimer, // helper: measure operation duration
} = require('./src/logger');Log at any level directly:
logger.info('Server started', { port: 3000 });
logger.warn('Memory usage high', { usedMb: 480, limitMb: 512 });
logger.error('Redis unreachable', { host: 'localhost', port: 6379 });
logger.fatal('Critical config missing', { key: 'DATABASE_URL' });
logger.debug('Cache lookup', { key: 'user:u_001', hit: false });
logger.trace('Entering getUserById()', { userId: 'u_001' });Create a scoped logger that adds fixed metadata to every entry:
const paymentsLogger = logger.child({ module: 'payments' });
paymentsLogger.info('Charge succeeded', { amount: 9900, currency: 'USD' });
// → { level: 'info', module: 'payments', message: 'Charge succeeded', amount: 9900, ... }Create a request-scoped logger with a correlation ID. All logs share the same ID, making it easy to trace a single request across dozens of log lines.
// In Express middleware:
app.use((req, res, next) => {
req.log = createRequestLogger(req.headers['x-request-id']);
next();
});
// In route handler:
req.log.info('Fetching user', { userId: req.params.id });
req.log.info('User found', { email: user.email });
req.log.debug('Cache primed', { key: `user:${req.params.id}` });
// All three share the same correlationId automaticallyLog to the security level — routed to security.log with integrity tagging:
logSecurity('Failed login attempt', {
userId: 'u_456',
ip: '203.0.113.42',
attempts: 5,
blocked: true,
});
logSecurity('Unauthorized access', {
userId: 'u_321',
resource: '/admin/users',
method: 'DELETE',
});Log to the audit level — compliance trail, routed to security.log:
logAudit('User role changed', {
actor: 'admin_1',
targetUser: 'u_789',
oldRole: 'viewer',
newRole: 'editor',
reason: 'Promotion approved by manager',
});
logAudit('Record deleted', {
actor: 'u_123',
table: 'invoices',
recordId: 'inv_9901',
});Log an error with full stack trace always captured:
try {
await db.query(sql, params);
} catch (err) {
logError('Database query failed', err, {
query: sql,
params,
durationMs: 5012,
});
}Profile how long any operation takes. Returns a stop() function:
const timer = startTimer('db-query', { table: 'orders' });
const rows = await db.query('SELECT * FROM orders');
timer.stop({ rows: rows.length });
// → debug log: "db-query completed" { durationMs: 42, table: 'orders', rows: 500 }Log a completed HTTP request. Status code drives the log level automatically:
| Status | Level |
|---|---|
| 2xx / 3xx | http |
| 4xx | warn |
| 5xx | error |
// Manual usage:
const start = Date.now();
res.on('finish', () => logRequest(req, res, Date.now() - start));
// Or just use the middleware (see below) — it handles this automatically.Add two lines to your Express app:
const express = require('express');
const { requestLogger, errorLogger } = require('./src/middleware');
const app = express();
app.use(requestLogger); // ← before all routes
app.get('/api/users', (req, res) => {
req.log.info('Fetching users'); // req.log is available everywhere
res.json({ users: [] });
});
app.use(errorLogger); // ← after all routes (4-argument signature is required by Express)requestLogger does the following automatically on every request:
- Generates a
correlationId(UUID) or reads it fromX-Request-Idheader - Attaches
req.log— a child logger with the correlation ID pre-bound - Sets
X-Request-Idresponse header - Logs the completed request with method, URL, status, duration, IP, and user agent
[09:44:47] INFO [auth]: User login successful {"userId":"u_123","ip":"192.168.1.10"}
[09:44:47] ERROR [database]: Query timeout {"query":"SELECT * FROM orders","durationMs":5012}
[09:44:47] SECURITY [auth]: Failed login attempt {"userId":"u_456","attempts":5,"blocked":true}
[09:44:47] AUDIT [audit]: User role changed {"actor":"admin_1","oldRole":"viewer","newRole":"editor"}
[09:44:47] HTTP [http] {_xyz_789}: HTTP request {"method":"GET","status":200,"durationMs":24}
{
"level": "error",
"message": "Database query timeout",
"service": "my-app",
"version": "1.0.0",
"environment": "production",
"module": "database",
"timestamp": "2025-05-15T09:44:47.880+00:00",
"pid": 1234,
"host": "prod-server-01",
"error": "Query exceeded 5000ms",
"stack": "Error: Query exceeded 5000ms\n at ...",
"query": "SELECT * FROM orders WHERE status = ?",
"durationMs": 5012
}{
"level": "security",
"logType": "security",
"integrityVersion": "1",
"message": "Failed login attempt",
"userId": "u_456",
"ip": "203.0.113.42",
"attempts": 5,
"blocked": true,
"timestamp": "2025-05-15T09:44:47.875+00:00",
"service": "my-app",
"pid": 1234,
"host": "prod-server-01"
}The following field name patterns are automatically redacted before any log is written to any destination — including console, files, and remote transports:
password · token · secret · authorization · creditcard · ssn · apikey · api_key
Matching is case-insensitive and substring-based:
logger.info('User signup', {
userId: 'u_999',
email: 'jane@example.com', // ✅ logged as-is
password: 'hunter2', // → [REDACTED]
accessToken: 'eyJhbG...', // → [REDACTED] (contains "token")
apiKey: 'sk-abc123', // → [REDACTED] (contains "apikey")
});To add more sensitive field patterns, edit SENSITIVE_KEYS in src/logger/formats.js.
npm run dev
# NODE_ENV=development — debug level, colorized console, debug.log creatednpm run prod
# NODE_ENV=production — info level, JSON console, debug.log skippedLOG_LEVEL=warn node src/app.js
# Only fatal, error, security, warn are writtenLOG_CONSOLE_SILENT=trueUseful when your container runtime (Docker, Kubernetes) already collects stdout and you want logs only in files.
- Fork the repository
- Create a feature branch:
git checkout -b feat/your-feature - Commit with a clear message:
git commit -m "feat: add MongoDB transport" - Push and open a Pull Request
Please follow the existing code style — inline comments explaining why, not just what.
MIT © 2026 — see LICENSE for full text.