From 18dd676d03d50ee823df601cb5d8b14480499c53 Mon Sep 17 00:00:00 2001 From: Alex Boten Date: Mon, 9 Mar 2020 14:22:05 -0700 Subject: [PATCH] Initial commit for Correlation Context API This change removes Distributed Context and replaces it with the Correlations Context API. Things to do: - add more tests - implement correlation context propagation and add it to the default propagator Signed-off-by: Alex Boten --- .../correlationcontext/__init__.py | 124 +++++++++++++++ .../distributedcontext/__init__.py | 145 ------------------ .../test_correlation_context.py | 23 +++ .../test_distributed_context.py | 112 -------------- .../sdk/correlationcontext/__init__.py | 72 +++++++++ .../sdk/distributedcontext/__init__.py | 61 -------- .../propagation/__init__.py | 0 .../__init__.py | 0 .../test_correlation_context.py | 29 ++++ .../test_distributed_context.py | 42 ----- 10 files changed, 248 insertions(+), 360 deletions(-) create mode 100644 opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py delete mode 100644 opentelemetry-api/src/opentelemetry/distributedcontext/__init__.py create mode 100644 opentelemetry-api/tests/correlationcontext/test_correlation_context.py delete mode 100644 opentelemetry-api/tests/distributedcontext/test_distributed_context.py create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/correlationcontext/__init__.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/__init__.py delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/distributedcontext/propagation/__init__.py rename opentelemetry-sdk/tests/{distributedcontext => correlationcontext}/__init__.py (100%) create mode 100644 opentelemetry-sdk/tests/correlationcontext/test_correlation_context.py delete mode 100644 opentelemetry-sdk/tests/distributedcontext/test_distributed_context.py diff --git a/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py new file mode 100644 index 00000000000..aa0e2d47451 --- /dev/null +++ b/opentelemetry-api/src/opentelemetry/correlationcontext/__init__.py @@ -0,0 +1,124 @@ +# 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 itertools +import string +import typing +from contextlib import contextmanager + +from opentelemetry.context import attach, get_value, set_value +from opentelemetry.context.context import Context + +CORRELATION_CONTEXT_KEY = "correlation-context" + + +class CorrelationContext(abc.ABC): + """A container for correlation context""" + + @abc.abstractmethod + def get_correlations(self, context: typing.Optional[Context] = None): + """ 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 + """ + + @abc.abstractmethod + def get_correlation( + self, name, context: typing.Optional[Context] = None + ) -> typing.Optional[object]: + """ Provides access to the value for a name/value pair by a prior event + + 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. + """ + + @abc.abstractmethod + def set_correlation( + self, name, value, context: typing.Optional[Context] = None + ) -> Context: + """ + + 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 + """ + + @abc.abstractmethod + def remove_correlation( + self, name, context: typing.Optional[Context] = None + ) -> Context: + """ + + 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 + """ + + @abc.abstractmethod + def clear_correlations( + self, context: typing.Optional[Context] = None + ) -> Context: + """ + Args: + context: the Context to use. If not set, uses current Context + + Returns: + a Context with all correlations removed + """ + + +class DefaultCorrelationContext(CorrelationContext): + """ Default no-op implementation of CorrelationContext """ + + def get_correlations( + self, context: typing.Optional[Context] = None + ) -> typing.Dict: + return {} + + def get_correlation( + self, name, context: typing.Optional[Context] = None + ) -> typing.Optional[object]: + return None + + def set_correlation( + self, name, value, context: typing.Optional[Context] = None + ) -> Context: + return context + + def remove_correlation( + self, name, context: typing.Optional[Context] = None + ) -> Context: + return context + + def clear_correlations( + self, context: typing.Optional[Context] = None + ) -> Context: + return context 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/tests/correlationcontext/test_correlation_context.py b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py new file mode 100644 index 00000000000..223dbe401da --- /dev/null +++ b/opentelemetry-api/tests/correlationcontext/test_correlation_context.py @@ -0,0 +1,23 @@ +# 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 correlationcontext + + +class TestCorrelationContext(unittest.TestCase): + def test_correlation_context(self): + default_context = correlationcontext.DefaultCorrelationContext() + self.assertEqual(default_context.get_correlations(), {}) 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-sdk/src/opentelemetry/sdk/correlationcontext/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/correlationcontext/__init__.py new file mode 100644 index 00000000000..3ffa65ad54e --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/correlationcontext/__init__.py @@ -0,0 +1,72 @@ +# 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 +from contextlib import contextmanager + +from opentelemetry import correlationcontext as cctx_api +from opentelemetry.context import get_value, set_value, get_current +from opentelemetry.context.context import Context + + +class CorrelationContext(cctx_api.CorrelationContext): + def get_correlations(self, context: typing.Optional[Context] = None): + correlations = get_value( + cctx_api.CORRELATION_CONTEXT_KEY, context=context + ) + if correlations: + return correlations + return {} + + def get_correlation( + self, name, context: typing.Optional[Context] = None + ) -> typing.Optional[object]: + correlations = get_value( + cctx_api.CORRELATION_CONTEXT_KEY, context=context + ) + if correlations: + return correlations.get(name) + return None + + def set_correlation( + self, name, value, context: typing.Optional[Context] = None + ) -> Context: + correlations = get_value( + cctx_api.CORRELATION_CONTEXT_KEY, context=context + ) + if correlations: + correlations[name] = value + else: + correlations = {name: value} + return set_value( + cctx_api.CORRELATION_CONTEXT_KEY, correlations, context=context + ) + + def remove_correlation( + self, name, context: typing.Optional[Context] = None + ) -> Context: + correlations = get_value( + cctx_api.CORRELATION_CONTEXT_KEY, context=context + ) + if correlations and name in correlations: + del correlations[name] + + return set_value( + cctx_api.CORRELATION_CONTEXT_KEY, correlations, context=context + ) + + def clear_correlations( + self, context: typing.Optional[Context] = None + ) -> Context: + return set_value(cctx_api.CORRELATION_CONTEXT_KEY, {}, context=context) 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/correlationcontext/test_correlation_context.py b/opentelemetry-sdk/tests/correlationcontext/test_correlation_context.py new file mode 100644 index 00000000000..31da110143c --- /dev/null +++ b/opentelemetry-sdk/tests/correlationcontext/test_correlation_context.py @@ -0,0 +1,29 @@ +# 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.sdk import correlationcontext + + +class TestCorrelationContextManager(unittest.TestCase): + def test_use_context(self): + cctx = correlationcontext.CorrelationContext() + 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") 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)