77import socket
88import platform
99import logging
10+ from threading import Lock
1011from time import perf_counter
1112from datetime import datetime , timezone
1213from typing import Dict , Any
@@ -39,6 +40,11 @@ def add_fields(self, log_record, record, message_dict):
3940
4041# Application startup time
4142start_time = datetime .now (timezone .utc )
43+ visits_lock = Lock ()
44+
45+ # Persistent visits counter configuration
46+ DEFAULT_DATA_DIR = os .getenv ('DATA_DIR' , '/data' )
47+ DEFAULT_VISITS_FILE = os .getenv ('VISITS_FILE' , os .path .join (DEFAULT_DATA_DIR , 'visits' ))
4248
4349# Prometheus metrics (RED method + app-specific metrics)
4450http_requests_total = Counter (
@@ -161,6 +167,81 @@ def get_system_info() -> Dict[str, Any]:
161167 }
162168
163169
170+ def get_visits_file_path () -> str :
171+ """Get visits file path from environment with a safe default."""
172+ return os .getenv ('VISITS_FILE' , DEFAULT_VISITS_FILE )
173+
174+
175+ def _atomic_write_text (file_path : str , content : str ) -> None :
176+ """Write file content atomically to avoid partial updates."""
177+ temp_file_path = f"{ file_path } .tmp"
178+ with open (temp_file_path , 'w' , encoding = 'utf-8' ) as temp_file :
179+ temp_file .write (content )
180+ os .replace (temp_file_path , file_path )
181+
182+
183+ def _ensure_visits_storage (visits_file_path : str ) -> None :
184+ """Ensure visits counter file exists and contains a valid integer."""
185+ visits_dir = os .path .dirname (visits_file_path )
186+ if visits_dir :
187+ os .makedirs (visits_dir , exist_ok = True )
188+
189+ if not os .path .exists (visits_file_path ):
190+ _atomic_write_text (visits_file_path , '0\n ' )
191+ logger .info ('Visits counter initialized' , extra = {
192+ 'visits_file' : visits_file_path ,
193+ 'visits_count' : 0 ,
194+ })
195+ return
196+
197+ try :
198+ with open (visits_file_path , 'r' , encoding = 'utf-8' ) as visits_file :
199+ int ((visits_file .read ().strip () or '0' ))
200+ except (OSError , ValueError ):
201+ logger .warning ('Visits counter file was invalid and has been reset' , extra = {
202+ 'visits_file' : visits_file_path ,
203+ })
204+ _atomic_write_text (visits_file_path , '0\n ' )
205+
206+
207+ def get_visits_count () -> int :
208+ """Read current visits count from persistent storage."""
209+ visits_file_path = get_visits_file_path ()
210+
211+ with visits_lock :
212+ _ensure_visits_storage (visits_file_path )
213+ try :
214+ with open (visits_file_path , 'r' , encoding = 'utf-8' ) as visits_file :
215+ return int ((visits_file .read ().strip () or '0' ))
216+ except (OSError , ValueError ):
217+ logger .warning ('Visits counter read failed, resetting to 0' , extra = {
218+ 'visits_file' : visits_file_path ,
219+ })
220+ _atomic_write_text (visits_file_path , '0\n ' )
221+ return 0
222+
223+
224+ def increment_visits_count () -> int :
225+ """Increment visits count and persist the new value."""
226+ visits_file_path = get_visits_file_path ()
227+
228+ with visits_lock :
229+ _ensure_visits_storage (visits_file_path )
230+
231+ try :
232+ with open (visits_file_path , 'r' , encoding = 'utf-8' ) as visits_file :
233+ current_count = int ((visits_file .read ().strip () or '0' ))
234+ except (OSError , ValueError ):
235+ logger .warning ('Visits counter read failed during increment, resetting to 0' , extra = {
236+ 'visits_file' : visits_file_path ,
237+ })
238+ current_count = 0
239+
240+ new_count = current_count + 1
241+ _atomic_write_text (visits_file_path , f'{ new_count } \n ' )
242+ return new_count
243+
244+
164245@app .get ("/" )
165246async def root (request : Request ) -> Dict [str , Any ]:
166247 """
@@ -170,6 +251,7 @@ async def root(request: Request) -> Dict[str, Any]:
170251 Dict containing service, system, runtime, request info and available endpoints
171252 """
172253 devops_info_endpoint_calls_total .labels (endpoint = "/" ).inc ()
254+ visits_count = increment_visits_count ()
173255
174256 system_info_start = perf_counter ()
175257 system_info = get_system_info ()
@@ -189,7 +271,8 @@ async def root(request: Request) -> Dict[str, Any]:
189271 "uptime_seconds" : uptime ['seconds' ],
190272 "uptime_human" : uptime ['human' ],
191273 "current_time" : datetime .now (timezone .utc ).isoformat (),
192- "timezone" : "UTC"
274+ "timezone" : "UTC" ,
275+ "visits" : visits_count
193276 },
194277 "request" : {
195278 "client_ip" : request .client .host if request .client else "unknown" ,
@@ -212,6 +295,11 @@ async def root(request: Request) -> Dict[str, Any]:
212295 "path" : "/metrics" ,
213296 "method" : "GET" ,
214297 "description" : "Prometheus metrics"
298+ },
299+ {
300+ "path" : "/visits" ,
301+ "method" : "GET" ,
302+ "description" : "Current visits counter"
215303 }
216304 ]
217305 }
@@ -243,14 +331,29 @@ async def metrics() -> Response:
243331 return Response (content = generate_latest (), media_type = CONTENT_TYPE_LATEST )
244332
245333
334+ @app .get ("/visits" )
335+ async def visits () -> Dict [str , Any ]:
336+ """Return current visits counter value from persistent storage."""
337+ devops_info_endpoint_calls_total .labels (endpoint = "/visits" ).inc ()
338+ return {
339+ 'visits' : get_visits_count (),
340+ 'visits_file' : get_visits_file_path (),
341+ }
342+
343+
246344# Startup event
247345@app .on_event ("startup" )
248346async def startup_event ():
249347 """Log application startup"""
348+ visits_file_path = get_visits_file_path ()
349+ with visits_lock :
350+ _ensure_visits_storage (visits_file_path )
351+
250352 logger .info ("Application started successfully" , extra = {
251353 "service" : "devops-info-service" ,
252354 "version" : "1.0.0" ,
253- "startup_time" : start_time .isoformat ()
355+ "startup_time" : start_time .isoformat (),
356+ "visits_file" : visits_file_path ,
254357 })
255358
256359
0 commit comments