def test_websocket_traceresponse_header(self): """Test a traceresponse header is set for websocket messages""" orig = get_global_response_propagator() set_global_response_propagator(TraceResponsePropagator()) self.scope = { "type": "websocket", "http_version": "1.1", "scheme": "ws", "path": "/", "query_string": b"", "headers": [], "client": ("127.0.0.1", 32767), "server": ("127.0.0.1", 80), } app = otel_asgi.OpenTelemetryMiddleware(simple_asgi) self.seed_app(app) self.send_input({"type": "websocket.connect"}) self.send_input({"type": "websocket.receive", "text": "ping"}) self.send_input({"type": "websocket.disconnect"}) _, socket_send, *_ = self.get_all_output() span = self.memory_exporter.get_finished_spans()[-1] self.assertEqual(trace_api.SpanKind.SERVER, span.kind) traceresponse = "00-{0}-{1}-01".format( format_trace_id(span.get_span_context().trace_id), format_span_id(span.get_span_context().span_id), ) self.assertListEqual( socket_send["headers"], [ [b"traceresponse", f"{traceresponse}".encode()], [b"access-control-expose-headers", b"traceresponse"], ], ) set_global_response_propagator(orig)
def inject( self, carrier: CarrierT, context: typing.Optional[Context] = None, setter: Setter = default_setter, ) -> None: span = trace.get_current_span(context=context) span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return sampled = (trace.TraceFlags.SAMPLED & span_context.trace_flags) != 0 setter.set( carrier, self.TRACE_ID_KEY, format_trace_id(span_context.trace_id), ) setter.set( carrier, self.SPAN_ID_KEY, format_span_id(span_context.span_id) ) setter.set(carrier, self.SAMPLED_KEY, "1" if sampled else "0")
async def test_trace_response_headers(self): response = await self.async_client.get("/span_name/1234/") self.assertFalse(response.has_header("Server-Timing")) self.memory_exporter.clear() set_global_response_propagator(TraceResponsePropagator()) response = await self.async_client.get("/span_name/1234/") span = self.memory_exporter.get_finished_spans()[0] self.assertTrue(response.has_header("traceresponse")) self.assertEqual( response["Access-Control-Expose-Headers"], "traceresponse", ) trace_id = format_trace_id(span.get_span_context().trace_id) span_id = format_span_id(span.get_span_context().span_id) self.assertEqual( response["traceresponse"], f"00-{trace_id}-{span_id}-01", ) self.memory_exporter.clear()
def test_trace_response(self): orig = get_global_response_propagator() set_global_response_propagator(TraceResponsePropagator()) response = self.client.get("/hello/123") headers = response.headers span_list = self.memory_exporter.get_finished_spans() self.assertEqual(len(span_list), 1) span = span_list[0] self.assertIn("traceresponse", headers) self.assertEqual( headers["access-control-expose-headers"], "traceresponse", ) self.assertEqual( headers["traceresponse"], "00-{0}-{1}-01".format( trace.format_trace_id(span.get_span_context().trace_id), trace.format_span_id(span.get_span_context().span_id), ), ) set_global_response_propagator(orig)
async def test_trace_response_headers(self): response = await self.async_client.get("/span_name/1234/") self.assertNotIn("Server-Timing", response.headers) self.memory_exporter.clear() set_global_response_propagator(TraceResponsePropagator()) response = await self.async_client.get("/span_name/1234/") span = self.memory_exporter.get_finished_spans()[0] self.assertIn("traceresponse", response.headers) self.assertEqual( response.headers["Access-Control-Expose-Headers"], "traceresponse", ) self.assertEqual( response.headers["traceresponse"], "00-{0}-{1}-01".format( format_trace_id(span.get_span_context().trace_id), format_span_id(span.get_span_context().span_id), ), ) self.memory_exporter.clear()
def inject( self, carrier: textmap.CarrierT, context: typing.Optional[Context] = None, setter: textmap.Setter = textmap.default_setter, ) -> None: """Injects SpanContext into the carrier. See `opentelemetry.propagators.textmap.TextMapPropagator.inject` """ span = trace.get_current_span(context) span_context = span.get_span_context() if span_context == trace.INVALID_SPAN_CONTEXT: return traceparent_string = "00-{trace_id}-{span_id}-{:02x}".format( span_context.trace_flags, trace_id=format_trace_id(span_context.trace_id), span_id=format_span_id(span_context.span_id), ) setter.set(carrier, self._TRACEPARENT_HEADER_NAME, traceparent_string) if span_context.trace_state: tracestate_string = span_context.trace_state.to_header() setter.set(carrier, self._TRACESTATE_HEADER_NAME, tracestate_string)
def _extract_logs_from_span(span): logs = [] ctx = span.get_context() trace_id = ctx.trace_id p_span_id = ctx.span_id for event in span.events: l = { 'start_time': datetime.datetime.utcfromtimestamp(event.timestamp / float(1e9)), 'duration_ms': 0, 'name': event.name, 'trace.trace_id': trace_api.format_trace_id(trace_id)[2:], 'trace.parent_id': trace_api.format_span_id(p_span_id)[2:], 'meta.span_type': 'span_event', } l.update(event.attributes) logs.append(l) return logs
def _format_context(context): x_ctx = OrderedDict() x_ctx["trace_id"] = trace_api.format_trace_id(context.trace_id) x_ctx["span_id"] = trace_api.format_span_id(context.span_id) x_ctx["trace_state"] = repr(context.trace_state) return x_ctx
def _encode_span_id(span_id: int) -> str: return format_span_id(span_id)
def _translate_to_cloud_trace( self, spans: Sequence[ReadableSpan] ) -> List[Dict[str, Any]]: """Translate the spans to Cloud Trace format. Args: spans: Sequence of spans to convert """ cloud_trace_spans = [] for span in spans: ctx = span.get_span_context() trace_id = format_trace_id(ctx.trace_id) span_id = format_span_id(ctx.span_id) span_name = "projects/{}/traces/{}/spans/{}".format( self.project_id, trace_id, span_id ) parent_id = None if span.parent: parent_id = format_span_id(span.parent.span_id) start_time = _get_time_from_ns(span.start_time) end_time = _get_time_from_ns(span.end_time) if span.attributes and len(span.attributes) > MAX_SPAN_ATTRS: logger.warning( "Span has more then %s attributes, some will be truncated", MAX_SPAN_ATTRS, ) # Span does not support a MonitoredResource object. We put the # information into attributes instead. resources_and_attrs = { **(span.attributes or {}), **_extract_resources(span.resource), } cloud_trace_spans.append( { "name": span_name, "span_id": span_id, "display_name": _get_truncatable_str_object( span.name, 128 ), "start_time": start_time, "end_time": end_time, "parent_span_id": parent_id, "attributes": _extract_attributes( resources_and_attrs, MAX_SPAN_ATTRS, add_agent_attr=True, ), "links": _extract_links(span.links), # type: ignore[has-type] "status": _extract_status(span.status), # type: ignore[arg-type] "time_events": _extract_events(span.events), "span_kind": _extract_span_kind(span.kind), } ) # TODO: Leverage more of the Cloud Trace API, e.g. # same_process_as_parent_span and child_span_count return cloud_trace_spans
def extract( self, getter: Getter[TextMapPropagatorT], carrier: TextMapPropagatorT, context: typing.Optional[Context] = None, ) -> Context: trace_id = format_trace_id(trace.INVALID_TRACE_ID) span_id = format_span_id(trace.INVALID_SPAN_ID) sampled = "0" flags = None single_header = _extract_first_element( getter.get(carrier, self.SINGLE_HEADER_KEY)) if single_header: # The b3 spec calls for the sampling state to be # "deferred", which is unspecified. This concept does not # translate to SpanContext, so we set it as recorded. sampled = "1" fields = single_header.split("-", 4) if len(fields) == 1: sampled = fields[0] elif len(fields) == 2: trace_id, span_id = fields elif len(fields) == 3: trace_id, span_id, sampled = fields elif len(fields) == 4: trace_id, span_id, sampled, _ = fields else: return trace.set_span_in_context(trace.INVALID_SPAN) else: trace_id = (_extract_first_element( getter.get(carrier, self.TRACE_ID_KEY)) or trace_id) span_id = (_extract_first_element( getter.get(carrier, self.SPAN_ID_KEY)) or span_id) sampled = (_extract_first_element( getter.get(carrier, self.SAMPLED_KEY)) or sampled) flags = (_extract_first_element(getter.get( carrier, self.FLAGS_KEY)) or flags) if (self._trace_id_regex.fullmatch(trace_id) is None or self._span_id_regex.fullmatch(span_id) is None): id_generator = trace.get_tracer_provider().id_generator trace_id = id_generator.generate_trace_id() span_id = id_generator.generate_span_id() sampled = "0" else: trace_id = int(trace_id, 16) span_id = int(span_id, 16) options = 0 # The b3 spec provides no defined behavior for both sample and # flag values set. Since the setting of at least one implies # the desire for some form of sampling, propagate if either # header is set to allow. if sampled in self._SAMPLE_PROPAGATE_VALUES or flags == "1": options |= trace.TraceFlags.SAMPLED return trace.set_span_in_context( trace.NonRecordingSpan( trace.SpanContext( # trace an span ids are encoded in hex, so must be converted trace_id=trace_id, span_id=span_id, is_remote=True, trace_flags=trace.TraceFlags(options), trace_state=trace.TraceState(), )))
def setUpClass(cls): generator = id_generator.RandomIdGenerator() cls.serialized_trace_id = trace_api.format_trace_id( generator.generate_trace_id()) cls.serialized_span_id = trace_api.format_span_id( generator.generate_span_id())