Summary
episodic/logging.py is intended as the project's logging seam, exposing
log_info, log_warning, and log_error over femtologging. In practice most
logging bypasses it: 29 of 48 logging call sites in episodic/ call a
logger method directly.
$ grep -rn "log_info(\|log_warning(\|log_error(" --include=*.py episodic/ \
| grep -v "def log_\|episodic/logging.py" | wc -l
19
$ grep -rEn "\b(logger|_log|_logger|effective_log)\.(debug|info|warning|error|critical|log)\(" \
--include=*.py episodic/ | grep -v "episodic/logging.py" | wc -l
29
The bypassing sites are in episodic/concurrent_interpreters.py (8),
episodic/canonical/storage/migration_check.py (7),
episodic/generation/chapter_marker_generator.py (4),
episodic/worker/runtime.py (2), episodic/qa/chrono.py (2),
episodic/qa/chrono_langgraph.py (2), and one each in
episodic/api/authorization.py, episodic/llm/openai_api/utils.py,
episodic/canonical/storage/uow.py, episodic/canonical/services.py, and
episodic/canonical/ingestion_service.py.
There is nothing wrong with any individual call. The problem is that the seam
is advisory, so any cross-cutting change to logging behaviour silently covers
39% of the codebase and misses the rest, with no gate to say so.
Why this surfaced now
Roadmap item 4.1.3 (request correlation) needs every log line a request produces
to carry that request's correlation identifier. femtologging cannot carry
ambient structured context — see
leynos/femtologging#417, #418, and #419 — so the identifier has to be attached
at the seam. The execplan
(docs/execplans/4-1-3-integrate-request-correlation.md) originally claimed
that decorating the three helpers gave complete coverage. It does not, and two
of the uncovered sites are precisely the ones the roadmap names: the
authorization denial (#287) and _log_error_event in
episodic/llm/openai_api/utils.py:108-111, the structured provider-error path
that fires on the provider failures the feature exists to correlate.
4.1.3 will close those two by hand. This issue is about making the seam
enforceable so the next cross-cutting change does not have to rediscover the
same 29 sites.
Proposal
1. Make episodic/logging.py an explicit, documented logging port.
Give it the full level surface the codebase actually needs — it currently lacks
log_debug, which is one reason episodic/api/authorization.py calls
logger.log(LogLevel.DEBUG, ...) directly — and document the contract in
docs/developers-guide.md: what a call site may assume, what the port
guarantees to attach (correlation identifier, and whatever comes later), and
why the raw femtologging logger is not the public surface.
2. Enforce the import edge with hecate.
hecate can gate this, but only after a configuration change:
include_external_packages defaults to False
(hecate/config.py:91) and [tool.hecate] at pyproject.toml:785 does not set
it, so today the checker sees zero third-party import edges. Setting it to
true and declaring a group that permits femtologging only from
episodic.logging would make from femtologging import get_logger anywhere
else a build failure.
Two cautions:
- Turning on
include_external_packages will surface every other external edge
in the repository at once. Expect a first pass of classifying falcon,
sqlalchemy, celery, httpx, and friends into groups.
- hecate matches on first matching prefix, so a broad prefix silently voids
later groups while still reporting a pass. Order the new groups carefully and
add a fixture under tests/fixtures/architecture/ that would fail if the
rule were shadowed.
3. Close the gap hecate cannot see.
hecate reasons about import edges, not call sites. It cannot catch
logger = get_logger(__name__) followed by logger.info(...), because the
import is legitimate. Two options, not mutually exclusive:
- Type the handle. Have the port return an opaque handle that does not
expose .info/.debug/.log, so a direct call is a typecheck failure under
ty and pyright. Strongest option, and it costs a Protocol rather than a
new tool.
- Add a lint rule. The repository already runs a custom Pylint plugin
(df12-python-lints) under CPython 3.14, which is the natural home for a
check banning level-method calls on objects returned by get_logger.
4. Migrate the 29 sites, ideally in one mechanical commit per package so the
diff stays reviewable.
Acceptance
docs/developers-guide.md documents the port and the rule.
make check-architecture fails on a fixture that imports femtologging
outside episodic/logging.py.
- Either
make typecheck or make lint fails on a fixture that calls a level
method directly.
grep for direct logger calls in episodic/ returns only episodic/logging.py.
Related
Summary
episodic/logging.pyis intended as the project's logging seam, exposinglog_info,log_warning, andlog_errorover femtologging. In practice mostlogging bypasses it: 29 of 48 logging call sites in
episodic/call alogger method directly.
The bypassing sites are in
episodic/concurrent_interpreters.py(8),episodic/canonical/storage/migration_check.py(7),episodic/generation/chapter_marker_generator.py(4),episodic/worker/runtime.py(2),episodic/qa/chrono.py(2),episodic/qa/chrono_langgraph.py(2), and one each inepisodic/api/authorization.py,episodic/llm/openai_api/utils.py,episodic/canonical/storage/uow.py,episodic/canonical/services.py, andepisodic/canonical/ingestion_service.py.There is nothing wrong with any individual call. The problem is that the seam
is advisory, so any cross-cutting change to logging behaviour silently covers
39% of the codebase and misses the rest, with no gate to say so.
Why this surfaced now
Roadmap item 4.1.3 (request correlation) needs every log line a request produces
to carry that request's correlation identifier. femtologging cannot carry
ambient structured context — see
leynos/femtologging#417, #418, and #419 — so the identifier has to be attached
at the seam. The execplan
(
docs/execplans/4-1-3-integrate-request-correlation.md) originally claimedthat decorating the three helpers gave complete coverage. It does not, and two
of the uncovered sites are precisely the ones the roadmap names: the
authorization denial (#287) and
_log_error_eventinepisodic/llm/openai_api/utils.py:108-111, the structured provider-error paththat fires on the provider failures the feature exists to correlate.
4.1.3 will close those two by hand. This issue is about making the seam
enforceable so the next cross-cutting change does not have to rediscover the
same 29 sites.
Proposal
1. Make
episodic/logging.pyan explicit, documented logging port.Give it the full level surface the codebase actually needs — it currently lacks
log_debug, which is one reasonepisodic/api/authorization.pycallslogger.log(LogLevel.DEBUG, ...)directly — and document the contract indocs/developers-guide.md: what a call site may assume, what the portguarantees to attach (correlation identifier, and whatever comes later), and
why the raw femtologging logger is not the public surface.
2. Enforce the import edge with hecate.
hecate can gate this, but only after a configuration change:
include_external_packagesdefaults toFalse(
hecate/config.py:91) and[tool.hecate]atpyproject.toml:785does not setit, so today the checker sees zero third-party import edges. Setting it to
trueand declaring a group that permitsfemtologgingonly fromepisodic.loggingwould makefrom femtologging import get_loggeranywhereelse a build failure.
Two cautions:
include_external_packageswill surface every other external edgein the repository at once. Expect a first pass of classifying
falcon,sqlalchemy,celery,httpx, and friends into groups.later groups while still reporting a pass. Order the new groups carefully and
add a fixture under
tests/fixtures/architecture/that would fail if therule were shadowed.
3. Close the gap hecate cannot see.
hecate reasons about import edges, not call sites. It cannot catch
logger = get_logger(__name__)followed bylogger.info(...), because theimport is legitimate. Two options, not mutually exclusive:
expose
.info/.debug/.log, so a direct call is a typecheck failure undertyandpyright. Strongest option, and it costs aProtocolrather than anew tool.
(
df12-python-lints) under CPython 3.14, which is the natural home for acheck banning level-method calls on objects returned by
get_logger.4. Migrate the 29 sites, ideally in one mechanical commit per package so the
diff stays reviewable.
Acceptance
docs/developers-guide.mddocuments the port and the rule.make check-architecturefails on a fixture that importsfemtologgingoutside
episodic/logging.py.make typecheckormake lintfails on a fixture that calls a levelmethod directly.
grepfor direct logger calls inepisodic/returns onlyepisodic/logging.py.Related
further down the stack.
docs/execplans/4-1-3-integrate-request-correlation.md, Decision D5 andmilestone EP-M3b.