Skip to main content

Flask: Middleware for Logging and Metrics

Flask: Middleware for Logging and Metrics

Middleware in Flask is a powerful way to centralize logging and metrics collection, enabling developers to monitor application behavior, track performance, and debug issues efficiently. By leveraging WSGI middleware, Flask applications can capture request details, measure latency, and integrate with tools like Prometheus for metrics. This tutorial explores Flask middleware for logging and metrics, covering setup, implementation, integration with Prometheus, and best practices for robust monitoring.


01. Why Use Middleware for Logging and Metrics in Flask?

Middleware allows global processing of requests and responses, making it ideal for logging request details (e.g., method, path, client IP) and collecting metrics (e.g., latency, request counts). Flask’s WSGI-based architecture supports middleware to centralize these concerns, reducing code duplication and improving maintainability. Logging and metrics ensure observability, performance optimization, and proactive issue detection, critical for production-grade applications.

Example: Basic Logging Middleware

from flask import Flask
from werkzeug.wrappers import Request
import logging

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        logger.info(f"{request.method} {request.path} from {request.remote_addr}")
        return self.wsgi_app(environ, start_response)

app.wsgi_app = LoggingMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Hello, Flask!"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
INFO:__main__:GET / from 127.0.0.1

Explanation:

  • LoggingMiddleware - Logs request method, path, and client IP for all routes.
  • app.wsgi_app - Integrates middleware into Flask’s WSGI pipeline.

02. Key Middleware Techniques for Logging and Metrics

Middleware for logging and metrics can capture detailed request data, measure performance, and integrate with monitoring systems. These techniques ensure comprehensive observability. The table below summarizes key techniques and their applications:

Technique Description Use Case
Request Logging Log request details (method, path, IP) Auditing, debugging
File-Based Logging Save logs to files Persistent monitoring
Latency Metrics Measure request processing time Performance optimization
Prometheus Metrics Expose metrics for scraping Real-time monitoring
Error Tracking Log exceptions and errors Issue diagnosis


2.1 Request Logging Middleware

Example: Detailed Request Logging

from flask import Flask
from werkzeug.wrappers import Request
import logging

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class DetailedLoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        logger.info(
            f"Request: {request.method} {request.path}, "
            f"IP: {request.remote_addr}, User-Agent: {request.user_agent}"
        )
        return self.wsgi_app(environ, start_response)

app.wsgi_app = DetailedLoggingMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Detailed Logging"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
INFO:__main__:Request: GET /, IP: 127.0.0.1, User-Agent: Mozilla/5.0 ...

Explanation:

  • Logs detailed request info, including method, path, IP, and User-Agent.
  • Useful for auditing and debugging.

2.2 File-Based Logging Middleware

Example: Logging to File

from flask import Flask
from werkzeug.wrappers import Request
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = RotatingFileHandler('app.log', maxBytes=1000000, backupCount=5)
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
logger.addHandler(handler)

class FileLoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        logger.info(f"{request.method} {request.path} from {request.remote_addr}")
        return self.wsgi_app(environ, start_response)

app.wsgi_app = FileLoggingMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Logged to File"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output (app.log):

2025-05-12 10:00:00,123 - GET / from 127.0.0.1

Explanation:

  • RotatingFileHandler - Saves logs to a file with rotation to manage size.
  • Ensures persistent logging for later analysis.

2.3 Latency Metrics Middleware

Example: Measuring Request Latency

from flask import Flask
from werkzeug.wrappers import Request
import logging
import time

app = Flask(__name__)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LatencyMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        start_time = time.time()
        response = self.wsgi_app(environ, start_response)
        latency = time.time() - start_time
        logger.info(f"Latency: {latency:.3f}s for {request.method} {request.path}")
        return response

app.wsgi_app = LatencyMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Latency Measured"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
INFO:__main__:Latency: 0.002s for GET /

Explanation:

  • Measures and logs request latency for performance monitoring.
  • Helps identify slow endpoints.

2.4 Prometheus Metrics Middleware

Example: Prometheus Metrics Collection

from flask import Flask
from werkzeug.wrappers import Request
from prometheus_client import Counter, Histogram, make_wsgi_app
from werkzeug.middleware.dispatcher import DispatcherMiddleware
import time

app = Flask(__name__)

# Define Prometheus metrics
request_count = Counter('flask_requests_total', 'Total requests', ['method', 'path', 'status'])
request_latency = Histogram('flask_request_latency_seconds', 'Request latency', ['path'])

class MetricsMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        start_time = time.time()

        # Capture response status
        status = [None]
        def custom_start_response(status_code, headers, *args):
            status[0] = status_code.split(' ')[0]  # Extract status code (e.g., 200)
            return start_response(status_code, headers, *args)

        response = self.wsgi_app(environ, custom_start_response)
        latency = time.time() - start_time

        # Record metrics
        request_count.labels(method=request.method, path=request.path, status=status[0]).inc()
        request_latency.labels(path=request.path).observe(latency)

        return response

app.wsgi_app = DispatcherMiddleware(MetricsMiddleware(app.wsgi_app), {
    '/metrics': make_wsgi_app()
})

@app.route('/')
def index():
    return "Metrics Tracked"

@app.route('/error')
def error():
    return "Error", 500

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output (http://127.0.0.1:5000/metrics):

flask_requests_total{method="GET",path="/",status="200"} 1.0
flask_request_latency_seconds_sum{path="/"} 0.002
flask_requests_total{method="GET",path="/error",status="500"} 1.0

Explanation:

  • Tracks request counts and latency with Prometheus metrics.
  • DispatcherMiddleware exposes metrics at /metrics for Prometheus scraping.

2.5 Error Tracking Middleware

Example: Logging Errors

from flask import Flask
from werkzeug.wrappers import Request, Response
import logging

app = Flask(__name__)

logging.basicConfig(level=logging.ERROR)
logger = logging.getLogger(__name__)

class ErrorLoggingMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        try:
            return self.wsgi_app(environ, start_response)
        except Exception as e:
            logger.error(f"Error in {request.method} {request.path}: {str(e)}", exc_info=True)
            res = Response('Internal Server Error', status=500)
            return res(environ, start_response)

app.wsgi_app = ErrorLoggingMiddleware(app.wsgi_app)

@app.route('/error')
def error():
    raise ValueError("Test error")

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
ERROR:__main__:Error in GET /error: Test error
(Request to /error: Returns 500 Internal Server Error)

Explanation:

  • Catches and logs exceptions with stack traces.
  • Provides user-friendly error responses.

2.6 Incorrect Middleware Setup

Example: Missing Metrics Endpoint

from flask import Flask
from prometheus_client import Counter
from werkzeug.wrappers import Request

app = Flask(__name__)

request_count = Counter('flask_requests_total', 'Total requests', ['path'])

class MetricsMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        request_count.labels(path=request.path).inc()
        return self.wsgi_app(environ, start_response)

app.wsgi_app = MetricsMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "No Metrics Endpoint"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
(No /metrics endpoint; Prometheus cannot scrape metrics)

Explanation:

  • Missing make_wsgi_app prevents Prometheus metrics exposure.
  • Solution: Use DispatcherMiddleware to add /metrics.

03. Effective Usage

3.1 Recommended Practices

  • Combine logging and metrics middleware with file rotation and Prometheus for comprehensive monitoring.

Example: Comprehensive Logging and Metrics Middleware

from flask import Flask
from werkzeug.wrappers import Request, Response
from prometheus_client import Counter, Histogram, make_wsgi_app
from werkzeug.middleware.dispatcher import DispatcherMiddleware
import logging
from logging.handlers import RotatingFileHandler
import time

app = Flask(__name__)

# Setup logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = RotatingFileHandler('app.log', maxBytes=1000000, backupCount=5)
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(handler)

# Define Prometheus metrics
request_count = Counter('flask_requests_total', 'Total requests', ['method', 'path', 'status'])
request_latency = Histogram('flask_request_latency_seconds', 'Request latency', ['path'])

class LoggingAndMetricsMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        start_time = time.time()

        # Log request
        logger.info(f"{request.method} {request.path} from {request.remote_addr}")

        # Capture response status
        status = [None]
        def custom_start_response(status_code, headers, *args):
            status[0] = status_code.split(' ')[0]
            return start_response(status_code, headers, *args)

        try:
            response = self.wsgi_app(environ, custom_start_response)
            latency = time.time() - start_time

            # Record metrics
            request_count.labels(method=request.method, path=request.path, status=status[0]).inc()
            request_latency.labels(path=request.path).observe(latency)

            return response
        except Exception as e:
            logger.error(f"Error in {request.method} {request.path}: {str(e)}", exc_info=True)
            res = Response('Internal Server Error', status=500)
            return res(environ, start_response)

app.wsgi_app = DispatcherMiddleware(LoggingAndMetricsMiddleware(app.wsgi_app), {
    '/metrics': make_wsgi_app()
})

@app.route('/')
def index():
    return "Logged and Monitored"

@app.route('/error')
def error():
    raise ValueError("Test error")

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output (app.log):

2025-05-12 10:00:00,123 - INFO - GET / from 127.0.0.1
2025-05-12 10:00:01,456 - ERROR - Error in GET /error: Test error

Output (http://127.0.0.1:5000/metrics):

flask_requests_total{method="GET",path="/",status="200"} 1.0
flask_request_latency_seconds_sum{path="/"} 0.002
flask_requests_total{method="GET",path="/error",status="500"} 1.0
  • Combines logging (console and file) with Prometheus metrics.
  • Handles errors and exposes metrics for monitoring.
  • Uses file rotation to manage log size.

3.2 Practices to Avoid

  • Avoid excessive logging or metrics collection to prevent performance overhead.

Example: Overloaded Logging and Metrics

from flask import Flask
from werkzeug.wrappers import Request
from prometheus_client import Counter
import logging

app = Flask(__name__)

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

request_count = Counter('flask_requests_total', 'Total requests', ['method', 'path', 'ip', 'agent'])

class OverloadedMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        logger.debug(f"Request: {request.method} {request.path} {request.remote_addr} {request.user_agent}")
        request_count.labels(
            method=request.method, path=request.path,
            ip=request.remote_addr, agent=str(request.user_agent)
        ).inc()
        return self.wsgi_app(environ, start_response)

app.wsgi_app = OverloadedMiddleware(app.wsgi_app)

@app.route('/')
def index():
    return "Overloaded Logging"

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output:

* Running on http://127.0.0.1:5000
DEBUG:__main__:Request: GET / 127.0.0.1 Mozilla/5.0 ...
(Excessive logs and metrics slow down app)
  • DEBUG logging and too many metric labels cause overhead.
  • Solution: Use INFO level and limit metric labels (e.g., method, path).

04. Common Use Cases

4.1 API Request Monitoring

Log and monitor API requests for performance and usage insights.

Example: API Monitoring Middleware

from flask import Flask, jsonify
from werkzeug.wrappers import Request
from prometheus_client import Counter, Histogram, make_wsgi_app
from werkzeug.middleware.dispatcher import DispatcherMiddleware
import logging
import time

app = Flask(__name__)

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
logger.addHandler(handler)

request_count = Counter('api_requests_total', 'Total API requests', ['method', 'path'])
request_latency = Histogram('api_request_latency_seconds', 'API latency', ['path'])

class APIMonitoringMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        if not request.path.startswith('/api/'):
            return self.wsgi_app(environ, start_response)

        start_time = time.time()
        logger.info(f"API Request: {request.method} {request.path}")

        status = [None]
        def custom_start_response(status_code, headers, *args):
            status[0] = status_code.split(' ')[0]
            return start_response(status_code, headers, *args)

        response = self.wsgi_app(environ, custom_start_response)
        latency = time.time() - start_time

        request_count.labels(method=request.method, path=request.path).inc()
        request_latency.labels(path=request.path).observe(latency)

        return response

app.wsgi_app = DispatcherMiddleware(APIMonitoringMiddleware(app.wsgi_app), {
    '/metrics': make_wsgi_app()
})

@app.route('/api/data')
def data():
    return jsonify({'data': 'secure'})

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output (Console):

2025-05-12 10:00:00,123 - API Request: GET /api/data

Output (http://127.0.0.1:5000/metrics):

api_requests_total{method="GET",path="/api/data"} 1.0
api_request_latency_seconds_sum{path="/api/data"} 0.003

Explanation:

  • Logs and tracks metrics for API routes only.
  • Provides insights into API performance and usage.

4.2 Error Monitoring

Log and track errors for debugging and reliability.

Example: Error Monitoring Middleware

from flask import Flask
from werkzeug.wrappers import Request, Response
from prometheus_client import Counter, make_wsgi_app
from werkzeug.middleware.dispatcher import DispatcherMiddleware
import logging

app = Flask(__name__)

logger = logging.getLogger(__name__)
logger.setLevel(logging.ERROR)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
logger.addHandler(handler)

error_count = Counter('flask_errors_total', 'Total errors', ['path'])

class ErrorMonitoringMiddleware:
    def __init__(self, wsgi_app):
        self.wsgi_app = wsgi_app

    def __call__(self, environ, start_response):
        request = Request(environ)
        try:
            return self.wsgi_app(environ, start_response)
        except Exception as e:
            logger.error(f"Error in {request.method} {request.path}: {str(e)}", exc_info=True)
            error_count.labels(path=request.path).inc()
            res = Response('Internal Server Error', status=500)
            return res(environ, start_response)

app.wsgi_app = DispatcherMiddleware(ErrorMonitoringMiddleware(app.wsgi_app), {
    '/metrics': make_wsgi_app()
})

@app.route('/error')
def error():
    raise ValueError("Test error")

if __name__ == '__main__':
    app.run(debug=True, port=5000)

Output (Console):

2025-05-12 10:00:00,123 - Error in GET /error: Test error

Output (http://127.0.0.1:5000/metrics):

flask_errors_total{path="/error"} 1.0

Explanation:

  • Logs errors and tracks error counts with Prometheus.
  • Facilitates debugging and error rate analysis.

Conclusion

Middleware for logging and metrics in Flask enhances observability and performance monitoring. Key takeaways:

  • Use middleware to centralize logging (console/file) and metrics collection.
  • Integrate with Prometheus for real-time metrics and monitoring.
  • Implement error tracking for robust debugging.
  • Avoid excessive logging or metrics to maintain performance.

With these practices, you can build Flask applications with comprehensive logging and metrics for reliable operation and optimization!

Comments