grpc-server-kit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. grpc_server_kit/__init__.py +34 -0
  2. grpc_server_kit/__version__.py +1 -0
  3. grpc_server_kit/aio/__init__.py +44 -0
  4. grpc_server_kit/aio/app.py +368 -0
  5. grpc_server_kit/aio/builder.py +63 -0
  6. grpc_server_kit/aio/dishka/__init__.py +28 -0
  7. grpc_server_kit/aio/dishka/bundle.py +70 -0
  8. grpc_server_kit/aio/dishka/interceptors.py +116 -0
  9. grpc_server_kit/aio/dishka/metrics.py +50 -0
  10. grpc_server_kit/aio/dishka/observability.py +86 -0
  11. grpc_server_kit/aio/dishka/server.py +51 -0
  12. grpc_server_kit/aio/health/__init__.py +36 -0
  13. grpc_server_kit/aio/health/cache.py +89 -0
  14. grpc_server_kit/aio/health/checkers/__init__.py +16 -0
  15. grpc_server_kit/aio/health/checkers/postgres.py +115 -0
  16. grpc_server_kit/aio/health/checkers/redis.py +88 -0
  17. grpc_server_kit/aio/health/checkers/utils.py +97 -0
  18. grpc_server_kit/aio/health/orchestrator.py +122 -0
  19. grpc_server_kit/aio/health/protocols.py +43 -0
  20. grpc_server_kit/aio/health/servicer.py +229 -0
  21. grpc_server_kit/aio/interceptors/__init__.py +62 -0
  22. grpc_server_kit/aio/interceptors/base.py +235 -0
  23. grpc_server_kit/aio/interceptors/context.py +179 -0
  24. grpc_server_kit/aio/interceptors/exception_handler.py +167 -0
  25. grpc_server_kit/aio/interceptors/metadata.py +57 -0
  26. grpc_server_kit/aio/interceptors/metrics.py +104 -0
  27. grpc_server_kit/aio/interceptors/protocols.py +54 -0
  28. grpc_server_kit/aio/interceptors/request_logger.py +103 -0
  29. grpc_server_kit/aio/interceptors/sentry.py +102 -0
  30. grpc_server_kit/aio/interceptors/tracing.py +130 -0
  31. grpc_server_kit/aio/observability/__init__.py +5 -0
  32. grpc_server_kit/aio/observability/tracing.py +48 -0
  33. grpc_server_kit/aio/runtime.py +206 -0
  34. grpc_server_kit/aio/server.py +191 -0
  35. grpc_server_kit/config.py +100 -0
  36. grpc_server_kit/constants.py +34 -0
  37. grpc_server_kit/credentials.py +112 -0
  38. grpc_server_kit/dishka/__init__.py +38 -0
  39. grpc_server_kit/dishka/settings.py +41 -0
  40. grpc_server_kit/interceptors/__init__.py +15 -0
  41. grpc_server_kit/interceptors/constants.py +44 -0
  42. grpc_server_kit/interceptors/utils.py +74 -0
  43. grpc_server_kit/observability/__init__.py +12 -0
  44. grpc_server_kit/observability/metrics.py +116 -0
  45. grpc_server_kit/observability/naming.py +26 -0
  46. grpc_server_kit/observability/protocols.py +86 -0
  47. grpc_server_kit/observability/sentry.py +63 -0
  48. grpc_server_kit/options.py +136 -0
  49. grpc_server_kit/protocols.py +114 -0
  50. grpc_server_kit/py.typed +0 -0
  51. grpc_server_kit/server.py +59 -0
  52. grpc_server_kit/settings.py +151 -0
  53. grpc_server_kit/signals.py +228 -0
  54. grpc_server_kit-0.1.0.dist-info/METADATA +484 -0
  55. grpc_server_kit-0.1.0.dist-info/RECORD +57 -0
  56. grpc_server_kit-0.1.0.dist-info/WHEEL +4 -0
  57. grpc_server_kit-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,97 @@
1
+ """Internal utilities for health checkers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from collections.abc import Awaitable, Callable
8
+ from functools import wraps
9
+ from typing import ParamSpec
10
+
11
+ from grpc_server_kit.constants import DEFAULT_CHECKER_TIMEOUT
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ P = ParamSpec("P")
16
+
17
+
18
+ def handle_check_exceptions(
19
+ check_name: str,
20
+ ) -> Callable[[Callable[P, Awaitable[bool]]], Callable[P, Awaitable[bool]]]:
21
+ """Decorator for consistent exception handling in health check functions.
22
+
23
+ This decorator provides uniform error handling for all health check implementations:
24
+ - Logs timeout errors with warning level
25
+ - Propagates CancelledError and KeyboardInterrupt for proper shutdown
26
+ - Catches and logs all other exceptions, returning False (unhealthy)
27
+
28
+ Args:
29
+ check_name: Human-readable name of the check for logging (e.g. "Database", "Redis").
30
+
31
+ Returns:
32
+ Decorator function that wraps health check implementations.
33
+ """
34
+
35
+ def decorator(func: Callable[P, Awaitable[bool]]) -> Callable[P, Awaitable[bool]]:
36
+ @wraps(func)
37
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> bool:
38
+ try:
39
+ return await func(*args, **kwargs)
40
+ except TimeoutError:
41
+ timeout = kwargs.get("timeout", "unknown")
42
+ logger.warning(f"{check_name} health check timed out", extra={"check": check_name, "timeout": timeout})
43
+ return False
44
+ except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
45
+ raise
46
+ except (ConnectionError, OSError) as e:
47
+ # Common networking errors for health checks (e.g. Redis down, Postgres unreachable)
48
+ logger.warning(
49
+ f"{check_name} health check failed (network/OS error)",
50
+ extra={"check": check_name, "error": str(e), "error_type": type(e).__name__},
51
+ )
52
+ return False
53
+ except Exception:
54
+ logger.exception(f"{check_name} health check failed with unexpected error", extra={"check": check_name})
55
+ return False
56
+
57
+ return wrapper
58
+
59
+ return decorator
60
+
61
+
62
+ class FunctionalHealthChecker:
63
+ """Generic health checker that wraps a health check function.
64
+
65
+ This class provides a common wrapper for any health check function that follows
66
+ the `check_func(resource, timeout=...)` pattern.
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ check_func: Callable[..., Awaitable[bool]],
72
+ resource: object | None = None,
73
+ timeout: float = DEFAULT_CHECKER_TIMEOUT,
74
+ ) -> None:
75
+ """Initialize health checker.
76
+
77
+ Args:
78
+ check_func: Asynchronous function that performs the health check.
79
+ resource: The resource (e.g. session maker, client) to check.
80
+ timeout: Maximum time to wait for the check to complete in seconds.
81
+
82
+ Raises:
83
+ ValueError: If timeout is not positive.
84
+ """
85
+ if timeout <= 0:
86
+ raise ValueError(f"timeout must be positive, got {timeout}")
87
+ self._check_func = check_func
88
+ self._resource = resource
89
+ self._timeout = timeout
90
+
91
+ async def check(self) -> bool:
92
+ """Execute the wrapped health check function.
93
+
94
+ Returns:
95
+ True if healthy, False otherwise.
96
+ """
97
+ return await self._check_func(self._resource, timeout=self._timeout)
@@ -0,0 +1,122 @@
1
+ """Overall health-check orchestration (async)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import functools
7
+ import logging
8
+ from typing import TYPE_CHECKING
9
+
10
+ from grpc_health.v1 import health_pb2
11
+
12
+ from grpc_server_kit.constants import DEFAULT_HEALTH_CHECK_TIMEOUT
13
+
14
+ from .protocols import AsyncHealthCacheProtocol, AsyncHealthChecker
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Sequence
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ async def check_async_overall_health(
23
+ checkers: list[AsyncHealthChecker] | None = None,
24
+ cache: AsyncHealthCacheProtocol | None = None,
25
+ timeout: float = DEFAULT_HEALTH_CHECK_TIMEOUT,
26
+ ) -> health_pb2.HealthCheckResponse.ServingStatus:
27
+ """Check overall service health including all dependencies (async)."""
28
+ if not checkers:
29
+ return health_pb2.HealthCheckResponse.SERVING
30
+
31
+ if cache is not None:
32
+ return await cache.get_or_set(
33
+ check_func=functools.partial(_perform_checks, checkers, timeout=timeout),
34
+ )
35
+
36
+ return await _perform_checks(checkers, timeout=timeout)
37
+
38
+
39
+ async def _perform_checks(
40
+ checkers: Sequence[AsyncHealthChecker], timeout: float = DEFAULT_HEALTH_CHECK_TIMEOUT
41
+ ) -> health_pb2.HealthCheckResponse.ServingStatus:
42
+ """Perform all health checks in parallel and aggregate results.
43
+
44
+ Uses asyncio.gather with return_exceptions to collect pass/fail per checker
45
+ without using exceptions for control flow.
46
+
47
+ Args:
48
+ checkers: Sequence of health checkers to execute. Must not be empty.
49
+ timeout: Maximum seconds to wait for all health checks to complete.
50
+
51
+ Returns:
52
+ SERVING if all checks pass, NOT_SERVING if any check fails or unexpected error occurs.
53
+ """
54
+ loop = asyncio.get_running_loop()
55
+ start_time = loop.time()
56
+ checker_count = len(checkers)
57
+ checker_names = [type(c).__name__ for c in checkers]
58
+
59
+ try:
60
+ async with asyncio.timeout(timeout):
61
+ results = await asyncio.gather(
62
+ *(c.check() for c in checkers),
63
+ return_exceptions=True,
64
+ )
65
+ except (asyncio.CancelledError, KeyboardInterrupt):
66
+ raise
67
+ except TimeoutError:
68
+ elapsed = loop.time() - start_time
69
+ logger.warning(
70
+ "Health checks timed out",
71
+ extra={
72
+ "checker_count": checker_count,
73
+ "checkers": checker_names,
74
+ "timeout": timeout,
75
+ "elapsed_seconds": round(elapsed, 3),
76
+ },
77
+ )
78
+ return health_pb2.HealthCheckResponse.NOT_SERVING
79
+
80
+ failed_checkers: list[str] = []
81
+ unexpected_errors: list[tuple[str, Exception]] = []
82
+
83
+ for checker, outcome in zip(checkers, results, strict=True):
84
+ name = type(checker).__name__
85
+ if isinstance(outcome, BaseException) and not isinstance(outcome, Exception):
86
+ raise outcome
87
+ if outcome is False:
88
+ failed_checkers.append(name)
89
+ elif isinstance(outcome, Exception):
90
+ unexpected_errors.append((name, outcome))
91
+
92
+ elapsed = loop.time() - start_time
93
+ extra = {
94
+ "checker_count": checker_count,
95
+ "elapsed_seconds": round(elapsed, 3),
96
+ }
97
+
98
+ if failed_checkers:
99
+ logger.warning(
100
+ "Health checks failed",
101
+ extra={**extra, "failed_count": len(failed_checkers), "failed_checkers": failed_checkers},
102
+ )
103
+ return health_pb2.HealthCheckResponse.NOT_SERVING
104
+
105
+ if unexpected_errors:
106
+ # Log each error WITH its own traceback: logger.exception outside an
107
+ # except block has no active exception and would log "NoneType: None".
108
+ for name, error in unexpected_errors:
109
+ logger.error(
110
+ "Health checker raised unexpected error",
111
+ extra={**extra, "checker": name, "error_type": type(error).__name__},
112
+ exc_info=error,
113
+ )
114
+ return health_pb2.HealthCheckResponse.NOT_SERVING
115
+
116
+ # DEBUG, not INFO: this fires for every Check probe / Watch iteration and
117
+ # carries no information when everything is healthy.
118
+ logger.debug(
119
+ "Health checks passed",
120
+ extra={**extra, "checkers": checker_names},
121
+ )
122
+ return health_pb2.HealthCheckResponse.SERVING
@@ -0,0 +1,43 @@
1
+ """Protocols for health checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from typing import TYPE_CHECKING, Protocol
7
+
8
+ if TYPE_CHECKING:
9
+ from grpc_health.v1 import health_pb2
10
+
11
+
12
+ class AsyncHealthChecker(Protocol):
13
+ """Protocol for component health checks (DB, Redis, external service, ...).
14
+
15
+ One shape (``async check() -> bool``) serves the kit's gRPC health servicer
16
+ and any external health registry alike.
17
+ """
18
+
19
+ async def check(self) -> bool:
20
+ """Check component health. Returns True if healthy."""
21
+ ...
22
+
23
+
24
+ class AsyncHealthCacheProtocol(Protocol):
25
+ """Protocol for a health status cache with TTL support.
26
+
27
+ Implementations must be safe for concurrent coroutine access and must
28
+ ensure that only one concurrent task performs the underlying check on a
29
+ cache miss (double-checked locking or equivalent).
30
+
31
+ Note: Cache instances should not be shared across different event loops.
32
+ """
33
+
34
+ async def get_or_set(
35
+ self,
36
+ check_func: Callable[[], Awaitable[health_pb2.HealthCheckResponse.ServingStatus]],
37
+ now: float | None = None,
38
+ ) -> health_pb2.HealthCheckResponse.ServingStatus:
39
+ """Return the cached health status or perform the check and cache the result."""
40
+ ...
41
+
42
+
43
+ __all__ = ["AsyncHealthCacheProtocol", "AsyncHealthChecker"]
@@ -0,0 +1,229 @@
1
+ """Dynamic health check servicer with real-time dependency checks (async)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import TYPE_CHECKING, NoReturn
8
+
9
+ import grpc
10
+ from grpc_health.v1 import health_pb2, health_pb2_grpc
11
+
12
+ from grpc_server_kit.constants import (
13
+ DEFAULT_HEALTH_CHECK_INTERVAL,
14
+ DEFAULT_HEALTH_CHECK_TIMEOUT,
15
+ DEFAULT_HEALTH_HEARTBEAT_INTERVAL,
16
+ )
17
+ from grpc_server_kit.server import is_valid_service_name
18
+
19
+ from .orchestrator import check_async_overall_health
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import AsyncGenerator
23
+
24
+ from .protocols import AsyncHealthCacheProtocol, AsyncHealthChecker
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class AsyncDynamicHealthServicer(health_pb2_grpc.HealthServicer):
30
+ """Async health servicer with dynamic dependency checks.
31
+
32
+ Implements the standard gRPC Health Checking Protocol v1 with real-time
33
+ dependency verification. Supports both Check (unary) and Watch (streaming) modes.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ checkers: list[AsyncHealthChecker] | None = None,
39
+ cache: AsyncHealthCacheProtocol | None = None,
40
+ service_names: list[str] | None = None,
41
+ check_interval: float = DEFAULT_HEALTH_CHECK_INTERVAL,
42
+ heartbeat_interval: float = DEFAULT_HEALTH_HEARTBEAT_INTERVAL,
43
+ check_timeout: float = DEFAULT_HEALTH_CHECK_TIMEOUT,
44
+ ) -> None:
45
+ """Initialize health servicer.
46
+
47
+ Args:
48
+ checkers: Optional list of asynchronous health checkers. If None or empty,
49
+ the service always returns SERVING (no dependencies to check).
50
+ cache: Optional health status cache following AsyncHealthCacheProtocol.
51
+ Strongly recommended with multiple Watch clients: watchers share
52
+ one real dependency check per TTL instead of each running their own.
53
+ service_names: Optional list of additional service names to support.
54
+ The empty string "" is always supported and represents overall health.
55
+ check_interval: Frequency of dependency checks in Watch mode (seconds). Must be positive.
56
+ heartbeat_interval: Interval to send status even if unchanged in Watch (seconds).
57
+ Must be >= check_interval.
58
+ check_timeout: Maximum seconds to wait for each health check run. Must be positive.
59
+
60
+ Raises:
61
+ ValueError: If check_interval, heartbeat_interval or check_timeout are invalid.
62
+ """
63
+ if check_interval <= 0:
64
+ raise ValueError(f"check_interval must be positive, got {check_interval}")
65
+ if heartbeat_interval < check_interval:
66
+ raise ValueError(f"heartbeat_interval ({heartbeat_interval}) must be >= check_interval ({check_interval})")
67
+ if check_timeout <= 0:
68
+ raise ValueError(f"check_timeout must be positive, got {check_timeout}")
69
+
70
+ self._checkers = checkers or []
71
+ self._cache = cache
72
+ self._service_names = set(service_names or [])
73
+ self._service_names.add("") # Always support overall health check
74
+ self._check_interval = check_interval
75
+ self._heartbeat_interval = heartbeat_interval
76
+ self._check_timeout = check_timeout
77
+
78
+ def _validate_service_name(self, service: str) -> bool:
79
+ """Validate gRPC service name ("" is the overall-health wildcard)."""
80
+ if not service:
81
+ return True
82
+ return is_valid_service_name(service)
83
+
84
+ def _is_service_known(self, service: str) -> bool:
85
+ """Check if the requested service name is registered."""
86
+ return service in self._service_names
87
+
88
+ async def _perform_health_check(self, service: str = "") -> health_pb2.HealthCheckResponse.ServingStatus:
89
+ """Perform health check with error handling.
90
+
91
+ Returns:
92
+ SERVING if all checks pass, NOT_SERVING on any failure or exception.
93
+ """
94
+ try:
95
+ return await check_async_overall_health(
96
+ cache=self._cache,
97
+ checkers=self._checkers,
98
+ timeout=self._check_timeout,
99
+ )
100
+ except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
101
+ raise
102
+ except (RuntimeError, ValueError, TimeoutError) as e:
103
+ # Expected/recoverable errors during health check execution
104
+ logger.warning("Health check execution failed (recoverable)", extra={"service": service, "error": str(e)})
105
+ return health_pb2.HealthCheckResponse.NOT_SERVING
106
+ except Exception:
107
+ logger.exception("Health check failed due to unexpected error", extra={"service": service})
108
+ return health_pb2.HealthCheckResponse.NOT_SERVING
109
+
110
+ def _should_send_update(
111
+ self,
112
+ status: health_pb2.HealthCheckResponse.ServingStatus,
113
+ last_status: health_pb2.HealthCheckResponse.ServingStatus | None,
114
+ last_heartbeat: float,
115
+ now: float,
116
+ ) -> bool:
117
+ """Determine if a status update should be sent in Watch mode.
118
+
119
+ Returns:
120
+ True if update should be sent (status changed or heartbeat interval reached).
121
+ """
122
+ is_changed = status != last_status
123
+ is_heartbeat = (now - last_heartbeat) >= self._heartbeat_interval
124
+ return is_changed or is_heartbeat
125
+
126
+ async def Check(
127
+ self,
128
+ request: health_pb2.HealthCheckRequest,
129
+ _context: grpc.aio.ServicerContext[health_pb2.HealthCheckRequest, health_pb2.HealthCheckResponse],
130
+ ) -> health_pb2.HealthCheckResponse:
131
+ """Check service health with real-time dependency checks (unary).
132
+
133
+ Implements the Check RPC from gRPC Health Checking Protocol v1.
134
+
135
+ Returns:
136
+ Health check response with status (SERVING, NOT_SERVING, or SERVICE_UNKNOWN).
137
+ """
138
+ if not self._validate_service_name(request.service):
139
+ logger.warning("Health check with invalid service name", extra={"service": request.service})
140
+ return health_pb2.HealthCheckResponse(status=health_pb2.HealthCheckResponse.SERVICE_UNKNOWN)
141
+
142
+ if not self._is_service_known(request.service):
143
+ logger.debug("Health check for unknown service", extra={"service": request.service})
144
+ return health_pb2.HealthCheckResponse(status=health_pb2.HealthCheckResponse.SERVICE_UNKNOWN)
145
+
146
+ status = await self._perform_health_check(service=request.service)
147
+ logger.debug(
148
+ "Health check completed",
149
+ extra={
150
+ "service": request.service,
151
+ "status": health_pb2.HealthCheckResponse.ServingStatus.Name(status),
152
+ },
153
+ )
154
+ return health_pb2.HealthCheckResponse(status=status)
155
+
156
+ async def Watch(
157
+ self,
158
+ request: health_pb2.HealthCheckRequest,
159
+ context: grpc.aio.ServicerContext[health_pb2.HealthCheckRequest, health_pb2.HealthCheckResponse],
160
+ ) -> AsyncGenerator[health_pb2.HealthCheckResponse, None]:
161
+ """Watch service health status with streaming updates.
162
+
163
+ Implements the Watch RPC from gRPC Health Checking Protocol v1.
164
+ Sends updates when status changes or on heartbeat interval. Per the
165
+ protocol a Watch stream never completes normally: internal failures
166
+ abort the stream with INTERNAL so clients can distinguish a broken
167
+ watch from a deliberate server shutdown.
168
+
169
+ Yields:
170
+ Streaming health check responses as status changes.
171
+ """
172
+ if not self._validate_service_name(request.service):
173
+ logger.warning("Health watch with invalid service name", extra={"service": request.service})
174
+ yield health_pb2.HealthCheckResponse(status=health_pb2.HealthCheckResponse.SERVICE_UNKNOWN)
175
+ return
176
+
177
+ if not self._is_service_known(request.service):
178
+ logger.debug("Health watch for unknown service", extra={"service": request.service})
179
+ yield health_pb2.HealthCheckResponse(status=health_pb2.HealthCheckResponse.SERVICE_UNKNOWN)
180
+ return
181
+
182
+ logger.info(
183
+ "Health watch started",
184
+ extra={
185
+ "service": request.service,
186
+ "check_interval": self._check_interval,
187
+ "heartbeat_interval": self._heartbeat_interval,
188
+ },
189
+ )
190
+
191
+ last_status: health_pb2.HealthCheckResponse.ServingStatus | None = None
192
+ last_heartbeat = 0.0
193
+
194
+ try:
195
+ while not context.done():
196
+ status = await self._perform_health_check(service=request.service)
197
+ now = asyncio.get_running_loop().time()
198
+
199
+ if self._should_send_update(status, last_status, last_heartbeat, now):
200
+ last_status = status
201
+ last_heartbeat = now
202
+ yield health_pb2.HealthCheckResponse(status=status)
203
+
204
+ if context.done():
205
+ break
206
+
207
+ await asyncio.sleep(self._check_interval)
208
+
209
+ except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
210
+ logger.info("Health watch cancelled", extra={"service": request.service})
211
+ raise
212
+ except grpc.aio.AbortError:
213
+ raise
214
+ except (RuntimeError, ValueError, TimeoutError) as e:
215
+ logger.warning("Health watch failed (recoverable)", extra={"service": request.service, "error": str(e)})
216
+ await self._abort_watch(context)
217
+ except Exception:
218
+ logger.exception("Health watch failed due to unexpected error", extra={"service": request.service})
219
+ await self._abort_watch(context)
220
+ finally:
221
+ logger.info("Health watch stopped", extra={"service": request.service})
222
+
223
+ @staticmethod
224
+ async def _abort_watch(
225
+ context: grpc.aio.ServicerContext[health_pb2.HealthCheckRequest, health_pb2.HealthCheckResponse],
226
+ ) -> NoReturn:
227
+ """Terminate a broken Watch stream with an error status (never OK EOF)."""
228
+ await context.abort(grpc.StatusCode.INTERNAL, "Health watch failed")
229
+ raise RuntimeError("ServicerContext.abort() returned instead of raising")
@@ -0,0 +1,62 @@
1
+ """Async gRPC server interceptors.
2
+
3
+ All interceptors are streaming-aware: they wrap the whole RPC (including full
4
+ response streams), so timings, error mapping, and cleanup cover the entire
5
+ call. Build custom interceptors by subclassing
6
+ :class:`~grpc_server_kit.aio.interceptors.base.AsyncServerInterceptor`.
7
+
8
+ Canonical execution order (outermost first):
9
+ 1. ``AsyncMetricsInterceptor`` (metrics — measures everything below)
10
+ 2. ``AsyncContextInterceptor`` (extract metadata into context vars)
11
+ 3. ``AsyncRequestLoggerInterceptor`` (request/response logging)
12
+ 4. ``AsyncTracingInterceptor`` (tracing span)
13
+ 5. ``AsyncSentryInterceptor`` (error tracking)
14
+ 6. ``AsyncExceptionHandlerInterceptor`` (exception → gRPC status mapping)
15
+ """
16
+
17
+ from .base import AsyncServerInterceptor, RpcCall, split_method_name
18
+ from .context import AsyncContextInterceptor, HeaderConfig
19
+ from .exception_handler import (
20
+ GRPC_DEFAULT_ERROR_STATUS_MAP,
21
+ GRPC_SAFE_ERROR_MESSAGES,
22
+ AsyncExceptionHandlerInterceptor,
23
+ ErrorDetailFactory,
24
+ default_error_detail,
25
+ find_mapped_status,
26
+ )
27
+ from .metadata import get_metadata_dict
28
+ from .metrics import AsyncMetricsInterceptor
29
+ from .protocols import (
30
+ ErrorReporterProtocol,
31
+ GrpcServerMetricsProtocol,
32
+ SpanAttributeValue,
33
+ SpanProtocol,
34
+ TracerProtocol,
35
+ )
36
+ from .request_logger import AsyncRequestLoggerInterceptor
37
+ from .sentry import AsyncSentryInterceptor
38
+ from .tracing import AsyncTracingInterceptor
39
+
40
+ __all__ = [
41
+ "GRPC_DEFAULT_ERROR_STATUS_MAP",
42
+ "GRPC_SAFE_ERROR_MESSAGES",
43
+ "AsyncContextInterceptor",
44
+ "AsyncExceptionHandlerInterceptor",
45
+ "AsyncMetricsInterceptor",
46
+ "AsyncRequestLoggerInterceptor",
47
+ "AsyncSentryInterceptor",
48
+ "AsyncServerInterceptor",
49
+ "AsyncTracingInterceptor",
50
+ "ErrorDetailFactory",
51
+ "ErrorReporterProtocol",
52
+ "GrpcServerMetricsProtocol",
53
+ "HeaderConfig",
54
+ "RpcCall",
55
+ "SpanAttributeValue",
56
+ "SpanProtocol",
57
+ "TracerProtocol",
58
+ "default_error_detail",
59
+ "find_mapped_status",
60
+ "get_metadata_dict",
61
+ "split_method_name",
62
+ ]