Skip to content

feat: ship PrometheusRedisMetricsProvider and a cached get_redis_metrics - #37

Merged
AlexeyShalaev merged 2 commits into
masterfrom
feat/prometheus-redis-metrics-provider
Sep 15, 2026
Merged

AlexeyShalaev merged 2 commits into
masterfrom
feat/prometheus-redis-metrics-provider

Conversation

@AlexeyShalaev

Copy link
Copy Markdown
Member

Closes #36.

Problem

AsyncRedisProvider reads RedisMetricsProtocol | None and RedisMetrics implements it, but the kit ships nothing that provides the one from the other, so every consumer carries the same dishka provider. And RedisMetrics(prefix=None) twice raises Prometheus's duplicated-timeseries error, so a container rebuilt per test cannot ask for the collector twice.

What ships

Two feat commits.

redis_client_kit.metrics.get_redis_metrics(prefix=None) — one RedisMetrics per prefix on the default registry, cached, the shape of grpc-client-kit's get_grpc_client_metrics. "" and None are the same unprefixed instance, because RedisMetrics treats them alike. RedisMetrics itself is unchanged: building two by hand still raises, and the docstring and the docs say that is Prometheus and point at the getter.

redis_client_kit.providers.PrometheusRedisMetricsProvider(*, prefix=None)Scope.APP, requests RedisSettingsProtocol, provides RedisMetricsProtocol | None as get_redis_metrics(prefix) when settings.metrics_enabled and None otherwise, so it drops into AsyncRedisProvider(provide_default_metrics=False) (or after AsyncRedisProvider()) and the client is instrumented exactly when the settings say so. RedisSettingsProtocol gains metrics_enabled: bool for that; BaseRedisSettings has carried the field from the start, a hand-written settings class needs the one attribute, and the configuration guide's example now has it. With metrics_enabled on and the metrics extra missing, resolving the collector raises the ImportError naming the extra, the way every other extra here fails.

Rejected

  • The reporter's PrometheusMetricsSettingsProtocol + default_prefix keys (sqlalchemy-foundation-kit's shape). Those are the application's keys, and a kit provider that requests them makes every consumer provide them. The prefix is a constructor argument, as in grpc-client-kit.
  • A warning and None when the extra is missing (grpc-client-kit's choice). Silent in production; this kit fails loud on a missing extra everywhere else.
  • A registry= parameter on RedisMetrics. Nothing here needs it.

Tests

  • Getter: the same prefix twice is the same object; different prefixes differ; "" is None; RedisMetrics twice raises.
  • Provider, on the reporter's container (AsyncRedisProvider(provide_default_metrics=False), PrometheusRedisMetricsProvider()): metrics off gives None and a plain Redis; metrics on gives InstrumentedRedis whose _metrics is get_redis_metrics(); prefix= names the series; two containers in a row hand out the same collector; registered after AsyncRedisProvider() it wins; a missing extra raises ImportError naming it.
  • test_metrics_init.py plants its mocked modules through monkeypatch now, so they leave sys.modules after each test instead of staying there for the rest of the session (they fed the provider's lazy import a MagicMock).

Gate: uv run ruff check ., uv run ruff format --check ., uv run mypy redis_client_kit, uv run pytest -m unit — 160 passed, unit coverage 99.66%.

Docs: advanced guide (metrics and Dishka sections), configuration guide, agents page, README.

Prometheus registers a metric name once per registry, so the second
RedisMetrics(prefix=None) in a process raises "Duplicated timeseries in
CollectorRegistry" -- what a test suite runs into when it rebuilds its
container per test. get_redis_metrics(prefix=None) caches one instance per
prefix on the default registry and hands the same one back afterwards, the
shape grpc-client-kit's get_grpc_client_metrics has; "" and None are the
same unprefixed instance, since RedisMetrics treats them alike.

RedisMetrics itself is unchanged: building two by hand still raises, and
the docstring and the docs now say so and point at the getter.
AsyncRedisProvider reads RedisMetricsProtocol | None and RedisMetrics
implements it, but nothing in the kit provided the one from the other, so
every consumer carried the same dishka provider. PrometheusRedisMetricsProvider
(*, prefix=None) is that join: Scope.APP, requests RedisSettingsProtocol,
provides RedisMetricsProtocol | None as get_redis_metrics(prefix) when
settings.metrics_enabled and None otherwise -- so it drops into
AsyncRedisProvider(provide_default_metrics=False) and the client is
instrumented exactly when the settings say so, and a container rebuilt per
test hands out the same collector instead of registering the series twice.

RedisSettingsProtocol gains metrics_enabled: bool for that. BaseRedisSettings
has carried the field from the start; a hand-written settings class needs
the one attribute, and the configuration guide's example now has it. With
metrics_enabled on and the metrics extra missing, resolving the collector
raises the ImportError naming the extra, the way every other extra here fails.

test_metrics_init.py now plants its mocked modules through monkeypatch, so
they leave sys.modules after each test instead of staying there for the rest
of the session and feeding the provider's lazy import a MagicMock.
@AlexeyShalaev
AlexeyShalaev merged commit fbe8feb into master Sep 15, 2026
6 checks passed
@AlexeyShalaev
AlexeyShalaev deleted the feat/prometheus-redis-metrics-provider branch September 15, 2026 12:31
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.

RedisMetrics and AsyncRedisProvider both ship, but nothing joins them: every consumer writes the same dishka provider

1 participant