diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py new file mode 100644 index 00000000000..b0b3624d720 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py @@ -0,0 +1,100 @@ +# Copyright 2020, 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. + +import abc +import typing + +from opentelemetry.context import get_value, set_value +from opentelemetry.context.context import Context + +_CORRELATION_CONTEXT_KEY = "correlation-context" + + +def get_correlations( + context: typing.Optional[Context] = None, +) -> typing.Dict[str, object]: + """ Returns the name/value pairs in the CorrelationContext + + Args: + context: The Context to use. If not set, uses current Context + + Returns: + Name/value pairs in the CorrelationContext + """ + correlations = get_value(_CORRELATION_CONTEXT_KEY, context=context) + if isinstance(correlations, dict): + return correlations.copy() + return {} + + +def get_correlation( + name: str, context: typing.Optional[Context] = None +) -> typing.Optional[object]: + """ Provides access to the value for a name/value pair in the CorrelationContext + + Args: + name: The name of the value to retrieve + context: The Context to use. If not set, uses current Context + + Returns: + The value associated with the given name, or null if the given name is + not present. + """ + return get_correlations(context=context).get(name) + + +def set_correlation( + name: str, value: object, context: typing.Optional[Context] = None +) -> Context: + """Sets a value in the CorrelationContext + + Args: + name: The name of the value to set + value: The value to set + context: The Context to use. If not set, uses current Context + + Returns: + A Context with the value updated + """ + correlations = get_correlations(context=context) + correlations[name] = value + return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context) + + +def remove_correlation( + name: str, context: typing.Optional[Context] = None +) -> Context: + """Removes a value from the CorrelationContext + Args: + name: The name of the value to remove + context: The Context to use. If not set, uses current Context + + Returns: + A Context with the name/value removed + """ + correlations = get_correlations(context=context) + correlations.pop(name, None) + + return set_value(_CORRELATION_CONTEXT_KEY, correlations, context=context) + + +def clear_correlations(context: typing.Optional[Context] = None) -> Context: + """Removes all values from the CorrelationContext + Args: + context: The Context to use. If not set, uses current Context + + Returns: + A Context with all correlations removed + """ + return set_value(_CORRELATION_CONTEXT_KEY, {}, context=context) diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py new file mode 100644 index 00000000000..72ad80de1a9 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/propagation/__init__.py @@ -0,0 +1,108 @@ +# Copyright 2020, 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. +# +import re +import typing +import urllib.parse + +from opentelemetry import correlationcontext +from opentelemetry.context import get_current +from opentelemetry.context.context import Context +from opentelemetry.trace.propagation import httptextformat + + +class CorrelationContextPropagator(httptextformat.HTTPTextFormat): + MAX_HEADER_LENGTH = 8192 + MAX_PAIR_LENGTH = 4096 + MAX_PAIRS = 180 + _CORRELATION_CONTEXT_HEADER_NAME = "otcorrelationcontext" + + def extract( + self, + get_from_carrier: httptextformat.Getter[ + httptextformat.HTTPTextFormatT + ], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> Context: + """ Extract CorrelationContext from the carrier. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.extract` + """ + + if context is None: + context = get_current() + + header = _extract_first_element( + get_from_carrier(carrier, self._CORRELATION_CONTEXT_HEADER_NAME) + ) + + if not header or len(header) > self.MAX_HEADER_LENGTH: + return context + + correlations = header.split(",") + total_correlations = self.MAX_PAIRS + for correlation in correlations: + if total_correlations <= 0: + return context + total_correlations -= 1 + if len(correlation) > self.MAX_PAIR_LENGTH: + continue + try: + name, value = correlation.split("=", 1) + except Exception: # pylint: disable=broad-except + continue + context = correlationcontext.set_correlation( + urllib.parse.unquote(name).strip(), + urllib.parse.unquote(value).strip(), + context=context, + ) + + return context + + def inject( + self, + set_in_carrier: httptextformat.Setter[httptextformat.HTTPTextFormatT], + carrier: httptextformat.HTTPTextFormatT, + context: typing.Optional[Context] = None, + ) -> None: + """Injects CorrelationContext into the carrier. + + See `opentelemetry.trace.propagation.httptextformat.HTTPTextFormat.inject` + """ + correlations = correlationcontext.get_correlations(context=context) + if not correlations: + return + + correlation_context_string = _format_correlations(correlations) + set_in_carrier( + carrier, + self._CORRELATION_CONTEXT_HEADER_NAME, + correlation_context_string, + ) + + +def _format_correlations(correlations: typing.Dict[str, object]) -> str: + return ",".join( + key + "=" + urllib.parse.quote_plus(str(value)) + for key, value in correlations.items() + ) + + +def _extract_first_element( + items: typing.Iterable[httptextformat.HTTPTextFormatT], +) -> typing.Optional[httptextformat.HTTPTextFormatT]: + if items is None: + return None + return next(iter(items), None) diff --git a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py b/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py deleted file mode 100644 index dbc7b7e79bd..00000000000 --- a/opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py +++ /dev/null @@ -1,145 +0,0 @@ -# Copyright 2019, 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. - -import itertools -import string -import typing -from contextlib import contextmanager - -from opentelemetry.context import attach, get_value, set_value -from opentelemetry.context.context import Context - -PRINTABLE = frozenset( - itertools.chain( - string.ascii_letters, string.digits, string.punctuation, " " - ) -) - - -class EntryMetadata: - """A class representing metadata of a DistributedContext entry - - Args: - entry_ttl: The time to live (in service hops) of an entry. Must be - initially set to either :attr:`EntryMetadata.NO_PROPAGATION` - or :attr:`EntryMetadata.UNLIMITED_PROPAGATION`. - """ - - NO_PROPAGATION = 0 - UNLIMITED_PROPAGATION = -1 - - def __init__(self, entry_ttl: int) -> None: - self.entry_ttl = entry_ttl - - -class EntryKey(str): - """A class representing a key for a DistributedContext entry""" - - def __new__(cls, value: str) -> "EntryKey": - return cls.create(value) - - @staticmethod - def create(value: str) -> "EntryKey": - # pylint: disable=len-as-condition - if not 0 < len(value) <= 255 or any(c not in PRINTABLE for c in value): - raise ValueError("Invalid EntryKey", value) - - return typing.cast(EntryKey, value) - - -class EntryValue(str): - """A class representing the value of a DistributedContext entry""" - - def __new__(cls, value: str) -> "EntryValue": - return cls.create(value) - - @staticmethod - def create(value: str) -> "EntryValue": - if any(c not in PRINTABLE for c in value): - raise ValueError("Invalid EntryValue", value) - - return typing.cast(EntryValue, value) - - -class Entry: - def __init__( - self, metadata: EntryMetadata, key: EntryKey, value: EntryValue - ) -> None: - self.metadata = metadata - self.key = key - self.value = value - - -class DistributedContext: - """A container for distributed context entries""" - - def __init__(self, entries: typing.Iterable[Entry]) -> None: - self._container = {entry.key: entry for entry in entries} - - def get_entries(self) -> typing.Iterable[Entry]: - """Returns an immutable iterator to entries.""" - return self._container.values() - - def get_entry_value(self, key: EntryKey) -> typing.Optional[EntryValue]: - """Returns the entry associated with a key or None - - Args: - key: the key with which to perform a lookup - """ - if key in self._container: - return self._container[key].value - return None - - -class DistributedContextManager: - def get_current_context( - self, context: typing.Optional[Context] = None - ) -> typing.Optional[DistributedContext]: - """Gets the current DistributedContext. - - Returns: - A DistributedContext instance representing the current context. - """ - - @contextmanager # type: ignore - def use_context( - self, context: DistributedContext - ) -> typing.Iterator[DistributedContext]: - """Context manager for controlling a DistributedContext lifetime. - - Set the context as the active DistributedContext. - - On exiting, the context manager will restore the parent - DistributedContext. - - Args: - context: A DistributedContext instance to make current. - """ - # pylint: disable=no-self-use - yield context - - -_DISTRIBUTED_CONTEXT_KEY = "DistributedContext" - - -def distributed_context_from_context( - context: typing.Optional[Context] = None, -) -> DistributedContext: - return get_value(_DISTRIBUTED_CONTEXT_KEY, context) # type: ignore - - -def with_distributed_context( - dctx: DistributedContext, context: typing.Optional[Context] = None -) -> None: - attach(set_value(_DISTRIBUTED_CONTEXT_KEY, dctx, context=context)) diff --git a/opentelemetry-api/src/opentelemetry/propagators/__init__.py b/opentelemetry-api/src/opentelemetry/propagators/__init__.py index f9b537cd866..264fe06b5c3 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagators/__init__.py @@ -58,6 +58,10 @@ def example_route(): import opentelemetry.trace as trace from opentelemetry.context import get_current from opentelemetry.context.context import Context +from opentelemetry.correlationcontext.propagation import ( + CorrelationContextPropagator, +) +from opentelemetry.propagators import composite from opentelemetry.trace.propagation import httptextformat from opentelemetry.trace.propagation.tracecontexthttptextformat import ( TraceContextHTTPTextFormat, @@ -106,8 +110,8 @@ def inject( get_global_httptextformat().inject(set_in_carrier, carrier, context) -_HTTP_TEXT_FORMAT = ( - TraceContextHTTPTextFormat() +_HTTP_TEXT_FORMAT = composite.CompositeHTTPPropagator( + [TraceContextHTTPTextFormat(), CorrelationContextPropagator()], ) # type: httptextformat.HTTPTextFormat diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py new file mode 100644 index 00000000000..3bddb951e26 --- /dev/null +++ b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py @@ -0,0 +1,70 @@ +# Copyright 2020, 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. + +import unittest + +from opentelemetry import context +from opentelemetry import correlationcontext as cctx + + +class TestCorrelationContextManager(unittest.TestCase): + def test_set_correlation(self): + self.assertEqual({}, cctx.get_correlations()) + + ctx = cctx.set_correlation("test", "value") + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + + ctx = cctx.set_correlation("test", "value2", context=ctx) + self.assertEqual(cctx.get_correlation("test", context=ctx), "value2") + + def test_correlations_current_context(self): + token = context.attach(cctx.set_correlation("test", "value")) + self.assertEqual(cctx.get_correlation("test"), "value") + context.detach(token) + self.assertEqual(cctx.get_correlation("test"), None) + + def test_set_multiple_correlations(self): + ctx = cctx.set_correlation("test", "value") + ctx = cctx.set_correlation("test2", "value2", context=ctx) + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + self.assertEqual(cctx.get_correlation("test2", context=ctx), "value2") + self.assertEqual( + cctx.get_correlations(context=ctx), + {"test": "value", "test2": "value2"}, + ) + + def test_modifying_correlations(self): + ctx = cctx.set_correlation("test", "value") + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + correlations = cctx.get_correlations(context=ctx) + correlations["test"] = "mess-this-up" + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + + def test_remove_correlations(self): + self.assertEqual({}, cctx.get_correlations()) + + ctx = cctx.set_correlation("test", "value") + ctx = cctx.set_correlation("test2", "value2", context=ctx) + ctx = cctx.remove_correlation("test", context=ctx) + self.assertEqual(cctx.get_correlation("test", context=ctx), None) + self.assertEqual(cctx.get_correlation("test2", context=ctx), "value2") + + def test_clear_correlations(self): + self.assertEqual({}, cctx.get_correlations()) + + ctx = cctx.set_correlation("test", "value") + self.assertEqual(cctx.get_correlation("test", context=ctx), "value") + + ctx = cctx.clear_correlations(context=ctx) + self.assertEqual(cctx.get_correlations(context=ctx), {}) diff --git a/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py new file mode 100644 index 00000000000..d0ecbdb17fd --- /dev/null +++ b/opentelemetry-api/tests/correlationcontext/test_correlation_context_propagation.py @@ -0,0 +1,151 @@ +# Copyright 2020, 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. +# +import typing +import unittest + +from opentelemetry import correlationcontext +from opentelemetry.context import get_current +from opentelemetry.correlationcontext.propagation import ( + CorrelationContextPropagator, +) + + +def get_as_list( + dict_object: typing.Dict[str, typing.List[str]], key: str +) -> typing.List[str]: + return dict_object.get(key, []) + + +class TestCorrelationContextPropagation(unittest.TestCase): + def setUp(self): + self.propagator = CorrelationContextPropagator() + + def _extract(self, header_value): + """Test helper""" + header = {"otcorrelationcontext": [header_value]} + return correlationcontext.get_correlations( + self.propagator.extract(get_as_list, header) + ) + + def _inject(self, values): + """Test helper""" + ctx = get_current() + for k, v in values.items(): + ctx = correlationcontext.set_correlation(k, v, context=ctx) + output = {} + self.propagator.inject(dict.__setitem__, output, context=ctx) + return output.get("otcorrelationcontext") + + def test_no_context_header(self): + correlations = correlationcontext.get_correlations( + self.propagator.extract(get_as_list, {}) + ) + self.assertEqual(correlations, {}) + + def test_empty_context_header(self): + header = "" + self.assertEqual(self._extract(header), {}) + + def test_valid_header(self): + header = "key1=val1,key2=val2" + expected = {"key1": "val1", "key2": "val2"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_space(self): + header = "key1 = val1, key2 =val2 " + expected = {"key1": "val1", "key2": "val2"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_properties(self): + header = "key1=val1,key2=val2;prop=1" + expected = {"key1": "val1", "key2": "val2;prop=1"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_url_escaped_comma(self): + header = "key%2C1=val1,key2=val2%2Cval3" + expected = {"key,1": "val1", "key2": "val2,val3"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_invalid_value(self): + header = "key1=val1,key2=val2,a,val3" + expected = {"key1": "val1", "key2": "val2"} + self.assertEqual(self._extract(header), expected) + + def test_valid_header_with_empty_value(self): + header = "key1=,key2=val2" + expected = {"key1": "", "key2": "val2"} + self.assertEqual(self._extract(header), expected) + + def test_invalid_header(self): + header = "header1" + expected = {} + self.assertEqual(self._extract(header), expected) + + def test_header_too_long(self): + long_value = "s" * (CorrelationContextPropagator.MAX_HEADER_LENGTH + 1) + header = "key1={}".format(long_value) + expected = {} + self.assertEqual(self._extract(header), expected) + + def test_header_contains_too_many_entries(self): + header = ",".join( + [ + "key{}=val".format(k) + for k in range(CorrelationContextPropagator.MAX_PAIRS + 1) + ] + ) + self.assertEqual( + len(self._extract(header)), CorrelationContextPropagator.MAX_PAIRS + ) + + def test_header_contains_pair_too_long(self): + long_value = "s" * (CorrelationContextPropagator.MAX_PAIR_LENGTH + 1) + header = "key1=value1,key2={},key3=value3".format(long_value) + expected = {"key1": "value1", "key3": "value3"} + self.assertEqual(self._extract(header), expected) + + def test_inject_no_correlations(self): + values = {} + output = self._inject(values) + self.assertEqual(None, output) + + def test_inject(self): + values = { + "key1": "val1", + "key2": "val2", + } + output = self._inject(values) + self.assertIn("key1=val1", output) + self.assertIn("key2=val2", output) + + def test_inject_escaped_values(self): + values = { + "key1": "val1,val2", + "key2": "val3=4", + } + output = self._inject(values) + self.assertIn("key1=val1%2Cval2", output) + self.assertIn("key2=val3%3D4", output) + + def test_inject_non_string_values(self): + values = { + "key1": True, + "key2": 123, + "key3": 123.567, + } + output = self._inject(values) + self.assertIn("key1=True", output) + self.assertIn("key2=123", output) + self.assertIn("key3=123.567", output) diff --git a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py b/opentelemetry-api/tests/distributedcontext/test_distributed_context.py deleted file mode 100644 index c730603b162..00000000000 --- a/opentelemetry-api/tests/distributedcontext/test_distributed_context.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright 2019, 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. - -import unittest - -from opentelemetry import distributedcontext - - -class TestEntryMetadata(unittest.TestCase): - def test_entry_ttl_no_propagation(self): - metadata = distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.NO_PROPAGATION - ) - self.assertEqual(metadata.entry_ttl, 0) - - def test_entry_ttl_unlimited_propagation(self): - metadata = distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.UNLIMITED_PROPAGATION - ) - self.assertEqual(metadata.entry_ttl, -1) - - -class TestEntryKey(unittest.TestCase): - def test_create_empty(self): - with self.assertRaises(ValueError): - distributedcontext.EntryKey.create("") - - def test_create_too_long(self): - with self.assertRaises(ValueError): - distributedcontext.EntryKey.create("a" * 256) - - def test_create_invalid_character(self): - with self.assertRaises(ValueError): - distributedcontext.EntryKey.create("\x00") - - def test_create_valid(self): - key = distributedcontext.EntryKey.create("ok") - self.assertEqual(key, "ok") - - def test_key_new(self): - key = distributedcontext.EntryKey("ok") - self.assertEqual(key, "ok") - - -class TestEntryValue(unittest.TestCase): - def test_create_invalid_character(self): - with self.assertRaises(ValueError): - distributedcontext.EntryValue.create("\x00") - - def test_create_valid(self): - key = distributedcontext.EntryValue.create("ok") - self.assertEqual(key, "ok") - - def test_key_new(self): - key = distributedcontext.EntryValue("ok") - self.assertEqual(key, "ok") - - -class TestDistributedContext(unittest.TestCase): - def setUp(self): - entry = self.entry = distributedcontext.Entry( - distributedcontext.EntryMetadata( - distributedcontext.EntryMetadata.NO_PROPAGATION - ), - distributedcontext.EntryKey("key"), - distributedcontext.EntryValue("value"), - ) - self.context = distributedcontext.DistributedContext((entry,)) - - def test_get_entries(self): - self.assertIn(self.entry, self.context.get_entries()) - - def test_get_entry_value_present(self): - value = self.context.get_entry_value(self.entry.key) - self.assertIs(value, self.entry.value) - - def test_get_entry_value_missing(self): - key = distributedcontext.EntryKey("missing") - value = self.context.get_entry_value(key) - self.assertIsNone(value) - - -class TestDistributedContextManager(unittest.TestCase): - def setUp(self): - self.manager = distributedcontext.DistributedContextManager() - - def test_get_current_context(self): - self.assertIsNone(self.manager.get_current_context()) - - def test_use_context(self): - expected = distributedcontext.DistributedContext( - ( - distributedcontext.Entry( - distributedcontext.EntryMetadata(0), - distributedcontext.EntryKey("0"), - distributedcontext.EntryValue(""), - ), - ) - ) - with self.manager.use_context(expected) as output: - self.assertIs(output, expected) diff --git a/opentelemetry-api/tests/propagators/test_global_httptextformat.py b/opentelemetry-api/tests/propagators/test_global_httptextformat.py new file mode 100644 index 00000000000..6045feb6754 --- /dev/null +++ b/opentelemetry-api/tests/propagators/test_global_httptextformat.py @@ -0,0 +1,71 @@ +# Copyright 2020, 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. + +import typing +import unittest + +from opentelemetry import correlationcontext, trace +from opentelemetry.propagators import extract, inject +from opentelemetry.trace.propagation import ( + get_span_from_context, + set_span_in_context, +) + + +def get_as_list( + dict_object: typing.Dict[str, typing.List[str]], key: str +) -> typing.List[str]: + value = dict_object.get(key) + return value if value is not None else [] + + +class TestDefaultGlobalPropagator(unittest.TestCase): + """Test ensures the default global composite propagator works as intended + """ + + TRACE_ID = int("12345678901234567890123456789012", 16) # type:int + SPAN_ID = int("1234567890123456", 16) # type:int + + def test_propagation(self): + traceparent_value = "00-{trace_id}-{span_id}-00".format( + trace_id=format(self.TRACE_ID, "032x"), + span_id=format(self.SPAN_ID, "016x"), + ) + tracestate_value = "foo=1,bar=2,baz=3" + headers = { + "otcorrelationcontext": ["key1=val1,key2=val2"], + "traceparent": [traceparent_value], + "tracestate": [tracestate_value], + } + ctx = extract(get_as_list, headers) + correlations = correlationcontext.get_correlations(context=ctx) + expected = {"key1": "val1", "key2": "val2"} + self.assertEqual(correlations, expected) + span_context = get_span_from_context(context=ctx).get_context() + + self.assertEqual(span_context.trace_id, self.TRACE_ID) + self.assertEqual(span_context.span_id, self.SPAN_ID) + + span = trace.DefaultSpan(span_context) + ctx = correlationcontext.set_correlation("key3", "val3") + ctx = correlationcontext.set_correlation("key4", "val4", context=ctx) + ctx = set_span_in_context(span, context=ctx) + output = {} + inject(dict.__setitem__, output, context=ctx) + self.assertEqual(traceparent_value, output["traceparent"]) + self.assertIn("key3=val3", output["otcorrelationcontext"]) + self.assertIn("key4=val4", output["otcorrelationcontext"]) + self.assertIn("foo=1", output["tracestate"]) + self.assertIn("bar=2", output["tracestate"]) + self.assertIn("baz=3", output["tracestate"]) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py deleted file mode 100644 index 7a0a66a8a9a..00000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright 2019, 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. - -import typing -from contextlib import contextmanager - -from opentelemetry import distributedcontext as dctx_api -from opentelemetry.context import Context, get_value, set_value -from opentelemetry.distributedcontext import ( - distributed_context_from_context, - with_distributed_context, -) - - -class DistributedContextManager(dctx_api.DistributedContextManager): - """See `opentelemetry.distributedcontext.DistributedContextManager` - - """ - - def get_current_context( - self, context: typing.Optional[Context] = None - ) -> typing.Optional[dctx_api.DistributedContext]: - """Gets the current DistributedContext. - - Returns: - A DistributedContext instance representing the current context. - """ - return distributed_context_from_context(context=context) - - @contextmanager - def use_context( - self, context: dctx_api.DistributedContext - ) -> typing.Iterator[dctx_api.DistributedContext]: - """Context manager for controlling a DistributedContext lifetime. - - Set the context as the active DistributedContext. - - On exiting, the context manager will restore the parent - DistributedContext. - - Args: - context: A DistributedContext instance to make current. - """ - snapshot = distributed_context_from_context() - with_distributed_context(context) - - try: - yield context - finally: - with_distributed_context(snapshot) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/opentelemetry-sdk/tests/distributedcontext/__init__.py b/opentelemetry-sdk/tests/correlationcontext/__init__.py similarity index 100% rename from opentelemetry-sdk/tests/distributedcontext/__init__.py rename to opentelemetry-sdk/tests/correlationcontext/__init__.py diff --git a/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py b/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py deleted file mode 100644 index eddb61330dc..00000000000 --- a/opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2019, 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. - -import unittest - -from opentelemetry import distributedcontext as dctx_api -from opentelemetry.sdk import distributedcontext - - -class TestDistributedContextManager(unittest.TestCase): - def setUp(self): - self.manager = distributedcontext.DistributedContextManager() - - def test_use_context(self): - # Context is None initially - self.assertIsNone(self.manager.get_current_context()) - - # Start initial context - dctx = dctx_api.DistributedContext(()) - with self.manager.use_context(dctx) as current: - self.assertIs(current, dctx) - self.assertIs(self.manager.get_current_context(), dctx) - - # Context is overridden - nested_dctx = dctx_api.DistributedContext(()) - with self.manager.use_context(nested_dctx) as current: - self.assertIs(current, nested_dctx) - self.assertIs(self.manager.get_current_context(), nested_dctx) - - # Context is restored - self.assertIs(self.manager.get_current_context(), dctx)