diff --git a/opentelemetry-api/CHANGELOG.md b/opentelemetry-api/CHANGELOG.md index 434bae64042..adccf96cdf2 100644 --- a/opentelemetry-api/CHANGELOG.md +++ b/opentelemetry-api/CHANGELOG.md @@ -10,6 +10,8 @@ ([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153)) - Update baggage propagation header ([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194)) +- Make instances of SpanContext immutable + ([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134)) ## Version 0.13b0 diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 27bbc223368..99620ed1443 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -1,10 +1,13 @@ import abc +import logging import types as python_types import typing from opentelemetry.trace.status import Status from opentelemetry.util import types +_logger = logging.getLogger(__name__) + class Span(abc.ABC): """A span represents a single operation within a trace.""" @@ -143,7 +146,9 @@ def get_default(cls) -> "TraceState": DEFAULT_TRACE_STATE = TraceState.get_default() -class SpanContext: +class SpanContext( + typing.Tuple[int, int, bool, "TraceFlags", "TraceState", bool] +): """The state of a Span to propagate between processes. This class includes the immutable attributes of a :class:`.Span` that must @@ -157,26 +162,58 @@ class SpanContext: is_remote: True if propagated from a remote parent. """ - def __init__( - self, + def __new__( + cls, trace_id: int, span_id: int, is_remote: bool, trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS, trace_state: "TraceState" = DEFAULT_TRACE_STATE, - ) -> None: + ) -> "SpanContext": if trace_flags is None: trace_flags = DEFAULT_TRACE_OPTIONS if trace_state is None: trace_state = DEFAULT_TRACE_STATE - self.trace_id = trace_id - self.span_id = span_id - self.trace_flags = trace_flags - self.trace_state = trace_state - self.is_remote = is_remote - self.is_valid = ( - self.trace_id != INVALID_TRACE_ID - and self.span_id != INVALID_SPAN_ID + + is_valid = trace_id != INVALID_TRACE_ID and span_id != INVALID_SPAN_ID + + return tuple.__new__( + cls, + (trace_id, span_id, is_remote, trace_flags, trace_state, is_valid), + ) + + @property + def trace_id(self) -> int: + return self[0] # pylint: disable=unsubscriptable-object + + @property + def span_id(self) -> int: + return self[1] # pylint: disable=unsubscriptable-object + + @property + def is_remote(self) -> bool: + return self[2] # pylint: disable=unsubscriptable-object + + @property + def trace_flags(self) -> "TraceFlags": + return self[3] # pylint: disable=unsubscriptable-object + + @property + def trace_state(self) -> "TraceState": + return self[4] # pylint: disable=unsubscriptable-object + + @property + def is_valid(self) -> bool: + return self[5] # pylint: disable=unsubscriptable-object + + def __setattr__(self, *args: str) -> None: + _logger.debug( + "Immutable type, ignoring call to set attribute", stack_info=True + ) + + def __delattr__(self, *args: str) -> None: + _logger.debug( + "Immutable type, ignoring call to set attribute", stack_info=True ) def __repr__(self) -> str: diff --git a/opentelemetry-api/tests/trace/test_immutablespancontext.py b/opentelemetry-api/tests/trace/test_immutablespancontext.py new file mode 100644 index 00000000000..7e98470e130 --- /dev/null +++ b/opentelemetry-api/tests/trace/test_immutablespancontext.py @@ -0,0 +1,58 @@ +# 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. + +import unittest + +from opentelemetry import trace +from opentelemetry.trace import TraceFlags, TraceState + + +class TestImmutableSpanContext(unittest.TestCase): + def test_ctor(self): + context = trace.SpanContext( + 1, + 1, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ) + + self.assertEqual(context.trace_id, 1) + self.assertEqual(context.span_id, 1) + self.assertEqual(context.is_remote, False) + self.assertEqual(context.trace_flags, trace.DEFAULT_TRACE_OPTIONS) + self.assertEqual(context.trace_state, trace.DEFAULT_TRACE_STATE) + + def test_attempt_change_attributes(self): + context = trace.SpanContext( + 1, + 2, + is_remote=False, + trace_flags=trace.DEFAULT_TRACE_OPTIONS, + trace_state=trace.DEFAULT_TRACE_STATE, + ) + + # attempt to change the attribute values + context.trace_id = 2 # type: ignore + context.span_id = 3 # type: ignore + context.is_remote = True # type: ignore + context.trace_flags = TraceFlags(3) # type: ignore + context.trace_state = TraceState([("test", "test")]) # type: ignore + + # check if attributes changed + self.assertEqual(context.trace_id, 1) + self.assertEqual(context.span_id, 2) + self.assertEqual(context.is_remote, False) + self.assertEqual(context.trace_flags, trace.DEFAULT_TRACE_OPTIONS) + self.assertEqual(context.trace_state, trace.DEFAULT_TRACE_STATE)