Skip to content

Commit 568641f

Browse files
author
Jason Liu
authored
Make Instances of SpanContext Immutable (#1134)
1 parent 14fad78 commit 568641f

File tree

3 files changed

+109
-12
lines changed

3 files changed

+109
-12
lines changed

opentelemetry-api/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
([#1153](https://github.com/open-telemetry/opentelemetry-python/pull/1153))
1111
- Update baggage propagation header
1212
([#1194](https://github.com/open-telemetry/opentelemetry-python/pull/1194))
13+
- Make instances of SpanContext immutable
14+
([#1134](https://github.com/open-telemetry/opentelemetry-python/pull/1134))
1315

1416
## Version 0.13b0
1517

opentelemetry-api/src/opentelemetry/trace/span.py

+49-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import abc
2+
import logging
23
import types as python_types
34
import typing
45

56
from opentelemetry.trace.status import Status
67
from opentelemetry.util import types
78

9+
_logger = logging.getLogger(__name__)
10+
811

912
class Span(abc.ABC):
1013
"""A span represents a single operation within a trace."""
@@ -143,7 +146,9 @@ def get_default(cls) -> "TraceState":
143146
DEFAULT_TRACE_STATE = TraceState.get_default()
144147

145148

146-
class SpanContext:
149+
class SpanContext(
150+
typing.Tuple[int, int, bool, "TraceFlags", "TraceState", bool]
151+
):
147152
"""The state of a Span to propagate between processes.
148153
149154
This class includes the immutable attributes of a :class:`.Span` that must
@@ -157,26 +162,58 @@ class SpanContext:
157162
is_remote: True if propagated from a remote parent.
158163
"""
159164

160-
def __init__(
161-
self,
165+
def __new__(
166+
cls,
162167
trace_id: int,
163168
span_id: int,
164169
is_remote: bool,
165170
trace_flags: "TraceFlags" = DEFAULT_TRACE_OPTIONS,
166171
trace_state: "TraceState" = DEFAULT_TRACE_STATE,
167-
) -> None:
172+
) -> "SpanContext":
168173
if trace_flags is None:
169174
trace_flags = DEFAULT_TRACE_OPTIONS
170175
if trace_state is None:
171176
trace_state = DEFAULT_TRACE_STATE
172-
self.trace_id = trace_id
173-
self.span_id = span_id
174-
self.trace_flags = trace_flags
175-
self.trace_state = trace_state
176-
self.is_remote = is_remote
177-
self.is_valid = (
178-
self.trace_id != INVALID_TRACE_ID
179-
and self.span_id != INVALID_SPAN_ID
177+
178+
is_valid = trace_id != INVALID_TRACE_ID and span_id != INVALID_SPAN_ID
179+
180+
return tuple.__new__(
181+
cls,
182+
(trace_id, span_id, is_remote, trace_flags, trace_state, is_valid),
183+
)
184+
185+
@property
186+
def trace_id(self) -> int:
187+
return self[0] # pylint: disable=unsubscriptable-object
188+
189+
@property
190+
def span_id(self) -> int:
191+
return self[1] # pylint: disable=unsubscriptable-object
192+
193+
@property
194+
def is_remote(self) -> bool:
195+
return self[2] # pylint: disable=unsubscriptable-object
196+
197+
@property
198+
def trace_flags(self) -> "TraceFlags":
199+
return self[3] # pylint: disable=unsubscriptable-object
200+
201+
@property
202+
def trace_state(self) -> "TraceState":
203+
return self[4] # pylint: disable=unsubscriptable-object
204+
205+
@property
206+
def is_valid(self) -> bool:
207+
return self[5] # pylint: disable=unsubscriptable-object
208+
209+
def __setattr__(self, *args: str) -> None:
210+
_logger.debug(
211+
"Immutable type, ignoring call to set attribute", stack_info=True
212+
)
213+
214+
def __delattr__(self, *args: str) -> None:
215+
_logger.debug(
216+
"Immutable type, ignoring call to set attribute", stack_info=True
180217
)
181218

182219
def __repr__(self) -> str:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
17+
from opentelemetry import trace
18+
from opentelemetry.trace import TraceFlags, TraceState
19+
20+
21+
class TestImmutableSpanContext(unittest.TestCase):
22+
def test_ctor(self):
23+
context = trace.SpanContext(
24+
1,
25+
1,
26+
is_remote=False,
27+
trace_flags=trace.DEFAULT_TRACE_OPTIONS,
28+
trace_state=trace.DEFAULT_TRACE_STATE,
29+
)
30+
31+
self.assertEqual(context.trace_id, 1)
32+
self.assertEqual(context.span_id, 1)
33+
self.assertEqual(context.is_remote, False)
34+
self.assertEqual(context.trace_flags, trace.DEFAULT_TRACE_OPTIONS)
35+
self.assertEqual(context.trace_state, trace.DEFAULT_TRACE_STATE)
36+
37+
def test_attempt_change_attributes(self):
38+
context = trace.SpanContext(
39+
1,
40+
2,
41+
is_remote=False,
42+
trace_flags=trace.DEFAULT_TRACE_OPTIONS,
43+
trace_state=trace.DEFAULT_TRACE_STATE,
44+
)
45+
46+
# attempt to change the attribute values
47+
context.trace_id = 2 # type: ignore
48+
context.span_id = 3 # type: ignore
49+
context.is_remote = True # type: ignore
50+
context.trace_flags = TraceFlags(3) # type: ignore
51+
context.trace_state = TraceState([("test", "test")]) # type: ignore
52+
53+
# check if attributes changed
54+
self.assertEqual(context.trace_id, 1)
55+
self.assertEqual(context.span_id, 2)
56+
self.assertEqual(context.is_remote, False)
57+
self.assertEqual(context.trace_flags, trace.DEFAULT_TRACE_OPTIONS)
58+
self.assertEqual(context.trace_state, trace.DEFAULT_TRACE_STATE)

0 commit comments

Comments
 (0)