Skip to main content

metrics-python

Generic set of metrics for Python applications.

Labels

Common labels like app, env, cluster, component, role, etc. is added to the metrics using the scrape config. Adding these metrics is not a responsibility we have in the metrics-python package.

Monitoring of periodic and cron jobs

metrics-python is a utility library that leverages the prometheus-client to gather and present metrics for Prometheus. As Prometheus operates on a pull-based model rather than a push-based one, collecting metrics from short-lived jobs can be problematic.

While the push-gateway component addresses this issue, it comes with its own set of drawbacks.

Monitoring cron jobs using Prometheus and AlertManager poses additional challenges due to the inability to interpret cron expressions in PromQL.

As a result, metrics-python does not support periodic or cron job monitoring.

Application info

Some properties from the application is not added as metric labels by default by the scrape config. One example is the application version. metrics-python has a util to expose labels like this to Prometheus.

from metrics_python.generics.info import expose_application_info

expose_application_info(version="your-application-version")

Exposing metrics

metrics-python can serve a Prometheus endpoint from a background thread.

from metrics_python.prometheus import start_prometheus_background_server

start_prometheus_background_server()
Environment variable Default Effect
PROMETHEUS_ENABLED false Nothing is served unless this is true.
PROMETHEUS_METRICS_PORT 8001 Port to listen on. The port argument takes precedence.
PROMETHEUS_MULTIPROC_DIR unset Directory prometheus-client writes its per process files to. Required for multiprocess mode.

Multiprocess mode

Applications that fork, which both gunicorn and celery do, have to run prometheus-client in multiprocess mode. Every process writes its own files into PROMETHEUS_MULTIPROC_DIR, and the endpoint merges them when scraped. Without it each worker keeps its own counters and a scrape returns whichever worker happened to answer.

Point PROMETHEUS_MULTIPROC_DIR at a writable directory and pass multiprocess=True:

start_prometheus_background_server(multiprocess=True)

Files left behind by a previous run are removed on startup, they would otherwise be merged into the exposition.

Three metrics depend on multiprocess mode being set up correctly:

Metric Mode
generics_workers_workers_by_state livesum
generics_info_application_version livemostrecent
celery_task_last_execution mostrecent

The live modes stop counting a process once its file has been removed, which is what mark_process_dead does. See the Gunicorn section for where to call it.

expose_application_info also writes the application_version gauge for this reason: the Info metric it populates is not supported in multiprocess mode.

The collectors argument registers extra collectors on the same registry. They must not use the regular metric classes, prometheus-client would then export each value twice, once from the collector and once from the multiprocess directory.

Configuration

Everything is configured with environment variables. Metrics are created when the module defining them is imported, and the documented setup calls patch_caching() and setup_celery_metrics() from inside settings.py, so Django settings are not available yet at that point.

A variable starting with METRICS_PYTHON_ that is not recognised is reported with a warning at startup. A misspelled flag would otherwise silently do nothing, which is the worst outcome for a switch meant to keep costs down.

Enabling and disabling metrics

Each metric has a flag, named after the exported metric without the metrics_python prefix and without the unit suffix. So metrics_python_django_signal_duration_seconds is:

METRICS_PYTHON_DJANGO_SIGNAL_DURATION_ENABLED=false

A disabled metric is never registered, so it costs nothing, and the work needed to produce its value is skipped rather than thrown away.

These four are disabled by default, because nothing we could find reads them and they are not free to produce:

Metric Why
graphql_lifecycle_step_duration Times every parse, validation and resolver.
celery_task_last_execution A mostrecent gauge, awkward in multiprocess mode.
django_view_duplicate_query_count Walks the stack on every query.
django_celery_duplicate_query_count Walks the stack on every query.

Turn one on when you need it:

METRICS_PYTHON_DJANGO_VIEW_DUPLICATE_QUERY_COUNT_ENABLED=true

Before disabling a metric, check what reads it. A dashboard panel goes blank, but a recording rule that selects the metric will keep evaluating and produce a wrong result, which is easy to miss when it feeds an SLO.

Duplicate queries

Duplicate query detection reports queries that were executed more than once from the same place in the code. It is off by default, see the table above.

Set METRICS_PYTHON_PRINT_DUPLICATE_QUERIES=true to also print each duplicate and the stack that produced it to stdout. That implies counting them, so one variable is enough when debugging locally.

Histogram buckets

Every histogram uses the prometheus-client default buckets. Buckets are the main lever on how many time series the library produces: a histogram costs one series per bucket per label combination, plus _sum and _count.

METRICS_PYTHON_DJANGO_MIDDLEWARE_DURATION_BUCKETS="0.001,0.005,0.025,0.1"

METRICS_PYTHON_DEFAULT_BUCKETS applies to every histogram without its own override. The value is a comma separated list of increasing upper bounds in the metric's unit; the +Inf bucket is added automatically. A value that cannot be parsed is ignored with a warning, so a typo does not stop the application from starting.

Two things to keep in mind before changing buckets:

  • Every replica of a deployment has to resolve the same buckets. Prometheus aggregates a histogram by summing over le, and replicas that disagree produce a histogram that cannot be aggregated.
  • Dashboards and recording rules select individual le values. Removing a bucket that a query pins makes that query return nothing. Check what is selecting the metric before narrowing its buckets.

ASGI

metrics-python contains an ASGI middleware to measure request/response durations and sizes.

Starlette

from starlette.middleware import Middleware
from metrics_python.asgi import ASGIMiddleware

app = Starlette(
    middleware=[Middleware(ASGIMiddleware)]
)

fastapi

from metrics_python.asgi import ASGIMiddleware

app = FastAPI()
app.add_middleware(ASGIMiddleware)

Django

Cache

Cache metrics can be observed by adding patch_caching() to your settings file.

from metrics_python.django.cache import patch_caching

patch_caching()

Middleware

The execution of middlewares can be observed by adding patch_middlewares() to your settings file.

from metrics_python.django.middleware import patch_middlewares

patch_middlewares()

Signals

The execution of signals can be observed by adding patch_signals() to your settings file.

from metrics_python.django.signals import patch_signals

patch_signals()

Views

View processing, request and response sizes can be measured using the MetricsMiddleware.

MIDDLEWARE = [
    ...
    # It is important to place the MetricsMiddleware before the CommonMiddleware.
    "metrics_python.django.middleware.MetricsMiddleware",
    "django.middleware.common.CommonMiddleware",
]

Query count and duration in views

Database query count, duration, and duplicate queries can be observed by adding the QueryCountMiddleware. Add the middleware as early as possible in the list of middlewares to observe queries executed by other middlewares.

MIDDLEWARE = [
    ...
    "metrics_python.django.middleware.QueryCountMiddleware",
]

Duplicate query detection is off by default, it walks the stack on every query, see Duplicate queries for how to turn it on.

Query count and duration in Celery tasks

Database metrics can also be observed in Celery. Execute setup_celery_database_metrics bellow setup_celery_metrics, look into the Celery section of this document for more information.

from metrics_python.django.celery import setup_celery_database_metrics

setup_celery_database_metrics()

Postgres database connection metrics

The get_new_connection and init_connection_state methods in the PostgreSQL database connection engine can be observed by using a custom connection engine from metrics-python.

DATABASES = {
    "default": {
        "ENGINE": 'metrics_python.django.postgres_engine',
        ...
    }
}

Celery

To setup Celery monitoring, import and execute setup_celery_metrics as early as possible in your application to connect Celery signals. This is usually done in the settings.py file in Django applications.

from metrics_python.celery import setup_celery_metrics

setup_celery_metrics()

django-api-decorator

To measure request durations to views served by django-api-decorator, add the DjangoAPIDecoratorMetricsMiddleware.

MIDDLEWARE = [
    ...
    "metrics_python.django_api_decorator.DjangoAPIDecoratorMetricsMiddleware",
]

django-ninja

To measure request durations to views served by django-ninja, add the DjangoNinjaMetricsMiddleware.

MIDDLEWARE = [
    ...
    "metrics_python.django_ninja.DjangoNinjaMetricsMiddleware",
]

GraphQL

Strawberry

The Prometheus extension needs to be added to the schema to instrument GraphQL operations.

import strawberry
from metrics_python.graphql.strawberry import PrometheusExtension

schema = strawberry.Schema(
    Query,
    extensions=[
        PrometheusExtension,
    ],
)

Graphene

metrics-python has a Graphene middleware to instrument GraphQL operations. Add the middleware to Graphene by changing the GRAPHENE config in settings.py.

GRAPHENE = {
    ...
    "MIDDLEWARE": ["metrics_python.graphql.graphene.MetricsMiddleware"],
}

Gunicorn

To setup Gunicorn monitoring, add the Prometheus logger (to measure request durations) and add the worker state signals to the gunicorn config.

from typing import Any

from metrics_python.generics.workers import export_worker_busy_state
from metrics_python.prometheus import mark_process_dead

logger_class = "metrics_python.gunicorn.Prometheus"


def pre_request(worker: Any, req: Any) -> None:
    export_worker_busy_state(worker_type="gunicorn", busy=True)


def post_request(worker: Any, req: Any, environ: Any, resp: Any) -> None:
    export_worker_busy_state(worker_type="gunicorn", busy=False)


def post_fork(server: Any, worker: Any) -> None:
    export_worker_busy_state(worker_type="gunicorn", busy=False)


def child_exit(server: Any, worker: Any) -> None:
    mark_process_dead(worker.pid)

Worker cleanup in multiprocess mode

child_exit only matters when running in multiprocess mode. The worker state gauge uses livesum, and prometheus-client has no way to tell that a process has gone: mark_process_dead removing the worker's file is what makes it stop counting. Without the hook a worker that exits leaves its last busy or idle value behind, and the gauge counts workers that no longer exist.

The mark_process_dead from metrics-python does nothing when PROMETHEUS_MULTIPROC_DIR is unset, so the hook is safe to add either way.

This only shows up when workers are replaced while the application is running, for example with --max-requests. If your workers live as long as the process, nothing accumulates and the gauge is correct either way.

child_exit runs in the master process after a worker exits, which is the hook prometheus-client documents for this.

Release new version

We use release-please from Google to relese new versions, this is done automatically.

Metadata

Release files for metrics-python 0.5.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for metrics-python 0.5.0
File Size Uploaded
metrics_python-0.5.0.tar.gz 105.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for metrics-python 0.5.0
File Interpreter ABI Platform
metrics_python-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 159.1 kB

Release files / metrics_python-0.5.0.tar.gz

Download URL metrics_python-0.5.0.tar.gz
Size 105.9 kB
Tags Source
SHA-256 checksum
How to use checksums
bf8471623fd312a35fa718fb84b4b7dba2f6309577143fac314c88a558c741d4
BLAKE2b-256 checksum
How to use checksums
7f1666a724347c300d0356dfb5252653b3c7904e1d4acae15c84e276884f8094
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.7.1 CPython/3.12.3 Linux/6.17.0-1022-azure

Release files / metrics_python-0.5.0-py3-none-any.whl

Download URL metrics_python-0.5.0-py3-none-any.whl
Size 53.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f9cd7f28ecdf4263488af71749d07aa33e14528a9075fd468aeb655e6cb31219
BLAKE2b-256 checksum
How to use checksums
9ea5d7184908fb38a6767d257cd273d040d7a744ecfcbb85723dc57d58f360c7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via poetry/1.7.1 CPython/3.12.3 Linux/6.17.0-1022-azure

Release history Release notifications | RSS feed

This release

0.5.0 This release

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.1

2 release files

0.3.0

2 release files

0.2.4

2 release files

0.2.3

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

0.0.20

2 release files

0.0.18

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page