Skip to content

Commit 94caac9

Browse files
authored
Add start, end, attach, and detach to LogfireSpan (#914)
1 parent 4416b25 commit 94caac9

File tree

2 files changed

+102
-22
lines changed

2 files changed

+102
-22
lines changed

logfire/_internal/main.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,35 +2138,48 @@ def __init__(
21382138
def __getattr__(self, name: str) -> Any:
21392139
return getattr(self._span, name)
21402140

2141-
def __enter__(self) -> LogfireSpan:
2142-
with handle_internal_errors:
2143-
if self._span is None: # pragma: no branch
2144-
self._span = self._tracer.start_span(
2145-
name=self._span_name,
2146-
attributes=self._otlp_attributes,
2147-
links=self._links,
2148-
)
2149-
self._span.__enter__()
2150-
if self._token is None: # pragma: no branch
2151-
self._token = context_api.attach(trace_api.set_span_in_context(self._span))
2141+
@handle_internal_errors
2142+
def _start(self):
2143+
if self._span is not None:
2144+
return
2145+
self._span = self._tracer.start_span(
2146+
name=self._span_name,
2147+
attributes=self._otlp_attributes,
2148+
links=self._links,
2149+
)
2150+
2151+
@handle_internal_errors
2152+
def _attach(self):
2153+
if self._token is not None:
2154+
return
2155+
assert self._span is not None
2156+
self._token = context_api.attach(trace_api.set_span_in_context(self._span))
21522157

2158+
def __enter__(self) -> LogfireSpan:
2159+
self._start()
2160+
self._attach()
21532161
return self
21542162

21552163
@handle_internal_errors
2156-
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: Any) -> None:
2157-
if self._token is None: # pragma: no cover
2164+
def _end(self):
2165+
if not self._span or not self._span.is_recording():
21582166
return
2159-
assert self._span is not None
2167+
if self._added_attributes:
2168+
self._span.set_attribute(ATTRIBUTES_JSON_SCHEMA_KEY, attributes_json_schema(self._json_schema_properties))
2169+
self._span.end()
21602170

2171+
def _detach(self):
2172+
if self._token is None:
2173+
return
21612174
context_api.detach(self._token)
21622175
self._token = None
2163-
if self._span.is_recording():
2164-
with handle_internal_errors:
2165-
if self._added_attributes:
2166-
self._span.set_attribute(
2167-
ATTRIBUTES_JSON_SCHEMA_KEY, attributes_json_schema(self._json_schema_properties)
2168-
)
2169-
self._span.__exit__(exc_type, exc_value, traceback)
2176+
2177+
@handle_internal_errors
2178+
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: Any) -> None:
2179+
self._detach()
2180+
if self._span and self._span.is_recording() and isinstance(exc_value, BaseException):
2181+
self._span.record_exception(exc_value, escaped=True)
2182+
self._end()
21702183

21712184
@property
21722185
def message_template(self) -> str | None: # pragma: no cover

tests/test_logfire.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from opentelemetry.sdk.metrics.export import InMemoryMetricReader
2020
from opentelemetry.sdk.trace import ReadableSpan
2121
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor
22-
from opentelemetry.trace import StatusCode, get_tracer
22+
from opentelemetry.trace import INVALID_SPAN, StatusCode, get_current_span, get_tracer
2323
from pydantic import BaseModel, __version__ as pydantic_version
2424
from pydantic_core import ValidationError
2525

@@ -3323,3 +3323,70 @@ def test_default_id_generator(exporter: TestExporter) -> None:
33233323
export['attributes']['i'] for export in sorted(exported, key=lambda span: span['start_time'])
33243324
]
33253325
assert sorted_by_trace_id == sorted_by_start_timestamp
3326+
3327+
3328+
def test_start_end_attach_detach(exporter: TestExporter, caplog: pytest.LogCaptureFixture):
3329+
span = logfire.span('test')
3330+
assert not span.is_recording()
3331+
assert not exporter.exported_spans
3332+
assert get_current_span() is INVALID_SPAN
3333+
3334+
span._start() # type: ignore
3335+
span._start() # type: ignore
3336+
assert span.is_recording()
3337+
assert len(exporter.exported_spans) == 1
3338+
assert get_current_span() is INVALID_SPAN
3339+
3340+
span._attach() # type: ignore
3341+
span._attach() # type: ignore
3342+
assert get_current_span() is span._span # type: ignore
3343+
3344+
span._detach() # type: ignore
3345+
span._detach() # type: ignore
3346+
assert span.is_recording()
3347+
assert len(exporter.exported_spans) == 1
3348+
assert get_current_span() is INVALID_SPAN
3349+
3350+
span._end() # type: ignore
3351+
span._end() # type: ignore
3352+
assert not span.is_recording()
3353+
assert len(exporter.exported_spans) == 2
3354+
assert get_current_span() is INVALID_SPAN
3355+
3356+
assert not caplog.messages
3357+
3358+
assert exporter.exported_spans_as_dict(_include_pending_spans=True) == snapshot(
3359+
[
3360+
{
3361+
'name': 'test',
3362+
'context': {'trace_id': 1, 'span_id': 2, 'is_remote': False},
3363+
'parent': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
3364+
'start_time': 1000000000,
3365+
'end_time': 1000000000,
3366+
'attributes': {
3367+
'code.filepath': 'test_logfire.py',
3368+
'code.function': 'test_start_end_attach_detach',
3369+
'code.lineno': 123,
3370+
'logfire.msg_template': 'test',
3371+
'logfire.msg': 'test',
3372+
'logfire.span_type': 'pending_span',
3373+
'logfire.pending_parent_id': '0000000000000000',
3374+
},
3375+
},
3376+
{
3377+
'name': 'test',
3378+
'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
3379+
'parent': None,
3380+
'start_time': 1000000000,
3381+
'end_time': 2000000000,
3382+
'attributes': {
3383+
'code.filepath': 'test_logfire.py',
3384+
'code.function': 'test_start_end_attach_detach',
3385+
'code.lineno': 123,
3386+
'logfire.msg_template': 'test',
3387+
'logfire.msg': 'test',
3388+
'logfire.span_type': 'span',
3389+
},
3390+
},
3391+
]
3392+
)

0 commit comments

Comments
 (0)