Prometheus Metrics
Per-task execution counters and a duration histogram from middleware, plus polled queue depth and dead-letter gauges.
Per-task execution counters and a duration histogram from middleware, plus polled queue depth and dead-letter gauges.
FlexiQ exports Prometheus metrics through two pieces — a middleware that records per-task execution counters and a duration histogram, and a stats collector that polls queue depth and dead-letter size onto gauges on an interval. Both write into the same
prometheus-clientprom-clientMicrometer
registry, so one HTTP endpoint can serve everything.
Java has no Prometheus contrib. Metrics come from Micrometer: the
FlexiQObservation middleware wraps each task execution in a Micrometer
Observation, which a PrometheusMeterRegistry turns into a timer (and, if you
add a tracing bridge, a span). See the dedicated
Micrometer guide for the full API.
The SDK compiles against io.micrometer:micrometer-observation as compileOnly,
so add it plus a Prometheus registry to your build:
io.micrometer:micrometer-observationio.micrometer:micrometer-registry-prometheus (brings micrometer-core, which
provides DefaultMeterObservationHandler and PrometheusMeterRegistry)import io.micrometer.core.instrument.observation.DefaultMeterObservationHandler;
import io.micrometer.observation.ObservationRegistry;
import io.micrometer.prometheusmetrics.PrometheusConfig;
import io.micrometer.prometheusmetrics.PrometheusMeterRegistry;
import org.byteveda.flexiq.contrib.FlexiQObservation;
PrometheusMeterRegistry prometheus = new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
ObservationRegistry observations = ObservationRegistry.create();
observations.observationConfig()
.observationHandler(new DefaultMeterObservationHandler(prometheus));
flexiq.use(new FlexiQObservation(observations));Each execution attempt starts an observation named flexiq.task (customizable
via new FlexiQObservation(registry, name, taskFilter)) tagged with
flexiq.task = the task name. Micrometer's meter handler converts it into a
timer on the Prometheus registry; the exposed series name and suffixes follow
Micrometer's Prometheus naming convention (dots become underscores, a _seconds
suffix is added), not a fixed flexiq_* name from the contrib. Retries are
separate attempts, each producing its own observation.
Serve prometheus.scrape() from any HTTP endpoint, or use Spring Boot Actuator's
/actuator/prometheus, which wires the registry for you. The flexiq_jobs_total
/ flexiq_dlq_size names and the polled queue-depth gauges below are Python/Node
only — build Java dashboards and alerts on the timer series your
Micrometer→Prometheus registry emits.
pip install flexiq[prometheus]This installs prometheus-client as a dependency.
Peer dependency: prom-client. Import from the flexiq/contrib/prometheus
subpath — it is not exported from the main barrel.
| Metric | Type | Labels | Description |
|---|---|---|---|
flexiq_jobs_total | Counter | task, status | Total jobs processed (status is completed or failed) |
flexiq_job_duration_seconds | Histogram | task | Job execution duration |
flexiq_active_workers | Gauge | — | Number of currently executing workers |
flexiq_retries_total | Counter | task | Total retry attempts |
flexiq_queue_depth | Gauge | queue | Number of pending jobs, polled by PrometheusStatsCollector |
flexiq_dlq_size | Gauge | — | Number of dead-letter jobs, polled |
flexiq_worker_utilization | Gauge | queue | Ratio of running jobs to total workers for that queue (0.0–1.0), polled |
The first four are written by PrometheusMiddleware as each job runs; the
last three are polled by PrometheusStatsCollector. Metrics are created once
per namespace and reused, so it's safe to construct the middleware and the
collector independently as long as they share a namespace.
| Metric | Type | Labels | Description |
|---|---|---|---|
flexiq_jobs_total | Counter | task, status | Finished executions by outcome (status is completed or failed) |
flexiq_job_duration_seconds | Histogram | task | Execution duration |
flexiq_active_workers | Gauge | — | Jobs currently executing |
flexiq_retries_total | Counter | task | Retry attempts |
flexiq_queue_depth | Gauge | queue | Pending jobs per queue, polled by PrometheusStatsCollector |
flexiq_dlq_size | Gauge | — | Dead-letter queue size, polled |
There's no worker_utilization gauge on Node — PrometheusStatsCollector
only polls queue depth and DLQ size.
Add PrometheusMiddleware to your queue to track per-task execution metrics:
from flexiq import Queue
from flexiq.contrib.prometheus import PrometheusMiddleware
queue = Queue(db_path="myapp.db", middleware=[PrometheusMiddleware()])PrometheusMiddleware(
namespace="myapp",
disabled_metrics={"resource", "proxy"},
task_filter=lambda name: not name.startswith("internal."),
)| Parameter | Type | Default | Description |
|---|---|---|---|
namespace | str | "flexiq" | Prefix for all metric names. |
extra_labels_fn | Callable[[JobContext], dict[str, str]] | None | None | Accepted for forward compatibility — not currently applied to any metric's labels. |
disabled_metrics | set[str] | None | None | Metric groups or individual names to skip. Groups: "jobs", "queue", "resource", "proxy", "intercept". |
task_filter | Callable[[str], bool] | None | None | Predicate that receives a task name. Return True to export metrics for the task, False to skip it. None exports all tasks. |
Register prometheusMiddleware to record per-job counters and the duration
histogram:
import { prometheusMiddleware } from "@byteveda/flexiq/contrib/prometheus";
queue.use(prometheusMiddleware());| Option | Type | Default | Description |
|---|---|---|---|
namespace | string | "flexiq" | Metric name prefix |
register | Registry | global register | prom-client registry to register metrics into |
taskFilter | (taskName) => boolean | — | Return false to skip a task |
buckets | number[] | prom-client defaults | Histogram bucket boundaries (seconds) |
For queue-level metrics, use the stats collector. It polls queue.stats()
(and resource/proxy/interception stats) on a background thread:
from flexiq.contrib.prometheus import PrometheusStatsCollector
collector = PrometheusStatsCollector(queue, interval=10)
collector.start()PrometheusStatsCollector(
queue,
interval=10,
namespace="myapp",
disabled_metrics={"intercept"},
)| Parameter | Type | Default | Description |
|---|---|---|---|
queue | Queue | — | The Queue instance to poll. |
interval | float | 10.0 | Seconds between polls. |
namespace | str | "flexiq" | Prefix for metric names. Must match PrometheusMiddleware's namespace to share metric objects. |
disabled_metrics | set[str] | None | None | Metric groups or names to skip. Same groups as PrometheusMiddleware. |
Call collector.stop() on shutdown — it signals the background thread and
joins it (up to 5s).
PrometheusStatsCollector polls queue depth and DLQ size on an interval:
import { PrometheusStatsCollector } from "@byteveda/flexiq/contrib/prometheus";
const collector = new PrometheusStatsCollector(queue);
collector.start();| Option | Type | Default | Description |
|---|---|---|---|
namespace | string | "flexiq" | Metric name prefix |
register | Registry | global register | Registry to write gauges into |
intervalMs | number | 10000 | Poll interval in milliseconds |
The internal timer is unref'd, so it never keeps the process alive on its
own. Call .stop() on graceful shutdown:
process.on("SIGTERM", () => {
collector.stop();
worker.stop();
});Metrics for one namespace are built once per registry, so registering
multiple middlewares is safe. Pass the same register to the middleware and
the collector to keep everything in one registry.
Start a standalone /metrics endpoint for Prometheus to scrape:
from flexiq.contrib.prometheus import start_metrics_server
start_metrics_server(port=9090)This uses prometheus_client.start_http_server under the hood.
/metrics than the dashboard'sstart_metrics_server opens its own dedicated HTTP server — it is not the
same endpoint as the dashboard's GET /metrics (see
REST API). Both ultimately
call prometheus_client.generate_latest(), but each only serves metrics
registered in its own process:
start_metrics_server in the same process as your
PrometheusMiddleware / PrometheusStatsCollector (typically the worker
process) — this is the endpoint you'll usually scrape.GET /metrics only shows something useful if the
dashboard process itself registered metrics (e.g. it also runs
PrometheusMiddleware). If your worker and dashboard run as separate
processes — the common deployment — scrape the worker's
start_metrics_server instead.There's no standalone metrics server built in — mount the registry on your own HTTP server:
import { register } from "prom-client";
import express from "express";
const app = express();
app.get("/metrics", async (_req, res) => {
res.set("Content-Type", register.contentType);
res.end(await register.metrics());
});The dashboard's own
GET /api/metrics
is a separate, JSON-based aggregation, not a Prometheus text-exposition
endpoint — scrape the server above instead.
from flexiq import Queue
from flexiq.contrib.prometheus import (
PrometheusMiddleware,
PrometheusStatsCollector,
start_metrics_server,
)
queue = Queue(db_path="myapp.db", middleware=[PrometheusMiddleware()])
# Start metrics endpoint
start_metrics_server(port=9090)
# Start queue stats polling
collector = PrometheusStatsCollector(queue, interval=10)
collector.start()import { Queue } from "@byteveda/flexiq";
import {
prometheusMiddleware,
PrometheusStatsCollector,
} from "@byteveda/flexiq/contrib/prometheus";
import { register } from "prom-client";
import express from "express";
const queue = new Queue({ dbPath: "myapp.db" });
queue.use(prometheusMiddleware());
const collector = new PrometheusStatsCollector(queue);
collector.start();
const app = express();
app.get("/metrics", async (_req, res) => {
res.set("Content-Type", register.contentType);
res.end(await register.metrics());
});
app.listen(9090);Prometheus scrape config for either SDK:
scrape_configs:
- job_name: flexiq
static_configs:
- targets: ["localhost:9090"]rate(flexiq_jobs_total[5m]) by task and statushistogram_quantile(0.95, rate(flexiq_job_duration_seconds_bucket[5m]))flexiq_queue_depth by queueflexiq_dlq_size with an alert thresholdflexiq_worker_utilization by queuerate(flexiq_jobs_total[5m]) by task and statushistogram_quantile(0.95, rate(flexiq_job_duration_seconds_bucket[5m]))flexiq_queue_depth by queueflexiq_dlq_size with an alert thresholdUsing the same setup as the full example above, a couple of
alerting rules worth adding — flexiq_dlq_size and flexiq_jobs_total
carry the same names in the Python and Node exporters, so this rule file
works unchanged for both:
groups:
- name: flexiq
rules:
- alert: HighDLQSize
expr: flexiq_dlq_size > 10
for: 5m
labels:
severity: warning
annotations:
summary: "flexiq dead letter queue has {{ $value }} entries"
- alert: HighErrorRate
expr: rate(flexiq_jobs_total{status="failed"}[5m]) > 0.1
for: 2m
labels:
severity: critical
annotations:
summary: "High task failure rate: {{ $value }} failures/sec"These expressions assume the default flexiq namespace. If you configured a
custom one (namespace: "myapp" in the Options above), rewrite
the prefixes to match — myapp_dlq_size, myapp_jobs_total — or the alerts
silently match no series.
Java exports metrics through Micrometer, not this contrib — the metric names in these rules don't exist there. Build the equivalent alerts on the names your Micrometer→Prometheus registry emits instead.
PrometheusMiddleware composes with other middleware:
from flexiq.contrib.otel import OpenTelemetryMiddleware
from flexiq.contrib.sentry import SentryMiddleware
queue = Queue(
db_path="myapp.db",
middleware=[
OpenTelemetryMiddleware(),
PrometheusMiddleware(),
SentryMiddleware(),
],
)See the Middleware guide for more on combining middleware.
prometheusMiddleware composes with other middleware — register each with
its own queue.use() call, in the order you want their hooks to run:
import { otelMiddleware } from "@byteveda/flexiq/contrib/otel";
import { sentryMiddleware } from "@byteveda/flexiq/contrib/sentry";
import { prometheusMiddleware } from "@byteveda/flexiq/contrib/prometheus";
queue.use(otelMiddleware());
queue.use(prometheusMiddleware());
queue.use(sentryMiddleware());See Middleware for more on hook ordering.