diff --git a/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/config/RequestLoggingFilter.java b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/config/RequestLoggingFilter.java
new file mode 100644
index 0000000..6097939
--- /dev/null
+++ b/mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/config/RequestLoggingFilter.java
@@ -0,0 +1,105 @@
+package com.mongodb.samplemflix.config;
+
+import jakarta.servlet.FilterChain;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * HTTP Request Logging Filter
+ *
+ *
This filter logs all incoming HTTP requests with useful information
+ * including method, URL, status code, and response time.
+ * It helps with debugging and monitoring application traffic.
+ *
+ *
Log output format:
+ *
+ * INFO - GET /api/movies 200 - 45ms
+ * WARN - GET /api/movies/invalid 400 - 2ms
+ * ERROR - POST /api/movies 500 - 120ms
+ *
+ *
+ * The filter is ordered to run first in the filter chain to ensure
+ * accurate timing measurements.
+ */
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE)
+public class RequestLoggingFilter extends OncePerRequestFilter {
+
+ private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
+
+ @Override
+ protected void doFilterInternal(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+
+ // Record the start time
+ long startTime = System.currentTimeMillis();
+
+ // Log incoming request at debug level
+ logger.debug("Incoming request: {} {} from {}",
+ request.getMethod(),
+ request.getRequestURI(),
+ request.getRemoteAddr());
+
+ try {
+ // Continue with the filter chain
+ filterChain.doFilter(request, response);
+ } finally {
+ // Calculate response time
+ long responseTime = System.currentTimeMillis() - startTime;
+
+ // Log the completed request with appropriate level based on status code
+ logRequest(request.getMethod(), request.getRequestURI(), response.getStatus(), responseTime);
+ }
+ }
+
+ /**
+ * Logs the HTTP request with appropriate log level based on status code.
+ *
+ *
Log levels:
+ *
+ * - ERROR: 5xx server errors
+ * - WARN: 4xx client errors
+ * - INFO: 2xx and 3xx success/redirect
+ *
+ *
+ * @param method HTTP method (GET, POST, etc.)
+ * @param uri Request URI
+ * @param statusCode HTTP response status code
+ * @param responseTime Response time in milliseconds
+ */
+ private void logRequest(String method, String uri, int statusCode, long responseTime) {
+ String message = String.format("%s %s %d - %dms", method, uri, statusCode, responseTime);
+
+ if (statusCode >= 500) {
+ logger.error(message);
+ } else if (statusCode >= 400) {
+ logger.warn(message);
+ } else {
+ logger.info(message);
+ }
+ }
+
+ /**
+ * Skip logging for static resources and health checks to reduce noise.
+ */
+ @Override
+ protected boolean shouldNotFilter(HttpServletRequest request) {
+ String path = request.getRequestURI();
+ return path.startsWith("/swagger-ui")
+ || path.startsWith("/api-docs")
+ || path.startsWith("/v3/api-docs")
+ || path.equals("/favicon.ico")
+ || path.startsWith("/actuator");
+ }
+}
+
diff --git a/mflix/server/java-spring/src/main/resources/application.properties b/mflix/server/java-spring/src/main/resources/application.properties
index 58b8785..ffe611f 100644
--- a/mflix/server/java-spring/src/main/resources/application.properties
+++ b/mflix/server/java-spring/src/main/resources/application.properties
@@ -19,11 +19,22 @@ voyage.api.key=${VOYAGE_API_KEY:}
spring.application.name=sample-app-java-mflix
# Logging Configuration
-logging.level.com.mongodb.samplemflix=INFO
+# Log level can be overridden with LOG_LEVEL environment variable
+# Available levels: TRACE, DEBUG, INFO, WARN, ERROR
+logging.level.com.mongodb.samplemflix=${LOG_LEVEL:INFO}
logging.level.org.mongodb.driver=WARN
# Suppress connection pool maintenance warnings (these are usually harmless)
logging.level.org.mongodb.driver.connection=ERROR
+# Console logging pattern with colors and timestamps
+logging.pattern.console=%clr(%d{HH:mm:ss}){faint} %clr(%5p){highlight} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n
+
+# File logging (optional - enabled when LOG_FILE is set)
+logging.file.name=${LOG_FILE:}
+logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} %5p --- [%15.15t] %-40.40logger{39} : %m%n
+logging.logback.rollingpolicy.max-file-size=5MB
+logging.logback.rollingpolicy.max-history=5
+
# Jackson Configuration (JSON serialization)
spring.jackson.default-property-inclusion=non_null
spring.jackson.serialization.write-dates-as-timestamps=false
diff --git a/mflix/server/js-express/.env.example b/mflix/server/js-express/.env.example
index 725cc0d..c870ee0 100644
--- a/mflix/server/js-express/.env.example
+++ b/mflix/server/js-express/.env.example
@@ -10,6 +10,10 @@ VOYAGE_API_KEY=your_voyage_api_key
PORT=3001
NODE_ENV=development
+# Logging Configuration
+# Available levels: error, warn, info, http, debug
+# Default: debug (development), info (production), error (test)
+LOG_LEVEL=debug
# CORS Configuration
# Allowed origin for cross-origin requests (frontend URL)
diff --git a/mflix/server/js-express/package.json b/mflix/server/js-express/package.json
index 01311ba..85f09a2 100644
--- a/mflix/server/js-express/package.json
+++ b/mflix/server/js-express/package.json
@@ -24,7 +24,8 @@
"express": "^5.1.0",
"mongodb": "^7.0.0",
"swagger-jsdoc": "^6.2.8",
- "swagger-ui-express": "^5.0.1"
+ "swagger-ui-express": "^5.0.1",
+ "winston": "^3.19.0"
},
"devDependencies": {
"@types/cors": "^2.8.17",
diff --git a/mflix/server/js-express/src/app.ts b/mflix/server/js-express/src/app.ts
index 9c5a7e8..6e3d389 100644
--- a/mflix/server/js-express/src/app.ts
+++ b/mflix/server/js-express/src/app.ts
@@ -18,6 +18,8 @@ import {
import { errorHandler } from "./utils/errorHandler";
import moviesRouter from "./routes/movies";
import { swaggerSpec } from "./config/swagger";
+import logger from "./utils/logger";
+import { requestLogger } from "./middleware/requestLogger";
// Load environment variables from .env file
// This must be called before any other imports that use environment variables
@@ -46,6 +48,12 @@ app.use(
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
+/**
+ * Request Logging Middleware
+ * Logs all incoming HTTP requests with method, URL, status code, and response time
+ */
+app.use(requestLogger);
+
/**
* Swagger API Documentation
* Provides interactive API documentation at /api-docs
@@ -119,25 +127,25 @@ app.use(errorHandler);
*/
async function startServer() {
try {
- console.log("Starting MongoDB Sample MFlix API...");
+ logger.info("Starting MongoDB Sample MFlix API...");
// Connect to MongoDB database
- console.log("Connecting to MongoDB...");
+ logger.info("Connecting to MongoDB...");
await connectToDatabase();
- console.log("Connected to MongoDB successfully");
+ logger.info("Connected to MongoDB successfully");
// Verify that all required indexes and sample data exist
- console.log("Verifying requirements (indexes and sample data)...");
+ logger.info("Verifying requirements (indexes and sample data)...");
await verifyRequirements();
- console.log("All requirements verified successfully");
+ logger.info("All requirements verified successfully");
// Start the Express server
app.listen(PORT, () => {
- console.log(`Server running on port ${PORT}`);
- console.log(`API documentation available at http://localhost:${PORT}/api-docs`);
+ logger.info(`Server running on port ${PORT}`);
+ logger.info(`API documentation available at http://localhost:${PORT}/api-docs`);
});
} catch (error) {
- console.error("Failed to start server:", error);
+ logger.error("Failed to start server:", error);
// Exit the process if we can't start properly
// This ensures the application doesn't run in a broken state
@@ -150,13 +158,13 @@ async function startServer() {
* Ensures the application shuts down cleanly when terminated
*/
process.on("SIGINT", () => {
- console.log("\nReceived SIGINT. Shutting down...");
+ logger.info("Received SIGINT. Shutting down gracefully...");
closeDatabaseConnection();
process.exit(0);
});
process.on("SIGTERM", () => {
- console.log("\nReceived SIGTERM. Shutting down...");
+ logger.info("Received SIGTERM. Shutting down gracefully...");
closeDatabaseConnection();
process.exit(0);
});
diff --git a/mflix/server/js-express/src/config/database.ts b/mflix/server/js-express/src/config/database.ts
index 09c5884..62d5a7e 100644
--- a/mflix/server/js-express/src/config/database.ts
+++ b/mflix/server/js-express/src/config/database.ts
@@ -7,6 +7,7 @@
*/
import { MongoClient, Db, Collection, Document } from "mongodb";
+import logger from "../utils/logger";
let client: MongoClient;
let database: Db;
@@ -40,7 +41,7 @@ async function _connectToDatabase(): Promise {
// Get reference to the sample_mflix database
database = client.db("sample_mflix");
- console.log(`Connected to database: ${database.databaseName}`);
+ logger.debug(`Connected to database: ${database.databaseName}`);
return database;
} catch (error) {
@@ -85,7 +86,7 @@ export function getCollection(
export async function closeDatabaseConnection(): Promise {
if (client) {
await client.close();
- console.log("Database connection closed");
+ logger.info("Database connection closed");
}
}
@@ -100,9 +101,9 @@ export async function verifyRequirements(): Promise {
// Check if the movies collection exists and has data
await verifyMoviesCollection(db);
- console.log("All database requirements verified successfully");
+ logger.debug("All database requirements verified successfully");
} catch (error) {
- console.error("Requirements verification failed:", error);
+ logger.error("Requirements verification failed:", error);
throw error;
}
}
@@ -117,22 +118,51 @@ async function verifyMoviesCollection(db: Db): Promise {
const movieCount = await moviesCollection.estimatedDocumentCount();
if (movieCount === 0) {
- console.warn(
+ logger.warn(
"Movies collection is empty. Please ensure sample_mflix data is loaded."
);
}
// Create text search index on plot field for full-text search
+ await createTextSearchIndex(moviesCollection);
+}
+
+/**
+ * Creates a text search index on the movies collection if it doesn't already exist.
+ *
+ * MongoDB only allows one text index per collection, so we check for any existing
+ * text index before attempting to create one.
+ */
+async function createTextSearchIndex(moviesCollection: Collection): Promise {
+ const TEXT_INDEX_NAME = "text_search_index";
+
try {
+ // Check if any text index already exists
+ const existingIndexes = await moviesCollection.listIndexes().toArray();
+ const textIndexExists = existingIndexes.some(
+ (index) => index.key && index.key._fts === "text"
+ );
+
+ if (textIndexExists) {
+ const existingTextIndex = existingIndexes.find(
+ (index) => index.key && index.key._fts === "text"
+ );
+ logger.debug(`Text search index '${existingTextIndex?.name}' already exists on movies collection`);
+ return;
+ }
+
+ // Create the text index
await moviesCollection.createIndex(
{ plot: "text", title: "text", fullplot: "text" },
{
- name: "text_search_index",
+ name: TEXT_INDEX_NAME,
background: true,
}
);
- console.log("Text search index created for movies collection");
+ logger.info(`Text search index '${TEXT_INDEX_NAME}' created successfully for movies collection`);
} catch (error) {
- console.error("Could not create text search index:", error);
+ // Log as warning, not error - the application can still function without the index
+ logger.warn("Could not create text search index:", error);
+ logger.warn("Text search functionality may not work without the index");
}
}
diff --git a/mflix/server/js-express/src/controllers/movieController.ts b/mflix/server/js-express/src/controllers/movieController.ts
index 0d107e5..8861892 100644
--- a/mflix/server/js-express/src/controllers/movieController.ts
+++ b/mflix/server/js-express/src/controllers/movieController.ts
@@ -24,6 +24,7 @@ import {
createSuccessResponse,
validateRequiredFields,
} from "../utils/errorHandler";
+import logger from "../utils/logger";
import {
CreateMovieRequest,
UpdateMovieRequest,
@@ -874,7 +875,7 @@ export async function vectorSearchMovies(req: Request, res: Response): Promise 0 ? req.query : undefined,
+ ip: req.ip,
+ });
+
+ // Log when response finishes
+ res.on("finish", () => {
+ const responseTime = Date.now() - startTime;
+ logHttpRequest(req.method, req.url, res.statusCode, responseTime);
+ });
+
+ next();
+}
diff --git a/mflix/server/js-express/src/utils/errorHandler.ts b/mflix/server/js-express/src/utils/errorHandler.ts
index 35d79a6..865f906 100644
--- a/mflix/server/js-express/src/utils/errorHandler.ts
+++ b/mflix/server/js-express/src/utils/errorHandler.ts
@@ -8,6 +8,7 @@
import { Request, Response, NextFunction } from "express";
import { MongoError } from "mongodb";
import { SuccessResponse, ErrorResponse } from "../types";
+import logger from "./logger";
/**
* Custom ValidationError class for field validation errors
@@ -37,17 +38,13 @@ export function errorHandler(
next: NextFunction
): void {
// Log the error for debugging purposes
- // In production, we recommend using a logging service
- // Suppress error logging during tests to keep test output clean
- if (process.env.NODE_ENV !== "test") {
- console.error("Error occurred:", {
- message: err.message,
- stack: err.stack,
- url: req.url,
- method: req.method,
- timestamp: new Date().toISOString(),
- });
- }
+ // The logger automatically handles environment-specific behavior
+ logger.error("Error occurred:", {
+ message: err.message,
+ stack: err.stack,
+ url: req.url,
+ method: req.method,
+ });
// Determine the appropriate HTTP status code and error message
const errorDetails = parseErrorDetails(err);
diff --git a/mflix/server/js-express/src/utils/logger.ts b/mflix/server/js-express/src/utils/logger.ts
new file mode 100644
index 0000000..60474e1
--- /dev/null
+++ b/mflix/server/js-express/src/utils/logger.ts
@@ -0,0 +1,171 @@
+/**
+ * Logger Utility
+ *
+ * This module provides a centralized logging solution using Winston.
+ * It supports multiple log levels, console and file transports, and
+ * environment-aware formatting for better developer and user experience.
+ *
+ * Log Levels (from highest to lowest priority):
+ * - error: Error events that might still allow the application to continue
+ * - warn: Potentially harmful situations
+ * - info: Informational messages highlighting application progress
+ * - http: HTTP request logging
+ * - debug: Detailed debug information
+ */
+
+import winston from "winston";
+import path from "path";
+
+// Define log levels with custom colors
+const levels = {
+ error: 0,
+ warn: 1,
+ info: 2,
+ http: 3,
+ debug: 4,
+};
+
+// Define colors for each log level
+const colors = {
+ error: "red",
+ warn: "yellow",
+ info: "green",
+ http: "magenta",
+ debug: "cyan",
+};
+
+// Add colors to Winston
+winston.addColors(colors);
+
+/**
+ * Determine the log level based on environment
+ * - In development: show all logs (debug level)
+ * - In production: show info and above
+ * - In test: show only errors (or suppress entirely)
+ */
+function getLogLevel(): string {
+ const env = process.env.NODE_ENV || "development";
+ const envLogLevel = process.env.LOG_LEVEL;
+
+ // Allow explicit override via LOG_LEVEL env var
+ if (envLogLevel) {
+ return envLogLevel;
+ }
+
+ // Default levels based on environment
+ switch (env) {
+ case "production":
+ return "info";
+ case "test":
+ return "error";
+ default:
+ return "debug";
+ }
+}
+
+/**
+ * Console format for development - colorized and readable
+ */
+const devConsoleFormat = winston.format.combine(
+ winston.format.timestamp({ format: "HH:mm:ss" }),
+ winston.format.colorize({ all: true }),
+ winston.format.printf(({ timestamp, level, message, ...meta }) => {
+ const metaStr = Object.keys(meta).length ? ` ${JSON.stringify(meta)}` : "";
+ return `[${timestamp}] ${level}: ${message}${metaStr}`;
+ })
+);
+
+/**
+ * Console format for production - structured JSON
+ */
+const prodConsoleFormat = winston.format.combine(
+ winston.format.timestamp(),
+ winston.format.json()
+);
+
+/**
+ * File format - JSON for easy parsing
+ */
+const fileFormat = winston.format.combine(
+ winston.format.timestamp(),
+ winston.format.uncolorize(),
+ winston.format.json()
+);
+
+/**
+ * Create transports based on environment
+ */
+function createTransports(): winston.transport[] {
+ const env = process.env.NODE_ENV || "development";
+ const transports: winston.transport[] = [];
+
+ // Console transport - always enabled except in test
+ if (env !== "test") {
+ transports.push(
+ new winston.transports.Console({
+ format: env === "production" ? prodConsoleFormat : devConsoleFormat,
+ })
+ );
+ }
+
+ // File transports - enabled in production and development (not test)
+ if (env !== "test") {
+ const logsDir = path.join(process.cwd(), "logs");
+
+ // Error log - only errors
+ transports.push(
+ new winston.transports.File({
+ filename: path.join(logsDir, "error.log"),
+ level: "error",
+ format: fileFormat,
+ maxsize: 5242880, // 5MB
+ maxFiles: 5,
+ })
+ );
+
+ // Combined log - all logs
+ transports.push(
+ new winston.transports.File({
+ filename: path.join(logsDir, "combined.log"),
+ format: fileFormat,
+ maxsize: 5242880, // 5MB
+ maxFiles: 5,
+ })
+ );
+ }
+
+ return transports;
+}
+
+/**
+ * Create the Winston logger instance
+ */
+const logger = winston.createLogger({
+ level: getLogLevel(),
+ levels,
+ transports: createTransports(),
+ // Don't exit on handled exceptions
+ exitOnError: false,
+});
+
+/**
+ * Log an HTTP request
+ * @param method - HTTP method (GET, POST, etc.)
+ * @param url - Request URL
+ * @param statusCode - Response status code
+ * @param responseTime - Response time in milliseconds
+ */
+export function logHttpRequest(
+ method: string,
+ url: string,
+ statusCode: number,
+ responseTime: number
+): void {
+ const statusColor =
+ statusCode >= 500 ? "error" : statusCode >= 400 ? "warn" : "http";
+
+ logger.log(statusColor, `${method} ${url} ${statusCode} - ${responseTime}ms`);
+}
+
+export default logger;
+
diff --git a/mflix/server/python-fastapi/.env.example b/mflix/server/python-fastapi/.env.example
index d42141d..6dc1d0d 100644
--- a/mflix/server/python-fastapi/.env.example
+++ b/mflix/server/python-fastapi/.env.example
@@ -10,3 +10,9 @@ VOYAGE_API_KEY=your_voyage_api_key
# CORS Configuration
# Comma-separated list of allowed origins for CORS
CORS_ORIGINS="http://localhost:3000,http://localhost:3001"
+
+# Logging Configuration
+# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)
+LOG_LEVEL=INFO
+# Optional: Path to log file (if not set, logs only to console)
+# LOG_FILE=app.log
diff --git a/mflix/server/python-fastapi/main.py b/mflix/server/python-fastapi/main.py
index 99913ae..81048f9 100644
--- a/mflix/server/python-fastapi/main.py
+++ b/mflix/server/python-fastapi/main.py
@@ -6,6 +6,8 @@
from src.database.mongo_client import db, get_collection
from src.utils.exceptions import VoyageAuthError, VoyageAPIError
from src.utils.errorResponse import create_error_response
+from src.utils.logger import logger
+from src.middleware.request_logging import RequestLoggingMiddleware
import os
from dotenv import load_dotenv
@@ -21,12 +23,12 @@ async def lifespan(app: FastAPI):
await ensure_vector_search_index()
await ensure_standard_index()
- # Print server information
- print(f"\n{'='*60}")
- print(f" Server started at http://127.0.0.1:3001")
- print(f" Documentation at http://127.0.0.1:3001/docs")
- print(f" Interactive API docs at http://127.0.0.1:3001/redoc")
- print(f"{'='*60}\n")
+ # Log server information
+ logger.info("=" * 60)
+ logger.info(" Server started at http://127.0.0.1:3001")
+ logger.info(" Documentation at http://127.0.0.1:3001/docs")
+ logger.info(" Interactive API docs at http://127.0.0.1:3001/redoc")
+ logger.info("=" * 60)
yield
# Shutdown: Clean up resources if needed
@@ -133,8 +135,8 @@ async def ensure_standard_index():
await comments_collection.create_index([("movie_id", 1)], name=standard_index_name)
except Exception as e:
- print(f"Failed to create standard index on 'comments' collection: {str(e)}. ")
- print(f"Performance may be degraded. Please check your MongoDB configuration.")
+ logger.warning(f"Failed to create standard index on 'comments' collection: {str(e)}")
+ logger.warning("Performance may be degraded. Please check your MongoDB configuration.")
app = FastAPI(lifespan=lifespan)
@@ -174,5 +176,8 @@ async def voyage_api_error_handler(request: Request, exc: VoyageAPIError):
allow_headers=["*"],
)
+# Add request logging middleware
+app.add_middleware(RequestLoggingMiddleware)
+
app.include_router(movies.router, prefix="/api/movies", tags=["movies"])
diff --git a/mflix/server/python-fastapi/src/middleware/__init__.py b/mflix/server/python-fastapi/src/middleware/__init__.py
new file mode 100644
index 0000000..674d994
--- /dev/null
+++ b/mflix/server/python-fastapi/src/middleware/__init__.py
@@ -0,0 +1,6 @@
+"""Middleware package for FastAPI application."""
+
+from src.middleware.request_logging import RequestLoggingMiddleware
+
+__all__ = ["RequestLoggingMiddleware"]
+
diff --git a/mflix/server/python-fastapi/src/middleware/request_logging.py b/mflix/server/python-fastapi/src/middleware/request_logging.py
new file mode 100644
index 0000000..fc997bf
--- /dev/null
+++ b/mflix/server/python-fastapi/src/middleware/request_logging.py
@@ -0,0 +1,97 @@
+"""
+Request logging middleware for FastAPI.
+
+This middleware logs all incoming HTTP requests with useful information
+including method, URL, status code, and response time.
+
+Log output format:
+ INFO - GET /api/movies 200 - 45ms
+ WARN - GET /api/movies/invalid 400 - 2ms
+ ERROR - POST /api/movies 500 - 120ms
+"""
+
+import time
+from fastapi import Request, Response
+from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
+from src.utils.logger import logger
+
+
+# Paths to skip logging (reduces noise)
+SKIP_PATHS = {
+ "/docs",
+ "/redoc",
+ "/openapi.json",
+ "/favicon.ico",
+ "/health",
+}
+
+
+class RequestLoggingMiddleware(BaseHTTPMiddleware):
+ """
+ Middleware that logs HTTP requests with timing information.
+
+ Features:
+ - Logs method, path, status code, and response time
+ - Uses appropriate log level based on status code:
+ - ERROR: 5xx server errors
+ - WARNING: 4xx client errors
+ - INFO: 2xx and 3xx success/redirect
+ - Skips logging for documentation and static paths
+ """
+
+ async def dispatch(
+ self, request: Request, call_next: RequestResponseEndpoint
+ ) -> Response:
+ # Skip logging for certain paths
+ if request.url.path in SKIP_PATHS:
+ return await call_next(request)
+
+ # Record start time
+ start_time = time.perf_counter()
+
+ # Log incoming request at debug level
+ logger.debug(
+ f"Incoming request: {request.method} {request.url.path} from {request.client.host if request.client else 'unknown'}"
+ )
+
+ # Process the request
+ response = await call_next(request)
+
+ # Calculate response time in milliseconds
+ response_time_ms = (time.perf_counter() - start_time) * 1000
+
+ # Log the completed request with appropriate level
+ self._log_request(
+ method=request.method,
+ path=request.url.path,
+ status_code=response.status_code,
+ response_time_ms=response_time_ms
+ )
+
+ return response
+
+ def _log_request(
+ self,
+ method: str,
+ path: str,
+ status_code: int,
+ response_time_ms: float
+ ) -> None:
+ """
+ Log the HTTP request with appropriate log level based on status code.
+
+ Args:
+ method: HTTP method (GET, POST, etc.)
+ path: Request path
+ status_code: HTTP response status code
+ response_time_ms: Response time in milliseconds
+ """
+ message = f"{method} {path} {status_code} - {response_time_ms:.0f}ms"
+
+ if status_code >= 500:
+ logger.error(message)
+ elif status_code >= 400:
+ logger.warning(message)
+ else:
+ logger.info(message)
+
diff --git a/mflix/server/python-fastapi/src/utils/logger.py b/mflix/server/python-fastapi/src/utils/logger.py
new file mode 100644
index 0000000..703ce3b
--- /dev/null
+++ b/mflix/server/python-fastapi/src/utils/logger.py
@@ -0,0 +1,155 @@
+"""
+Logging configuration for the FastAPI application.
+
+This module provides a centralized logging setup with:
+- Colorized console output for better readability
+- Configurable log levels via environment variables
+- Optional file logging
+- Request logging middleware
+
+Usage:
+ from src.utils.logger import logger
+
+ logger.info("Server started")
+ logger.debug("Processing request")
+ logger.error("Something went wrong", exc_info=True)
+"""
+
+import logging
+import os
+import sys
+from datetime import datetime
+from typing import Optional
+
+# ANSI color codes for terminal output
+class Colors:
+ """ANSI color codes for colorized log output."""
+ RESET = "\033[0m"
+ BOLD = "\033[1m"
+ FAINT = "\033[2m"
+
+ # Log level colors
+ DEBUG = "\033[36m" # Cyan
+ INFO = "\033[32m" # Green
+ WARNING = "\033[33m" # Yellow
+ ERROR = "\033[31m" # Red
+ CRITICAL = "\033[35m" # Magenta
+
+ # Component colors
+ TIMESTAMP = "\033[90m" # Gray
+ LOGGER_NAME = "\033[36m" # Cyan
+
+
+class ColoredFormatter(logging.Formatter):
+ """
+ Custom formatter that adds colors to log output.
+
+ Format: HH:MM:SS LEVEL --- [logger_name] : message
+ """
+
+ LEVEL_COLORS = {
+ logging.DEBUG: Colors.DEBUG,
+ logging.INFO: Colors.INFO,
+ logging.WARNING: Colors.WARNING,
+ logging.ERROR: Colors.ERROR,
+ logging.CRITICAL: Colors.CRITICAL,
+ }
+
+ def format(self, record: logging.LogRecord) -> str:
+ # Get the color for this log level
+ level_color = self.LEVEL_COLORS.get(record.levelno, Colors.RESET)
+
+ # Format timestamp
+ timestamp = datetime.fromtimestamp(record.created).strftime("%H:%M:%S")
+
+ # Format level name (padded to 5 chars)
+ level_name = f"{record.levelname:>5}"
+
+ # Format logger name (truncated to 40 chars)
+ logger_name = record.name[-40:] if len(record.name) > 40 else record.name
+
+ # Build the formatted message
+ formatted = (
+ f"{Colors.FAINT}{timestamp}{Colors.RESET} "
+ f"{level_color}{level_name}{Colors.RESET} "
+ f"{Colors.FAINT}---{Colors.RESET} "
+ f"{Colors.FAINT}[{Colors.RESET}"
+ f"{Colors.LOGGER_NAME}{logger_name:>40}{Colors.RESET}"
+ f"{Colors.FAINT}]{Colors.RESET} "
+ f"{Colors.FAINT}:{Colors.RESET} "
+ f"{record.getMessage()}"
+ )
+
+ # Add exception info if present
+ if record.exc_info:
+ formatted += "\n" + self.formatException(record.exc_info)
+
+ return formatted
+
+
+class PlainFormatter(logging.Formatter):
+ """Plain text formatter for file logging (no colors)."""
+
+ def format(self, record: logging.LogRecord) -> str:
+ timestamp = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
+ level_name = f"{record.levelname:>5}"
+ logger_name = record.name[-40:] if len(record.name) > 40 else record.name
+
+ formatted = f"{timestamp} {level_name} --- [{logger_name:>40}] : {record.getMessage()}"
+
+ if record.exc_info:
+ formatted += "\n" + self.formatException(record.exc_info)
+
+ return formatted
+
+
+def setup_logger(
+ name: str = "mflix",
+ level: Optional[str] = None,
+ log_file: Optional[str] = None
+) -> logging.Logger:
+ """
+ Set up and configure a logger instance.
+
+ Args:
+ name: Logger name (default: "mflix")
+ level: Log level (default: from LOG_LEVEL env var or INFO)
+ log_file: Optional file path for file logging
+
+ Returns:
+ Configured logger instance
+ """
+ # Get log level from environment or parameter
+ log_level_str = level or os.getenv("LOG_LEVEL", "INFO").upper()
+ log_level = getattr(logging, log_level_str, logging.INFO)
+
+ # Create logger
+ logger = logging.getLogger(name)
+ logger.setLevel(log_level)
+
+ # Remove existing handlers to avoid duplicates
+ logger.handlers.clear()
+
+ # Console handler with colors
+ console_handler = logging.StreamHandler(sys.stdout)
+ console_handler.setLevel(log_level)
+ console_handler.setFormatter(ColoredFormatter())
+ logger.addHandler(console_handler)
+
+ # File handler (optional)
+ file_path = log_file or os.getenv("LOG_FILE")
+ if file_path:
+ file_handler = logging.FileHandler(file_path)
+ file_handler.setLevel(log_level)
+ file_handler.setFormatter(PlainFormatter())
+ logger.addHandler(file_handler)
+
+ # Prevent propagation to root logger
+ logger.propagate = False
+
+ return logger
+
+
+# Create the default application logger
+logger = setup_logger()
+