1212import platform
1313import socket
1414import sys
15+ import time
1516from datetime import datetime , timezone
1617
17- from flask import Flask , jsonify , request
18+ from flask import Flask , Response , g , jsonify , request
19+ from prometheus_client import (CONTENT_TYPE_LATEST , Counter , Gauge ,
20+ Histogram , generate_latest )
1821
1922# Flask application instance
2023app = Flask (__name__ )
3538 "framework" : "Flask" ,
3639}
3740
41+ # Prometheus metrics
42+ # Counter: total HTTP requests by method / endpoint / status code
43+ http_requests_total = Counter (
44+ "http_requests_total" ,
45+ "Total HTTP requests" ,
46+ ["method" , "endpoint" , "status_code" ],
47+ )
48+
49+ # Histogram: request duration distribution
50+ http_request_duration_seconds = Histogram (
51+ "http_request_duration_seconds" ,
52+ "HTTP request duration in seconds" ,
53+ ["method" , "endpoint" , "status_code" ],
54+ )
55+
56+ # Gauge: current number of requests being processed
57+ http_requests_in_progress = Gauge (
58+ "http_requests_in_progress" ,
59+ "HTTP requests currently being processed" ,
60+ )
61+
3862
3963# JSON log formatter
4064# Produces structured logs suitable for Loki / Promtail / Grafana
@@ -147,6 +171,17 @@ def request_info():
147171 }
148172
149173
174+ def normalized_endpoint ():
175+ """
176+ Return a normalized endpoint for metrics labels.
177+
178+ Uses Flask route rule when available, fallback to raw request path.
179+ """
180+ if request .url_rule is not None :
181+ return request .url_rule .rule
182+ return request .path
183+
184+
150185def endpoints_info ():
151186 """
152187 Build an API endpoints list dynamically from Flask URL map.
@@ -201,6 +236,12 @@ def health():
201236 return jsonify (payload )
202237
203238
239+ @app .get ("/metrics" )
240+ def metrics ():
241+ """Prometheus metrics endpoint."""
242+ return Response (generate_latest (), content_type = CONTENT_TYPE_LATEST )
243+
244+
204245# Test-only endpoint to trigger HTTP 500 (uncomment to verify error handler)
205246@app .get ("/crash" )
206247def crash ():
@@ -256,6 +297,14 @@ def internal_error(error):
256297def log_requests ():
257298 """Log basic request metadata before handling."""
258299
300+ # Skip Prometheus self-scrape from app HTTP metrics
301+ g .track_metrics = request .path != "/metrics"
302+
303+ # Save request start time and increment in-progress gauge
304+ if g .track_metrics :
305+ g .request_start_time = time .perf_counter ()
306+ http_requests_in_progress .inc ()
307+
259308 info = request_info ()
260309 app .logger .info (
261310 "request_started" ,
@@ -273,16 +322,44 @@ def log_response(response):
273322 """Log response status code after handling."""
274323
275324 info = request_info ()
276- app .logger .info (
277- "request_finished" ,
278- extra = {
279- "method" : info ["method" ],
280- "path" : info ["path" ],
281- "status_code" : response .status_code ,
282- "client_ip" : info ["client_ip" ],
283- "user_agent" : info ["user_agent" ],
284- },
285- )
325+
326+ # Record Prometheus metrics after request is processed
327+ if getattr (g , "track_metrics" , False ):
328+ duration = time .perf_counter () - g .request_start_time
329+ endpoint = normalized_endpoint ()
330+ status_code = str (response .status_code )
331+
332+ http_requests_total .labels (
333+ method = request .method ,
334+ endpoint = endpoint ,
335+ status_code = status_code ,
336+ ).inc ()
337+
338+ http_request_duration_seconds .labels (
339+ method = request .method ,
340+ endpoint = endpoint ,
341+ status_code = status_code ,
342+ ).observe (duration )
343+
344+ http_requests_in_progress .dec ()
345+
346+ latency_ms = round (duration * 1000 , 2 )
347+ else :
348+ latency_ms = None
349+
350+ extra_payload = {
351+ "method" : info ["method" ],
352+ "path" : info ["path" ],
353+ "status_code" : response .status_code ,
354+ "client_ip" : info ["client_ip" ],
355+ "user_agent" : info ["user_agent" ],
356+ }
357+
358+ # Add measured latency for normal app requests
359+ if latency_ms is not None :
360+ extra_payload ["latency_ms" ] = latency_ms
361+
362+ app .logger .info ("request_finished" , extra = extra_payload )
286363
287364 return response
288365
0 commit comments