From 87af04e1750e131afa608caefa602073a3d8033e Mon Sep 17 00:00:00 2001
From: Dorian Koch <doriank@fsmpi.rwth-aachen.de>
Date: Wed, 25 Sep 2024 15:36:46 +0200
Subject: [PATCH] Add ServerTimings API

---
 src/api/routes/route.py | 53 ++++++++++++++++++++++-------------------
 1 file changed, 29 insertions(+), 24 deletions(-)

diff --git a/src/api/routes/route.py b/src/api/routes/route.py
index d5fe3bd..74dbc8b 100644
--- a/src/api/routes/route.py
+++ b/src/api/routes/route.py
@@ -72,7 +72,7 @@ def _handle_internal_server_error(e=None):
 
 
 class ApiResponse:
-    
+
     def __init__(self,
                  data,
                  status: int = HTTP_200_OK,
@@ -83,14 +83,14 @@ class ApiResponse:
         if mime_type == "application/json" and (isinstance(data, dict) or isinstance(data, list)):
             data = json.dumps(data)
         self._default_cache_control_max_age_sec = default_cache_control_max_age_sec
-        
+
         self.response = Response(
             data,
             status=status,
             headers=headers,
             mimetype=mime_type
         )
-    
+
     def build_response(self):
         if "Cache-Control" not in self.response.headers:
             cache_params: list[str] = []
@@ -104,7 +104,7 @@ class ApiResponse:
             else:
                 cache_params.append("no-store")
             self.response.headers["Cache-Control"] = ",".join(cache_params)
-        
+
         return self.response
 
 
@@ -117,7 +117,7 @@ def api_route(path: str, methods: list[str],
         func = api_function(allow_while_readonly=allow_while_readonly, allow_while_disabled=allow_while_disabled)(func)
         func = api_add_route(path, methods, min_api_version, max_api_version)(func)
         return func
-    
+
     return decorator
 
 
@@ -127,14 +127,14 @@ def api_add_route(path: str, methods: list[str],
     def decorator(func):
         if not hasattr(func, "is_api_route") or not func.is_api_route:
             raise Exception("@api_add_route() seems to be applied before @api_function()")
-        
+
         for version in range(min_api_version, max_api_version + 1):
             full_path = get_api_path(version, path)
             if DEBUG_ENABLED:
                 print(f"Registering api route: {full_path}")
             api.app.add_url_rule(full_path, methods=methods, view_func=func)
         return func
-    
+
     return decorator
 
 
@@ -146,7 +146,7 @@ def api_function(track_in_diagnostics: bool = True,
         if hasattr(func, "is_api_route") and func.is_api_route:
             raise Exception("An @api_function() decorator has already been applied. Are you using multiple @api_route()? "
                             "Use @api_add_route(...)@api_add_route(..)@api_function() instead")
-        
+
         call_counter = None
         call_time_counter = None
         if track_in_diagnostics:
@@ -157,10 +157,10 @@ def api_function(track_in_diagnostics: bool = True,
             func_name = func_name[len("api_route_"):]
             if len(func_name) == 0:
                 raise RuntimeError("Api route function has no name (just 'api_route_')")  # pragma: no cover
-            
+
             call_counter = DIAGNOSTICS_TRACKER.register_counter(f"route.{func_name}")
             call_time_counter = DIAGNOSTICS_TRACKER.register_counter(f"route.{func_name}.time")
-        
+
         @wraps(func)
         def wrapper(*args, **kwargs):
             try:
@@ -168,39 +168,44 @@ def api_function(track_in_diagnostics: bool = True,
                     raise ApiClientException(ERROR_SITE_IS_DISABLED)
                 if api.live_config.is_readonly() and not allow_while_readonly:
                     raise ApiClientException(ERROR_SITE_IS_READONLY)
-                
+
                 if call_counter is not None:
                     call_counter.trigger()
-                
+
                 if "X_REAL_IP" in request.headers:
                     ip_string = request.headers["X_REAL_IP"]
                     for rate_limiter in rate_limiters:
                         if not rate_limiter.check_new_request(ip_string):
                             raise ApiClientException(ERROR_RATE_LIMITED)
-                
+
                 if DEBUG_ENABLED and "API_ROULETTE_MODE" in api.config:
                     import random
                     from api.miscellaneous.errors import ALL_ERRORS_RANDOM
                     if random.random() * 100 < int(api.config["API_ROULETTE_MODE"]):
                         raise ApiClientException(random.choice(ALL_ERRORS_RANDOM))
-                
-                start_time = time.time()
+
+                start_time = time.time_ns()
                 result = func(*args, **kwargs)
+                stop_time = time.time_ns()
                 if call_time_counter is not None:
-                    call_time_counter.trigger(int((time.time() - start_time) * 1000))
-                
+                    call_time_counter.trigger(int((stop_time - start_time) / 1000000))
+
                 if isinstance(result, Response):
-                    return result
+                    resp = result
                 elif isinstance(result, ApiResponse):
-                    return result.build_response()
+                    resp = result.build_response()
                 elif result is None:
-                    return ApiResponse(None, HTTP_200_OK, None).build_response()
+                    resp = ApiResponse(None, HTTP_200_OK, None).build_response()
                 elif isinstance(result, dict):
-                    return ApiResponse(result).build_response()
+                    resp = ApiResponse(result).build_response()
                 elif isinstance(result, tuple) and len(result) == 2:
-                    return ApiResponse(result[0], result[1]).build_response()
+                    resp = ApiResponse(result[0], result[1]).build_response()
                 else:  # pragma: no cover
                     raise Exception(f"Api route {truncate_string(request.path)} returned result of unknown type: {str(result)}")
+
+                if call_time_counter is not None:  # i.e. diagnostics are enabled
+                    resp.headers["Server-Timing"] = f"api;dur={(stop_time - start_time) / 1000000}"
+                return resp
             except ApiClientException as e:
                 return api_on_error(e.error)
             except (TransactionConflictError, NoAvailableConnectionError) as e:
@@ -211,8 +216,8 @@ def api_function(track_in_diagnostics: bool = True,
                 print(f"An exception occurred while handling api request '{truncate_string(request.path, 200)}':")
                 traceback.print_exception(e)
                 return api_on_error(ERROR_INTERNAL_SERVER_ERROR)
-        
+
         wrapper.is_api_route = True
         return wrapper
-    
+
     return decorator
-- 
GitLab