Metrics

Track custom metrics in your Python application

Encore.py has built-in support for custom metrics: counters, up-down counters, gauges and histograms. You declare a metric in code and record to it where the thing it measures happens. Encore exports it with the rest of your app's telemetry, and you can view it locally with encore metrics and in the Encore Cloud dashboard.

See the platform metrics documentation for exporting metrics to third-party services such as Grafana Cloud and Datadog.

Defining a metric

Declare a metric as a module-level variable, then call its methods to record:

orders/metrics.py
from encore import metrics orders_placed = metrics.Counter("orders_placed", description="Orders placed.")
orders/service.py
from orders.metrics import orders_placed async def place_order(order) -> None: ... orders_placed.increment()

Metric types

TypeMeasuresMethods
CounterA total that only goes up, such as requests handled or bytes processed.increment(), add(amount)
UpDownCounterA total that goes up and down, such as requests in flight or jobs queued.increment(), decrement(), add(amount)
GaugeWhat something is right now, such as a temperature or a pool's size.set(value)
HistogramHow values are distributed, such as request durations or upload sizes.record(value)
from encore import metrics orders = metrics.Counter("orders_placed", description="Orders placed.") queued = metrics.UpDownCounter("jobs_queued", unit="{job}") oven = metrics.Gauge("oven_temperature", unit="Cel") latency = metrics.Histogram("order_latency", unit="s", buckets=[0.1, 1, 10]) orders.increment() queued.add(12) oven.set(180.5) latency.record(0.25)

A counter only goes up: add ignores negative amounts. Its value resets when the process restarts.

Every metric takes these keyword-only options:

  • unit: the unit, as it should be exported. UCUM is the convention: "s", "ms", "By", "{request}".
  • description: what the metric measures.
  • buckets (histograms only): the upper bounds of the histogram's buckets, strictly increasing. A value is counted in the first bucket it's no greater than, or in the overflow bucket after the last. Without buckets, OpenTelemetry's default bounds are used.

Rules for declaring metrics

Encore reads metric declarations from the source, so:

  • Declare metrics at module level.
  • The name must be a string literal: a letter, then letters, digits, _, ., - and /, at most 255 characters.
  • Pass the name as the only positional argument, and unit, description and buckets as keyword arguments with literal values: buckets=[0.1, 1, 10], not a variable.
  • Use a metric by calling its methods. Encore records which services record to which metrics from these calls, so it rejects passing the metric itself around. Use a reference instead.

Scopes

The class you declare a metric with decides who records to it, and how it's named when exported:

ClassesBelongs toExported as
Counter, UpDownCounter, Gauge, HistogramThe service that declares it, which alone records to it. Must be declared inside a service.<service>.<name>
SharedCounter, SharedUpDownCounter, SharedGauge, SharedHistogramThe app, shared between services. Encore records each service that records to it.<name>
GlobalCounter, GlobalUpDownCounter, GlobalGauge, GlobalHistogramThe whole app. Any code may record to it, including libraries outside every service, and who does isn't recorded.<name>

For example, metrics.Counter("orders_placed") declared in the shop service is exported as shop.orders_placed. A service's metric names are unique within the service; shared and global metric names are unique within the app. A name can't be one of the metrics Encore records itself, such as http.server.request.duration.

from encore import metrics # Shared between the services that record to it. cache_hits = metrics.SharedCounter("cache.hits", unit="{hit}")

Declare each shared or global metric once in the app. Code in other modules records to it by importing the declaration.

Binding a metric declared elsewhere

To record to a shared or global metric declared in another language, or one you don't want to import, bind it by name with named on the class of its kind:

from encore import metrics cache_hits = metrics.Counter.named("cache.hits")

For a group, give its label type too: metrics.CounterGroup[Outcome].named("responses_total"). named takes the metric's name alone. The metric keeps the unit, description and buckets of its declaration, and its name isn't prefixed with a service's. Like a declaration, the binding must be a module-level variable and the name a string literal, and Encore checks that a shared or global metric of that kind and labels exists. named is available on Counter, UpDownCounter, Gauge, Histogram and their groups.

Labels

A group records one series for each combination of its labels. Define the labels as a TypedDict or a dataclass, and declare the metric with the group form of its class, CounterGroup, UpDownCounterGroup, GaugeGroup or HistogramGroup (or their Shared… and Global… versions):

from typing import TypedDict from encore import metrics class Outcome(TypedDict): method: str status: int requests_handled = metrics.CounterGroup[Outcome]("requests_handled") requests_handled.with_labels({"method": "GET", "status": 200}).increment()

Or with a dataclass:

from dataclasses import dataclass from encore import metrics @dataclass(frozen=True) class Route: route: str request_latency = metrics.HistogramGroup[Route]("request_latency", unit="s") request_latency.with_labels(Route("/orders")).record(0.025)

with_labels returns the metric for those labels, with the same methods as an unlabeled metric. Keep it in a variable where you record to it often.

  • Name the label type directly in the subscript, CounterGroup[Outcome], so Encore can read it.
  • Label values are str, int and bool. Any other value raises TypeError.
  • Label keys are exported in snake_case: a key routeName becomes route_name.
Take care

Every distinct combination of label values is a time series of its own, tracked in memory and stored by the monitoring system. Use labels for a handful of values, such as a status or a region, never for something like a user ID. Keep the number of series per metric to tens or hundreds, not thousands.

Passing metrics to other code

To hand a metric to other code, pass a reference to it from ref(). The code it's handed to records to the metric through it:

from encore import metrics signups = metrics.Counter("signups") def count(counter: metrics.Counter) -> None: counter.increment() count(signups.ref())

ref() works on every metric and group.

OpenTelemetry instruments

Encore's metrics are instruments of the OpenTelemetry Metrics API. You can also record with the OpenTelemetry API directly, for example for observable instruments, or attributes that labels can't express. Instruments made from Encore's meter provider are exported alongside Encore's metrics:

import asyncio from opentelemetry.metrics import CallbackOptions, Observation from encore.metrics import meter_provider job_queue: asyncio.Queue = asyncio.Queue() meter = meter_provider().get_meter(__name__) batch_size = meter.create_histogram("users.batch.size", unit="{user}") batch_size.record(25) def queue_depth(options: CallbackOptions): yield Observation(job_queue.qsize()) meter.create_observable_gauge("jobs.queue.depth", callbacks=[queue_depth])

To have instrumentation packages record through Encore too, install its provider as the OpenTelemetry global, as described in Tracing:

from opentelemetry import metrics from encore.metrics import meter_provider metrics.set_meter_provider(meter_provider())

Instruments made with the OpenTelemetry API are named exactly as you create them, without a service prefix, and Encore's parser doesn't see them: the declaration rules above don't apply.

A measurement recorded while a sampled span is current carries that span as an exemplar, linking it to the trace.

Viewing metrics

Locally, encore metrics shows the metrics your services reported in the last hour:

$ encore metrics list # every metric, with stats over the last 5 minutes
$ encore metrics list --watch # refresh every few seconds
$ encore metrics get orders.orders_placed # one metric's series
$ encore metrics get orders.requests_handled --attr status=500

Under encore test, metrics are recorded as usual but not exported.