Skip to content

build(metrics)!: make prometheus-client an optional extra - #1235

Open
daavoo wants to merge 1 commit into
mainfrom
refactor/metrics-optional-dependency
Open

daavoo wants to merge 1 commit into
mainfrom
refactor/metrics-optional-dependency

Conversation

@daavoo

@daavoo daavoo commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Description

Rebased onto main after #1205 landed, which is what this draft said it was
waiting for. The ! in the title is still a real decision for a maintainer to
confirm rather than mine to make.

Prometheus stops being something every deployment pays for. prometheus-client
is a core dependency today, imported on every startup whether or not anything
scrapes: enable_metrics gates only the request middleware and the /metrics
route, so a gateway that never scrapes still pays the import, registers a
process collector, and keeps counters incrementing that nothing can read.

Measured on this branch: the import is 32ms and 138 modules, of which
prometheus_client.exposition is 25ms. It pulls in http.server,
wsgiref.simple_server, socketserver, socket, select and the whole
email package, none of which anything else here needs. Against a 2.66s
import gateway.main the time is about 1.2%, so the case is the dependency
surface (a process that never scrapes currently imports a WSGI server and an
email parser) rather than the milliseconds.

It becomes a metrics extra, alongside the existing ocr and s3 ones.
#1205 is what makes the seam clean: with every metric now declared beside the
code that increments it, gateway/metrics.py is purely the re-export point
those declarations go through, so intercepting it there covers all of them.
It re-exports the metric types, real when the extra is installed and no-op
stand-ins when it is not. The declarations still run and every recorder stays
callable either way, so nothing on a hot path needs a guard and no call site
changes. Types come from the real library under TYPE_CHECKING, which the dev
group installs unconditionally, so mypy checks every declaration against the
real signatures regardless of what the runtime environment has.

The fallback covers the custom-collector surface too, which #1205 introduced:
Collector stays subclassable and CollectorRegistry.register stays callable,
because gateway/core/database.py registers a pool collector at import time.

Setting enable_metrics without the extra refuses to start, with the install
hint. That is the one case worth failing on: registering the scrape on top of
no-op metrics would answer with an empty body, which reads as a broken exporter
rather than a missing install.

The Docker image installs the extra (--extra metrics), so image-based and
otari.ai deployments are unaffected and a scrape stays one config flag away. A
source install that sets enable_metrics needs pip install gateway[metrics].

How to test it locally

make lint && make typecheck
uv run pytest tests/unit/test_metrics_optional_dependency.py tests/unit/test_gateway_metrics.py

tests/unit/test_gateway_metrics.py is unchanged and still pins the exposed
families, so the present case is covered by what already covered it. The new
file covers the absent case: it blocks prometheus_client in a subprocess and
asserts the module imports, that every recorder across the six modules that now
declare metrics no-ops, that labels() chains, that the pool collector still
subclasses and registers, and that the scrape body is empty, plus the three
startup outcomes (off and absent starts, on and absent refuses, on and present
starts).

To see the real absent case rather than the simulated one, which is what the OSS
edition smoke gate installs:

uv sync --frozen --no-dev        # no extras, so prometheus-client is absent
uv run --frozen --no-dev python -c "
import gateway.main
from gateway.core.config import GatewayConfig
from gateway.metrics import PROMETHEUS_AVAILABLE
print(PROMETHEUS_AVAILABLE)
gateway.main.create_app(GatewayConfig())
gateway.main._validate_metrics_support(GatewayConfig(enable_metrics=True))
"
uv sync --frozen                 # restore the dev environment

Confirmed on the rebase: False, the app builds, and the last line raises with
pip install gateway[metrics]. uv run --frozen --no-dev python scripts/oss_edition_smoke.py also passes end to end in that same no-extras
environment.

Run locally: make lint, make typecheck, the OSS edition smoke gate, and the
full tests/unit suite. The integration suite was left to CI; nothing here
touches a route, a model or a migration.

Notes for review

  • The lock diff is hand-written on purpose. uv lock on this machine
    (uv 0.11.7) rewrites ~1000 lines of uv.lock, adding sys_platform markers
    across every package, none of it related to this change. The lines this
    change actually needs were applied by hand instead, and uv lock --check
    accepts the result. Worth a second pair of eyes, and worth knowing that
    whoever next runs uv lock here will produce that churn.
  • The OSS edition smoke gate becomes the test for the fallback. It runs
    uv sync --frozen --no-dev with no extras, so from here on it boots and
    serves a completion with prometheus absent. The no-op branch cannot rot into
    dead code.
  • gateway[metrics], not otari[metrics]. The distribution name is
    gateway, which is what docs/files.md already uses for the ocr extra.
    Noting in passing that S3FileStore says pip install otari[s3], which looks
    wrong for the same reason; left alone as out of scope.
  • Deliberately not done: gating the instrumentation on enable_metrics,
    so that counters stop incrementing when it is off. The metric objects are
    module-level and built at import time, and the import chain reaches them
    before config is loaded, so that would need an indirection layer at every
    declaration and call site. The import is the cost worth removing; an increment
    on an in-memory float is not.
  • Changelog visibility: ^build and ^refactor are both skipped in
    cliff.toml, but protect_breaking_commits = true, so the ! is what keeps
    this in the release notes. It should be there, since an upgrade can refuse to
    start. If a maintainer would rather not mark it breaking, it needs a different
    type to stay visible.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Follow-up to #1205 (#1177), now merged. Discussed there; no issue filed yet.

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test). Lint, typecheck, the OSS edition smoke gate and the full unit suite were run; the integration suite was left to CI.
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec. No API contract change; no generated artifact is affected.

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

AI Model/Tool used: Claude Code (Claude Opus 5)

Any additional AI details you'd like to share: Opened as a reference draft at
the request of the repository owner, to accompany the packaging comment on #1205.
The agent wrote the seam, the validation, the tests and the docs, ran lint,
typecheck, the smoke gate and the unit suite, and verified the absent case
against a real runtime-only environment rather than only the subprocess
simulation.

  • I am an AI Agent filling out this form (check box if true)

🤖 Generated with Claude Code

Summary

  • Made prometheus-client an optional metrics extra.
  • Added no-op metric implementations for installations without Prometheus support.
  • Added startup validation when enable_metrics is enabled without the extra.
  • Kept Prometheus support enabled in Docker images.
  • Updated documentation, dependency locks, and tests.
  • Added coverage for fallback metrics, collector registration, startup validation, and scrape output.

Source installations that enable metrics must now install gateway[metrics].

@daavoo
daavoo deployed to integration-tests September 16, 2026 13:31 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 395b961f-f6a1-4944-a655-f9a12f1949da

📥 Commits

Reviewing files that changed from the base of the PR and between 8d80407 and 3a51798.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !**/uv.lock
📒 Files selected for processing (8)
  • Dockerfile
  • docs/configuration.md
  • docs/dashboard.md
  • docs/deployment.md
  • pyproject.toml
  • src/gateway/main.py
  • src/gateway/metrics.py
  • tests/unit/test_metrics_optional_dependency.py

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


Walkthrough

Prometheus support is now optional. The application provides no-op metrics objects when the dependency is absent and refuses startup when metrics are enabled without it. The Docker image installs the metrics extra, and documentation describes the requirement.

Changes

Optional metrics support

Layer / File(s) Summary
Optional dependency and runtime fallback
pyproject.toml, src/gateway/metrics.py, tests/unit/test_metrics_optional_dependency.py
prometheus-client is defined in the metrics extra and dev dependencies. Runtime imports expose availability and provide callable no-op implementations when Prometheus is absent. Tests cover both dependency states.
Startup validation for enabled metrics
src/gateway/main.py, tests/unit/test_metrics_optional_dependency.py
create_app validates metrics support. Enabled metrics raise a ValueError without Prometheus; disabled metrics continue startup.
Docker installation and deployment documentation
Dockerfile, docs/configuration.md, docs/dashboard.md, docs/deployment.md
The Docker builder installs the metrics extra. Documentation describes the installation requirement and startup behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Refactor

Merge Risk: ⚪ Minimal · up to 3a517

The optional metrics installation and startup behavior are consistent across packaging, Docker, and the documented configuration paths. This is ready to merge.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title clearly describes the change and uses imperative mood, but it starts with the unsupported build: prefix. The requirements allow feat:, fix:, docs:, refactor:, chore:, test:, `p… Rename the title with an allowed prefix, such as refactor!: make prometheus-client an optional extra. Resource the breaking-change marker if the maintainer confirms it is required.
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 3 files. (5 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and directly covers the change, testing steps, PR type, related issue context, checklist status, documentation updates, and AI usage. The unchecked full Definition of Done …
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title clearly describes the change and uses imperative mood, but it starts with the unsupported build: prefix. The requirements allow feat:, fix:, docs:, refactor:, chore:, test:, perf:, or ci:.

Full details: Docstring Coverage

Explanation

Docstring coverage is 19.05% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 3 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@daavoo
daavoo force-pushed the refactor/metrics-optional-dependency branch from ed70f94 to d157da5 Compare September 16, 2026 13:52
@daavoo
daavoo requested a review from peteski22 September 16, 2026 13:52
@daavoo
daavoo deployed to integration-tests September 16, 2026 13:52 — with GitHub Actions Active
@daavoo
daavoo force-pushed the refactor/metrics-optional-dependency branch from d157da5 to 1f1e552 Compare September 18, 2026 08:56
@daavoo
daavoo deployed to integration-tests September 18, 2026 08:56 — with GitHub Actions Active
@daavoo
daavoo deployed to integration-tests September 18, 2026 08:56 — with GitHub Actions Active
@daavoo
daavoo deployed to integration-tests September 18, 2026 08:56 — with GitHub Actions Active
@daavoo
daavoo deployed to integration-tests September 18, 2026 08:56 — with GitHub Actions Active
@daavoo daavoo self-assigned this Sep 18, 2026
@daavoo
daavoo marked this pull request as ready for review September 18, 2026 08:59
prometheus-client was a core dependency imported on every startup, whether or
not anything scraped. enable_metrics gates only the middleware and the /metrics
route, so a deployment that never scrapes still paid the import, the
ProcessCollector registration and 14 always-incrementing counters nothing could
read. Most of the import cost is prometheus_client.exposition pulling in
http.server and wsgiref.simple_server, which nothing else here needs.

It moves to a metrics extra. gateway/metrics.py becomes the seam: it re-exports
Counter, Gauge, Histogram, CollectorRegistry, ProcessCollector and
generate_latest, real when the extra is installed and no-op stands-in when it is
not, so the declarations and the record_* helpers stay callable either way and
no call site needs a guard. Types are taken from the real library under
TYPE_CHECKING, which the dev group installs, so mypy checks the declarations
against it regardless of the runtime environment.

enable_metrics with the extra absent refuses to start rather than registering a
scrape that answers with an empty body, which would read as a broken exporter
rather than a missing install.

BREAKING CHANGE: a source install that sets enable_metrics must now install the
metrics extra (pip install gateway[metrics]) or the gateway refuses to start.
The Docker image installs it, so image-based deployments are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@daavoo
daavoo force-pushed the refactor/metrics-optional-dependency branch from 1f1e552 to 3a51798 Compare September 18, 2026 09:07
@daavoo
daavoo deployed to integration-tests September 18, 2026 09:07 — with GitHub Actions Active
@daavoo
daavoo deployed to integration-tests September 18, 2026 09:07 — with GitHub Actions Active
@daavoo
daavoo deployed to integration-tests September 18, 2026 09:07 — with GitHub Actions Active
@daavoo
daavoo deployed to integration-tests September 18, 2026 09:07 — with GitHub Actions Active
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