feat: add Logfire phased instrumentation#692
Conversation
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 3m 31s —— View job PR Review: feat/logfire-phased-instrumentationTasks:
SummaryThis is a well-structured telemetry integration. The config-gated bootstrap, zero-overhead no-op path when disabled, and layered Issues1.
|
| Item | Status | Notes |
|---|---|---|
| Follows Basic Memory coding conventions | ✅ | Literate comments, section headers, decision-point comments |
| Python 3.12+ type annotations and async patterns | ✅ | |
| SQLAlchemy 2.0 best practices | ✅ | No DB changes |
| FastAPI and Typer conventions | ✅ | ctx.with_resource() for CLI span is idiomatic |
| 100-character line length | ✅ | |
| No commented-out code | ✅ | |
| Unit tests for new functions | ✅ | Comprehensive fake-logfire coverage |
| Integration tests for MCP tools | ✅ | test_tool_telemetry.py covers all major tools |
| Test coverage edge cases | ✅ | Missing dependency, disabled, retry without send_to_logfire |
| 100% test coverage maintained | # pragma: no cover on tested init functions (see Issue 1) |
|
| Documentation updated | ✅ | docs/logfire-instrumentation-strategy.md added |
| CLAUDE.md updated if conventions change | N/A | No convention changes |
| MCP tools follow atomic, composable design | ✅ | |
| Database changes include Alembic migrations | ✅ | No DB changes |
| Preserves local-first architecture | ✅ | Zero overhead path when disabled |
| No hardcoded secrets | ✅ | |
| Input validation for MCP tools | ✅ | |
| Proper error handling and logging | ✅ | pop_telemetry_warnings() surfaces errors after logger init |
| Performance when disabled | ✅ | telemetry_enabled() short-circuits all paths |
| No sensitive data in logs | ✅ | Only structural metadata in span attributes |
| Windows compatibility | ✅ | No path operations in telemetry code |
| No emojis/unicode in console/log output | ✅ |
Minor Observations
telemetry.pyline 120–123 uses triple# pragma: no coveron adjacent lines — standard practice is a single pragma on theexcept Exceptionline only; the pragma on the body lines is redundant but harmless._load_logfire()is called on everyspan()invocation (when enabled). Python's import cache makes this fast, but a module-level cache of the resolvedlogfireobject afterconfigure_telemetrywould be marginally cleaner.- The
with telemetry.scope(...)inresolve_workspace_parameterandget_active_projectare synchronous context managers used in async functions — this is correct with asyncio/contextvars and works as expected.
Overall this is a solid, well-tested implementation. Issues 1 (coverage pragma) and 6 (hard dependency) are the most important to address before merge.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 23e0073ce6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # project. Resolving it through /v2/projects/resolve creates noisy false-negative | ||
| # logs before the tool recovers with the active project anyway. | ||
| # Outcome: skip project resolution and keep the path within the active project. | ||
| if project_prefix and (configured_prefix is not None or project is None): |
There was a problem hiding this comment.
Resolve memory URL prefixes even when project is preselected
The new guard if project_prefix and (configured_prefix is not None or project is None) skips /v2/projects/resolve whenever a project is already fixed and the prefix is not present in local config. In cloud routing, local config may not contain all accessible projects, so valid inputs like memory://other-project/note can be misinterpreted as a path inside the active project (for example <active>/other-project/note) instead of routing to other-project. This regresses cross-project memory URL behavior in any flow that has already resolved project.
Useful? React with 👍 / 👎.
The telemetry PR introduced a guard that skipped /v2/projects/resolve when a project was already fixed and the URL prefix wasn't in local config. This broke cloud routing where remote projects aren't locally configured — valid URLs like memory://other-project/note would be misinterpreted as paths within the active project. Revert to the original unconditional `if project_prefix:` guard and remove the now-unused `_configured_project_name()` helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 1m 10s —— View job PR Review Update: feat/logfire-phased-instrumentationTasks:
What changed in
|
| # | Issue | Status |
|---|---|---|
| 1 | # pragma: no cover on init_cli_logging, init_mcp_logging, init_api_logging in config.py — these functions are fully tested |
Still open |
| 2 | PR description says "LOGFIRE_TOKEN enables" but actual gate is logfire_enabled: bool in config — docs/README need clarification |
Still open |
| 4 | telemetry.operation is a pure pass-through to telemetry.scope — semantic distinction is convention only, not enforced |
Still open (minor) |
| 5 | routing.resolve_client span in get_project_client wraps the yield, so it stays open for the entire tool execution rather than just routing resolution time |
Still open (minor) |
| 6 | logfire is a hard dependency in pyproject.toml — should be an optional extras group given the "zero overhead when off" design goal |
Still open |
Issues 1 and 6 remain the most important before merge. The fix in this commit is solid — the cross-project routing regression is the kind of subtle correctness bug that would be painful to track down in production.
- Remove `# pragma: no cover` from init_cli/mcp/api_logging since they are fully tested (issue 1) - Move logfire to optional dependency `pip install basic-memory[telemetry]` while keeping it in dev deps for testing (issue 6) - Collapse `telemetry.operation` into an alias for `telemetry.scope` with a comment explaining the convention (issue 4) - Rename `routing.resolve_client` span to `routing.client_session` to accurately reflect that it covers the full tool execution lifetime, not just routing resolution (issue 5) - Update PR description to clarify two-step setup: logfire_enabled config flag + optional LOGFIRE_TOKEN (issue 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 2m 3s —— View job PR Review Update: feat/logfire-phased-instrumentationTasks:
What changed in
|
| # | Prior Issue | Status |
|---|---|---|
| 1 | # pragma: no cover on init_cli/mcp/api_logging in config.py — these are fully tested |
✅ Fixed — pragmas removed |
| 2 | PR description said "LOGFIRE_TOKEN enables" but actual gate is logfire_enabled: bool |
✅ Fixed — PR description updated; logfire_enabled = true is now the documented first step |
| 3 | Duplicate _canonicalize_project_name / _configured_project_name helpers |
✅ Already fixed in fe32ac5 (prior review cycle) |
| 4 | telemetry.operation was a pure pass-through to telemetry.scope with no enforcement |
✅ Fixed — collapsed to a module-level alias with a comment documenting the convention-only distinction |
| 5 | routing.resolve_client span stayed open for the full tool execution lifetime, not just routing |
✅ Fixed — renamed to routing.client_session, accurately reflecting what it measures |
| 6 | logfire was a hard dependency for all users |
✅ Fixed — moved to [project.optional-dependencies] telemetry = ["logfire>=4.19.0"]; kept in [dependency-groups] dev for tests |
Remaining # pragma: no cover (all appropriate)
The remaining pragmas in telemetry.py are on genuinely hard-to-test paths:
- Lines 120–123:
except Exception as excinsidelogfire.configure()— requires failure injection into a third-party call - Lines 190–192:
if logfire is Noneinsidespan()— only reachable if logfire is importable atconfigure_telemetrytime but disappears afterward (can't happen in practice)
These are appropriate uses per the project's # pragma: no cover policy.
Team Checklist
| Item | Status | Notes |
|---|---|---|
| Follows Basic Memory coding conventions | ✅ | Literate comments, section headers, decision-point comments throughout |
| Python 3.12+ type annotations and async patterns | ✅ | |
| SQLAlchemy 2.0 best practices | ✅ | No DB changes |
| FastAPI and Typer conventions | ✅ | |
| 100-character line length | ✅ | |
| No commented-out code | ✅ | |
| Unit tests for new functions | ✅ | Comprehensive fake-logfire coverage in tests/test_telemetry.py |
| Integration tests for MCP tools | ✅ | test_tool_telemetry.py covers all major tools |
| Test coverage edge cases | ✅ | Missing dependency, disabled path, retry without send_to_logfire |
| 100% test coverage maintained | ✅ | Pragmas removed from init_cli/mcp/api_logging (Issue 1 resolved) |
| Documentation updated | ✅ | docs/logfire-instrumentation-strategy.md added; span name updated in docs to match rename |
| CLAUDE.md updated if conventions change | N/A | No convention changes |
| MCP tools follow atomic, composable design | ✅ | |
| Database changes include Alembic migrations | N/A | No DB changes |
| Preserves local-first architecture | ✅ | Zero overhead path when disabled |
| No hardcoded secrets | ✅ | |
| Input validation for MCP tools | ✅ | |
| Proper error handling and logging | ✅ | pop_telemetry_warnings() surfaces errors after logger init |
| Performance when disabled | ✅ | telemetry_enabled() short-circuits all paths |
| No sensitive data in logs | ✅ | Only structural metadata in span attributes |
| Windows compatibility | ✅ | No path operations in telemetry code |
| No emojis/unicode in console/log output | ✅ | |
logfire optional dependency |
✅ | Moved to [telemetry] extras; dev deps retain it for tests |
Summary
All prior issues are resolved. The PR is clean:
logfireis an optional install (pip install basic-memory[telemetry])- Telemetry is fully no-op when
logfire_enabled = false(default) - Coverage pragmas are only on genuinely untestable paths
- Span names accurately reflect their lifetime scope
telemetry.operation/telemetry.scopedistinction is documented as convention-only with an alias
This is ready to merge.
Summary
logfire_enabled = truein config enables;falseby default — zero overhead when off)logfireis an optional dependency (pip install basic-memory[telemetry])Details
Phased instrumentation strategy following
docs/logfire-instrumentation-strategy.md:telemetry.pymodule withconfigure_telemetry()and span helpersSetup:
pip install basic-memory[telemetry](orlogfireis already included in dev dependencies)logfire_enabled = truein~/.basic-memory/config.jsonLOGFIRE_TOKENenv var for backend export to LogfireIncludes comprehensive test coverage (telemetry tests for each instrumented layer) and a smoke workflow (
just test-smoke-logfire).Test plan
just fast-checkpassesjust test-smoke-logfiresmoke test validates spans are emitted when enabledlogfire_enabledisfalse(telemetry is fully no-op)logfirepackage is not installed🤖 Generated with Claude Code