By the end of this lesson, you will understand:
- Why console.log is bad for production
- How structured logging helps debugging
- How request tracing works
- Best practices for log levels and messages
A user reports: "I got an error when trying to reserve"
Without good logs:
[2024-01-31] Error: undefined is not a function
With good logs:
{
"timestamp": "2024-01-31T12:00:00.000Z",
"level": "error",
"requestId": "abc-123",
"method": "POST",
"path": "/api/v1/reserve",
"userId": "user_1",
"itemId": "item_1",
"qty": 1,
"error": "Reservation failed: OUT_OF_STOCK"
}Which one helps you debug faster?
Instead of:
console.log(`User ${userId} reserved ${itemId}`);Do this:
logger.info({
msg: 'Reservation created',
requestId: req.requestId,
userId,
itemId,
reservationId: res.id
});export const logger = pino({
level: 'info',
redact: ['req.headers.authorization'], // Hide secrets
timestamp: pino.stdTimeFunctions.isoTime,
});
export function logInfo(msg: string, context?: Record<string, unknown>) {
logger.info({ ...context }, msg);
}User request flows through:
- Load balancer
- API server
- Database
- Cache
- Payment gateway
How do you find all logs for a single request?
export function requestIdMiddleware(req, res, next) {
// Get or generate request ID
const incomingId = req.headers['x-request-id'];
req.requestId = incomingId || crypto.randomUUID();
// Add to response header
res.setHeader('x-request-id', req.requestId);
// Add to logger context
req.log = logger.child({ requestId: req.requestId });
next();
}{
"requestId": "abc-123",
"msg": "Reservation created",
...
}Now you can search logs for "requestId": "abc-123" and see the entire request journey!
| Level | Usage | Example |
|---|---|---|
trace |
Very detailed debugging | Function entry/exit |
debug |
Development info | Cache hits, query details |
info |
Normal operation | Request received, item reserved |
warn |
Unexpected but not critical | Retry attempted, high latency |
error |
Error that needs attention | Database connection failed |
fatal |
Application can't continue | Out of memory, can't connect to DB |
logger.debug('Cache hit', { key: 'items' });
logger.info('Item reserved', { itemId: 'item_1', qty: 2 });
logger.warn('High latency', { durationMs: 5000 });
logger.error('Database error', error, { query: 'SELECT * FROM items' });
logger.fatal('Out of memory', { maxHeap: 1000000 });export function requestLoggingMiddleware(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const durationMs = Date.now() - start;
logger.info({
method: req.method,
path: req.path,
status: res.statusCode,
durationMs,
requestId: req.requestId,
userId: req.userId,
}, `${req.method} ${req.path} ${res.statusCode}`);
});
next();
}{
"level": "info",
"time": "2024-01-31T12:00:00.000Z",
"requestId": "abc-123",
"method": "POST",
"path": "/api/v1/reserve",
"status": 201,
"durationMs": 45,
"msg": "POST /api/v1/reserve 201"
}Server starting on port 3000
User user_1 reserved item_1
Database connected
User user_2 reserved item_2
Error: something failed
Problems:
- Can't search/filter easily
- No consistent format
- Missing context
- Hard to parse
{"level":"info","time":"2024-01-31T12:00:00.000Z","msg":"Server started"}
{"level":"info","time":"2024-01-31T12:00:01.000Z","msg":"Item reserved","userId":"user_1","itemId":"item_1"}
{"level":"error","time":"2024-01-31T12:00:02.000Z","msg":"Reservation failed","error":"OUT_OF_STOCK"}Benefits:
- Searchable (ELK, Splunk, CloudWatch)
- Filterable by level, requestId, userId
- Parseable by log aggregators
- Rich context
| Information | Example |
|---|---|
| Request entry | { requestId, method, path } |
| Errors with context | { error, stack, requestId, userId } |
| Business events | { action: 'reserved', itemId, qty } |
| Performance metrics | { durationMs, queryTime, cacheHit } |
| State changes | { from: 'reserved', to: 'confirmed' } |
| Information | Reason |
|---|---|
| Passwords | Security risk |
| API keys | Security risk |
| Credit card numbers | Security risk (PCI compliance) |
| Full request body | Too large, may contain PII |
| Binary data | Not readable |
const logger = pino({
redact: [
'req.headers.authorization',
'req.body.password',
'req.body.apiKey',
'req.body.creditCard'
]
});// Counters
incrementCounter('requests.reserve.success');
incrementCounter('requests.reserve.error');
// Histograms
recordHistogram('request.duration', durationMs);
recordHistogram('database.query.latency', queryTime);
// Gauges
setGauge('active.connections', db.getActiveConnections());npm run devcurl http://localhost:3000/api/v1/items
curl -X POST http://localhost:3000/api/v1/reserve \
-H "Content-Type: application/json" \
-d '{"userId":"user_test","itemId":"item_1","qty":1}'Logs will appear in console with request IDs:
{"level":"info","requestId":"req-abc","msg":"GET /api/v1/items 200","durationMs":12}
{"level":"info","requestId":"req-abc","msg":"Item reserved","itemId":"item_1"}- Collect - Ship logs to central service
- Index - Make logs searchable
- Visualize - Dashboards and alerts
Popular options:
- ELK Stack (Elasticsearch, Logstash, Kibana)
- CloudWatch (AWS)
- Splunk
- Datadog
- Google Cloud Logging
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
// Don't use pretty print in production
// Logs go to stdout, collected by container/runtime
});Report: "Some reservations fail with OUT_OF_STOCK even though items show in stock"
With structured logs:
// Request 1
{"requestId":"abc","itemId":"item_1","available":3,"requested":2}
{"requestId":"abc","kind":"OUT_OF_STOCK"} // Wait, what?
// Request 2 (10ms later)
{"requestId":"def","itemId":"item_1","available":3,"requested":2}
{"requestId":"def","kind":"OK"} // This one worked!Root cause: Race condition! Both requests saw availableQty=3 before either wrote.
Solution: See Lesson 3 on Concurrency
| File | Purpose |
|---|---|
src/observability/index.ts |
Logging & metrics |
src/server.ts |
Middleware setup |
src/middleware/security.ts |
Security logging |
- Structured logs = Machine-readable JSON
- Request tracing = requestId ties logs together
- Log levels = Use appropriate severity
- No secrets = Redact sensitive data
- Context matters = Include userId, action, result
Task: Add custom logging
- Add a log when a reservation expires
- Include: reservationId, userId, how long it was held
- Log at
warnlevel (business event) - Test by waiting 10 minutes or manually expiring
Congratulations! You've completed all 6 lessons:
- ✅ Validation - Never trust client input
- ✅ Concurrency - Atomic operations prevent race conditions
- ✅ Idempotency - Handle duplicate requests safely
- ✅ Caching - Improve performance with TTL
- ✅ Logging - Debug with structured logs and tracing
You're now ready to build production-ready REST APIs!
- Explore the source code in
src/ - Add new features to the API
- Implement the exercise in each lesson
- Deploy to production (AWS, Heroku, Railway)
💡 Tip: Good logs are the difference between "I have no idea what's wrong" and "I see the problem immediately!"