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,104 @@
1
+ """Metrics interceptor for gRPC (backend-neutral recorder seam)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import time
8
+ from typing import TYPE_CHECKING
9
+
10
+ import grpc
11
+
12
+ from grpc_server_kit.interceptors.constants import SKIPPED_HEALTH_METHODS
13
+ from grpc_server_kit.interceptors.utils import resolve_status_code
14
+
15
+ from .base import AsyncServerInterceptor, RpcCall
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import AsyncIterator, Collection
19
+
20
+ from .protocols import GrpcServerMetricsProtocol
21
+
22
+ __all__ = ["AsyncMetricsInterceptor"]
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class AsyncMetricsInterceptor(AsyncServerInterceptor):
28
+ """Interceptor that records gRPC request metrics via a metrics recorder.
29
+
30
+ Recording is delegated to any object satisfying ``GrpcServerMetricsProtocol``,
31
+ so the kit never imports a metrics backend directly. Skips gRPC health check
32
+ methods by default, and metric-recording failures never fail the request.
33
+ For streaming RPCs the recorded duration covers the whole stream.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ metrics: GrpcServerMetricsProtocol | None = None,
39
+ service_name: str = "unknown",
40
+ *,
41
+ skip_methods: Collection[str] = SKIPPED_HEALTH_METHODS,
42
+ ) -> None:
43
+ """Initialize interceptor with the metrics recorder and service name.
44
+
45
+ Args:
46
+ metrics: A ``GrpcServerMetricsProtocol`` implementation or None.
47
+ service_name: The name of the service to include in labels.
48
+ skip_methods: Full RPC method names to exclude from metrics
49
+ (defaults to the gRPC health-check methods).
50
+
51
+ Raises:
52
+ ValueError: If ``service_name`` is empty.
53
+ """
54
+ super().__init__(skip_methods=skip_methods)
55
+ if not service_name:
56
+ raise ValueError("service_name cannot be empty")
57
+ self._service_name = service_name
58
+ self._metrics = metrics
59
+
60
+ async def around_call(self, call: RpcCall) -> AsyncIterator[None]:
61
+ """Record request count and duration around the RPC."""
62
+ if self._metrics is None:
63
+ yield
64
+ return
65
+
66
+ start_time = time.perf_counter()
67
+ status = "success"
68
+ grpc_code = grpc.StatusCode.OK
69
+
70
+ try:
71
+ yield
72
+ except BaseException as exc:
73
+ status = "error"
74
+ grpc_code = self._classify_error(exc, call)
75
+ raise
76
+ else:
77
+ grpc_code = resolve_status_code(None, call.context, default=grpc.StatusCode.OK)
78
+ finally:
79
+ duration = time.perf_counter() - start_time
80
+ try:
81
+ self._metrics.record_request(
82
+ service=self._service_name,
83
+ method=call.method_name,
84
+ status=status,
85
+ grpc_code=grpc_code.name,
86
+ duration=duration,
87
+ )
88
+ except Exception:
89
+ # Metrics errors should not fail the request
90
+ logger.exception("Failed to record gRPC server metrics")
91
+
92
+ def _classify_error(self, exc: BaseException, call: RpcCall) -> grpc.StatusCode:
93
+ """Resolve the status code to record for a failed RPC."""
94
+ if isinstance(exc, asyncio.CancelledError | GeneratorExit):
95
+ # Task cancellation / client gone mid-stream.
96
+ return grpc.StatusCode.CANCELLED
97
+ if isinstance(exc, grpc.aio.AbortError):
98
+ # Deliberate abort — the context carries the real status.
99
+ return resolve_status_code(None, call.context)
100
+ code = resolve_status_code(exc if isinstance(exc, grpc.RpcError) else None, call.context)
101
+ if code in (grpc.StatusCode.OK, grpc.StatusCode.UNKNOWN) and not isinstance(exc, grpc.RpcError):
102
+ # An exception with no explicit status is an internal error.
103
+ return grpc.StatusCode.INTERNAL
104
+ return code
@@ -0,0 +1,54 @@
1
+ """Structural protocols for optional interceptor dependencies (duck-typing seams).
2
+
3
+ The cross-cutting observability seams (span/tracer, error reporter) are owned by
4
+ :mod:`grpc_server_kit.observability.protocols` and re-exported here for
5
+ interceptor consumers. The protocols defined locally are gRPC-specific shapes
6
+ that belong to this package.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Protocol
12
+
13
+ from grpc_server_kit.observability.protocols import (
14
+ ErrorReporterProtocol,
15
+ SpanAttributeValue,
16
+ SpanProtocol,
17
+ TracerProtocol,
18
+ )
19
+
20
+
21
+ class GrpcServerMetricsProtocol(Protocol):
22
+ """Protocol for a gRPC server metrics recorder.
23
+
24
+ The kit's prometheus-backed ``GrpcServerMetrics`` satisfies this
25
+ structurally, as does any recorder with the same shape.
26
+ """
27
+
28
+ def record_request(
29
+ self,
30
+ service: str,
31
+ method: str,
32
+ status: str,
33
+ grpc_code: str,
34
+ duration: float,
35
+ ) -> None:
36
+ """Record a completed gRPC request.
37
+
38
+ Args:
39
+ service: Name of the service.
40
+ method: Name of the method.
41
+ status: Status of the request (success, error).
42
+ grpc_code: gRPC status code name.
43
+ duration: Request duration in seconds.
44
+ """
45
+ ...
46
+
47
+
48
+ __all__ = [
49
+ "ErrorReporterProtocol",
50
+ "GrpcServerMetricsProtocol",
51
+ "SpanAttributeValue",
52
+ "SpanProtocol",
53
+ "TracerProtocol",
54
+ ]
@@ -0,0 +1,103 @@
1
+ """Request logger interceptor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import grpc
10
+
11
+ from grpc_server_kit.interceptors.constants import SKIPPED_HEALTH_METHODS
12
+ from grpc_server_kit.interceptors.utils import is_server_error, resolve_status_code
13
+
14
+ from .base import AsyncServerInterceptor, RpcCall
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import AsyncIterator, Collection
18
+
19
+ __all__ = ["AsyncRequestLoggerInterceptor"]
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _sanitize_peer(peer: str | None) -> str:
25
+ """Sanitize peer address to avoid logging internal IPs."""
26
+ if not peer:
27
+ return "unknown"
28
+ # Log only the protocol, hide the actual IP
29
+ for proto in ("ipv4", "ipv6", "unix"):
30
+ if peer.startswith(f"{proto}:"):
31
+ return proto
32
+ return "unknown"
33
+
34
+
35
+ class AsyncRequestLoggerInterceptor(AsyncServerInterceptor):
36
+ """Interceptor that logs start and completion of gRPC requests.
37
+
38
+ Logs via standard logging with ``extra=`` for method and optional peer protocol.
39
+ Skips logging for health check methods. For streaming RPCs the completion log
40
+ is emitted after the full stream has been produced.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ *,
46
+ log_peer: bool = False,
47
+ log_request_on_error: bool = False,
48
+ skip_methods: Collection[str] = SKIPPED_HEALTH_METHODS,
49
+ ) -> None:
50
+ """Initialize interceptor.
51
+
52
+ Args:
53
+ log_peer: Whether to log the peer protocol of the gRPC request.
54
+ log_request_on_error: Whether to include ``str(request)`` in server-error
55
+ logs. Off by default — request payloads may contain sensitive data.
56
+ skip_methods: Full RPC method names to exclude from logging
57
+ (defaults to the gRPC health-check methods).
58
+ """
59
+ super().__init__(skip_methods=skip_methods)
60
+ self._log_peer = log_peer
61
+ self._log_request_on_error = log_request_on_error
62
+
63
+ async def around_call(self, call: RpcCall) -> AsyncIterator[None]:
64
+ """Log request start/completion/failure around the RPC."""
65
+ log_data: dict[str, Any] = {"method": call.method_name}
66
+ if self._log_peer:
67
+ log_data["peer_protocol"] = _sanitize_peer(call.context.peer())
68
+
69
+ logger.info("gRPC request started", extra=log_data)
70
+ try:
71
+ yield
72
+ except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
73
+ logger.info("gRPC request cancelled", extra={"method": call.method_name})
74
+ raise
75
+ except grpc.aio.AbortError:
76
+ # An interceptor or the handler aborted with an explicit status; the
77
+ # context carries the real code. No stack trace — the abort was deliberate.
78
+ status = resolve_status_code(None, call.context)
79
+ self._log_failure(call, status, exc_info=False)
80
+ raise
81
+ except grpc.RpcError as exc:
82
+ status = resolve_status_code(exc, call.context)
83
+ self._log_failure(call, status, exc_info=is_server_error(status))
84
+ raise
85
+ except Exception:
86
+ # Unexpected exceptions that are not gRPC errors yet: log with stack trace.
87
+ extra: dict[str, Any] = {"method": call.method_name}
88
+ if self._log_request_on_error:
89
+ extra["request"] = str(call.request)
90
+ logger.exception("gRPC request failed (unexpected exception)", extra=extra)
91
+ raise
92
+
93
+ logger.info("gRPC request completed", extra={"method": call.method_name})
94
+
95
+ def _log_failure(self, call: RpcCall, status: grpc.StatusCode, *, exc_info: bool) -> None:
96
+ """Log a failed RPC at a severity matching the status code."""
97
+ extra: dict[str, Any] = {"method": call.method_name, "status": status.name}
98
+ if is_server_error(status):
99
+ if self._log_request_on_error:
100
+ extra["request"] = str(call.request)
101
+ logger.error("gRPC request failed (server error)", extra=extra, exc_info=exc_info)
102
+ else:
103
+ logger.info("gRPC request failed (client error)", extra=extra)
@@ -0,0 +1,102 @@
1
+ """Sentry interceptor."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import TYPE_CHECKING
7
+
8
+ import grpc
9
+
10
+ from grpc_server_kit.interceptors.constants import (
11
+ METHOD_KEY,
12
+ REQUEST_ID_KEY,
13
+ SENTRY_GRPC_METHOD,
14
+ SENTRY_REQUEST_ID,
15
+ SKIPPED_HEALTH_METHODS,
16
+ X_REQUEST_ID,
17
+ )
18
+
19
+ from .base import AsyncServerInterceptor, RpcCall
20
+ from .metadata import get_metadata_dict
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import AsyncIterator, Callable, Collection
24
+
25
+ from .protocols import ErrorReporterProtocol
26
+
27
+ __all__ = ["AsyncSentryInterceptor"]
28
+
29
+
30
+ class AsyncSentryInterceptor(AsyncServerInterceptor):
31
+ """Interceptor that adds error tracking and breadcrumbs.
32
+
33
+ Each RPC runs inside the reporter's isolated scope (``reporter.isolate()``),
34
+ so tags and breadcrumbs of concurrent RPCs never contaminate each other's
35
+ reports. Health-check methods are skipped by default.
36
+
37
+ Place this interceptor INSIDE the exception handler in the chain (later in
38
+ the interceptor list) so it observes raw handler exceptions before they are
39
+ mapped into ``grpc.aio.AbortError``. Deliberate aborts and gRPC errors are
40
+ re-raised without capture.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ sentry: ErrorReporterProtocol | None = None,
46
+ *,
47
+ capture_filter: Callable[[Exception], bool] | None = None,
48
+ skip_methods: Collection[str] = SKIPPED_HEALTH_METHODS,
49
+ ) -> None:
50
+ """Initialize interceptor.
51
+
52
+ Args:
53
+ sentry: An ``ErrorReporterProtocol`` implementation or None (no-op).
54
+ capture_filter: Predicate deciding whether an unexpected exception
55
+ is captured. None captures everything. The dishka bundle wires
56
+ a filter capturing only server-class errors (per the same
57
+ mapping the exception handler uses), so client-mapped errors
58
+ like ``ValueError`` do not spam the error tracker.
59
+ skip_methods: Full RPC method names to exclude from error tracking
60
+ (defaults to the gRPC health-check methods).
61
+ """
62
+ super().__init__(skip_methods=skip_methods)
63
+ self._sentry = sentry
64
+ self._capture_filter = capture_filter
65
+
66
+ async def around_call(self, call: RpcCall) -> AsyncIterator[None]:
67
+ """Tag the isolated scope, add a breadcrumb, and capture unexpected exceptions."""
68
+ if self._sentry is None:
69
+ yield
70
+ return
71
+
72
+ metadata = get_metadata_dict(call.context)
73
+ request_id = str(metadata.get(X_REQUEST_ID, ""))
74
+
75
+ with self._sentry.isolate():
76
+ self._sentry.set_tags(
77
+ **{
78
+ SENTRY_GRPC_METHOD: call.method_name,
79
+ SENTRY_REQUEST_ID: request_id,
80
+ }
81
+ )
82
+ self._sentry.add_breadcrumb(
83
+ message=f"gRPC call: {call.method_name}",
84
+ category="grpc",
85
+ level="info",
86
+ data={METHOD_KEY: call.method_name, REQUEST_ID_KEY: request_id},
87
+ )
88
+
89
+ try:
90
+ yield
91
+ except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
92
+ raise
93
+ except grpc.aio.AbortError:
94
+ # Deliberate abort with an explicit status — not an unexpected error.
95
+ raise
96
+ except grpc.RpcError:
97
+ # Re-raised gRPC errors are handled by the exception handler interceptor.
98
+ raise
99
+ except Exception as exc:
100
+ if self._capture_filter is None or self._capture_filter(exc):
101
+ self._sentry.capture_exception(exc)
102
+ raise
@@ -0,0 +1,130 @@
1
+ """Tracing interceptor (OpenTelemetry-compatible seam).
2
+
3
+ Creates one span per RPC with OTel semantic-convention attributes
4
+ (``rpc.system``, ``rpc.service``, ``rpc.method``, ``rpc.grpc.status_code``).
5
+ Tracing is delegated to any object satisfying ``TracerProtocol`` — the
6
+ OpenTelemetry tracer fits structurally.
7
+
8
+ Note:
9
+ This interceptor is a lightweight seam and does not extract the incoming
10
+ trace context (``traceparent``) from metadata. For full distributed-tracing
11
+ propagation use the official instrumentation via
12
+ :func:`grpc_server_kit.aio.observability.instrument_aio_server` (the
13
+ ``[tracing]`` extra) — the two approaches compose.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import asyncio
19
+ from typing import TYPE_CHECKING
20
+
21
+ import grpc
22
+
23
+ from grpc_server_kit.interceptors.constants import (
24
+ OTEL_REQUEST_ID,
25
+ OTEL_RPC_GRPC_STATUS_CODE,
26
+ OTEL_RPC_METHOD,
27
+ OTEL_RPC_SERVICE,
28
+ OTEL_RPC_SYSTEM,
29
+ SKIPPED_HEALTH_METHODS,
30
+ X_REQUEST_ID,
31
+ )
32
+ from grpc_server_kit.interceptors.utils import is_server_error, resolve_status_code
33
+
34
+ from .base import AsyncServerInterceptor, RpcCall, split_method_name
35
+ from .metadata import get_metadata_dict
36
+
37
+ if TYPE_CHECKING:
38
+ from collections.abc import AsyncIterator, Collection
39
+
40
+ from .protocols import SpanProtocol, TracerProtocol
41
+
42
+ __all__ = ["AsyncTracingInterceptor"]
43
+
44
+
45
+ class AsyncTracingInterceptor(AsyncServerInterceptor):
46
+ """Interceptor that adds a tracing span to each gRPC call.
47
+
48
+ The span covers the whole RPC (including full response streams), is named
49
+ after the RPC method, and carries OTel semconv attributes. Health-check
50
+ methods are skipped by default. Exceptions are recorded against the span
51
+ for server-side errors — including errors already mapped to an abort by an
52
+ inner exception handler (recovered from ``AbortError.__context__``).
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ service_name: str,
58
+ tracer: TracerProtocol | None = None,
59
+ *,
60
+ skip_methods: Collection[str] = SKIPPED_HEALTH_METHODS,
61
+ ) -> None:
62
+ """Initialize interceptor with the service name and tracer.
63
+
64
+ Args:
65
+ service_name: Fallback service name for span attributes when the RPC
66
+ method name cannot be parsed.
67
+ tracer: A ``TracerProtocol`` implementation or None (no-op).
68
+ skip_methods: Full RPC method names to exclude from tracing
69
+ (defaults to the gRPC health-check methods).
70
+
71
+ Raises:
72
+ ValueError: If ``service_name`` is empty.
73
+ """
74
+ super().__init__(skip_methods=skip_methods)
75
+ if not service_name:
76
+ raise ValueError("service_name cannot be empty")
77
+ self._service_name = service_name
78
+ self.tracer = tracer
79
+
80
+ async def around_call(self, call: RpcCall) -> AsyncIterator[None]:
81
+ """Wrap the RPC in a span with semconv attributes."""
82
+ if self.tracer is None:
83
+ yield
84
+ return
85
+
86
+ service, method = split_method_name(call.method_name)
87
+ span_name = call.method_name.removeprefix("/") or call.method_name
88
+
89
+ with self.tracer.start_as_current_span(span_name) as span:
90
+ span.set_attribute(OTEL_RPC_SYSTEM, "grpc")
91
+ span.set_attribute(OTEL_RPC_SERVICE, service or self._service_name)
92
+ span.set_attribute(OTEL_RPC_METHOD, method or call.method_name)
93
+
94
+ metadata = get_metadata_dict(call.context)
95
+ if X_REQUEST_ID in metadata:
96
+ span.set_attribute(OTEL_REQUEST_ID, metadata[X_REQUEST_ID])
97
+
98
+ try:
99
+ yield
100
+ except (asyncio.CancelledError, KeyboardInterrupt, SystemExit):
101
+ self._set_status(span, grpc.StatusCode.CANCELLED)
102
+ raise
103
+ except grpc.aio.AbortError as exc:
104
+ status = resolve_status_code(None, call.context)
105
+ self._set_status(span, status)
106
+ if is_server_error(status):
107
+ # An inner exception handler already mapped the original
108
+ # exception into this abort; recover it from the chain so
109
+ # server-error spans still carry the real exception event.
110
+ cause = exc.__cause__ or exc.__context__
111
+ if isinstance(cause, Exception) and not isinstance(cause, grpc.aio.AbortError):
112
+ span.record_exception(cause)
113
+ raise
114
+ except grpc.RpcError as exc:
115
+ status = resolve_status_code(exc, call.context)
116
+ self._set_status(span, status)
117
+ if is_server_error(status):
118
+ span.record_exception(exc)
119
+ raise
120
+ except Exception as exc:
121
+ self._set_status(span, resolve_status_code(None, call.context, default=grpc.StatusCode.INTERNAL))
122
+ span.record_exception(exc)
123
+ raise
124
+ else:
125
+ self._set_status(span, resolve_status_code(None, call.context, default=grpc.StatusCode.OK))
126
+
127
+ @staticmethod
128
+ def _set_status(span: SpanProtocol, status: grpc.StatusCode) -> None:
129
+ """Set the numeric ``rpc.grpc.status_code`` semconv attribute."""
130
+ span.set_attribute(OTEL_RPC_GRPC_STATUS_CODE, status.value[0])
@@ -0,0 +1,5 @@
1
+ """Async observability wiring (OpenTelemetry instrumentation for the aio gRPC server)."""
2
+
3
+ from .tracing import instrument_aio_server, uninstrument_aio_server
4
+
5
+ __all__ = ["instrument_aio_server", "uninstrument_aio_server"]
@@ -0,0 +1,48 @@
1
+ """OpenTelemetry tracing wiring for the async gRPC server (``[tracing]`` extra).
2
+
3
+ Thin helpers over ``GrpcAioInstrumentorServer``. Call :func:`instrument_aio_server` once
4
+ at startup (optionally passing ``tracer_provider=...``, ``filter_=...`` etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ try:
12
+ from opentelemetry.instrumentation.grpc import GrpcAioInstrumentorServer
13
+
14
+ HAS_OTEL_GRPC = True
15
+ except ImportError: # pragma: no cover - exercised only when the [tracing] extra is absent
16
+ HAS_OTEL_GRPC = False
17
+
18
+ __all__ = ["instrument_aio_server", "uninstrument_aio_server"]
19
+
20
+ _MISSING_MESSAGE = (
21
+ "Install grpc-server-kit[tracing] (opentelemetry-instrumentation-grpc) "
22
+ "to instrument the gRPC server with OpenTelemetry"
23
+ )
24
+
25
+
26
+ def instrument_aio_server(**kwargs: Any) -> None:
27
+ """Instrument the async gRPC server with OpenTelemetry.
28
+
29
+ Keyword arguments are forwarded to ``GrpcAioInstrumentorServer().instrument()``
30
+ (e.g. ``tracer_provider``, ``filter_``).
31
+
32
+ Raises:
33
+ ImportError: If the ``[tracing]`` extra is not installed.
34
+ """
35
+ if not HAS_OTEL_GRPC:
36
+ raise ImportError(_MISSING_MESSAGE)
37
+ GrpcAioInstrumentorServer().instrument(**kwargs)
38
+
39
+
40
+ def uninstrument_aio_server() -> None:
41
+ """Remove OpenTelemetry instrumentation from the async gRPC server.
42
+
43
+ Raises:
44
+ ImportError: If the ``[tracing]`` extra is not installed.
45
+ """
46
+ if not HAS_OTEL_GRPC:
47
+ raise ImportError(_MISSING_MESSAGE)
48
+ GrpcAioInstrumentorServer().uninstrument()