Skip to content

Commit 61f067a

Browse files
committed
Move use_span() method from Tracer to opentelemetry.trace.use_span()
Fixes: open-telemetry#1630
1 parent 9bf28fb commit 61f067a

File tree

10 files changed

+152
-105
lines changed

10 files changed

+152
-105
lines changed

.github/workflows/test.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ env:
1010
# Otherwise, set variable to the commit of your branch on
1111
# opentelemetry-python-contrib which is compatible with these Core repo
1212
# changes.
13-
CONTRIB_REPO_SHA: 16ae58b341b960df52c1cf4541695164821b3638
13+
CONTRIB_REPO_SHA: 8783e0ff97ad123006ff1ff2c2cf3f52161a406f
1414

1515
jobs:
1616
build:

CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Added
1010
- Add `max_attr_value_length` support to Jaeger exporter
1111
([#1633])(https://github.com/open-telemetry/opentelemetry-python/pull/1633)
12+
- Moved `use_span` from Tracer to `opentelemetry.trace.use_span`.
13+
([#1668](https://github.com/open-telemetry/opentelemetry-python/pull/1668))
14+
- `opentelemetry.trace.use_span()` will now overwrite previously set status on span in case an
15+
exception is raised inside the context manager and `set_status_on_exception` is set to `True`.
16+
([#1668](https://github.com/open-telemetry/opentelemetry-python/pull/1668))
1217

1318
### Changed
1419
- Rename `IdsGenerator` to `IdGenerator`

docs/examples/basic_context/async_context.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525

2626
async def async_span(span):
27-
with tracer.use_span(span):
27+
with trace.use_span(span):
2828
ctx = baggage.set_baggage("foo", "bar")
2929
return ctx
3030

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

+54-31
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@
8080
from logging import getLogger
8181
from typing import Iterator, Optional, Sequence, cast
8282

83+
from opentelemetry import context as context_api
8384
from opentelemetry.context.context import Context
8485
from opentelemetry.environment_variables import OTEL_PYTHON_TRACER_PROVIDER
8586
from opentelemetry.trace.propagation import (
87+
SPAN_KEY,
8688
get_current_span,
8789
set_span_in_context,
8890
)
@@ -101,7 +103,7 @@
101103
format_span_id,
102104
format_trace_id,
103105
)
104-
from opentelemetry.trace.status import Status
106+
from opentelemetry.trace.status import Status, StatusCode
105107
from opentelemetry.util import types
106108
from opentelemetry.util._providers import _load_provider
107109

@@ -324,7 +326,7 @@ def start_as_current_span(
324326
is equivalent to::
325327
326328
span = tracer.start_span(name)
327-
with tracer.use_span(span, end_on_exit=True):
329+
with opentelemetry.trace.use_span(span, end_on_exit=True):
328330
do_work()
329331
330332
Args:
@@ -350,28 +352,6 @@ def start_as_current_span(
350352
The newly-created span.
351353
"""
352354

353-
@contextmanager # type: ignore
354-
@abstractmethod
355-
def use_span(
356-
self, span: "Span", end_on_exit: bool = False,
357-
) -> Iterator[None]:
358-
"""Context manager for setting the passed span as the
359-
current span in the context, as well as resetting the
360-
context back upon exiting the context manager.
361-
362-
Set the given span as the current span in this tracer's context.
363-
364-
On exiting the context manager set the span that was previously active
365-
as the current span (this is usually but not necessarily the parent of
366-
the given span). If ``end_on_exit`` is ``True``, then the span is also
367-
ended when exiting the context manager.
368-
369-
Args:
370-
span: The span to start and make current.
371-
end_on_exit: Whether to end the span automatically when leaving the
372-
context manager.
373-
"""
374-
375355

376356
class DefaultTracer(Tracer):
377357
"""The default Tracer, used when no Tracer implementation is available.
@@ -409,13 +389,6 @@ def start_as_current_span(
409389
# pylint: disable=unused-argument,no-self-use
410390
yield INVALID_SPAN
411391

412-
@contextmanager # type: ignore
413-
def use_span(
414-
self, span: "Span", end_on_exit: bool = False,
415-
) -> Iterator[None]:
416-
# pylint: disable=unused-argument,no-self-use
417-
yield
418-
419392

420393
_TRACER_PROVIDER = None
421394

@@ -466,6 +439,55 @@ def get_tracer_provider() -> TracerProvider:
466439
return _TRACER_PROVIDER
467440

468441

442+
@contextmanager # type: ignore
443+
def use_span(
444+
span: Span,
445+
end_on_exit: bool = False,
446+
record_exception: bool = True,
447+
set_status_on_exception: bool = True,
448+
) -> Iterator[Span]:
449+
"""Takes a non-active span and activates it in the current context.
450+
451+
Args:
452+
span: The span that should be activated in the current context.
453+
end_on_exit: Whether to end the span automatically when leaving the
454+
context manager.
455+
record_exception: Whether to record any exceptions raised within the
456+
context as error event on the span.
457+
set_status_on_exception: Only relevant if the returned span is used
458+
in a with/context manager. Defines wether the span status will
459+
be automatically set to ERROR when an uncaught exception is
460+
raised in the span with block. The span status won't be set by
461+
this mechanism if it was previously set manually.
462+
"""
463+
try:
464+
token = context_api.attach(context_api.set_value(SPAN_KEY, span))
465+
try:
466+
yield span
467+
finally:
468+
context_api.detach(token)
469+
470+
except Exception as exc: # pylint: disable=broad-except
471+
if isinstance(span, Span) and span.is_recording():
472+
# Record the exception as an event
473+
if record_exception:
474+
span.record_exception(exc)
475+
476+
# Set status in case exception was raised
477+
if set_status_on_exception:
478+
span.set_status(
479+
Status(
480+
status_code=StatusCode.ERROR,
481+
description="{}: {}".format(type(exc).__name__, exc),
482+
)
483+
)
484+
raise
485+
486+
finally:
487+
if end_on_exit:
488+
span.end()
489+
490+
469491
__all__ = [
470492
"DEFAULT_TRACE_OPTIONS",
471493
"DEFAULT_TRACE_STATE",
@@ -492,5 +514,6 @@ def get_tracer_provider() -> TracerProvider:
492514
"get_tracer_provider",
493515
"set_tracer_provider",
494516
"set_span_in_context",
517+
"use_span",
495518
"Status",
496519
]

opentelemetry-api/src/opentelemetry/trace/propagation/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414
from typing import Optional
1515

16-
from opentelemetry.context import get_value, set_value
16+
from opentelemetry.context import attach, get_value, set_value
1717
from opentelemetry.context.context import Context
1818
from opentelemetry.trace.span import INVALID_SPAN, Span
1919

opentelemetry-api/tests/trace/test_globals.py

+71
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22
from unittest.mock import patch
33

44
from opentelemetry import context, trace
5+
from opentelemetry.trace.status import Status, StatusCode
6+
7+
8+
class TestSpan(trace.NonRecordingSpan):
9+
has_ended = False
10+
recorded_exception = None
11+
recorded_status = Status(status_code=StatusCode.UNSET)
12+
13+
def set_status(self, status):
14+
self.recorded_status = status
15+
16+
def end(self, end_time=None):
17+
self.has_ended = True
18+
19+
def is_recording(self):
20+
return not self.has_ended
21+
22+
def record_exception(
23+
self, exception, attributes=None, timestamp=None, escaped=False
24+
):
25+
self.recorded_exception = exception
526

627

728
class TestGlobals(unittest.TestCase):
@@ -38,3 +59,53 @@ def test_get_current_span(self):
3859
finally:
3960
context.detach(token)
4061
self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN)
62+
63+
64+
class TestUseTracer(unittest.TestCase):
65+
def test_use_span(self):
66+
self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN)
67+
span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT)
68+
with trace.use_span(span):
69+
self.assertIs(trace.get_current_span(), span)
70+
self.assertEqual(trace.get_current_span(), trace.INVALID_SPAN)
71+
72+
def test_use_span_end_on_exit(self):
73+
74+
test_span = TestSpan(trace.INVALID_SPAN_CONTEXT)
75+
76+
with trace.use_span(test_span):
77+
pass
78+
self.assertFalse(test_span.has_ended)
79+
80+
with trace.use_span(test_span, end_on_exit=True):
81+
pass
82+
self.assertTrue(test_span.has_ended)
83+
84+
def test_use_span_exception(self):
85+
class TestUseSpanException(Exception):
86+
pass
87+
88+
test_span = TestSpan(trace.INVALID_SPAN_CONTEXT)
89+
exception = TestUseSpanException("test exception")
90+
with self.assertRaises(TestUseSpanException):
91+
with trace.use_span(test_span):
92+
raise exception
93+
94+
self.assertEqual(test_span.recorded_exception, exception)
95+
96+
def test_use_span_set_status(self):
97+
class TestUseSpanException(Exception):
98+
pass
99+
100+
test_span = TestSpan(trace.INVALID_SPAN_CONTEXT)
101+
with self.assertRaises(TestUseSpanException):
102+
with trace.use_span(test_span):
103+
raise TestUseSpanException("test error")
104+
105+
self.assertEqual(
106+
test_span.recorded_status.status_code, StatusCode.ERROR
107+
)
108+
self.assertEqual(
109+
test_span.recorded_status.description,
110+
"TestUseSpanException: test error",
111+
)

opentelemetry-api/tests/trace/test_tracer.py

-5
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,6 @@ def test_start_as_current_span(self):
2929
with self.tracer.start_as_current_span("") as span:
3030
self.assertIsInstance(span, trace.Span)
3131

32-
def test_use_span(self):
33-
span = trace.NonRecordingSpan(trace.INVALID_SPAN_CONTEXT)
34-
with self.tracer.use_span(span):
35-
pass
36-
3732
def test_get_current_span(self):
3833
with self.tracer.start_as_current_span("test") as span:
3934
trace.get_current_span().set_attribute("test", "test")

opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

+7-43
Original file line numberDiff line numberDiff line change
@@ -776,10 +776,7 @@ def __exit__(
776776
self.record_exception(exception=exc_val, escaped=True)
777777
# Records status if span is used as context manager
778778
# i.e. with tracer.start_span() as span:
779-
if (
780-
self._status.status_code is StatusCode.UNSET
781-
and self._set_status_on_exception
782-
):
779+
if self._set_status_on_exception:
783780
self.set_status(
784781
Status(
785782
status_code=StatusCode.ERROR,
@@ -869,7 +866,12 @@ def start_as_current_span(
869866
record_exception=record_exception,
870867
set_status_on_exception=set_status_on_exception,
871868
)
872-
with self.use_span(span, end_on_exit=end_on_exit) as span_context:
869+
with trace_api.use_span(
870+
span,
871+
end_on_exit=end_on_exit,
872+
record_exception=record_exception,
873+
set_status_on_exception=set_status_on_exception,
874+
) as span_context:
873875
yield span_context
874876

875877
def start_span( # pylint: disable=too-many-locals
@@ -950,44 +952,6 @@ def start_span( # pylint: disable=too-many-locals
950952
span = trace_api.NonRecordingSpan(context=span_context)
951953
return span
952954

953-
@contextmanager
954-
def use_span(
955-
self, span: trace_api.Span, end_on_exit: bool = False,
956-
) -> Iterator[trace_api.Span]:
957-
try:
958-
token = context_api.attach(context_api.set_value(SPAN_KEY, span))
959-
try:
960-
yield span
961-
finally:
962-
context_api.detach(token)
963-
964-
except Exception as exc: # pylint: disable=broad-except
965-
# Record the exception as an event
966-
if isinstance(span, Span) and span.is_recording():
967-
# pylint:disable=protected-access
968-
if span._record_exception:
969-
span.record_exception(exc)
970-
971-
# Records status if use_span is used
972-
# i.e. with tracer.start_as_current_span() as span:
973-
if (
974-
span._status.status_code is StatusCode.UNSET
975-
and span._set_status_on_exception
976-
):
977-
span.set_status(
978-
Status(
979-
status_code=StatusCode.ERROR,
980-
description="{}: {}".format(
981-
type(exc).__name__, exc
982-
),
983-
)
984-
)
985-
raise
986-
987-
finally:
988-
if end_on_exit:
989-
span.end()
990-
991955

992956
class TracerProvider(trace_api.TracerProvider):
993957
"""See `opentelemetry.trace.TracerProvider`."""

opentelemetry-sdk/tests/trace/test_trace.py

+7-17
Original file line numberDiff line numberDiff line change
@@ -122,18 +122,6 @@ def run_general_code(shutdown_on_exit, explicit_shutdown):
122122
out = run_general_code(False, False)
123123
self.assertTrue(out.startswith(b"0"))
124124

125-
def test_use_span_exception(self):
126-
class TestUseSpanException(Exception):
127-
pass
128-
129-
default_span = trace_api.NonRecordingSpan(
130-
trace_api.INVALID_SPAN_CONTEXT
131-
)
132-
tracer = new_tracer()
133-
with self.assertRaises(TestUseSpanException):
134-
with tracer.use_span(default_span):
135-
raise TestUseSpanException()
136-
137125
def test_tracer_provider_accepts_concurrent_multi_span_processor(self):
138126
span_processor = trace.ConcurrentMultiSpanProcessor(2)
139127
tracer_provider = trace.TracerProvider(
@@ -307,7 +295,7 @@ def test_start_span_implicit(self):
307295
self.assertIsNone(root.end_time)
308296
self.assertEqual(root.kind, trace_api.SpanKind.INTERNAL)
309297

310-
with tracer.use_span(root, True):
298+
with trace_api.use_span(root, True):
311299
self.assertIs(trace_api.get_current_span(), root)
312300

313301
with tracer.start_span(
@@ -364,7 +352,7 @@ def test_start_span_explicit(self):
364352
self.assertIsNone(root.end_time)
365353

366354
# Test with the implicit root span
367-
with tracer.use_span(root, True):
355+
with trace_api.use_span(root, True):
368356
self.assertIs(trace_api.get_current_span(), root)
369357

370358
with tracer.start_span("stepchild", other_parent_context) as child:
@@ -947,7 +935,7 @@ def error_status_test(context):
947935
.start_as_current_span("root")
948936
)
949937

950-
def test_override_error_status(self):
938+
def test_last_status_wins(self):
951939
def error_status_test(context):
952940
with self.assertRaises(AssertionError):
953941
with context as root:
@@ -956,8 +944,10 @@ def error_status_test(context):
956944
)
957945
raise AssertionError("unknown")
958946

959-
self.assertIs(root.status.status_code, StatusCode.OK)
960-
self.assertEqual(root.status.description, "OK")
947+
self.assertIs(root.status.status_code, StatusCode.ERROR)
948+
self.assertEqual(
949+
root.status.description, "AssertionError: unknown"
950+
)
961951

962952
error_status_test(
963953
trace.TracerProvider().get_tracer(__name__).start_span("root")

0 commit comments

Comments
 (0)