99from datetime import datetime , timezone
1010
1111from dotenv import load_dotenv
12- from flask import Flask , g , jsonify , request
12+ from flask import Flask , Response , g , jsonify , request
1313from flask_swagger_ui import get_swaggerui_blueprint
14+ from prometheus_client import CONTENT_TYPE_LATEST , Counter , Gauge , Histogram , generate_latest
1415
1516app = Flask (__name__ )
1617
18+ TRACKED_ENDPOINTS = {'/' , '/health' , '/metrics' , '/swagger.json' }
19+
20+ HTTP_REQUESTS_TOTAL = Counter (
21+ 'http_requests_total' ,
22+ 'Total HTTP requests' ,
23+ ['method' , 'endpoint' , 'status_code' ]
24+ )
25+
26+ HTTP_REQUEST_DURATION_SECONDS = Histogram (
27+ 'http_request_duration_seconds' ,
28+ 'HTTP request duration in seconds' ,
29+ ['method' , 'endpoint' ]
30+ )
31+
32+ HTTP_REQUESTS_IN_PROGRESS = Gauge (
33+ 'http_requests_in_progress' ,
34+ 'HTTP requests currently being processed' ,
35+ ['method' , 'endpoint' , 'status_code' ]
36+ )
37+
38+ DEVOPS_INFO_ENDPOINT_CALLS_TOTAL = Counter (
39+ 'devops_info_endpoint_calls_total' ,
40+ 'Total endpoint calls for DevOps info service' ,
41+ ['endpoint' ]
42+ )
43+
44+ DEVOPS_INFO_SYSTEM_COLLECTION_SECONDS = Histogram (
45+ 'devops_info_system_collection_seconds' ,
46+ 'System info collection duration in seconds'
47+ )
48+
1749
1850class JSONFormatter (logging .Formatter ):
1951 def format (self , record : logging .LogRecord ) -> str :
@@ -77,6 +109,17 @@ def get_client_ip() -> str:
77109 return client_ip
78110
79111
112+ def normalize_endpoint () -> str :
113+ url_rule = getattr (request , 'url_rule' , None )
114+ endpoint = url_rule .rule if url_rule and url_rule .rule else request .path
115+
116+ if endpoint .startswith ('/docs' ):
117+ return '/docs'
118+ if endpoint in TRACKED_ENDPOINTS :
119+ return endpoint
120+ return '/other'
121+
122+
80123# conf
81124load_dotenv ()
82125HOST = os .getenv ('HOST' , '0.0.0.0' )
@@ -168,7 +211,8 @@ def get_endpoints() -> list[dict]:
168211 """return a list of available endpoints"""
169212 return [
170213 {'path' : '/' , 'method' : 'GET' , 'description' : 'Service information' },
171- {'path' : '/health' , 'method' : 'GET' , 'description' : 'Health check' }
214+ {'path' : '/health' , 'method' : 'GET' , 'description' : 'Health check' },
215+ {'path' : '/metrics' , 'method' : 'GET' , 'description' : 'Prometheus metrics' },
172216 ]
173217
174218
@@ -200,6 +244,16 @@ def get_endpoints() -> list[dict]:
200244 }
201245 }
202246 }
247+ },
248+ '/metrics' : {
249+ 'get' : {
250+ 'summary' : 'Prometheus metrics' ,
251+ 'responses' : {
252+ '200' : {
253+ 'description' : 'Prometheus text exposition format'
254+ }
255+ }
256+ }
203257 }
204258 }
205259}
@@ -208,6 +262,14 @@ def get_endpoints() -> list[dict]:
208262@app .before_request
209263def log_request () -> None :
210264 g .request_started_at = time .perf_counter ()
265+ g .normalized_endpoint = normalize_endpoint ()
266+
267+ HTTP_REQUESTS_IN_PROGRESS .labels (
268+ method = request .method ,
269+ endpoint = g .normalized_endpoint ,
270+ status_code = 'in_progress' ,
271+ ).inc ()
272+
211273 logger .info (
212274 'Incoming request' ,
213275 extra = {
@@ -223,9 +285,32 @@ def log_request() -> None:
223285@app .after_request
224286def log_response (response ):
225287 started_at = getattr (g , 'request_started_at' , None )
288+ duration_seconds = None
226289 duration_ms = None
227290 if started_at is not None :
228- duration_ms = round ((time .perf_counter () - started_at ) * 1000 , 2 )
291+ duration_seconds = time .perf_counter () - started_at
292+ duration_ms = round (duration_seconds * 1000 , 2 )
293+
294+ endpoint = getattr (g , 'normalized_endpoint' , normalize_endpoint ())
295+ status_code = str (response .status_code )
296+
297+ HTTP_REQUESTS_TOTAL .labels (
298+ method = request .method ,
299+ endpoint = endpoint ,
300+ status_code = status_code ,
301+ ).inc ()
302+
303+ if duration_seconds is not None :
304+ HTTP_REQUEST_DURATION_SECONDS .labels (
305+ method = request .method ,
306+ endpoint = endpoint ,
307+ ).observe (duration_seconds )
308+
309+ HTTP_REQUESTS_IN_PROGRESS .labels (
310+ method = request .method ,
311+ endpoint = endpoint ,
312+ status_code = 'in_progress' ,
313+ ).dec ()
229314
230315 log_extra : dict [str , object ] = {
231316 'event' : 'request_end' ,
@@ -248,10 +333,15 @@ def log_response(response):
248333@app .route ('/' )
249334def index ():
250335 """main endpoint"""
336+ DEVOPS_INFO_ENDPOINT_CALLS_TOTAL .labels (endpoint = '/' ).inc ()
337+
251338 uptime = get_uptime ()
339+ with DEVOPS_INFO_SYSTEM_COLLECTION_SECONDS .time ():
340+ system_info = get_system_info ()
341+
252342 payload = {
253343 'service' : get_service_info (),
254- 'system' : get_system_info () ,
344+ 'system' : system_info ,
255345 'runtime' : {
256346 'uptime_seconds' : uptime ['seconds' ],
257347 'uptime_human' : uptime ['human' ],
@@ -267,6 +357,7 @@ def index():
267357@app .route ('/health' )
268358def health ():
269359 """health check endpoint"""
360+ DEVOPS_INFO_ENDPOINT_CALLS_TOTAL .labels (endpoint = '/health' ).inc ()
270361 uptime = get_uptime ()
271362 return jsonify ({
272363 'status' : 'healthy' ,
@@ -275,8 +366,15 @@ def health():
275366 })
276367
277368
369+ @app .route ('/metrics' )
370+ def metrics ():
371+ DEVOPS_INFO_ENDPOINT_CALLS_TOTAL .labels (endpoint = '/metrics' ).inc ()
372+ return Response (generate_latest (), mimetype = CONTENT_TYPE_LATEST )
373+
374+
278375@app .route ('/swagger.json' )
279376def swagger_json ():
377+ DEVOPS_INFO_ENDPOINT_CALLS_TOTAL .labels (endpoint = '/swagger.json' ).inc ()
280378 return jsonify (OPENAPI_SPEC )
281379
282380
0 commit comments