diff --git a/controller/appointmentController.js b/controller/appointmentController.js index 854b96d..9ea0dad 100644 --- a/controller/appointmentController.js +++ b/controller/appointmentController.js @@ -1,5 +1,6 @@ const {addAppointment, addAppointmentModelV2, updateAppointmentModel, deleteAppointmentById} = require('../model/appointmentModel.js'); const {getAllAppointments, getAllAppointmentsV2 } = require('../model/getAppointments.js'); +const logger = require('../utils/logger'); const { validationResult } = require('express-validator'); @@ -20,7 +21,7 @@ const saveAppointment = async (req, res) => { // Respond with success message if appointment data is successfully saved res.status(201).json({ message: 'Appointment saved successfully' });//, appointmentId: result.id } catch (error) { - console.error('Error saving appointment:', error); + logger.error('Error saving appointment', { error: error.message, userId }); res.status(500).json({ error: 'Internal server error' }); } }; @@ -65,7 +66,7 @@ const saveAppointmentV2 = async (req, res) => { appointment, }); } catch (error) { - console.error("Error saving appointment:", error); + logger.error('Error saving appointment (V2)', { error: error.message, userId }); res.status(500).json({ error: "Internal server error" }); } }; @@ -114,7 +115,7 @@ const updateAppointment = async (req,res)=>{ appointment: updatedAppointment, }); } catch (error) { - console.error('Error updating appointment:', error); + logger.error('Error updating appointment', { error: error.message, appointmentId: id }); res.status(500).json({ error: 'Internal server error' }); } } @@ -133,7 +134,7 @@ const delAppointment = async (req,res)=>{ message: 'Appointment deleted successfully', }); } catch (error) { - console.error('Error deleting appointment:', error); + logger.error('Error deleting appointment', { error: error.message, appointmentId: id }); res.status(500).json({ error: 'Internal server error' }); } } @@ -149,7 +150,7 @@ const getAppointments = async (req, res) => { // Respond with the retrieved appointment data res.status(200).json(appointments); } catch (error) { - console.error('Error retrieving appointments:', error); + logger.error('Error retrieving appointments', { error: error.message }); res.status(500).json({ error: 'Internal server error' }); } }; @@ -174,7 +175,7 @@ const getAppointmentsV2 = async (req, res) => { appointments }); } catch (error) { - console.error("Error retrieving appointments:", error); + logger.error('Error retrieving appointments (V2)', { error: error.message }); res.status(500).json({ error: "Internal server error" }); } }; diff --git a/controller/authController.js b/controller/authController.js index b1a96aa..c6838a0 100644 --- a/controller/authController.js +++ b/controller/authController.js @@ -1,5 +1,6 @@ const authService = require('../services/authService'); const { isServiceError } = require('../services/serviceError'); +const logger = require('../utils/logger'); const TRUSTED_DEVICE_COOKIE = authService.trustedDeviceCookieName || 'trusted_device'; @@ -31,7 +32,7 @@ function handleServiceError(res, error, fallbackStatus, fallbackLogLabel) { }); } - console.error(fallbackLogLabel, error); + logger.error(fallbackLogLabel, { error: error.message }); return res.status(fallbackStatus).json({ success: false, error: error.message || 'Internal server error' @@ -50,6 +51,7 @@ exports.register = async (req, res) => { return res.status(201).json(result); } catch (error) { + logger.error('Registration error', { error: error.message, email: req.body.email }); return handleServiceError(res, error, 400, 'Registration error:'); } }; @@ -63,6 +65,7 @@ exports.login = async (req, res) => { return res.json(result); } catch (error) { + logger.error('Login error', { error: error.message, email: req.body.email }); return handleServiceError(res, error, 401, 'Login error:'); } }; @@ -72,6 +75,7 @@ exports.refreshToken = async (req, res) => { const result = await authService.refreshAccessToken(req.body.refreshToken, getDeviceInfo(req)); return res.json(result); } catch (error) { + logger.error('Token refresh error', { error: error.message }); return handleServiceError(res, error, 401, 'Token refresh error:'); } }; @@ -81,6 +85,7 @@ exports.logout = async (req, res) => { const result = await authService.logout(req.body.refreshToken); return res.json(result); } catch (error) { + logger.error('Logout error', { error: error.message, userId: req.user?.userId }); return handleServiceError(res, error, 500, 'Logout error:'); } }; @@ -95,6 +100,7 @@ exports.logoutAll = async (req, res) => { clearTrustedDeviceCookie(res); return res.json(result); } catch (error) { + logger.error('Logout all error', { error: error.message, userId: req.user?.userId }); return handleServiceError(res, error, 500, 'Logout all error:'); } }; @@ -114,6 +120,7 @@ exports.revokeTrustedDevices = async (req, res) => { revokedCount: result.revokedCount }); } catch (error) { + logger.error('Revoke trusted devices error', { error: error.message, userId: req.user?.userId }); return handleServiceError(res, error, 500, 'Revoke trusted devices error:'); } }; @@ -123,6 +130,7 @@ exports.getProfile = async (req, res) => { const result = await authService.getProfile(req.user.userId); return res.json(result); } catch (error) { + logger.error('Get profile error', { error: error.message, userId: req.user?.userId }); return handleServiceError(res, error, 500, 'Get profile error:'); } }; @@ -143,7 +151,7 @@ exports.logLoginAttempt = async (req, res) => { return res.status(error.statusCode).json({ error: error.message }); } - console.error('Failed to insert login log:', error); + logger.error('Failed to insert login log', { error: error.message, email: req.body.email }); return res.status(500).json({ error: 'Failed to log login attempt' }); } }; @@ -157,7 +165,7 @@ exports.sendSMSByEmail = async (req, res) => { return res.status(error.statusCode).json({ error: error.message }); } - console.error('Error sending SMS:', error); + logger.error('Error sending SMS', { error: error.message, email: req.body.email }); return res.status(500).json({ error: 'Internal server error' }); } -}; +}; \ No newline at end of file diff --git a/controller/chatbotController.js b/controller/chatbotController.js index 3289cdb..b5205d2 100644 --- a/controller/chatbotController.js +++ b/controller/chatbotController.js @@ -1,5 +1,6 @@ const { chatbotService } = require('../services/chatbotService'); const { isServiceError } = require('../services/serviceError'); +const logger = require('../utils/logger'); function serviceErrorToPayload(error) { return { @@ -8,8 +9,8 @@ function serviceErrorToPayload(error) { }; } -function handleUnexpectedError(res, label, error) { - console.error(`${label}:`, error); +function handleUnexpectedError(res, label, error, context = {}) { + logger.error(label, { error: error.message, ...context }); return res.status(500).json({ error: 'Internal server error', details: process.env.NODE_ENV === 'development' ? error.message : undefined @@ -28,7 +29,9 @@ async function getChatResponse(req, res) { return res.status(error.statusCode).json(serviceErrorToPayload(error)); } - return handleUnexpectedError(res, 'Error in chatbot response', error); + return handleUnexpectedError(res, 'Error in chatbot response', error, { + userId: req.body.user_id + }); } } @@ -41,7 +44,9 @@ async function addURL(req, res) { return res.status(error.statusCode).json({ error: error.message }); } - return handleUnexpectedError(res, 'Error processing URL', error); + return handleUnexpectedError(res, 'Error processing URL', error, { + urls: req.body.urls + }); } } @@ -50,7 +55,11 @@ async function addPDF(req, res) { const result = await chatbotService.addPdf(req.body.pdfs); return res.status(result.statusCode).json(result.body); } catch (error) { - return handleUnexpectedError(res, 'Error in chatbot response', error); + if (isServiceError(error)) { + return res.status(error.statusCode).json({ error: error.message }); + } + + return handleUnexpectedError(res, 'Error processing PDF', error); } } @@ -63,7 +72,9 @@ async function getChatHistory(req, res) { return res.status(error.statusCode).json(serviceErrorToPayload(error)); } - return handleUnexpectedError(res, 'Error retrieving chat history', error); + return handleUnexpectedError(res, 'Error retrieving chat history', error, { + userId: req.body.user_id + }); } } @@ -76,7 +87,9 @@ async function clearChatHistory(req, res) { return res.status(error.statusCode).json(serviceErrorToPayload(error)); } - return handleUnexpectedError(res, 'Error clearing chat history', error); + return handleUnexpectedError(res, 'Error clearing chat history', error, { + userId: req.body.user_id + }); } } @@ -86,4 +99,4 @@ module.exports = { addPDF, getChatHistory, clearChatHistory -}; +}; \ No newline at end of file diff --git a/controller/homeServiceController.js b/controller/homeServiceController.js index b913689..7e5bb22 100644 --- a/controller/homeServiceController.js +++ b/controller/homeServiceController.js @@ -1,4 +1,5 @@ const supabase = require("../dbConnection.js"); +const logger = require('../utils/logger'); const { createServiceModel, updateServiceModel, @@ -17,7 +18,7 @@ const getServiceContents = async (req, res) => { .select("title, description, image"); if (error) { - console.error("Error get service contents:", error.message); + logger.error('Error getting service contents', { error: error.message }); return res.status(500).json({ error: "Failed to get service contents" }); } @@ -25,7 +26,7 @@ const getServiceContents = async (req, res) => { .status(200) .json({ message: "Get service contents successfully", data }); } catch (error) { - console.error("Internal server error:", error.message); + logger.error('Internal server error in getServiceContents', { error: error.message }); return res.status(500).json({ error: "Internal server error" }); } }; @@ -62,7 +63,7 @@ const getServiceContentsPage = async (req, res) => { const { data, error, count } = await query; if (error) { - console.error("Error getting service contents:", error.message); + logger.error('Error getting service contents', { error: error.message }); return res.status(500).json({ error: "Failed to get service contents" }); } @@ -75,7 +76,7 @@ const getServiceContentsPage = async (req, res) => { data, }); } catch (error) { - console.error("Internal server error:", error.message); + logger.error('Internal server error in getServiceContentsPage', { error: error.message }); return res.status(500).json({ error: "Internal server error" }); } }; @@ -100,7 +101,7 @@ const createService = async (req, res) => { data: service, }); } catch (error) { - console.error("Error creating service:", error.message); + logger.error('Error creating service', { error: error.message }); res.status(500).json({ error: error.message }); } }; @@ -126,7 +127,7 @@ const updateService = async (req, res) => { data: service, }); } catch (error) { - console.error("Error updating service:", error.message); + logger.error('Error updating service', { error: error.message, serviceId: req.params.id }); res.status(500).json({ error: error.message }); } }; @@ -145,7 +146,7 @@ const deleteService = async (req, res) => { message: "Service deleted successfully", }); } catch (error) { - console.error("Error deleting service:", error.message); + logger.error('Error deleting service', { error: error.message, serviceId: req.params.id }); res.status(500).json({ error: error.message }); } }; @@ -167,7 +168,7 @@ const addSubscribe = async (req, res) => { data: subscribe, }); } catch (error) { - console.error("Error creating subscribe:", error.message); + logger.error('Error creating subscribe', { error: error.message, email: req.body.email }); res.status(500).json({ error: error.message }); } }; diff --git a/controller/imageClassificationController.js b/controller/imageClassificationController.js index 75600b7..16e673b 100644 --- a/controller/imageClassificationController.js +++ b/controller/imageClassificationController.js @@ -1,12 +1,13 @@ const fs = require('fs'); const path = require('path'); +const logger = require('../utils/logger'); const { executePythonScript } = require('../services/aiExecutionService'); // Utility to delete the uploaded file const deleteFile = (filePath) => { fs.unlink(filePath, (err) => { if (err) { - console.error('Error deleting file:', err); + logger.error('Error deleting image file', { filePath, error: err.message }); } }); }; @@ -58,7 +59,7 @@ const predictImage = async (req, res) => { error: null }); } catch (error) { - console.error('Error reading image file:', error); + logger.error('Error reading image file', { error: error.message, filePath: imagePath }); return res.status(500).json({ success: false, prediction: null, diff --git a/controller/loginController.js b/controller/loginController.js index 2812c9d..7b57e99 100644 --- a/controller/loginController.js +++ b/controller/loginController.js @@ -1,5 +1,6 @@ const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); +const logger = require('../utils/logger'); const logLoginEvent = require('../Monitor_&_Logging/loginLogger'); const getUserCredentials = require('../model/getUserCredentials.js'); const { addMfaToken, verifyMfaToken } = require('../model/addMfaToken.js'); @@ -207,7 +208,7 @@ const login = async (req, res) => { const token = createAccessToken(user); return res.status(200).json({ user, token }); } catch (err) { - console.error('Login error:', err); + logger.error('Login error', { error: err.message, email }); return res.status(500).json({ error: 'Internal server error' }); } }; @@ -259,7 +260,7 @@ const loginMfa = async (req, res) => { trusted_device: rememberDevice }); } catch (err) { - console.error('MFA login error:', err); + logger.error('MFA login error', { error: err.message, email }); return res.status(500).json({ error: 'Internal server error' }); } }; @@ -293,7 +294,7 @@ const resendMfa = async (req, res) => { message: 'A new MFA token has been sent to your email address' }); } catch (err) { - console.error('Resend MFA error:', err); + logger.error('Resend MFA error', { error: err.message, email }); return res.status(500).json({ error: 'Internal server error' }); } }; @@ -314,9 +315,9 @@ async function sendOtpEmail(email, token) {
- NutriHelp Security Team
` }); - console.log('OTP email sent successfully to', email); + logger.info('OTP email sent successfully', { email }); } catch (err) { - console.error('Error sending OTP email:', err.message); + logger.error('Error sending OTP email', { error: err.message, email }); } } @@ -335,9 +336,10 @@ async function sendFailedLoginAlert(email, ip) {- NutriHelp Security Team
` }); - } catch (error) { - console.error('Failed to send alert email:', error.message); + logger.info('Failed login alert sent', { email, ip }); + } catch (err) { + logger.error('Failed to send alert email', { error: err.message, email }); } } -module.exports = { login, loginMfa, resendMfa }; +module.exports = { login, loginMfa, resendMfa }; \ No newline at end of file diff --git a/controller/medicalPredictionController.js b/controller/medicalPredictionController.js index 8d32e63..e3b0ad9 100644 --- a/controller/medicalPredictionController.js +++ b/controller/medicalPredictionController.js @@ -1,5 +1,6 @@ const { medicalPredictionService } = require('../services/medicalPredictionService'); const { isServiceError } = require('../services/serviceError'); +const logger = require('../utils/logger'); async function predict(req, res) { try { @@ -13,9 +14,9 @@ async function predict(req, res) { }); } - console.error('[predict] Unexpected error:', error); + logger.error('Unexpected error in medical prediction', { error: error.message }); return res.status(500).json({ error: 'Internal server error' }); } } -module.exports = { predict }; +module.exports = { predict }; \ No newline at end of file diff --git a/controller/notificationController.js b/controller/notificationController.js index c4ce8e1..89ddc1a 100644 --- a/controller/notificationController.js +++ b/controller/notificationController.js @@ -1,4 +1,5 @@ const supabase = require('../dbConnection.js'); +const logger = require('../utils/logger'); // Create a new notification exports.createNotification = async (req, res) => { @@ -13,7 +14,7 @@ exports.createNotification = async (req, res) => { res.status(201).json({ message: 'Notification created', notification: data }); } catch (error) { - console.error('Error creating notification:', error); + logger.error('Error creating notification', { error: error.message, user_id: req.body.user_id }); res.status(500).json({ error: 'An error occurred while creating the notification' }); } }; @@ -36,7 +37,7 @@ exports.getNotificationsByUserId = async (req, res) => { res.status(200).json(data); } catch (error) { - console.error('Error retrieving notifications:', error); + logger.error('Error retrieving notifications', { error: error.message, user_id: req.params.user_id }); res.status(500).json({ error: 'An error occurred while retrieving notifications' }); } }; @@ -54,7 +55,7 @@ exports.updateNotificationStatusById = async (req, res) => { if (error) { - console.error('Error updating notification:', error); + logger.error('Error updating notification', { error: error.message, notificationId: id }); return res.status(500).json({ error: 'Failed to update notification' }); } @@ -65,7 +66,7 @@ exports.updateNotificationStatusById = async (req, res) => { res.status(200).json({ message: 'Notification updated successfully', notification: data }); } catch (error) { - console.error('Error updating notification:', error); + logger.error('Error updating notification', { error: error.message, notificationId: req.params.id }); res.status(500).json({ error: 'An error occurred while updating the notification' }); } }; @@ -82,7 +83,7 @@ exports.deleteNotificationById = async (req, res) => { if (error) { - console.error('Error deleting notification:', error); + logger.error('Error deleting notification', { error: error.message, notificationId: id }); return res.status(500).json({ error: 'Failed to delete notification' }); } @@ -93,7 +94,7 @@ exports.deleteNotificationById = async (req, res) => { res.status(200).json({ message: 'Notification deleted successfully' }); } catch (error) { - console.error('Error deleting notification:', error); + logger.error('Error deleting notification', { error: error.message, notificationId: req.params.id }); res.status(500).json({ error: 'An error occurred while deleting the notification' }); } }; @@ -121,7 +122,7 @@ exports.markAllUnreadNotificationsAsRead = async (req, res) => { res.status(200).json({ message: 'All unread notifications marked as read', updatedNotifications: data }); } catch (error) { - console.error('Error marking notifications as read:', error); + logger.error('Error marking notifications as read', { error: error.message, user_id: req.params.user_id }); res.status(500).json({ error: 'An error occurred while marking notifications as read' }); } }; diff --git a/controller/uploadController.js b/controller/uploadController.js index 04fb478..5e8ed24 100644 --- a/controller/uploadController.js +++ b/controller/uploadController.js @@ -1,4 +1,5 @@ const multer = require('multer'); +const logger = require('../utils/logger'); const { createClient } = require('@supabase/supabase-js'); const supabase = createClient( @@ -76,7 +77,7 @@ exports.uploadFile = async (req, res) => { return res.status(201).json({ message: 'File uploaded successfully', fileUrl: fileUrl }); } catch (error) { - console.error('❌ File upload failed:', error); + logger.error('File upload failed', { error: error.message, userId: req.user?.userId }); return res.status(500).json({ error: 'File upload failed' }); } }); diff --git a/controller/userPreferencesController.js b/controller/userPreferencesController.js index fc7b590..f99205c 100644 --- a/controller/userPreferencesController.js +++ b/controller/userPreferencesController.js @@ -1,4 +1,5 @@ const fetchUserPreferences = require("../model/fetchUserPreferences"); +const logger = require('../utils/logger'); const updateUserPreferences = require("../model/updateUserPreferences"); const getUserPreferences = async (req, res) => { @@ -17,7 +18,7 @@ const getUserPreferences = async (req, res) => { return res.status(200).json(userPreferences); } catch (error) { - console.error(error); + logger.error('Error fetching user preferences', { error: error.message, userId: req.user?.userId }); return res.status(500).json({ error: "Internal server error" }); } }; @@ -31,7 +32,7 @@ const postUserPreferences = async (req, res) => { .status(204) .json({ message: "User preferences updated successfully" }); } catch (error) { - console.error(error); + logger.error('Error updating user preferences', { error: error.message, userId: req.body.user?.userId }); return res.status(500).json({ error: "Internal server error" }); } }; diff --git a/middleware/requestLogger.js b/middleware/requestLogger.js new file mode 100644 index 0000000..8cf5bdb --- /dev/null +++ b/middleware/requestLogger.js @@ -0,0 +1,83 @@ +/** + * middleware/requestLogger.js + * + * Structured request logging middleware + * Logs all incoming requests and responses + */ + +const logger = require('../utils/logger'); + +/** + * Generate unique request ID + */ +const generateRequestId = () => { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; +}; + +/** + * Main request logging middleware + */ +const requestLoggingMiddleware = (req, res, next) => { + // Generate and attach request ID + const requestId = req.headers['x-request-id'] || generateRequestId(); + req.id = requestId; + + // Track request start time + const startTime = Date.now(); + + // Extract useful request info + const method = req.method; + const path = req.path; + const ip = req.ip || req.connection.remoteAddress; + const userAgent = req.headers['user-agent'] || 'unknown'; + + // Log incoming request + logger.info(`→ ${method} ${path}`, { + requestId, + method, + path, + ip, + userAgent, + ...(req.user ? { userId: req.user.id } : {}), + query: Object.keys(req.query).length > 0 ? req.query : undefined + }); + + // Capture response details + const originalSend = res.send; + res.send = function(data) { + // Calculate duration + const duration = Date.now() - startTime; + const statusCode = res.statusCode; + + // Determine log level based on status code + let logLevel = 'info'; + if (statusCode >= 500) logLevel = 'error'; + else if (statusCode >= 400) logLevel = 'warn'; + else if (duration > 5000) logLevel = 'warn'; // Slow request + + // Log response + const logMessage = `← ${method} ${path} ${statusCode} (${duration}ms)`; + + logger[logLevel](logMessage, { + requestId, + method, + path, + statusCode, + duration, + ...(req.user ? { userId: req.user.id } : {}), + contentLength: res.get('content-length'), + ...(logLevel === 'error' ? { responseBody: data } : {}) + }); + + // Call original send + return originalSend.call(this, data); + }; + + // Attach logger to request for use in controllers + req.logger = logger; + req.requestId = requestId; + + next(); +}; + +module.exports = { requestLoggingMiddleware, generateRequestId }; diff --git a/middleware/structuredErrorHandler.js b/middleware/structuredErrorHandler.js new file mode 100644 index 0000000..8e01851 --- /dev/null +++ b/middleware/structuredErrorHandler.js @@ -0,0 +1,79 @@ +/** + * middleware/structuredErrorHandler.js + * + * Centralized error handling with structured logging + * Should be used as the last middleware in Express + */ + +const logger = require('../utils/logger'); + +/** + * Structured error handling middleware + * Must be defined after all other middleware and routes + */ +const structuredErrorHandler = (err, req, res, next) => { + // Determine error status code + const statusCode = err.statusCode || err.status || 500; + const isClientError = statusCode >= 400 && statusCode < 500; + const isServerError = statusCode >= 500; + + // Build error context + const errorContext = { + requestId: req.id || req.requestId, + userId: req.user?.id, + method: req.method, + path: req.path, + ip: req.ip, + statusCode, + errorType: err.constructor.name, + errorCode: err.code, + validation: err.validation, // For validation errors + details: err.details // Additional error details + }; + + // Log the error with appropriate level + if (isServerError) { + logger.error(`Server Error: ${err.message}`, { + ...errorContext, + stack: err.stack, + ...(err.originalError && { originalError: err.originalError }) + }); + } else if (isClientError) { + logger.warn(`Client Error: ${err.message}`, { + ...errorContext, + // Don't include stack trace for client errors + }); + } else { + logger.info(`Error: ${err.message}`, errorContext); + } + + // Send error response + res.status(statusCode).json({ + success: false, + error: { + message: err.message, + code: err.code || 'INTERNAL_ERROR', + ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }) + }, + requestId: req.id || req.requestId, + ...(process.env.NODE_ENV === 'development' && { details: err.details }) + }); +}; + +/** + * Custom error class for structured errors + */ +class AppError extends Error { + constructor(message, statusCode = 500, code = 'ERROR', details = {}) { + super(message); + this.statusCode = statusCode; + this.code = code; + this.details = details; + Error.captureStackTrace(this, this.constructor); + } +} + +module.exports = { + structuredErrorHandler, + AppError +}; diff --git a/package.json b/package.json index aa8d5f5..963fc66 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "sinon": "18.0.0", "swagger-ui-express": "5.0.0", "twilio": "5.9.0", + "winston": "^3.11.0", "yamljs": "0.3.0" } } \ No newline at end of file diff --git a/server.js b/server.js index b1c8ffd..d385836 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,10 @@ require("dotenv").config(); +// Structured Logging - NEW +const logger = require('./utils/logger'); +const { requestLoggingMiddleware } = require('./middleware/requestLogger'); +const { structuredErrorHandler } = require('./middleware/structuredErrorHandler'); + //Logging & Metrics const { metricsMiddleware, @@ -74,8 +79,8 @@ const port = process.env.PORT || 80; // DB let db = require("./dbConnection"); -// System routes -app.use("/api/system", systemRoutes); +// ⚠️ CRITICAL: Add request logging middleware FIRST, before any routes +app.use(requestLoggingMiddleware); // CORS app.use( @@ -146,6 +151,9 @@ app.use(express.urlencoded({ limit: "50mb", extended: true })); app.use(metricsMiddleware); app.get("/metrics", metricsEndpoint); +// System routes (early in chain) +app.use("/api/system", systemRoutes); + // Main routes registrar const routes = require("./routes"); routes(app); @@ -161,20 +169,8 @@ app.use("/security", securityEventsRoutes); // Error handler app.use(errorLogger); -// Final error handler -app.use((err, req, res, next) => { - const status = err.status || 500; - const message = - process.env.NODE_ENV === "production" - ? "Internal Server Error" - : err.message; - - res.status(status).json({ - success: false, - error: message, - timestamp: new Date().toISOString(), - }); -}); +// Structured error handling middleware (MUST be last) +app.use(structuredErrorHandler); // Global error handler const { diff --git a/utils/logger.js b/utils/logger.js new file mode 100644 index 0000000..60d4d26 --- /dev/null +++ b/utils/logger.js @@ -0,0 +1,163 @@ +/** + * utils/logger.js + * + * Centralized structured logging using Winston + * + * Usage: + * const logger = require('../utils/logger'); + * logger.info('User logged in', { userId: 123, email: 'user@example.com' }); + * logger.error('Database error', { error: err, query: sql }); + * logger.warn('Rate limit approaching', { userId: 123, limit: 1000 }); + */ + +const winston = require('winston'); +const path = require('path'); +const fs = require('fs'); + +// Ensure logs directory exists +const logsDir = path.join(process.cwd(), 'logs'); +if (!fs.existsSync(logsDir)) { + fs.mkdirSync(logsDir, { recursive: true }); +} + +/** + * Custom format for structured logging + */ +const customFormat = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.json(), + // Custom printf for console (more readable) + winston.format.printf(({ timestamp, level, message, requestId, userId, ...meta }) => { + let baseLog = `${timestamp} [${level.toUpperCase()}]`; + + if (requestId) baseLog += ` [REQ:${requestId}]`; + if (userId) baseLog += ` [USER:${userId}]`; + + baseLog += ` ${message}`; + + // Add metadata if present + if (Object.keys(meta).length > 0) { + // Filter out the symbol used by winston + const cleanMeta = Object.fromEntries( + Object.entries(meta).filter(([key]) => !key.startsWith('Symbol')) + ); + if (Object.keys(cleanMeta).length > 0) { + baseLog += ` ${JSON.stringify(cleanMeta)}`; + } + } + + return baseLog; + }) +); + +/** + * Create logger with Winston + */ +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'), + format: customFormat, + defaultMeta: { + service: 'nutrihelp-api', + environment: process.env.NODE_ENV || 'development' + }, + transports: [ + // Console output (always) + new winston.transports.Console({ + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + format: winston.format.combine( + winston.format.colorize(), + winston.format.printf(({ timestamp, level, message, requestId, userId, ...meta }) => { + let output = `${timestamp} ${level} `; + if (requestId) output += `(${requestId}) `; + if (userId) output += `[${userId}] `; + output += message; + + if (Object.keys(meta).length > 0) { + const cleanMeta = Object.fromEntries( + Object.entries(meta).filter(([key]) => !key.startsWith('Symbol')) + ); + if (Object.keys(cleanMeta).length > 0) { + output += ` ${JSON.stringify(cleanMeta, null, 2)}`; + } + } + return output; + }) + ) + }), + + // File logging - all logs + new winston.transports.File({ + filename: path.join(logsDir, 'app.log'), + maxsize: 10485760, // 10MB + maxFiles: 5, + format: winston.format.json() + }), + + // File logging - errors only + new winston.transports.File({ + filename: path.join(logsDir, 'error.log'), + level: 'error', + maxsize: 10485760, + maxFiles: 10, + format: winston.format.json() + }), + + // File logging - requests (if in production) + ...(process.env.NODE_ENV === 'production' ? [ + new winston.transports.File({ + filename: path.join(logsDir, 'requests.log'), + maxsize: 10485760, + maxFiles: 5, + format: winston.format.json() + }) + ] : []) + ] +}); + +/** + * Log levels: + * error (0): Something failed + * warn (1): Something concerning but not fatal + * info (2): General information + * http (3): HTTP requests/responses + * debug (4): Detailed debugging information + */ + +/** + * Helper to create error log with standard format + */ +logger.logError = (message, error, context = {}) => { + logger.error(message, { + error: { + message: error?.message, + code: error?.code, + stack: error?.stack + }, + ...context + }); +}; + +/** + * Helper to create HTTP log with standard format + */ +logger.logHttpRequest = (method, path, statusCode, duration, context = {}) => { + logger.info(`${method} ${path} - ${statusCode}`, { + http: { + method, + path, + statusCode, + duration: `${duration}ms` + }, + ...context + }); +}; + +/** + * Helper for database operations + */ +logger.logDB = (operation, table, context = {}) => { + logger.debug(`DB: ${operation} on ${table}`, context); +}; + +module.exports = logger;