Skip to content

Commit 33a7b63

Browse files
committed
param version
1 parent c94d11b commit 33a7b63

File tree

3 files changed

+95
-5
lines changed

3 files changed

+95
-5
lines changed

docs/guides/onboarding-checklist/add-manual-tracing.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,17 @@ my_function(3, 4)
234234
# Logs: Applying my_function to x=3 and y=4
235235
```
236236

237+
You can also access the span directly within your instrumented function by adding a `logfire_span` parameter:
238+
239+
```python
240+
@logfire.instrument('Processing {x=}')
241+
def process_data(x: int, logfire_span: logfire.LogfireSpan | None = None) -> int:
242+
# Access and modify the span directly
243+
if logfire_span:
244+
logfire_span.message = f'Custom message for x={x}'
245+
return x * 2
246+
```
247+
237248
!!! note
238249

239250
- The [`@logfire.instrument`][logfire.Logfire.instrument] decorator MUST be applied first, i.e., UNDER any other decorators.

logfire/_internal/instrument.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,19 +63,27 @@ def decorator(func: Callable[P, R]) -> Callable[P, R]:
6363
attributes = get_attributes(func, msg_template, tags)
6464
open_span = get_open_span(logfire, attributes, span_name, extract_args, func)
6565

66+
# Check if function has logfire_span parameter
67+
sig = inspect.signature(func)
68+
has_logfire_span_param = 'logfire_span' in sig.parameters
69+
6670
if inspect.isgeneratorfunction(func):
6771
if not allow_generator:
6872
warnings.warn(GENERATOR_WARNING_MESSAGE, stacklevel=2)
6973

7074
def wrapper(*func_args: P.args, **func_kwargs: P.kwargs): # type: ignore
71-
with open_span(*func_args, **func_kwargs):
75+
with open_span(*func_args, **func_kwargs) as span:
76+
if has_logfire_span_param:
77+
func_kwargs['logfire_span'] = span
7278
yield from func(*func_args, **func_kwargs)
7379
elif inspect.isasyncgenfunction(func):
7480
if not allow_generator:
7581
warnings.warn(GENERATOR_WARNING_MESSAGE, stacklevel=2)
7682

7783
async def wrapper(*func_args: P.args, **func_kwargs: P.kwargs): # type: ignore
78-
with open_span(*func_args, **func_kwargs):
84+
with open_span(*func_args, **func_kwargs) as span:
85+
if has_logfire_span_param:
86+
func_kwargs['logfire_span'] = span
7987
# `yield from` is invalid syntax in an async function.
8088
# This loop is not quite equivalent, because `yield from` also handles things like
8189
# sending values to the subgenerator.
@@ -90,6 +98,8 @@ async def wrapper(*func_args: P.args, **func_kwargs: P.kwargs): # type: ignore
9098

9199
async def wrapper(*func_args: P.args, **func_kwargs: P.kwargs) -> R: # type: ignore
92100
with open_span(*func_args, **func_kwargs) as span:
101+
if has_logfire_span_param:
102+
func_kwargs['logfire_span'] = span
93103
result = await func(*func_args, **func_kwargs)
94104
if record_return:
95105
# open_span returns a FastLogfireSpan, so we can't use span.set_attribute for complex types.
@@ -102,6 +112,8 @@ async def wrapper(*func_args: P.args, **func_kwargs: P.kwargs) -> R: # type: ig
102112
# Same as the above, but without the async/await
103113
def wrapper(*func_args: P.args, **func_kwargs: P.kwargs) -> R:
104114
with open_span(*func_args, **func_kwargs) as span:
115+
if has_logfire_span_param:
116+
func_kwargs['logfire_span'] = span
105117
result = func(*func_args, **func_kwargs)
106118
if record_return:
107119
set_user_attributes_on_raw_span(span._span, {'return': result})
@@ -122,27 +134,32 @@ def get_open_span(
122134
) -> Callable[P, AbstractContextManager[Any]]:
123135
final_span_name: str = span_name or attributes[ATTRIBUTES_MESSAGE_TEMPLATE_KEY] # type: ignore
124136

137+
# Check if function has logfire_span parameter
138+
sig = inspect.signature(func)
139+
has_logfire_span_param = 'logfire_span' in sig.parameters
140+
125141
# This is the fast case for when there are no arguments to extract
126142
def open_span(*_: P.args, **__: P.kwargs): # type: ignore
143+
if has_logfire_span_param:
144+
return logfire._span(final_span_name, attributes) # type: ignore
127145
return logfire._fast_span(final_span_name, attributes) # type: ignore
128146

129147
if extract_args is True:
130-
sig = inspect.signature(func)
131148
if sig.parameters: # only extract args if there are any
132149

133150
def open_span(*func_args: P.args, **func_kwargs: P.kwargs):
134151
bound = sig.bind(*func_args, **func_kwargs)
135152
bound.apply_defaults()
136153
args_dict = bound.arguments
154+
if has_logfire_span_param:
155+
return logfire._span(final_span_name, {**attributes, **args_dict}) # type: ignore
137156
return logfire._instrument_span_with_args( # type: ignore
138157
final_span_name, attributes, args_dict
139158
)
140159

141160
return open_span
142161

143162
if extract_args: # i.e. extract_args should be an iterable of argument names
144-
sig = inspect.signature(func)
145-
146163
if isinstance(extract_args, str):
147164
extract_args = [extract_args]
148165

@@ -165,6 +182,8 @@ def open_span(*func_args: P.args, **func_kwargs: P.kwargs):
165182
# This line is the only difference from the extract_args=True case
166183
args_dict = {k: args_dict[k] for k in extract_args_final}
167184

185+
if has_logfire_span_param:
186+
return logfire._span(final_span_name, {**attributes, **args_dict}) # type: ignore
168187
return logfire._instrument_span_with_args( # type: ignore
169188
final_span_name, attributes, args_dict
170189
)

tests/test_logfire.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,66 @@ def run(a: str) -> Model:
12731273
)
12741274

12751275

1276+
def test_instrument_with_logfire_span_parameter(exporter: TestExporter):
1277+
@logfire.instrument('Calling foo with {x=}')
1278+
def foo(x: int, logfire_span: logfire.LogfireSpan | None = None) -> int:
1279+
# Test that we can access the span and modify its message
1280+
assert logfire_span is not None
1281+
logfire_span.message = f'Modified message for x={x}'
1282+
return x * 2
1283+
1284+
result = foo(5)
1285+
assert result == 10
1286+
1287+
spans = exporter.exported_spans_as_dict(_strip_function_qualname=False)
1288+
assert len(spans) == 1
1289+
span = spans[0]
1290+
assert span['attributes']['logfire.msg'] == 'Modified message for x=5'
1291+
assert span['attributes']['x'] == 5
1292+
1293+
1294+
def test_instrument_with_logfire_span_parameter_async(exporter: TestExporter):
1295+
@logfire.instrument('Calling async foo with {x=}')
1296+
async def foo(x: int, logfire_span: logfire.LogfireSpan | None = None) -> int:
1297+
# Test that we can access the span and modify its message
1298+
assert logfire_span is not None
1299+
logfire_span.message = f'Async modified message for x={x}'
1300+
return x * 3
1301+
1302+
async def run_test():
1303+
return await foo(7)
1304+
1305+
import asyncio
1306+
1307+
result = asyncio.run(run_test())
1308+
assert result == 21
1309+
1310+
spans = exporter.exported_spans_as_dict(_strip_function_qualname=False)
1311+
assert len(spans) == 1
1312+
span = spans[0]
1313+
assert span['attributes']['logfire.msg'] == 'Async modified message for x=7'
1314+
assert span['attributes']['x'] == 7
1315+
1316+
1317+
def test_instrument_with_logfire_span_parameter_extract_args_false(exporter: TestExporter):
1318+
@logfire.instrument('Calling foo', extract_args=False)
1319+
def foo(x: int, logfire_span: logfire.LogfireSpan | None = None) -> int:
1320+
# Test that we can access the span and modify its message
1321+
assert logfire_span is not None
1322+
logfire_span.message = f'Extract args false message for x={x}'
1323+
return x * 4
1324+
1325+
result = foo(3)
1326+
assert result == 12
1327+
1328+
spans = exporter.exported_spans_as_dict(_strip_function_qualname=False)
1329+
assert len(spans) == 1
1330+
span = spans[0]
1331+
assert span['attributes']['logfire.msg'] == 'Extract args false message for x=3'
1332+
# x should not be in attributes since extract_args=False
1333+
assert 'x' not in span['attributes']
1334+
1335+
12761336
def test_validation_error_on_span(exporter: TestExporter) -> None:
12771337
class Model(BaseModel, plugin_settings={'logfire': {'record': 'off'}}):
12781338
a: int

0 commit comments

Comments
 (0)