Skip to content

fix(observability): wire logging, tracing spans, and FastAPI instrumentation (#64)#81

Merged
JumpTechCode merged 1 commit into
mainfrom
fix/observability-wiring
Jun 16, 2026
Merged

fix(observability): wire logging, tracing spans, and FastAPI instrumentation (#64)#81
JumpTechCode merged 1 commit into
mainfrom
fix/observability-wiring

Conversation

@JumpTechCode

Copy link
Copy Markdown
Collaborator

Closes #64.

The OpenTelemetry + structlog observability story was exported but inert at runtime. A TracerProvider existed but nothing created spans, and structured logging never activated.

What was broken

  • The lifespan called configure_tracing(settings) but never configure_logging → runtime logging fell back to bare stdlib, structlog never configured.
  • span_for_fetcher / span_for_llm had zero call sites → no enrichment or LLM spans ever emitted.
  • opentelemetry-instrumentation-fastapi was a declared dependency, but FastAPIInstrumentor was never imported → no request spans.

What changed

  • configure_observability(settings) — a single process-level entry point (logging + tracing) in sentinel/observability/__init__.py; the lifespan calls it instead of configure_tracing alone, so the "observability is wired" contract has one place to set up and one to test.
  • FastAPIInstrumentor.instrument_app(app) in build_app() — per-app (not the global .instrument()) and idempotent across the many test-fixture builds → a server span per request.
  • Real span call sitesspan_for_fetcher wraps each fetcher run via a thin _run → _run_guarded split that leaves the tested fetch/timeout/breaker/coerce logic untouched; span_for_llm wraps the whole diagnose_call (end-to-end LLM latency incl. timeout + retry).

Tests

  • configure_observability wires both logging and tracing (and runs for real without error).
  • build_app() instruments the app (_is_instrumented_by_opentelemetry).
  • An in-memory span exporter proves one span per fetcher and one per LLM call.
  • The lifespan-wiring test now asserts configure_observability is called — guards the exact gh#64 regression.

505 unit tests pass, 84.84% coverage; ruff + mypy --strict clean. Senior code-review pass: no Critical/Important findings.

🤖 Generated with Claude Code

…ntation (#64)

The OpenTelemetry + structlog story was exported but inert at runtime: the
lifespan called configure_tracing but never configure_logging, the span helpers
had zero call sites, and FastAPIInstrumentor was a declared dependency that was
never imported. A TracerProvider existed but nothing made spans, and structured
logging never activated.

- Add configure_observability(settings) as the single process-level entry point
  (logging + tracing); the lifespan calls it instead of configure_tracing alone.
- Instrument the app: FastAPIInstrumentor.instrument_app(app) in build_app()
  (per-app, idempotent across the many test-fixture builds) → a server span per
  request.
- Give the span helpers real call sites: span_for_fetcher wraps each fetcher run
  (via a thin _run → _run_guarded split that leaves the tested fetch/timeout/
  breaker logic untouched), span_for_llm wraps the whole diagnose_call.

Tests: configure_observability wires both; build_app instruments the app; an
in-memory span exporter proves a span per fetcher and one per LLM call; and the
lifespan-wiring test now asserts configure_observability is called (guards the
exact gh#64 regression). 505 unit tests pass, mypy --strict + ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JumpTechCode
JumpTechCode merged commit a1f43dc into main Jun 16, 2026
6 checks passed
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.

Observability: configure_logging / span helpers / FastAPIInstrumentor exported but never wired at runtime

1 participant