fix: correct false import suggestions#1788
Conversation
`logfire run` was always suggesting `requests`, `sqlite3`, and `urllib` for instrumentation, even when the user's script didn't use them. - `sqlite3` and `urllib` were always suggested because they were in `STANDARD_LIBRARY_PACKAGES` which bypassed the installed-check - `requests` was suggested because it's a transitive dependency of opentelemetry, so it appeared as "installed" The fix introduces AST-based import analysis of the user's script to detect which packages are actually used. Packages in the new `ALWAYS_AVAILABLE_PACKAGES` set (stdlib modules and known transitive deps) are only recommended when the script actually imports them. When no script/module is provided (e.g. `logfire inspect`), the behavior is unchanged -- all available packages are still recommended. Fixes pydantic#1296 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
| elif isinstance(node, ast.ImportFrom): | ||
| if node.module: | ||
| imports.add(node.module.split('.')[0]) |
There was a problem hiding this comment.
🟡 Relative imports incorrectly treated as absolute imports in find_script_imports
The find_script_imports function does not check node.level on ast.ImportFrom nodes, so relative imports like from .requests import foo (where node.module='requests' and node.level=1) are incorrectly treated as absolute imports of the requests package. This causes the package to be added to the script_imports set, which in turn causes find_recommended_instrumentations_to_install at logfire/_internal/cli/run.py:212-216 to recommend instrumentation for a package the user isn't actually importing. The fix is to additionally check node.level == 0 before adding the module name.
| elif isinstance(node, ast.ImportFrom): | |
| if node.module: | |
| imports.add(node.module.split('.')[0]) | |
| elif isinstance(node, ast.ImportFrom): | |
| if node.module and node.level == 0: | |
| imports.add(node.module.split('.')[0]) |
Was this helpful? React with 👍 or 👎 to provide feedback.
| elif module_name: | ||
| try: | ||
| spec = importlib.util.find_spec(module_name) | ||
| if spec and spec.origin: | ||
| with open(spec.origin) as f: | ||
| source = f.read() | ||
| except (ModuleNotFoundError, ValueError, OSError): | ||
| return None |
There was a problem hiding this comment.
🚩 Module mode analyzes framework source, not user application code
When using logfire run -m uvicorn main:app, the code passes module_name='uvicorn' to find_script_imports (logfire/_internal/cli/run.py:96), which calls importlib.util.find_spec('uvicorn'). This analyzes the framework's source code rather than the user's application (main.py). In practice, for most framework packages (like uvicorn), find_spec returns origin=None (since they're packages, not single-file modules), so find_script_imports correctly falls back to returning None and no filtering is applied. However, if a framework is a single-file module with an origin, the wrong file would be analyzed, potentially filtering out valid recommendations. This is a design limitation more than a bug, but worth being aware of.
Was this helpful? React with 👍 or 👎 to provide feedback.
| def find_script_imports(script_path: str | None = None, module_name: str | None = None) -> set[str] | None: | ||
| """Extract top-level import names from a script file or module. | ||
|
|
||
| Uses AST parsing to find all `import X` and `from X import ...` statements, | ||
| returning the set of top-level module names (e.g. 'requests', 'sqlite3'). | ||
|
|
||
| Returns None if the source cannot be determined (e.g. no script or module provided), | ||
| which signals that filtering should be skipped (all packages recommended as before). | ||
| """ | ||
| source: str | None = None | ||
|
|
||
| if script_path: | ||
| try: | ||
| with open(script_path) as f: | ||
| source = f.read() | ||
| except OSError: | ||
| return None | ||
| elif module_name: | ||
| try: | ||
| spec = importlib.util.find_spec(module_name) | ||
| if spec and spec.origin: | ||
| with open(spec.origin) as f: | ||
| source = f.read() | ||
| except (ModuleNotFoundError, ValueError, OSError): | ||
| return None | ||
|
|
||
| if source is None: | ||
| return None | ||
|
|
||
| try: | ||
| tree = ast.parse(source) | ||
| except SyntaxError: | ||
| return None | ||
|
|
||
| imports: set[str] = set() | ||
| for node in ast.walk(tree): | ||
| if isinstance(node, ast.Import): | ||
| for alias in node.names: | ||
| # 'import urllib.request' -> top-level is 'urllib' | ||
| imports.add(alias.name.split('.')[0]) | ||
| elif isinstance(node, ast.ImportFrom): | ||
| if node.module: | ||
| imports.add(node.module.split('.')[0]) | ||
| return imports |
There was a problem hiding this comment.
🚩 No new tests for the script_imports filtering feature
The existing test at tests/test_cli.py:353 calls find_recommended_instrumentations_to_install with only 3 arguments, so script_imports defaults to None. There are no tests verifying the behavior when script_imports is a non-None set (the core new functionality). There are also no tests for find_script_imports itself. This means the new filtering logic, relative import handling, and module resolution paths are all untested.
Was this helpful? React with 👍 or 👎 to provide feedback.
1. Move sys.path.insert(0, os.getcwd()) BEFORE collect_instrumentation_context() so that local modules (-m myapp) can be resolved by importlib.util.find_spec. Previously, find_spec ran before cwd was on sys.path, causing silent fallback to unfiltered recommendations. 2. For packages, analyze __main__.py instead of __init__.py when it exists. runpy.run_module() executes __main__.py at runtime, so import analysis should match the actual entrypoint to produce accurate recommendations.
|
Thanks for the thorough review @cubic-dev-ai! Both issues were valid — pushed fixes in 3dcf344: Fix 1 (P2: sys.path ordering): Moved Fix 2 (P2: init.py vs main.py): Added logic to prefer Both changes preserve backward compatibility — |
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="logfire/_internal/cli/run.py">
<violation number="1" location="logfire/_internal/cli/run.py:98">
P1: CWD is prepended to `sys.path` too early, allowing local module shadowing during instrumentation imports.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
|
||
| # Add the current directory to `sys.path` BEFORE import analysis so that | ||
| # local modules (e.g. `logfire run -m myapp`) can be resolved by find_spec. | ||
| sys.path.insert(0, os.getcwd()) |
There was a problem hiding this comment.
P1: CWD is prepended to sys.path too early, allowing local module shadowing during instrumentation imports.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At logfire/_internal/cli/run.py, line 98:
<comment>CWD is prepended to `sys.path` too early, allowing local module shadowing during instrumentation imports.</comment>
<file context>
@@ -93,6 +93,10 @@ def parse_run(args: argparse.Namespace) -> None:
+ # Add the current directory to `sys.path` BEFORE import analysis so that
+ # local modules (e.g. `logfire run -m myapp`) can be resolved by find_spec.
+ sys.path.insert(0, os.getcwd())
+
ctx = collect_instrumentation_context(exclude, script_path=script_path, module_name=module_name)
</file context>
Summary
Fixes #1296 —
logfire runalways suggestedrequests,sqlite3, andurllibfor instrumentation, even for a simpleprint('Hello, world!')script.Root cause:
sqlite3andurllibwere always recommended because they were inSTANDARD_LIBRARY_PACKAGES, which bypassed the "is this package installed?" checkrequestswas recommended because it's a transitive dependency of opentelemetry, so it appeared as "installed" even though the user's code doesn't use itFix:
find_script_imports()which uses Python'sastmodule to extract top-level imports from the user's script/moduleSTANDARD_LIBRARY_PACKAGEStoALWAYS_AVAILABLE_PACKAGESand addedrequeststo itALWAYS_AVAILABLE_PACKAGES, only recommend them if the user's code actually imports themlogfire inspect), behavior is unchanged — all available packages are still recommendedBefore:
After:
Test plan
find_recommended_instrumentations_to_installwithoutscript_importsparam behaves identically)logfire run script.pywithprint('Hello')shows no suggestions for requests/sqlite3/urlliblogfire run script.pywithimport sqlite3correctly suggests sqlite3 instrumentationlogfire inspectstill shows all available recommendations (no script context = no filtering)logfire run -m module_namecorrectly analyzes module imports🤖 Generated with Claude Code
Summary by cubic
Fixes #1296 by stopping false import suggestions in
logfire run. We now analyze the script or module entrypoint imports, sorequests,sqlite3, andurllibare only suggested when used.find_script_imports()(AST) to read imports from a script or-mmodule; prefers__main__.pyover__init__.pyand skips filtering if source can’t be read.STANDARD_LIBRARY_PACKAGESwithALWAYS_AVAILABLE_PACKAGES(addsrequests); only recommend these if actually imported by the user’s code.sys.path.insert(0, os.getcwd())before import analysis so local-mmodules resolve correctly.collect_instrumentation_context()andfind_recommended_instrumentations_to_install()now acceptscript_imports; behavior unchanged when no script/module is provided.Written for commit 3dcf344. Summary will update on new commits.