44import platform
55import socket
66import sys
7+ import threading
78import time
89from datetime import datetime , timezone
10+ from pathlib import Path
911
1012from flask import Flask , Response , g , has_request_context , jsonify , request
1113from prometheus_client import CONTENT_TYPE_LATEST , Counter , Gauge , Histogram , generate_latest
1820PORT = int (os .getenv ("PORT" , 5000 ))
1921DEBUG = os .getenv ("DEBUG" , "False" ).lower () == "true"
2022START_TIME = datetime .now (timezone .utc )
23+ DEFAULT_VISITS_FILE = "/data/visits"
2124
2225REQUEST_LATENCY_BUCKETS = (
2326 0.005 ,
@@ -101,6 +104,58 @@ def format(self, record: logging.LogRecord) -> str:
101104 return json .dumps (payload , ensure_ascii = False )
102105
103106
107+ class VisitCounter :
108+ """File-backed counter used to persist the number of root endpoint visits."""
109+
110+ def __init__ (self , path_getter ):
111+ self ._path_getter = path_getter
112+ self ._lock = threading .Lock ()
113+ self ._value = 0
114+ self .reload ()
115+
116+ def _path (self ) -> Path :
117+ return Path (self ._path_getter ())
118+
119+ def _read_from_disk (self ) -> int :
120+ path = self ._path ()
121+ try :
122+ return int (path .read_text (encoding = "utf-8" ).strip () or "0" )
123+ except FileNotFoundError :
124+ return 0
125+ except ValueError :
126+ logger .warning (
127+ "Visits file contained invalid data, resetting counter to 0" ,
128+ extra = {
129+ "event" : "visits.invalid_data" ,
130+ "service" : SERVICE_NAME ,
131+ },
132+ )
133+ return 0
134+
135+ def _write_to_disk (self , value : int ) -> None :
136+ path = self ._path ()
137+ path .parent .mkdir (parents = True , exist_ok = True )
138+ tmp_path = path .with_suffix (f"{ path .suffix } .tmp" )
139+ tmp_path .write_text (str (value ), encoding = "utf-8" )
140+ os .replace (tmp_path , path )
141+
142+ def reload (self ) -> int :
143+ with self ._lock :
144+ self ._value = self ._read_from_disk ()
145+ return self ._value
146+
147+ def get_value (self ) -> int :
148+ with self ._lock :
149+ return self ._value
150+
151+ def increment (self ) -> int :
152+ with self ._lock :
153+ self ._value += 1
154+ self ._write_to_disk (self ._value )
155+ return self ._value
156+
157+
158+
104159def configure_logging () -> logging .Logger :
105160 """Configure application logging to stdout in JSON format."""
106161 handler = logging .StreamHandler (sys .stdout )
@@ -121,6 +176,15 @@ def configure_logging() -> logging.Logger:
121176
122177logger = configure_logging ()
123178app = Flask (__name__ )
179+ app .config .setdefault ("VISITS_FILE" , os .getenv ("VISITS_FILE" , DEFAULT_VISITS_FILE ))
180+
181+
182+ def get_visits_file_path () -> str :
183+ """Return the configured path to the visits counter file."""
184+ return app .config .get ("VISITS_FILE" , DEFAULT_VISITS_FILE )
185+
186+
187+ visit_counter = VisitCounter (get_visits_file_path )
124188
125189
126190def get_platform_version () -> str :
@@ -135,6 +199,7 @@ def get_platform_version() -> str:
135199 return platform .platform ()
136200
137201
202+
138203def get_uptime () -> dict :
139204 """Calculate the application's uptime."""
140205 delta = datetime .now (timezone .utc ) - START_TIME
@@ -147,6 +212,7 @@ def get_uptime() -> dict:
147212 }
148213
149214
215+
150216def get_system_info () -> dict :
151217 """Collect system information."""
152218 with system_info_collection_seconds .time ():
@@ -160,6 +226,7 @@ def get_system_info() -> dict:
160226 }
161227
162228
229+
163230def get_client_ip () -> str | None :
164231 """Return the client IP, preferring X-Forwarded-For when present."""
165232 if not has_request_context ():
@@ -171,17 +238,19 @@ def get_client_ip() -> str | None:
171238 return request .remote_addr
172239
173240
241+
174242def normalize_endpoint () -> str :
175243 """Return a low-cardinality endpoint label for Prometheus metrics."""
176244 if not has_request_context ():
177245 return "unknown"
178246 if request .url_rule and request .url_rule .rule :
179247 return request .url_rule .rule
180- if request .path in {"/" , "/health" , "/metrics" }:
248+ if request .path in {"/" , "/health" , "/metrics" , "/ready" , "/visits" }:
181249 return request .path
182250 return "unmatched"
183251
184252
253+
185254def build_request_log_context (status_code : int | None = None ) -> dict :
186255 """Build structured context for request-related logs."""
187256 context : dict [str , object ] = {"event" : "http.request" , "service" : SERVICE_NAME }
@@ -209,6 +278,7 @@ def build_request_log_context(status_code: int | None = None) -> dict:
209278 return context
210279
211280
281+
212282def finalize_request_metrics (status_code : int ) -> None :
213283 """Record Prometheus metrics for the current request exactly once."""
214284 if not has_request_context () or not getattr (g , "metrics_tracked" , False ):
@@ -280,6 +350,7 @@ def cleanup_request_metrics(exc) -> None:
280350@app .route ("/" )
281351def index ():
282352 """Main endpoint - service and system information."""
353+ current_visits = visit_counter .increment ()
283354 uptime = get_uptime ()
284355
285356 response = {
@@ -304,8 +375,13 @@ def index():
304375 "method" : request .method ,
305376 "path" : request .path ,
306377 },
378+ "visits" : {
379+ "count" : current_visits ,
380+ "storage" : get_visits_file_path (),
381+ },
307382 "endpoints" : [
308383 {"path" : "/" , "method" : "GET" , "description" : "Service information" },
384+ {"path" : "/visits" , "method" : "GET" , "description" : "Visit counter" },
309385 {"path" : "/health" , "method" : "GET" , "description" : "Health check" },
310386 {"path" : "/ready" , "method" : "GET" , "description" : "Readiness check" },
311387 {"path" : "/metrics" , "method" : "GET" , "description" : "Prometheus metrics" },
@@ -315,6 +391,17 @@ def index():
315391 return jsonify (response )
316392
317393
394+ @app .route ("/visits" )
395+ def visits ():
396+ """Return the current persisted visit count."""
397+ return jsonify (
398+ {
399+ "visits" : visit_counter .get_value (),
400+ "storage" : get_visits_file_path (),
401+ }
402+ )
403+
404+
318405@app .route ("/health" )
319406def health ():
320407 """Health check endpoint for monitoring."""
@@ -397,6 +484,7 @@ def handle_unexpected_error(error):
397484
398485
399486if __name__ == "__main__" :
487+ visit_counter .reload ()
400488 logger .info (
401489 "Application startup" ,
402490 extra = {
0 commit comments