|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
15 | | -""" |
16 | | -AWS X-Ray Propagator |
17 | | --------------------- |
| 15 | +from opentelemetry.propagators.aws.aws_xray_propagator import AwsXRayPropagator |
18 | 16 |
|
19 | | -The **AWS X-Ray Propagator** provides a propagator that when used, adds a `trace |
20 | | -header`_ to outgoing traces that is compatible with the AWS X-Ray backend service. |
21 | | -This allows the trace context to be propagated when a trace spans multiple AWS |
22 | | -services. |
23 | | -
|
24 | | -The same propagator setup is used to extract a context sent by external systems |
25 | | -so that child span have the correct parent context. |
26 | | -
|
27 | | -**NOTE**: Because the parent context parsed from the ``X-Amzn-Trace-Id`` header |
28 | | -assumes the context is _not_ sampled by default, users should make sure to add |
29 | | -``Sampled=1`` to their ``X-Amzn-Trace-Id`` headers so that the child spans are |
30 | | -sampled. |
31 | | -
|
32 | | -Usage |
33 | | ------ |
34 | | -
|
35 | | -Use the provided AWS X-Ray Propagator to inject the necessary context into |
36 | | -traces sent to external systems. |
37 | | -
|
38 | | -This can be done by either setting this environment variable: |
39 | | -
|
40 | | -:: |
41 | | -
|
42 | | - export OTEL_PROPAGATORS = xray |
43 | | -
|
44 | | -
|
45 | | -Or by setting this propagator in your instrumented application: |
46 | | -
|
47 | | -.. code-block:: python |
48 | | -
|
49 | | - from opentelemetry.propagate import set_global_textmap |
50 | | - from opentelemetry.propagators.aws import AwsXRayFormat |
51 | | -
|
52 | | - set_global_textmap(AwsXRayFormat()) |
53 | | -
|
54 | | -API |
55 | | ---- |
56 | | -.. _trace header: https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader |
57 | | -""" |
58 | | - |
59 | | -import logging |
60 | | -import typing |
61 | | - |
62 | | -from opentelemetry import trace |
63 | | -from opentelemetry.context import Context |
64 | | -from opentelemetry.propagators.textmap import ( |
65 | | - CarrierT, |
66 | | - Getter, |
67 | | - Setter, |
68 | | - TextMapPropagator, |
69 | | - default_getter, |
70 | | - default_setter, |
71 | | -) |
72 | | - |
73 | | -TRACE_HEADER_KEY = "X-Amzn-Trace-Id" |
74 | | -KV_PAIR_DELIMITER = ";" |
75 | | -KEY_AND_VALUE_DELIMITER = "=" |
76 | | - |
77 | | -TRACE_ID_KEY = "Root" |
78 | | -TRACE_ID_LENGTH = 35 |
79 | | -TRACE_ID_VERSION = "1" |
80 | | -TRACE_ID_DELIMITER = "-" |
81 | | -TRACE_ID_DELIMITER_INDEX_1 = 1 |
82 | | -TRACE_ID_DELIMITER_INDEX_2 = 10 |
83 | | -TRACE_ID_FIRST_PART_LENGTH = 8 |
84 | | - |
85 | | -PARENT_ID_KEY = "Parent" |
86 | | -PARENT_ID_LENGTH = 16 |
87 | | - |
88 | | -SAMPLED_FLAG_KEY = "Sampled" |
89 | | -SAMPLED_FLAG_LENGTH = 1 |
90 | | -IS_SAMPLED = "1" |
91 | | -NOT_SAMPLED = "0" |
92 | | - |
93 | | - |
94 | | -_logger = logging.getLogger(__name__) |
95 | | - |
96 | | - |
97 | | -class AwsParseTraceHeaderError(Exception): |
98 | | - def __init__(self, message): |
99 | | - super().__init__() |
100 | | - self.message = message |
101 | | - |
102 | | - |
103 | | -class AwsXRayFormat(TextMapPropagator): |
104 | | - """Propagator for the AWS X-Ray Trace Header propagation protocol. |
105 | | -
|
106 | | - See: https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader |
107 | | - """ |
108 | | - |
109 | | - # AWS |
110 | | - |
111 | | - def extract( |
112 | | - self, |
113 | | - carrier: CarrierT, |
114 | | - context: typing.Optional[Context] = None, |
115 | | - getter: Getter = default_getter, |
116 | | - ) -> Context: |
117 | | - if context is None: |
118 | | - context = Context() |
119 | | - |
120 | | - trace_header_list = getter.get(carrier, TRACE_HEADER_KEY) |
121 | | - |
122 | | - if not trace_header_list or len(trace_header_list) != 1: |
123 | | - return context |
124 | | - |
125 | | - trace_header = trace_header_list[0] |
126 | | - |
127 | | - if not trace_header: |
128 | | - return context |
129 | | - |
130 | | - try: |
131 | | - ( |
132 | | - trace_id, |
133 | | - span_id, |
134 | | - sampled, |
135 | | - ) = AwsXRayFormat._extract_span_properties(trace_header) |
136 | | - except AwsParseTraceHeaderError as err: |
137 | | - _logger.debug(err.message) |
138 | | - return context |
139 | | - |
140 | | - options = 0 |
141 | | - if sampled: |
142 | | - options |= trace.TraceFlags.SAMPLED |
143 | | - |
144 | | - span_context = trace.SpanContext( |
145 | | - trace_id=trace_id, |
146 | | - span_id=span_id, |
147 | | - is_remote=True, |
148 | | - trace_flags=trace.TraceFlags(options), |
149 | | - trace_state=trace.TraceState(), |
150 | | - ) |
151 | | - |
152 | | - if not span_context.is_valid: |
153 | | - _logger.debug( |
154 | | - "Invalid Span Extracted. Inserting INVALID span into provided context." |
155 | | - ) |
156 | | - return context |
157 | | - |
158 | | - return trace.set_span_in_context( |
159 | | - trace.NonRecordingSpan(span_context), context=context |
160 | | - ) |
161 | | - |
162 | | - @staticmethod |
163 | | - def _extract_span_properties(trace_header): |
164 | | - trace_id = trace.INVALID_TRACE_ID |
165 | | - span_id = trace.INVALID_SPAN_ID |
166 | | - sampled = False |
167 | | - |
168 | | - for kv_pair_str in trace_header.split(KV_PAIR_DELIMITER): |
169 | | - try: |
170 | | - key_str, value_str = kv_pair_str.split(KEY_AND_VALUE_DELIMITER) |
171 | | - key, value = key_str.strip(), value_str.strip() |
172 | | - except ValueError as ex: |
173 | | - raise AwsParseTraceHeaderError( |
174 | | - ( |
175 | | - "Error parsing X-Ray trace header. Invalid key value pair: %s. Returning INVALID span context.", |
176 | | - kv_pair_str, |
177 | | - ) |
178 | | - ) from ex |
179 | | - if key == TRACE_ID_KEY: |
180 | | - if not AwsXRayFormat._validate_trace_id(value): |
181 | | - raise AwsParseTraceHeaderError( |
182 | | - ( |
183 | | - "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", |
184 | | - TRACE_HEADER_KEY, |
185 | | - trace_header, |
186 | | - ) |
187 | | - ) |
188 | | - |
189 | | - try: |
190 | | - trace_id = AwsXRayFormat._parse_trace_id(value) |
191 | | - except ValueError as ex: |
192 | | - raise AwsParseTraceHeaderError( |
193 | | - ( |
194 | | - "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", |
195 | | - TRACE_HEADER_KEY, |
196 | | - trace_header, |
197 | | - ) |
198 | | - ) from ex |
199 | | - elif key == PARENT_ID_KEY: |
200 | | - if not AwsXRayFormat._validate_span_id(value): |
201 | | - raise AwsParseTraceHeaderError( |
202 | | - ( |
203 | | - "Invalid ParentId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", |
204 | | - TRACE_HEADER_KEY, |
205 | | - trace_header, |
206 | | - ) |
207 | | - ) |
208 | | - |
209 | | - try: |
210 | | - span_id = AwsXRayFormat._parse_span_id(value) |
211 | | - except ValueError as ex: |
212 | | - raise AwsParseTraceHeaderError( |
213 | | - ( |
214 | | - "Invalid TraceId in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", |
215 | | - TRACE_HEADER_KEY, |
216 | | - trace_header, |
217 | | - ) |
218 | | - ) from ex |
219 | | - elif key == SAMPLED_FLAG_KEY: |
220 | | - if not AwsXRayFormat._validate_sampled_flag(value): |
221 | | - raise AwsParseTraceHeaderError( |
222 | | - ( |
223 | | - "Invalid Sampling flag in X-Ray trace header: '%s' with value '%s'. Returning INVALID span context.", |
224 | | - TRACE_HEADER_KEY, |
225 | | - trace_header, |
226 | | - ) |
227 | | - ) |
228 | | - |
229 | | - sampled = AwsXRayFormat._parse_sampled_flag(value) |
230 | | - |
231 | | - return trace_id, span_id, sampled |
232 | | - |
233 | | - @staticmethod |
234 | | - def _validate_trace_id(trace_id_str): |
235 | | - return ( |
236 | | - len(trace_id_str) == TRACE_ID_LENGTH |
237 | | - and trace_id_str.startswith(TRACE_ID_VERSION) |
238 | | - and trace_id_str[TRACE_ID_DELIMITER_INDEX_1] == TRACE_ID_DELIMITER |
239 | | - and trace_id_str[TRACE_ID_DELIMITER_INDEX_2] == TRACE_ID_DELIMITER |
240 | | - ) |
241 | | - |
242 | | - @staticmethod |
243 | | - def _parse_trace_id(trace_id_str): |
244 | | - timestamp_subset = trace_id_str[ |
245 | | - TRACE_ID_DELIMITER_INDEX_1 + 1 : TRACE_ID_DELIMITER_INDEX_2 |
246 | | - ] |
247 | | - unique_id_subset = trace_id_str[ |
248 | | - TRACE_ID_DELIMITER_INDEX_2 + 1 : TRACE_ID_LENGTH |
249 | | - ] |
250 | | - return int(timestamp_subset + unique_id_subset, 16) |
251 | | - |
252 | | - @staticmethod |
253 | | - def _validate_span_id(span_id_str): |
254 | | - return len(span_id_str) == PARENT_ID_LENGTH |
255 | | - |
256 | | - @staticmethod |
257 | | - def _parse_span_id(span_id_str): |
258 | | - return int(span_id_str, 16) |
259 | | - |
260 | | - @staticmethod |
261 | | - def _validate_sampled_flag(sampled_flag_str): |
262 | | - return len( |
263 | | - sampled_flag_str |
264 | | - ) == SAMPLED_FLAG_LENGTH and sampled_flag_str in ( |
265 | | - IS_SAMPLED, |
266 | | - NOT_SAMPLED, |
267 | | - ) |
268 | | - |
269 | | - @staticmethod |
270 | | - def _parse_sampled_flag(sampled_flag_str): |
271 | | - return sampled_flag_str[0] == IS_SAMPLED |
272 | | - |
273 | | - def inject( |
274 | | - self, |
275 | | - carrier: CarrierT, |
276 | | - context: typing.Optional[Context] = None, |
277 | | - setter: Setter = default_setter, |
278 | | - ) -> None: |
279 | | - span = trace.get_current_span(context=context) |
280 | | - |
281 | | - span_context = span.get_span_context() |
282 | | - if not span_context.is_valid: |
283 | | - return |
284 | | - |
285 | | - otel_trace_id = f"{span_context.trace_id:032x}" |
286 | | - xray_trace_id = TRACE_ID_DELIMITER.join( |
287 | | - [ |
288 | | - TRACE_ID_VERSION, |
289 | | - otel_trace_id[:TRACE_ID_FIRST_PART_LENGTH], |
290 | | - otel_trace_id[TRACE_ID_FIRST_PART_LENGTH:], |
291 | | - ] |
292 | | - ) |
293 | | - |
294 | | - parent_id = f"{span_context.span_id:016x}" |
295 | | - |
296 | | - sampling_flag = ( |
297 | | - IS_SAMPLED |
298 | | - if span_context.trace_flags & trace.TraceFlags.SAMPLED |
299 | | - else NOT_SAMPLED |
300 | | - ) |
301 | | - |
302 | | - # TODO: Add OT trace state to the X-Ray trace header |
303 | | - |
304 | | - trace_header = KV_PAIR_DELIMITER.join( |
305 | | - [ |
306 | | - KEY_AND_VALUE_DELIMITER.join([key, value]) |
307 | | - for key, value in [ |
308 | | - (TRACE_ID_KEY, xray_trace_id), |
309 | | - (PARENT_ID_KEY, parent_id), |
310 | | - (SAMPLED_FLAG_KEY, sampling_flag), |
311 | | - ] |
312 | | - ] |
313 | | - ) |
314 | | - |
315 | | - setter.set( |
316 | | - carrier, TRACE_HEADER_KEY, trace_header, |
317 | | - ) |
318 | | - |
319 | | - @property |
320 | | - def fields(self): |
321 | | - """Returns a set with the fields set in `inject`.""" |
322 | | - |
323 | | - return {TRACE_HEADER_KEY} |
| 17 | +__all__ = ["AwsXRayPropagator"] |
0 commit comments