From 1753e35cd3a233c2f320c4e4e3b02332610c3167 Mon Sep 17 00:00:00 2001 From: jango-blockchained Date: Mon, 3 Feb 2025 15:41:06 +0100 Subject: [PATCH] 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 --- .env.example | 24 +++++- src/config/app.config.ts | 18 +++++ src/index.ts | 11 ++- src/middleware/logging.middleware.ts | 106 +++++++++++++++++++++++++ src/routes/mcp.routes.ts | 40 +++++++++- src/utils/logger.ts | 112 +++++++++++++++++++++++++++ 6 files changed, 305 insertions(+), 6 deletions(-) create mode 100644 src/middleware/logging.middleware.ts create mode 100644 src/utils/logger.ts diff --git a/.env.example b/.env.example index a7efc7a..bca8a1f 100644 --- a/.env.example +++ b/.env.example @@ -48,4 +48,26 @@ CORS_ORIGINS=http://localhost:3000,http://localhost:8123 TEST_HASS_HOST=http://localhost:8123 TEST_HASS_TOKEN=test_token TEST_HASS_SOCKET_URL=ws://localhost:8123/api/websocket -TEST_PORT=3001 \ No newline at end of file +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 \ No newline at end of file diff --git a/src/config/app.config.ts b/src/config/app.config.ts index 4076e25..2b3c42d 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -46,6 +46,24 @@ export const APP_CONFIG = { 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 */ VERSION: '0.1.0' } as const; diff --git a/src/index.ts b/src/index.ts index af79b6b..64c6871 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,10 +13,13 @@ import express from 'express'; import { APP_CONFIG } from './config/app.config.js'; import { apiRoutes } from './routes/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 { 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 @@ -24,6 +27,9 @@ console.log('Initializing Home Assistant connection...'); */ const app = express(); +// Apply logging middleware first to catch all requests +app.use(requestLogger); + // Apply security middleware app.use(securityHeaders); app.use(rateLimiter); @@ -47,6 +53,7 @@ app.use('/api', apiRoutes); * Apply error handling middleware * This should be the last middleware in the chain */ +app.use(errorLogger); app.use(errorHandler); /** @@ -54,5 +61,5 @@ app.use(errorHandler); * The port is configured in the environment variables */ 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}`); }); \ No newline at end of file diff --git a/src/middleware/logging.middleware.ts b/src/middleware/logging.middleware.ts new file mode 100644 index 0000000..25f649d --- /dev/null +++ b/src/middleware/logging.middleware.ts @@ -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); +}; \ No newline at end of file diff --git a/src/routes/mcp.routes.ts b/src/routes/mcp.routes.ts index 9569efd..143aaf9 100644 --- a/src/routes/mcp.routes.ts +++ b/src/routes/mcp.routes.ts @@ -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 { MCP_SCHEMA } from '../mcp/schema.js'; import { APP_CONFIG } from '../config/app.config.js'; import { Tool } from '../types/index.js'; +/** + * Create router instance for MCP routes + */ 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[] = []; -// 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) => { 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) => { try { // 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 }; \ No newline at end of file diff --git a/src/utils/logger.ts b/src/utils/logger.ts new file mode 100644 index 0000000..ea8ae48 --- /dev/null +++ b/src/utils/logger.ts @@ -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 }; \ No newline at end of file