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