5 min read
Adding Logging to Your Node.js API
This tutorial builds on our paginated API by implementing comprehensive logging. We'll cover structured logging, different log levels, request tracking, error logging, and how to manage logs in different environments.
Prerequisites
- Completed Parts 1-7 of the tutorial
- Basic understanding of REST APIs
- Node.js installed
Project Setup
Install the required dependencies:
npm install winston morgan winston-daily-rotate-file uuid
Updated Project Structure
Add these new files to your project:
books-api/
├── src/
│ ├── config/
│ │ └── logging.js
│ ├── middleware/
│ │ └── logging.js
│ ├── utils/
│ │ └── logger.js
│ └── types/
│ └── logging.js
Logging Configuration
Create src/config/logging.js
:
module.exports = {
// Log levels and their priorities
LEVELS: {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4
},
// Default logging settings
DEFAULT_LEVEL: 'info',
LOG_FILE_PATH: 'logs',
MAX_FILE_SIZE: '20m',
MAX_FILES: '14d',
// Environment-specific settings
ENVIRONMENTS: {
development: {
level: 'debug',
console: true,
file: true
},
test: {
level: 'warn',
console: false,
file: false
},
production: {
level: 'info',
console: false,
file: true
}
},
// Request logging settings
REQUEST_LOGGING: {
skip: ['/health', '/metrics'],
maskFields: ['password', 'token', 'secret'],
maxBodyLength: 1000
}
};
Logger Implementation
Create src/utils/logger.js
:
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const { format } = winston;
const config = require('../config/logging');
// Custom format for structured logging
const structuredFormat = format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.metadata(),
format.json()
);
// Create base logger configuration
const createLogger = () => {
const env = process.env.NODE_ENV || 'development';
const envConfig = config.ENVIRONMENTS[env];
const transports = [];
// Add console transport if enabled
if (envConfig.console) {
transports.push(new winston.transports.Console({
format: format.combine(
format.colorize(),
format.simple()
)
}));
}
// Add file transport if enabled
if (envConfig.file) {
transports.push(new DailyRotateFile({
dirname: config.LOG_FILE_PATH,
filename: 'application-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: config.MAX_FILE_SIZE,
maxFiles: config.MAX_FILES,
format: structuredFormat
}));
// Separate error log file
transports.push(new DailyRotateFile({
dirname: config.LOG_FILE_PATH,
filename: 'error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
maxSize: config.MAX_FILE_SIZE,
maxFiles: config.MAX_FILES,
level: 'error',
format: structuredFormat
}));
}
return winston.createLogger({
level: envConfig.level,
levels: config.LEVELS,
transports
});
};
const logger = createLogger();
// Add request context
const addRequestContext = (req) => {
return {
requestId: req.id,
method: req.method,
path: req.path,
ip: req.ip,
userAgent: req.get('user-agent')
};
};
// Mask sensitive data
const maskSensitiveData = (data, fields = config.REQUEST_LOGGING.maskFields) => {
if (!data) return data;
const masked = { ...data };
fields.forEach(field => {
if (masked[field]) {
masked[field] = '********';
}
});
return masked;
};
module.exports = {
logger,
addRequestContext,
maskSensitiveData
};
Logging Middleware
Create src/middleware/logging.js
:
const morgan = require('morgan');
const { v4: uuidv4 } = require('uuid');
const { logger, addRequestContext, maskSensitiveData } = require('../utils/logger');
const config = require('../config/logging');
// Request ID middleware
const requestId = (req, res, next) => {
req.id = req.get('X-Request-ID') || uuidv4();
res.set('X-Request-ID', req.id);
next();
};
// Custom morgan token for request body
morgan.token('body', (req) => {
if (req.method === 'POST' || req.method === 'PUT') {
const masked = maskSensitiveData(req.body);
return JSON.stringify(masked).substring(0, config.REQUEST_LOGGING.maxBodyLength);
}
return '';
});
// Custom morgan format
const morganFormat = ':remote-addr - :remote-user [:date[clf]] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent" :response-time ms :body';
// HTTP request logging middleware
const httpLogger = morgan(morganFormat, {
skip: (req) => config.REQUEST_LOGGING.skip.includes(req.path),
stream: {
write: (message) => logger.http(message.trim())
}
});
// Error logging middleware
const errorLogger = (err, req, res, next) => {
const context = addRequestContext(req);
logger.error('Request error', {
...context,
error: {
message: err.message,
stack: err.stack,
code: err.code
}
});
next(err);
};
module.exports = {
requestId,
httpLogger,
errorLogger
};
Update Application Entry Point
Update src/app.js
to include logging:
const express = require('express');
const { requestId, httpLogger, errorLogger } = require('./middleware/logging');
const { logger } = require('./utils/logger');
const app = express();
// Add request ID to all requests
app.use(requestId);
// Log all HTTP requests
app.use(httpLogger);
// Existing middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.use('/api/books', require('./routes/books'));
// Error logging
app.use(errorLogger);
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info(`Server started on port ${PORT}`, {
port: PORT,
env: process.env.NODE_ENV,
node_version: process.version
});
});
Logging Tests
Create __tests__/integration/logging.test.js
:
const request = require('supertest');
const app = require('../../src/server');
const { logger } = require('../../src/utils/logger');
describe('Logging Tests', () => {
let logSpy;
beforeEach(() => {
logSpy = jest.spyOn(logger, 'info');
});
afterEach(() => {
logSpy.mockRestore();
});
it('should log HTTP requests', async () => {
await request(app)
.get('/api/books')
.expect(200);
expect(logSpy).toHaveBeenCalled();
});
it('should include request ID in response headers', async () => {
const response = await request(app)
.get('/api/books')
.expect(200);
expect(response.headers['x-request-id']).toBeDefined();
});
it('should mask sensitive information', async () => {
const response = await request(app)
.post('/api/books')
.send({
title: 'Test Book',
password: 'secret123'
})
.expect(201);
const logs = logSpy.mock.calls.flat();
expect(logs.some(log => log.includes('secret123'))).toBe(false);
});
it('should log errors with stack traces', async () => {
const errorSpy = jest.spyOn(logger, 'error');
await request(app)
.get('/api/books/invalid-id')
.expect(404);
expect(errorSpy).toHaveBeenCalled();
const errorLog = errorSpy.mock.calls[0][1];
expect(errorLog.error.stack).toBeDefined();
errorSpy.mockRestore();
});
});
Best Practices Implemented
Structured Logging
- JSON format for machine readability
- Consistent log levels
- Request context tracking
- Error stack traces
Security
- Sensitive data masking
- Request ID tracking
- IP address logging
- User agent tracking
Performance
- Log rotation
- Size limits
- Skip unnecessary paths
- Async logging
Maintenance
- Daily log files
- Environment-specific config
- Separate error logs
- Clean up old logs
Conclusion
You now have a robust logging system that:
- Tracks all API requests
- Handles errors properly
- Maintains security
- Supports different environments
- Follows best practices
Remember to:
- Monitor log storage
- Review error patterns
- Update logging rules
- Analyze log data
- Maintain log security
Related Posts
• 5 min read
APIs (Application Programming Interfaces) are the backbone of modern digital applications. They allow different software systems to communicate, exchange data, and collaborate seamlessly. As businesse...
• 4 min read
In today’s interconnected digital world, APIs (Application Programming Interfaces) are the backbone of communication between different software applications. From mobile apps to cloud services, APIs e...
• 5 min read
In the modern digital ecosystem, APIs (Application Programming Interfaces) serve as the backbone of connectivity. Whether you're building microservices, enabling integrations, or crafting data pipelin...