From bedd0c62f545f50cf3b02bc2dc04e087639214df Mon Sep 17 00:00:00 2001 From: Owais Lone Date: Mon, 12 Apr 2021 19:10:23 +0530 Subject: [PATCH] Added experimental HTTP backpropagators The experimental back propagators inject trace response headers into HTTP responses. These are meant to be used by instrumentations and are not considered stable. --- CHANGELOG.md | 2 + .../instrumentation/propagators.py | 108 ++++++++++++++++++ .../tests/test_propagators.py | 106 +++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py create mode 100644 opentelemetry-instrumentation/tests/test_propagators.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e7f76a9797f..b09e269989b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `SpanKind` to `should_sample` parameters, suggest using parent span context's tracestate instead of manually passed in tracestate in `should_sample` ([#1764](https://github.com/open-telemetry/opentelemetry-python/pull/1764)) +- Added experimental HTTP back propagators. + ([#1762](https://github.com/open-telemetry/opentelemetry-python/pull/1762)) ### Changed - Adjust `B3Format` propagator to be spec compliant by not modifying context diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py new file mode 100644 index 00000000000..12ef0e781c6 --- /dev/null +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/propagators.py @@ -0,0 +1,108 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module implements experimental propagators to inject trace response context +into HTTP responses. This is useful for server side frameworks that start traces +when server requests and want to share the trace context with the client so the +client can add it's spans to the same trace. + +This is part of an upcoming W3C spec and will eventually make it to the Otel spec. + +https://w3c.github.io/trace-context/#trace-context-http-response-headers-format +""" + +import typing + +import opentelemetry.trace as trace +from opentelemetry.context.context import Context +from opentelemetry.propagators import textmap +from opentelemetry.trace import format_span_id, format_trace_id + +_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" +_BACK_PROPAGATOR = None + + +def get_global_back_propagator(): + return _BACK_PROPAGATOR + + +def set_global_back_propagator(propagator): + global _BACK_PROPAGATOR # pylint:disable=global-statement + _BACK_PROPAGATOR = propagator + + +class DictHeaderSetter: + def set(self, carrier, key, value): # pylint: disable=no-self-use + old_value = carrier.get(key, "") + if old_value: + value = "{0}, {1}".format(old_value, value) + carrier[key] = value + + +default_setter = DictHeaderSetter() + + +class FuncSetter: + def __init__(self, func): + self._func = func + + def set(self, carrier, key, value): + self._func(carrier, key, value) + + +class TraceResponsePropagator: + """Experimental propagator that injects tracecontext into HTTP responses.""" + + _HEADER_NAME = "traceresponse" + + def _format_header(self, span_context): # pylint: disable=no-self-use + return "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), + ) + + def inject( + self, + carrier: textmap.CarrierT, + context: typing.Optional[Context] = None, + setter: textmap.Setter = default_setter, + ) -> None: + """Injects SpanContext into the HTTP response carrier.""" + span = trace.get_current_span(context) + span_context = span.get_span_context() + if span_context == trace.INVALID_SPAN_CONTEXT: + return + + setter.set( + carrier, self._HEADER_NAME, self._format_header(span_context) + ) + setter.set( + carrier, + _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, + self._HEADER_NAME, + ) + + +class ServerTimingPropagator(TraceResponsePropagator): + + _HEADER_NAME = "Server-Timing" + + def _format_header(self, span_context): + return 'traceparent;desc="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), + ) diff --git a/opentelemetry-instrumentation/tests/test_propagators.py b/opentelemetry-instrumentation/tests/test_propagators.py new file mode 100644 index 00000000000..7bc62050018 --- /dev/null +++ b/opentelemetry-instrumentation/tests/test_propagators.py @@ -0,0 +1,106 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# pylint: disable=protected-access + +from opentelemetry import trace +from opentelemetry.instrumentation import propagators +from opentelemetry.instrumentation.propagators import ( + DictHeaderSetter, + ServerTimingPropagator, + TraceResponsePropagator, + get_global_back_propagator, + set_global_back_propagator, +) +from opentelemetry.test.test_base import TestBase + + +class TestGlobals(TestBase): + def test_get_set(self): + original = propagators._BACK_PROPAGATOR + + propagators._BACK_PROPAGATOR = None + self.assertIsNone(get_global_back_propagator()) + + prop = TraceResponsePropagator() + set_global_back_propagator(prop) + self.assertIs(prop, get_global_back_propagator()) + + propagators._BACK_PROPAGATOR = original + + +class TestDictHeaderSetter(TestBase): + def test_simple(self): + setter = DictHeaderSetter() + carrier = {} + setter.set(carrier, "kk", "vv") + self.assertIn("kk", carrier) + self.assertEqual(carrier["kk"], "vv") + + def test_append(self): + setter = DictHeaderSetter() + carrier = {"kk": "old"} + setter.set(carrier, "kk", "vv") + self.assertIn("kk", carrier) + self.assertEqual(carrier["kk"], "old, vv") + + +class TestTraceResponsePropagator(TestBase): + def test_inject(self): + span = trace.NonRecordingSpan( + trace.SpanContext( + trace_id=1, + span_id=2, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ), + ) + + ctx = trace.set_span_in_context(span) + prop = TraceResponsePropagator() + carrier = {} + prop.inject(carrier, ctx) + self.assertEqual( + carrier["Access-Control-Expose-Headers"], "traceresponse" + ) + self.assertEqual( + carrier["traceresponse"], + "00-00000000000000000000000000000001-0000000000000002-00", + ) + + +class TestServerTimingPropagator(TestBase): + def test_inject(self): + span = trace.NonRecordingSpan( + trace.SpanContext( + trace_id=1, + span_id=2, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ), + ) + + ctx = trace.set_span_in_context(span) + prop = ServerTimingPropagator() + carrier = {} + prop.inject(carrier, ctx) + self.assertEqual( + carrier["Access-Control-Expose-Headers"], "Server-Timing" + ) + self.assertEqual( + carrier["Server-Timing"], + 'traceparent;desc="00-00000000000000000000000000000001-0000000000000002-00"', + )