Prometheus
VarMQ imports no metrics library. A worker exposes what Prometheus needs as plain methods: how many jobs are running and waiting, and four counters of what has happened. You wrap those methods in collectors from the Prometheus Go client. Its GaugeFunc and CounterFunc call a function each time Prometheus scrapes, so nothing of yours has to poll the worker.
A longer version of the program below is in examples/prometheus.
A worker with metrics
The program below runs a worker with concurrency 10, registers eight collectors for it, and serves them at /metrics. It adds 1000 jobs that each take 100 milliseconds.
14 collapsed lines
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/goptics/varmq"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
func main() {
worker := varmq.NewWorker(
func(j varmq.Job[string]) {
time.Sleep(100 * time.Millisecond)
},
varmq.WithConcurrency(10),
)
queue := worker.BindQueue()
registerMetrics(worker, "foo_worker")
http.Handle("/metrics", promhttp.Handler())
go func() {
for i := range 1000 {
queue.Add(fmt.Sprintf("task-%d", i))
}
}()
log.Println("metrics on http://localhost:8080/metrics")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// Each collector calls its function on every scrape, so a value is read when
// Prometheus asks for it.
func registerMetrics(w varmq.Worker, namespace string) {
gauge := func(name, help string, read func() int) {
prometheus.MustRegister(prometheus.NewGaugeFunc(
prometheus.GaugeOpts{Namespace: namespace, Name: name, Help: help},
func() float64 { return float64(read()) },
))
}
counter := func(name, help string, read func() uint64) {
prometheus.MustRegister(prometheus.NewCounterFunc(
prometheus.CounterOpts{Namespace: namespace, Name: name, Help: help},
func() float64 { return float64(read()) },
))
}
m := w.Metrics()
gauge("workers_processing", "Jobs being processed right now", w.NumProcessing)
gauge("workers_concurrency", "Maximum number of jobs processed at once", w.NumConcurrency)
gauge("workers_idle", "Idle goroutines in the pool", w.NumIdleWorkers)
gauge("jobs_waiting", "Jobs not started yet; can read high while batches run", w.NumPending)
counter("submitted_total", "Jobs the queue accepted", m.Submitted)
counter("completed_total", "Jobs whose function has returned", m.Completed)
counter("successful_total", "Jobs that returned without an error or a panic", m.Successful)
counter("failed_total", "Jobs that returned an error or panicked", m.Failed)
}Run locally
-
Make a module, save the program as
main.goand add the dependencies:mkdir prometheus-demo && cd prometheus-demo go mod init prometheus-demo go get github.com/goptics/varmq github.com/prometheus/client_golang -
Run it with
go run .. -
In another terminal, read the metrics while the jobs run:
curl -s localhost:8080/metrics | grep "^foo_worker"The output looks like this a moment after the start. The numbers depend on when you ask:
foo_worker_completed_total 190 foo_worker_failed_total 0 foo_worker_submitted_total 1000 foo_worker_successful_total 190 foo_worker_jobs_waiting 800 foo_worker_workers_concurrency 10 foo_worker_workers_idle 0 foo_worker_workers_processing 10 -
Ask again after about fifteen seconds. All 1000 jobs have completed,
jobs_waitingandworkers_processingread 0, andworkers_idlereads 1.
Leave out grep to see the # HELP and # TYPE lines too, and the Go runtime and process metrics. promhttp.Handler() serves the default registry, which has those as well.
The metrics
With the namespace foo_worker, each name starts with foo_worker_. The scene below replays the scrape of the program above. Move the clock, press a metric to read what it is, and see the PromQL for it. The numbers follow a real run of the program: 1000 jobs, 100 milliseconds each, 10 at a time.
A replay of what a Prometheus scrape of /metrics returns while the example program runs 1000 jobs with a concurrency of 10, each job taking 100 milliseconds. The left side lists the eight metrics in the text format, each with its HELP line, its TYPE line and its value. A slider sets the time from 0 to 11 seconds, and the values change with it. At 2 seconds the scrape shows completed 190, waiting 800 and processing 10. Press a metric to see its type, the VarMQ method behind it, what it means, a chart of its value over the run, and PromQL queries for it. A checkbox restarts the process at 5 seconds, which zeroes the counters.
curl -s localhost:8080/metrics | grep '^foo_worker'Numbers follow a real run of this program.
| Name | Type | From | What it is |
|---|---|---|---|
workers_processing | gauge | NumProcessing | jobs being processed right now |
workers_concurrency | gauge | NumConcurrency | the concurrency limit |
workers_idle | gauge | NumIdleWorkers | idle goroutines in the pool |
jobs_waiting | gauge | NumPending | jobs that have not started. While a batch runs it can read high: see waiting |
submitted_total | counter | Submitted | jobs the queue accepted |
completed_total | counter | Completed | jobs whose function has returned |
successful_total | counter | Successful | jobs that returned without an error or a panic |
failed_total | counter | Failed | jobs that returned an error or panicked |
The four counters are the four of Metrics(). Metrics explains what each counts. Waiting explains what NumPending and NumProcessing count.
Queries to start with
These use the namespace foo_worker. Change it to the namespace you pass to each collector.
| Question | PromQL |
|---|---|
| How many jobs finish each second? | rate(foo_worker_completed_total[5m]) |
| What share of finished jobs fail? | rate(foo_worker_failed_total[5m]) / rate(foo_worker_completed_total[5m]) |
| How full is the pool? (1 means every slot runs a job) | foo_worker_workers_processing / foo_worker_workers_concurrency |
| How large is the backlog? | foo_worker_jobs_waiting |
| Do jobs arrive faster than they finish? | rate(foo_worker_submitted_total[5m]) - rate(foo_worker_completed_total[5m]) |
The failure share is NaN while no job finishes. The pool can read above 1 just after TunePool lowers the limit, while the jobs already running finish. The last row is above 0 while jobs arrive faster than they finish. On a persistent queue it can mislead. Submitted counts only this process’s Add calls, so it does not count a job that another process added. Completed can then be higher than Submitted. On a distributed queue the counters may differ from what you expect. The overview says what each counter counts there.
What to watch for
- The reads are not one snapshot. A scrape reads eight values at slightly different moments, and a job can finish between two of them. A set read while jobs run can be off by a few. Metrics says more.
- The counters start at zero when the worker is created. They are totals. Use
rate()orincrease()in Prometheus to see how many jobs ran in a period. A restart of the process zeroes them, and Prometheus functions such asrate()read a drop as a counter reset. Resetmakes a counter fall. It zeroes the four counters, and Prometheus reads that as a reset, like a restart. Do not call it on a worker you export unless you want that.- One namespace for each worker.
MustRegisterpanics when a collector with the same metric name is registered twice, withduplicate metrics collector registration attempted. Register each worker under its own namespace. - A worker on a persistent or distributed queue asks its storage.
NumPendingreads the length from Redis or SQLite. So every scrape makes that call. For the counters of such a worker, see the overview.
Scraping it
Point Prometheus at the address the program listens on. A minimal scrape configuration is:
scrape_configs:
- job_name: varmq
static_configs:
- targets: ["localhost:8080"]Prometheus scrapes /metrics by default, which is where the program serves them. If you serve VarMQ’s REST API as well, it can sit on the same server: mount it with http.Handle("/varmq/", varmq.Handler("/varmq")) next to the metrics route. The REST API lists only named workers, so give the worker a name. The REST API returns the same worker numbers as JSON, which suits a dashboard, and Prometheus suits history and alerts.