Coverage for dataexcept/trace_context.py: 92%

91 statements  

« prev     ^ index     » next       coverage.py v7.16.0, created at 2026-09-27 14:44 +0000

1"""W3C Trace Context parsing without a tracing runtime dependency. 

2 

3DataExcept only needs enough trace context to keep failures correlated across 

4execution boundaries. It does not start spans or generate identifiers. The 

5raw carrier values are preserved for forwarding, while parsed identifiers are 

6available to logging, error-tracker and protocol adapters. 

7""" 

8 

9from __future__ import annotations 

10 

11import re 

12from collections.abc import Mapping 

13from dataclasses import dataclass, replace 

14 

15from .observability import OperationContext 

16 

17__all__ = [ 

18 "W3CTraceContext", 

19 "parse_traceparent", 

20 "trace_context_from_mapping", 

21] 

22 

23_LOWER_HEX = re.compile(r"^[0-9a-f]+$") 

24_ZERO_TRACE_ID = "0" * 32 

25_ZERO_PARENT_ID = "0" * 16 

26 

27 

28def _is_lower_hex(value: str, length: int) -> bool: 

29 return len(value) == length and _LOWER_HEX.fullmatch(value) is not None 

30 

31 

32def _safe_optional_header(value: object) -> str | None: 

33 """Return a propagation value only when it is a single safe text field.""" 

34 if not isinstance(value, str): 

35 return None 

36 if "\r" in value or "\n" in value: 

37 return None 

38 return value 

39 

40 

41def _has_valid_separators(value: str) -> bool: 

42 """Return whether the mandatory traceparent separators are present.""" 

43 return value[2] == "-" and value[35] == "-" and value[52] == "-" 

44 

45 

46def _mandatory_fields_are_valid( 

47 version: str, 

48 trace_id: str, 

49 parent_id: str, 

50 trace_flags: str, 

51) -> bool: 

52 """Validate the fixed-width fields shared by all traceparent versions.""" 

53 if not _is_lower_hex(version, 2) or version == "ff": 

54 return False 

55 if not _is_lower_hex(trace_id, 32) or trace_id == _ZERO_TRACE_ID: 

56 return False 

57 if not _is_lower_hex(parent_id, 16) or parent_id == _ZERO_PARENT_ID: 

58 return False 

59 return _is_lower_hex(trace_flags, 2) 

60 

61 

62def _version_shape_is_valid(value: str, version: str) -> bool: 

63 """Validate version-specific length and extension framing rules.""" 

64 if version == "00": 

65 return len(value) == 55 

66 return len(value) == 55 or value[55] == "-" 

67 

68 

69@dataclass(frozen=True, slots=True) 

70class W3CTraceContext: 

71 """Parsed W3C ``traceparent`` plus optional propagation companions. 

72 

73 ``parent_id`` is the caller's span identifier from the incoming carrier. 

74 It is deliberately not treated as the local ``OperationContext.span_id``. 

75 ``traceparent`` is retained verbatim so pass-through code can forward an 

76 unknown future version without reconstructing fields it does not understand. 

77 """ 

78 

79 traceparent: str 

80 version: str 

81 trace_id: str 

82 parent_id: str 

83 trace_flags: str 

84 tracestate: str | None = None 

85 baggage: str | None = None 

86 

87 @property 

88 def sampled(self) -> bool: 

89 """Whether the W3C sampled bit is set in ``trace_flags``.""" 

90 return bool(int(self.trace_flags, 16) & 0x01) 

91 

92 def to_carrier(self) -> dict[str, str]: 

93 """Return propagation fields without changing their received values.""" 

94 carrier = {"traceparent": self.traceparent} 

95 if self.tracestate is not None: 

96 carrier["tracestate"] = self.tracestate 

97 if self.baggage is not None: 

98 carrier["baggage"] = self.baggage 

99 return carrier 

100 

101 def to_operation_context( 

102 self, 

103 context: OperationContext | None = None, 

104 ) -> OperationContext: 

105 """Return *context* correlated with this trace without inventing a span. 

106 

107 Existing operation metadata is preserved. A conflicting trace ID is 

108 rejected because silently replacing provenance would make correlation 

109 less trustworthy than omitting it. 

110 """ 

111 if context is None: 111 ↛ 112line 111 didn't jump to line 112 because the condition on line 111 was never true

112 return OperationContext(trace_id=self.trace_id) 

113 if not isinstance(context, OperationContext): 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true

114 raise TypeError("context must be an OperationContext or None") 

115 if context.trace_id is not None and context.trace_id != self.trace_id: 

116 raise ValueError("context trace_id conflicts with traceparent") 

117 if context.trace_id == self.trace_id: 117 ↛ 118line 117 didn't jump to line 118 because the condition on line 117 was never true

118 return context 

119 return replace(context, trace_id=self.trace_id) 

120 

121 

122def parse_traceparent( 

123 value: str, 

124 *, 

125 tracestate: str | None = None, 

126 baggage: str | None = None, 

127) -> W3CTraceContext | None: 

128 """Parse a W3C ``traceparent`` value, returning ``None`` when invalid. 

129 

130 Version ``00`` uses the exact 55-character format. Higher versions are 

131 accepted when their mandatory prefix is parseable; any extension remains 

132 opaque and is preserved in ``traceparent`` for forwarding. No identifiers 

133 are generated when parsing fails. 

134 """ 

135 if not isinstance(value, str): 

136 raise TypeError("traceparent must be a string") 

137 if value != value.strip() or len(value) < 55: 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true

138 return None 

139 if not _has_valid_separators(value): 139 ↛ 140line 139 didn't jump to line 140 because the condition on line 139 was never true

140 return None 

141 

142 version = value[:2] 

143 trace_id = value[3:35] 

144 parent_id = value[36:52] 

145 trace_flags = value[53:55] 

146 

147 if not _mandatory_fields_are_valid(version, trace_id, parent_id, trace_flags): 

148 return None 

149 if not _version_shape_is_valid(value, version): 

150 return None 

151 

152 return W3CTraceContext( 

153 traceparent=value, 

154 version=version, 

155 trace_id=trace_id, 

156 parent_id=parent_id, 

157 trace_flags=trace_flags, 

158 tracestate=_safe_optional_header(tracestate), 

159 baggage=_safe_optional_header(baggage), 

160 ) 

161 

162 

163def trace_context_from_mapping( 

164 carrier: Mapping[str, object], 

165) -> W3CTraceContext | None: 

166 """Extract W3C trace context from a case-insensitive mapping-like carrier. 

167 

168 The mapping can represent HTTP headers, RPC metadata, broker properties or 

169 protocol metadata such as MCP ``_meta``. Invalid ``tracestate`` or 

170 ``baggage`` values are dropped without invalidating a valid ``traceparent``. 

171 """ 

172 if not isinstance(carrier, Mapping): 

173 raise TypeError("carrier must be a mapping") 

174 

175 values: dict[str, object] = {} 

176 for key, value in carrier.items(): 

177 if isinstance(key, str): 177 ↛ 176line 177 didn't jump to line 176 because the condition on line 177 was always true

178 normalized = key.lower() 

179 if normalized in {"traceparent", "tracestate", "baggage"}: 

180 values[normalized] = value 

181 

182 traceparent = values.get("traceparent") 

183 if traceparent is None: 

184 return None 

185 if not isinstance(traceparent, str): 

186 return None 

187 

188 return parse_traceparent( 

189 traceparent, 

190 tracestate=_safe_optional_header(values.get("tracestate")), 

191 baggage=_safe_optional_header(values.get("baggage")), 

192 )