Coverage for src/lexigram/web/interceptors/builtin/timing.py: 35%
23 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:37 +0800
1"""Timing interceptor for performance measurement.
3Adds Server-Timing header with handler execution time.
4"""
6from __future__ import annotations
8from typing import Any
10from lexigram.primitives import clock as ambient_clock
11from lexigram.web.protocols import (
12 CallHandlerProtocol,
13 ExecutionContextProtocol,
14 WebInterceptorProtocol,
15)
18class HandlerTimingInterceptor(WebInterceptorProtocol):
19 """Interceptor that measures and reports request handling time.
21 Adds Server-Timing header with the handler execution duration,
22 useful for performance debugging and client-side profiling.
24 Example:
25 ```python
26 # Add to global interceptors
27 router.add_interceptor(HandlerTimingInterceptor())
28 ```
30 The Server-Timing header format:
31 Server-Timing: handler;dur=12.5
32 """
34 def __init__(
35 self,
36 metric_name: str = "handler",
37 precision: int = 1,
38 ):
39 """Initialize the timing interceptor.
41 Args:
42 metric_name: Name for the timing metric in Server-Timing header.
43 precision: Number of decimal places for timing (default 1).
44 """
45 self._metric_name = metric_name
46 self._precision = precision
48 async def intercept(
49 self,
50 context: ExecutionContextProtocol,
51 next_handler: CallHandlerProtocol,
52 ) -> Any:
53 """Intercept and measure handler execution time.
55 Args:
56 context: The execution context.
57 next_handler: The next handler in the chain.
59 Returns:
60 The response from the handler, with timing header added.
61 """
62 start = ambient_clock.monotonic()
64 try:
65 result = await next_handler.handle()
66 finally:
67 elapsed = ambient_clock.monotonic() - start
68 duration_ms = elapsed * 1000
70 # Add Server-Timing header to response
71 if result is not None:
72 timing_value = (
73 f"{self._metric_name};dur={duration_ms:.{self._precision}f}"
74 )
76 # Add header to response if it has headers
77 if hasattr(result, "headers"):
78 # Use a setdefault to avoid overwriting existing headers
79 if "server-timing" not in result.headers:
80 result.headers["Server-Timing"] = timing_value
81 elif hasattr(result, "add_header"):
82 result.add_header("Server-Timing", timing_value)
84 return result
87__all__ = ["HandlerTimingInterceptor"]