Add comprehensive logging infrastructure with configuration and middleware

- Implemented centralized logging utility using Winston with daily log rotation
- Added logging middleware for request and error tracking
- Extended .env.example with logging configuration options
- Updated app configuration to support flexible logging settings
- Replaced console.log with structured logging in main application entry point
- Created logging middleware to capture request details and response times
This commit is contained in:
jango-blockchained
2025-02-03 15:41:06 +01:00
parent 397355c1ad
commit 1753e35cd3
6 changed files with 305 additions and 6 deletions

View File

@@ -49,3 +49,25 @@ TEST_HASS_HOST=http://localhost:8123
TEST_HASS_TOKEN=test_token TEST_HASS_TOKEN=test_token
TEST_HASS_SOCKET_URL=ws://localhost:8123/api/websocket TEST_HASS_SOCKET_URL=ws://localhost:8123/api/websocket
TEST_PORT=3001 TEST_PORT=3001
# Security Configuration
JWT_SECRET=your-secret-key
# Rate Limiting
RATE_LIMIT_WINDOW_MS=900000 # 15 minutes
RATE_LIMIT_MAX=100
# SSE Configuration
SSE_MAX_CLIENTS=1000
SSE_PING_INTERVAL=30000
# Logging Configuration
LOG_LEVEL=info
LOG_DIR=logs
LOG_MAX_SIZE=20m
LOG_MAX_DAYS=14d
LOG_COMPRESS=true
LOG_REQUESTS=true
# Version
VERSION=0.1.0

View File

@@ -46,6 +46,24 @@ export const APP_CONFIG = {
PING_INTERVAL: 30000 // 30 seconds PING_INTERVAL: 30000 // 30 seconds
}, },
/** Logging Configuration */
LOGGING: {
/** Log level (error, warn, info, http, debug) */
LEVEL: process.env.LOG_LEVEL || 'info',
/** Directory for log files */
DIR: process.env.LOG_DIR || 'logs',
/** Maximum log file size before rotation */
MAX_SIZE: process.env.LOG_MAX_SIZE || '20m',
/** Maximum number of days to keep log files */
MAX_DAYS: process.env.LOG_MAX_DAYS || '14d',
/** Whether to compress rotated logs */
COMPRESS: process.env.LOG_COMPRESS === 'true',
/** Format for timestamps in logs */
TIMESTAMP_FORMAT: 'YYYY-MM-DD HH:mm:ss:ms',
/** Whether to include request logging */
LOG_REQUESTS: process.env.LOG_REQUESTS === 'true',
},
/** Application Version */ /** Application Version */
VERSION: '0.1.0' VERSION: '0.1.0'
} as const; } as const;

View File

@@ -13,10 +13,13 @@ import express from 'express';
import { APP_CONFIG } from './config/app.config.js'; import { APP_CONFIG } from './config/app.config.js';
import { apiRoutes } from './routes/index.js'; import { apiRoutes } from './routes/index.js';
import { securityHeaders, rateLimiter, validateRequest, sanitizeInput, errorHandler } from './security/index.js'; import { securityHeaders, rateLimiter, validateRequest, sanitizeInput, errorHandler } from './security/index.js';
import { requestLogger, errorLogger } from './middleware/logging.middleware.js';
import { get_hass } from './hass/index.js'; import { get_hass } from './hass/index.js';
import { LiteMCP } from 'litemcp'; import { LiteMCP } from 'litemcp';
import { logger } from './utils/logger.js';
console.log('Initializing Home Assistant connection...'); logger.info('Starting Home Assistant MCP...');
logger.info('Initializing Home Assistant connection...');
/** /**
* Initialize Express application with security middleware * Initialize Express application with security middleware
@@ -24,6 +27,9 @@ console.log('Initializing Home Assistant connection...');
*/ */
const app = express(); const app = express();
// Apply logging middleware first to catch all requests
app.use(requestLogger);
// Apply security middleware // Apply security middleware
app.use(securityHeaders); app.use(securityHeaders);
app.use(rateLimiter); app.use(rateLimiter);
@@ -47,6 +53,7 @@ app.use('/api', apiRoutes);
* Apply error handling middleware * Apply error handling middleware
* This should be the last middleware in the chain * This should be the last middleware in the chain
*/ */
app.use(errorLogger);
app.use(errorHandler); app.use(errorHandler);
/** /**
@@ -54,5 +61,5 @@ app.use(errorHandler);
* The port is configured in the environment variables * The port is configured in the environment variables
*/ */
app.listen(APP_CONFIG.PORT, () => { app.listen(APP_CONFIG.PORT, () => {
console.log(`Server is running on port ${APP_CONFIG.PORT}`); logger.info(`Server is running on port ${APP_CONFIG.PORT}`);
}); });

View File

@@ -0,0 +1,106 @@
/**
* Logging Middleware
*
* This middleware provides request logging functionality.
* It logs incoming requests and their responses.
*
* @module logging-middleware
*/
import { Request, Response, NextFunction } from 'express';
import { logger } from '../utils/logger.js';
import { APP_CONFIG } from '../config/app.config.js';
/**
* Interface for extended request object with timing information
*/
interface TimedRequest extends Request {
startTime?: number;
}
/**
* Calculate the response time in milliseconds
* @param startTime - Start time in milliseconds
* @returns Response time in milliseconds
*/
const getResponseTime = (startTime: number): number => {
const NS_PER_SEC = 1e9; // nanoseconds per second
const NS_TO_MS = 1e6; // nanoseconds to milliseconds
const diff = process.hrtime();
return (diff[0] * NS_PER_SEC + diff[1]) / NS_TO_MS - startTime;
};
/**
* Get client IP address from request
* @param req - Express request object
* @returns Client IP address
*/
const getClientIp = (req: Request): string => {
return (
(req.headers['x-forwarded-for'] as string)?.split(',')[0] ||
req.socket.remoteAddress ||
'unknown'
);
};
/**
* Format log message for request
* @param req - Express request object
* @returns Formatted log message
*/
const formatRequestLog = (req: TimedRequest): string => {
return `${req.method} ${req.originalUrl} - IP: ${getClientIp(req)}`;
};
/**
* Format log message for response
* @param req - Express request object
* @param res - Express response object
* @param time - Response time in milliseconds
* @returns Formatted log message
*/
const formatResponseLog = (req: TimedRequest, res: Response, time: number): string => {
return `${req.method} ${req.originalUrl} - ${res.statusCode} - ${time.toFixed(2)}ms`;
};
/**
* Request logging middleware
* Logs information about incoming requests and their responses
*/
export const requestLogger = (req: TimedRequest, res: Response, next: NextFunction): void => {
if (!APP_CONFIG.LOGGING.LOG_REQUESTS) {
next();
return;
}
// Record start time
req.startTime = Date.now();
// Log request
logger.http(formatRequestLog(req));
// Log response
res.on('finish', () => {
const responseTime = Date.now() - (req.startTime || 0);
const logLevel = res.statusCode >= 400 ? 'warn' : 'http';
logger[logLevel](formatResponseLog(req, res, responseTime));
});
next();
};
/**
* Error logging middleware
* Logs errors that occur during request processing
*/
export const errorLogger = (err: Error, req: Request, res: Response, next: NextFunction): void => {
logger.error(`Error processing ${req.method} ${req.originalUrl}: ${err.message}`, {
error: err.stack,
method: req.method,
url: req.originalUrl,
body: req.body,
query: req.query,
ip: getClientIp(req)
});
next(err);
};

View File

@@ -1,19 +1,49 @@
/**
* MCP Routes Module
*
* This module provides routes for accessing and executing MCP functionality.
* It includes endpoints for retrieving the MCP schema and executing MCP tools.
*
* @module mcp-routes
*/
import { Router } from 'express'; import { Router } from 'express';
import { MCP_SCHEMA } from '../mcp/schema.js'; import { MCP_SCHEMA } from '../mcp/schema.js';
import { APP_CONFIG } from '../config/app.config.js'; import { APP_CONFIG } from '../config/app.config.js';
import { Tool } from '../types/index.js'; import { Tool } from '../types/index.js';
/**
* Create router instance for MCP routes
*/
const router = Router(); const router = Router();
// Array to track tools /**
* Array to track registered tools
* Tools are added to this array when they are registered with the MCP
*/
const tools: Tool[] = []; const tools: Tool[] = [];
// MCP schema endpoint - no auth required as it's just the schema /**
* GET /mcp
* Returns the MCP schema without requiring authentication
* This endpoint allows clients to discover available tools and their parameters
*/
router.get('/', (_req, res) => { router.get('/', (_req, res) => {
res.json(MCP_SCHEMA); res.json(MCP_SCHEMA);
}); });
// MCP execute endpoint - requires authentication /**
* POST /mcp/execute
* Execute a tool with the provided parameters
* Requires authentication via Bearer token
*
* @param {Object} req.body.tool - Name of the tool to execute
* @param {Object} req.body.parameters - Parameters for the tool
* @returns {Object} Tool execution result
* @throws {401} If authentication fails
* @throws {404} If tool is not found
* @throws {500} If execution fails
*/
router.post('/execute', async (req, res) => { router.post('/execute', async (req, res) => {
try { try {
// Get token from Authorization header // Get token from Authorization header
@@ -48,4 +78,8 @@ router.post('/execute', async (req, res) => {
} }
}); });
/**
* Export the configured router
* This will be mounted under /api/mcp in the main application
*/
export { router as mcpRoutes }; export { router as mcpRoutes };

112
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,112 @@
/**
* Logging Module
*
* This module provides logging functionality with rotation support.
* It uses winston for logging and winston-daily-rotate-file for rotation.
*
* @module logger
*/
import winston from 'winston';
import 'winston-daily-rotate-file';
import { APP_CONFIG } from '../config/app.config.js';
/**
* Log levels configuration
* Defines the severity levels for logging
*/
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
/**
* Log level colors configuration
* Defines colors for different log levels
*/
const colors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'white',
};
/**
* Add colors to winston
*/
winston.addColors(colors);
/**
* Log format configuration
* Defines how log messages are formatted
*/
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
winston.format.colorize({ all: true }),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`,
),
);
/**
* Transport for daily rotating file
* Configures how logs are rotated and stored
*/
const dailyRotateFileTransport = new winston.transports.DailyRotateFile({
filename: 'logs/%DATE%.log',
datePattern: 'YYYY-MM-DD',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(
winston.format.uncolorize(),
winston.format.timestamp(),
winston.format.json()
)
});
/**
* Transport for error logs
* Stores error logs in a separate file
*/
const errorFileTransport = new winston.transports.DailyRotateFile({
filename: 'logs/error-%DATE%.log',
datePattern: 'YYYY-MM-DD',
level: 'error',
zippedArchive: true,
maxSize: '20m',
maxFiles: '14d',
format: winston.format.combine(
winston.format.uncolorize(),
winston.format.timestamp(),
winston.format.json()
)
});
/**
* Create the logger instance
*/
const logger = winston.createLogger({
level: APP_CONFIG.NODE_ENV === 'development' ? 'debug' : 'info',
levels,
format,
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}),
dailyRotateFileTransport,
errorFileTransport
],
});
/**
* Export the logger instance
*/
export { logger };