Coverage for zenmqtt/instrumentation/tracing.py: 96%
67 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 21:55 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 21:55 +0000
1"""
2Tracing of messages: a producer span for every publish, and a consumer span for
3the processing of a received message. The trace context travels in the user
4properties of PUBLISH, so a consumer span continues the trace of the producer.
6Tracing is the base class with no-op methods; the OpenTelemetry implementation
7is zenmqtt.instrumentation.opentelemetry.OpenTelemetryTracing.
8"""
10from contextlib import AbstractContextManager, nullcontext
11from types import TracebackType
12from typing import Optional
14from zenmqtt.instrumentation.guard import HookErrors
15from zenmqtt.mqtt.publish import (
16 MQTTMessage,
17 PublishAcknowledgement,
18 PublishProperties,
19)
22class PublishTrace:
23 """
24 The trace of one publish call. `properties` are sent instead of the
25 properties of the call, e.g. with the trace context in user properties.
26 """
28 def __init__(self, properties: PublishProperties) -> None:
29 self.properties = properties
31 def end(
32 self,
33 result: Optional[PublishAcknowledgement],
34 error: Optional[BaseException],
35 ) -> None:
36 """
37 Called once when the publish call returns: `result` is the final
38 acknowledgement (None for QoS 0), `error` is the exception of the call.
39 """
42class Tracing:
43 def start_publish(
44 self,
45 client_id: str,
46 topic: str,
47 payload: bytes,
48 qos: int,
49 retain: bool,
50 properties: PublishProperties,
51 ) -> PublishTrace:
52 """Called when a publish call starts, before it waits for a connection."""
53 return PublishTrace(properties)
55 def trace(
56 self, client_id: str, message: MQTTMessage
57 ) -> AbstractContextManager[None]:
58 """
59 Wraps the processing of a received message, see MQTTClient.trace().
60 """
61 return nullcontext()
64class _GuardedPublishTrace(PublishTrace):
65 def __init__(self, trace: PublishTrace, errors: HookErrors) -> None:
66 super().__init__(trace.properties)
67 self._trace = trace
68 self._errors = errors
70 def end(
71 self,
72 result: Optional[PublishAcknowledgement],
73 error: Optional[BaseException],
74 ) -> None:
75 try:
76 self._trace.end(result, error)
77 except Exception as exc:
78 self._errors.report("PublishTrace.end", exc)
81class _GuardedTrace(AbstractContextManager[None]):
82 """
83 Exceptions of the tracing context are reported, and it never suppresses an
84 exception of the processing.
85 """
87 def __init__(
88 self, context: AbstractContextManager[None], errors: HookErrors
89 ) -> None:
90 self._context = context
91 self._errors = errors
92 self._entered = False
94 def __enter__(self) -> None:
95 try:
96 self._context.__enter__()
97 self._entered = True
98 except Exception as exc:
99 self._errors.report("trace.__enter__", exc)
101 def __exit__(
102 self,
103 exc_type: Optional[type[BaseException]],
104 exc: Optional[BaseException],
105 traceback: Optional[TracebackType],
106 ) -> None:
107 if not self._entered:
108 return
110 try:
111 self._context.__exit__(exc_type, exc, traceback)
112 except Exception as error:
113 self._errors.report("trace.__exit__", error)
116class _GuardedTracing(Tracing):
117 """
118 Calls a tracing implementation, its exceptions are reported and never
119 reach the client: e.g. a publish which the server acknowledged doesn't
120 fail because its span couldn't be ended.
121 """
123 def __init__(self, tracing: Tracing) -> None:
124 self.wrapped = tracing
125 self._errors = HookErrors(type(tracing).__name__, "mqtt_tracing.hook_error")
127 @property
128 def errors(self) -> dict[str, int]:
129 """Number of exceptions per hook."""
130 return self._errors.errors
132 def start_publish(
133 self,
134 client_id: str,
135 topic: str,
136 payload: bytes,
137 qos: int,
138 retain: bool,
139 properties: PublishProperties,
140 ) -> PublishTrace:
141 try:
142 trace = self.wrapped.start_publish(
143 client_id, topic, payload, qos, retain, properties
144 )
145 except Exception as exc:
146 self._errors.report("start_publish", exc)
147 return PublishTrace(properties)
149 return _GuardedPublishTrace(trace, self._errors)
151 def trace(
152 self, client_id: str, message: MQTTMessage
153 ) -> AbstractContextManager[None]:
154 try:
155 context = self.wrapped.trace(client_id, message)
156 except Exception as exc:
157 self._errors.report("trace", exc)
158 return nullcontext()
160 return _GuardedTrace(context, self._errors)
163def guard_tracing(tracing: Optional[Tracing]) -> Optional[Tracing]:
164 """
165 Wraps a tracing implementation, so its exceptions never reach the client;
166 None and a guarded one are returned as they are.
167 """
168 if tracing is None or isinstance(tracing, _GuardedTracing):
169 return tracing
171 return _GuardedTracing(tracing)