Skip to content

Commit b6b6822

Browse files
authored
Don't auto-trace generators (#386)
1 parent f2e87f3 commit b6b6822

File tree

5 files changed

+52
-38
lines changed

5 files changed

+52
-38
lines changed

docs/guides/advanced/generators.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ Traceback (most recent call last):
9090
ValueError: <Token var=<ContextVar name='current_context' default={} at 0x10afa3f60> at 0x10de034c0> was created in a different Context
9191
```
9292

93+
This is why generator functions are not traced by [`logfire.install_auto_tracing()`][logfire.Logfire.install_auto_tracing].
94+
9395
## What you can do
9496

9597
### Move the span outside the generator

docs/guides/onboarding_checklist/add_auto_tracing.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Auto-tracing
22

3-
The [`logfire.install_auto_tracing`][logfire.Logfire.install_auto_tracing]
3+
The [`logfire.install_auto_tracing()`][logfire.Logfire.install_auto_tracing] method
44
will trace all function calls in the specified modules.
55

66
This works by changing how those modules are imported,
@@ -21,6 +21,9 @@ from app.main import main
2121
main()
2222
```
2323

24+
!!! note
25+
Generator functions will not be traced for reasons explained [here](../advanced/generators.md).
26+
2427
## Filtering modules to trace
2528

2629
The `modules` argument can be a list of module names.

logfire/_internal/auto_trace/rewrite_ast.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

33
import ast
4+
import inspect
5+
import types
46
import uuid
57
from dataclasses import dataclass
68
from functools import partial
@@ -94,6 +96,12 @@ def visit_FunctionDef(self, node: ast.FunctionDef | ast.AsyncFunctionDef):
9496

9597
return super().visit_FunctionDef(node)
9698

99+
def rewrite_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualname: str) -> ast.AST:
100+
if is_generator_function(node):
101+
return node
102+
103+
return super().rewrite_function(node, qualname)
104+
97105
def logfire_method_call_node(self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualname: str) -> ast.Call:
98106
# See the exec_source docstring
99107
index = len(self.context_factories)
@@ -158,3 +166,15 @@ def no_auto_trace(x: T) -> T:
158166
This decorator simply returns the argument unchanged, so there is zero runtime overhead.
159167
"""
160168
return x # pragma: no cover
169+
170+
171+
GENERATOR_CODE_FLAGS = inspect.CO_GENERATOR | inspect.CO_ASYNC_GENERATOR
172+
173+
174+
def is_generator_function(func_def: ast.FunctionDef | ast.AsyncFunctionDef):
175+
module_node = ast.parse('')
176+
module_node.body = [func_def]
177+
code = compile(module_node, '<string>', 'exec')
178+
return any(
179+
isinstance(const, types.CodeType) and (const.co_flags & GENERATOR_CODE_FLAGS) for const in code.co_consts
180+
)

logfire/_internal/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -780,12 +780,14 @@ def install_auto_tracing(
780780
) -> None:
781781
"""Install automatic tracing.
782782
783-
This will trace all function calls in the modules specified by the modules argument.
783+
This will trace all non-generator function calls in the modules specified by the modules argument.
784784
It's equivalent to wrapping the body of every function in matching modules in `with logfire.span(...):`.
785785
786786
!!! note
787787
This function MUST be called before any of the modules to be traced are imported.
788788
789+
Generator functions will not be traced for reasons explained [here](https://docs.pydantic.dev/logfire/guides/advanced/generators/).
790+
789791
This works by inserting a new meta path finder into `sys.meta_path`, so inserting another finder before it
790792
may prevent it from working.
791793

tests/test_auto_trace.py

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -85,45 +85,12 @@ def test_auto_trace_sample(exporter: TestExporter) -> None:
8585
'logfire.pending_parent_id': '0000000000000001',
8686
},
8787
},
88-
{
89-
'name': 'Calling tests.auto_trace_samples.foo.gen (pending)',
90-
'context': {'trace_id': 1, 'span_id': 6, 'is_remote': False},
91-
'parent': {'trace_id': 1, 'span_id': 5, 'is_remote': False},
92-
'start_time': 3000000000,
93-
'end_time': 3000000000,
94-
'attributes': {
95-
'code.filepath': 'foo.py',
96-
'code.lineno': 123,
97-
'code.function': 'gen',
98-
'logfire.msg_template': 'Calling tests.auto_trace_samples.foo.gen',
99-
'logfire.msg': 'Calling tests.auto_trace_samples.foo.gen',
100-
'logfire.span_type': 'pending_span',
101-
'logfire.tags': ('auto-trace',),
102-
'logfire.pending_parent_id': '0000000000000003',
103-
},
104-
},
105-
{
106-
'name': 'Calling tests.auto_trace_samples.foo.gen',
107-
'context': {'trace_id': 1, 'span_id': 5, 'is_remote': False},
108-
'parent': {'trace_id': 1, 'span_id': 3, 'is_remote': False},
109-
'start_time': 3000000000,
110-
'end_time': 4000000000,
111-
'attributes': {
112-
'code.filepath': 'foo.py',
113-
'code.lineno': 123,
114-
'code.function': 'gen',
115-
'logfire.msg_template': 'Calling tests.auto_trace_samples.foo.gen',
116-
'logfire.span_type': 'span',
117-
'logfire.tags': ('auto-trace',),
118-
'logfire.msg': 'Calling tests.auto_trace_samples.foo.gen',
119-
},
120-
},
12188
{
12289
'name': 'Calling async_gen via @instrument',
12390
'context': {'trace_id': 1, 'span_id': 3, 'is_remote': False},
12491
'parent': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
12592
'start_time': 2000000000,
126-
'end_time': 5000000000,
93+
'end_time': 3000000000,
12794
'attributes': {
12895
'code.filepath': 'foo.py',
12996
'code.lineno': 123,
@@ -138,7 +105,7 @@ def test_auto_trace_sample(exporter: TestExporter) -> None:
138105
'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
139106
'parent': None,
140107
'start_time': 1000000000,
141-
'end_time': 7000000000,
108+
'end_time': 5000000000,
142109
'attributes': {
143110
'code.filepath': 'foo.py',
144111
'code.lineno': 123,
@@ -152,7 +119,7 @@ def test_auto_trace_sample(exporter: TestExporter) -> None:
152119
'events': [
153120
{
154121
'name': 'exception',
155-
'timestamp': 6000000000,
122+
'timestamp': 4000000000,
156123
'attributes': {
157124
'exception.type': 'IndexError',
158125
'exception.message': 'list index out of range',
@@ -443,6 +410,26 @@ def test_no_auto_trace():
443410
)
444411

445412

413+
# language=Python
414+
generators_sample = """
415+
def make_gen():
416+
def gen():
417+
async def foo():
418+
async def bar():
419+
pass
420+
yield bar()
421+
yield from foo()
422+
return gen
423+
"""
424+
425+
426+
def test_generators():
427+
assert get_calling_strings(generators_sample) == {
428+
'Calling module.name.make_gen',
429+
'Calling module.name.make_gen.<locals>.gen.<locals>.foo.<locals>.bar',
430+
}
431+
432+
446433
def test_min_duration(exporter: TestExporter):
447434
install_auto_tracing('tests.auto_trace_samples.simple_nesting', min_duration=5)
448435

0 commit comments

Comments
 (0)