Skip to content

refactor: replace InstrumentNotReadyWarning with is_configured classmethod + summary log#107

Merged
lesnik512 merged 8 commits into
mainfrom
refactor/instrument-skip-rework
Jun 2, 2026
Merged

refactor: replace InstrumentNotReadyWarning with is_configured classmethod + summary log#107
lesnik512 merged 8 commits into
mainfrom
refactor/instrument-skip-rework

Conversation

@lesnik512
Copy link
Copy Markdown
Member

Summary

PR #86 introduced InstrumentNotReadyWarning, escalating the not-ready skip path from a (silently dropped) logger.info to a real warnings.warn. The escalation surfaced expected-path events as warnings: every service using a subset of available instruments emits N warnings on bootstrap, and lite-bootstrap's own tests couldn't suppress without breaking the warning assertions.

This PR replaces the API:

  • is_ready instance method → is_configured classmethod taking config, runs before instantiation
  • InstrumentNotReadyWarning removed (hard delete; 4 weeks old)
  • New bootstrapper.skipped_instruments: list[tuple[type, str]] introspection
  • One INFO-level summary log at end of __init__ (silent by default; opt-in via logging.basicConfig)
  • InstrumentDependencyMissingWarning kept for the genuine deployment surprise (configured but optional dep missing)
  • PR fix: prevent FastStream bootstrap NameError when optional extras are missing #88's "deps before instantiation" constraint preservedis_configured is a classmethod so it runs without instantiation

Production-code subtlety: fresh logger per call

bootstrappers/base.py now uses _get_logger() (fresh per call) instead of a module-level cached logger. Reason: LoggingInstrument.bootstrap() sets cache_logger_on_first_use=True on structlog. A module-level cached BoundLogger would be memoized with whatever processors were active at first use, and structlog.testing.capture_logs() couldn't override. The fresh-per-call approach keeps the structlog pipeline reactive — which is what makes the new summary-log test (and existing teardown-error tests) work across the full test suite.

Scope

22 files modified (15 production + 6 tests + 2 docs). Bootstrapper-level is_ready methods (framework availability checks like "fastapi is not installed") are unchanged — only instrument-level migrates.

The framework instrument overrides (FastStreamOpenTelemetryInstrument, FastStreamPrometheusInstrument, LitestarSwaggerInstrument) carry # ty: ignore[invalid-method-override] because they narrow bootstrap_config to framework-specific types — LSP violation in static type but correct at runtime (each framework bootstrapper only calls them with its own config). Matches the existing covariant-narrowing pattern already accepted for bootstrap_config: field overrides.

Also opportunistically drops the dead import_checker.is_prometheus_client_installed conjunct in FastStreamPrometheusInstrument (covered by check_dependencies() — per audit finding DES-5).

Closes design docs/superpowers/specs/2026-06-01-instrument-skip-rework-design.md.

Test plan

  • just test — 150/150 passing.
  • just lint — clean (ruff, ty).
  • Existing test_*_bootstrapper_with_missing_instrument_dependency tests still pass — InstrumentDependencyMissingWarning continues to fire for the configured + dep-missing case.
  • New test_free_bootstrap_emits_summary_log pins the summary log behavior via capture_logs.
  • test_free_bootstrap_logging_disabled rewritten to assert on bootstrapper.skipped_instruments instead of pytest.warns(InstrumentNotReadyWarning).

Backward compatibility

  • InstrumentNotReadyWarning is publicly exported and hard-removed. User code importing it gets ImportError. 4 weeks old; acceptable churn.
  • is_ready instance method on instruments removed. User code calling instrument.is_ready() migrates to type(instrument).is_configured(config). Library-internal lifecycle; rare in user code.

🤖 Generated with Claude Code

…ethod + summary log

PR #86 introduced InstrumentNotReadyWarning, escalating the not-ready
skip path from a (silently dropped) logger.info call to a real
warnings.warn. The escalation surfaced expected-path events as
warnings: every service using a subset of available instruments emits
N warnings on bootstrap, and lite-bootstrap's own tests can't suppress
without breaking the warning assertions.

Replace is_ready (instance method, ran after instantiation) with
is_configured (classmethod taking config, runs before instantiation).
The bootstrapper now:

1. is_configured(config) False → silent skip; append to new
   bootstrapper.skipped_instruments: list[tuple[type, str]].
2. check_dependencies() False → InstrumentDependencyMissingWarning
   (kept; this is the genuine "configured but dep missing" deployment
   surprise).
3. Otherwise instantiate and register.

After the loop, emit one INFO-level summary log listing configured +
skipped instruments. Default Python logging suppresses INFO, so silent
by default with an opt-in path via logging.basicConfig.

PR #88's constraint (check_dependencies before instantiation, because
some instruments have default_factory that NameErrors when optional
extras aren't installed) is preserved: is_configured is a classmethod,
so it runs without instantiation.

Production-code subtlety: bootstrappers/base.py uses a fresh logger
per call via _get_logger() instead of a module-level cached one. The
module-level structlog.getLogger(__name__) interacts badly with
LoggingInstrument's cache_logger_on_first_use=True — the BoundLogger
gets cached with whatever processors were active at first use, and
structlog.testing.capture_logs() can't override the cache. The
fresh-per-call approach keeps the structlog pipeline reactive.

Removed:
- InstrumentNotReadyWarning class (hard removal; 4 weeks old; exports
  go too)
- BaseInstrument.is_ready instance method (replaced by is_configured)
- The _register_or_skip helper (flow inlined)
- The dead `import_checker.is_prometheus_client_installed` conjunct in
  FastStreamPrometheusInstrument.is_configured (covered by
  check_dependencies()).

Kept:
- InstrumentSkippedWarning (base for forward-compat)
- InstrumentDependencyMissingWarning (still useful for the genuine
  deployment surprise)
- not_ready_message class attribute (used in skipped_instruments
  tuples and the summary log)
- Bootstrapper-level is_ready methods (framework availability checks;
  unrelated)

Tests migrated to call XInstrument.is_configured(config) instead of
instrument.is_ready(). test_free_bootstrap_logging_disabled rewritten
to assert on bootstrapper.skipped_instruments. New
test_free_bootstrap_emits_summary_log pins the summary log behavior
via capture_logs.

Framework instrument overrides (FastStreamOpenTelemetryInstrument,
FastStreamPrometheusInstrument, LitestarSwaggerInstrument) need
`# ty: ignore[invalid-method-override]` because they narrow
bootstrap_config to framework-specific types — LSP violation in static
type but correct at runtime (each framework bootstrapper only calls
them with its own config).

22 files modified (15 production + 6 tests + 2 docs).

Closes design doc 2026-06-01-instrument-skip-rework-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

Flag Coverage Δ
unittests 100.00% <100.00%> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
lite_bootstrap/__init__.py 100.00% <ø> (ø)
lite_bootstrap/bootstrappers/base.py 100.00% <100.00%> (ø)
...bootstrap/bootstrappers/faststream_bootstrapper.py 100.00% <100.00%> (ø)
...e_bootstrap/bootstrappers/litestar_bootstrapper.py 100.00% <100.00%> (ø)
lite_bootstrap/exceptions.py 100.00% <ø> (ø)
lite_bootstrap/instruments/base.py 100.00% <100.00%> (ø)
lite_bootstrap/instruments/cors_instrument.py 100.00% <100.00%> (ø)
...e_bootstrap/instruments/healthchecks_instrument.py 100.00% <100.00%> (ø)
lite_bootstrap/instruments/logging_instrument.py 100.00% <100.00%> (ø)
..._bootstrap/instruments/opentelemetry_instrument.py 100.00% <100.00%> (ø)
... and 11 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lesnik512 lesnik512 force-pushed the refactor/instrument-skip-rework branch from 36e91e0 to f404d0a Compare June 2, 2026 06:57
lesnik512 added 2 commits June 2, 2026 10:37
Replace the _get_logger() structlog indirection with a module-level
logging.getLogger(__name__). The two log sites (INFO summary in __init__,
WARNING teardown error) describe bootstrapper lifecycle, not application
events, so stdlib logging is appropriate. Removes a global-structlog-state
landmine introduced by the previous fresh-per-call workaround.

Add BaseBootstrapper.build_summary() returning a multi-line human-readable
description of configured + skipped instruments. __init__ uses it for the
INFO summary; users can call it post-construction for debugging.

Migrate the two test sites that used structlog.testing.capture_logs() to
pytest's caplog fixture.
Asserts that constructing a bootstrapper with instruments whose config
indicates they should not run emits zero UserWarnings. Without this
regression test, a future change that re-introduces warnings.warn to the
is_configured=False branch would pass CI.
@lesnik512 lesnik512 self-assigned this Jun 2, 2026
lesnik512 added 4 commits June 2, 2026 10:53
CLAUDE.md and configuration.md now describe the build_summary() method and
the stdlib logging backend. The previous instrument-skip-rework plan gets a
supersession note pointing at the new design.
Add a one-liner to build_summary()'s docstring noting that calling it
before __init__ completes raises AttributeError — important context for
anyone wiring it to a health endpoint that might be invoked during a
bootstrapper construction failure path.

Tighten test_free_bootstrap_emits_summary_log to assert on "  configured:"
and "  skipped:" (with the two-space indent) so it matches the precision
of test_build_summary_format.
…ning leak

Add test_build_summary_renders_none_for_empty_sections to exercise the
(none) fallback for empty instruments and skipped_instruments lists,
closing the 1-line gap reported by coverage on bootstrappers/base.py:44.

Wrap the opentelemetry-missing construction in
test_faststream_bootstrap_without_opentelemetry with pytest.warns so the
expected InstrumentDependencyMissingWarning is captured at the call site
instead of leaking into the test session output.
…ocstring

Wrap the __init__ summary log in `if logger.isEnabledFor(logging.INFO)` so
build_summary() doesn't run when INFO is filtered. Bootstrap fires once
per service so the savings are negligible, but the guard is the
conventional stdlib pattern and matters more if a subclass overrides
build_summary() with something expensive.

Drop the "Raises AttributeError if called before __init__ completes" line
from the docstring — it documented an implementation detail rather than a
contract. The "post-construction debugging" phrasing in the prior sentence
already conveys the constraint.
@lesnik512 lesnik512 merged commit 5a76c7c into main Jun 2, 2026
7 checks passed
@lesnik512 lesnik512 deleted the refactor/instrument-skip-rework branch June 2, 2026 14:58
lesnik512 added a commit that referenced this pull request Jun 2, 2026
PR #107 (instrument skip rework) shipped a few days after the original
arc closed. Captured three new datapoints worth recording:

- Mid-design pivot worked: user pushback on the is_configured arg
  forced the design through pre-#88 history and confirmed the
  classmethod-with-arg pattern was correct.
- Defensive workaround was wrong design: my _get_logger() fresh-per-
  call structlog proxy made tests pass but the user's pivot to stdlib
  logging + public build_summary() was the actual right answer. caplog
  (which the original plan flagged but the subagent ignored) was the
  correct test mechanism.
- LSP violations on classmethod parameter overrides are an emergent
  pattern: 3 framework instrument overrides needed
  # ty: ignore[invalid-method-override] because they narrow
  bootstrap_config covariantly. Existing field-narrowing pattern is
  accepted; method-param narrowing now needs the explicit ignore.

Added action items #8 (workaround-vs-framework-choice heuristic), #9
(cap subagent dispatch scope; the ~60-minute connection drop orphaned
work), and #10 (document the LSP override pattern in CLAUDE.md).

Reaffirms the original retro's closing observation: subagent loop
reliably produces a green-tests implementation of the spec, but the
spec is rarely the right design. Design emerges during review
iteration. PR #107 needed 5 user follow-up commits after the
mechanical migration landed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lesnik512 added a commit that referenced this pull request Jun 2, 2026
`just test` previously reported coverage as informational only; a
regression to 99% (or any non-100% value) would still exit 0 and
nothing flagged it. The PR #107 implementer's coverage drop to 99%
slipped past `just test` and surfaced only when the user manually
reviewed the totals.

Match faststream-outbox's pytest config: append `--cov-fail-under=100`
to addopts so any drop below 100% fails the test run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant