mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
docs: Example on how to monitor Windmill (#2938)
* docs: Example on how to track arbitrary job metrics * Add how to monitor Windmill servers and workers * Update README.md --------- Co-authored-by: gbouv <guillaume@windmill.dev>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
Windmill monitoring with Prometheus
|
||||
===================================
|
||||
|
||||
## Intro
|
||||
|
||||
Windmill servers and workers both produces metrics Prometheus can scrap.
|
||||
|
||||
Individual jobs producing their own metric can be difficult to achieve due to the ephemeral nature of a job. Prometheus might not have the time to scrape the metric if the job is too quick. Prometheus has developed the [Prometheus Gateway](https://prometheus.io/docs/practices/pushing/) exactly for this usecase, and we're going to go through the initial setup. By the end of this example, you'll have a Windmill stack running a job that produces its own metric, and a Grafana simple dashboard displaying it.
|
||||
|
||||
Prometheus push gateway is hosted in [this repository](https://github.com/prometheus/pushgateway)
|
||||
|
||||
## Setup
|
||||
|
||||
First, we need to setup an entire stack composed of:
|
||||
- Windmill: a database, a server, and at least one worker
|
||||
- Prometheus: the server (if you don't already have one), and the gateway to which metrics will be pushed
|
||||
- Grafana to visualize the metrics (optional)
|
||||
|
||||
We have assembled a docker compose with all those services. You can run it with a simple:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
Once all the services are running, Windmill can be accessed at http://localhost:8000, Prometheus at http://localhost:9090, Grafana at http://localhost:3000 (credentials are set in [grafana.ini](./grafana/grafana.ini) config file).
|
||||
|
||||
The Gateway exposes the metrics on http://localhost:9091/metrics and also has an interface at http://localhost:9091 to manage metric groups (see the Gateway documentation for what a metric group is). You can check that Prometheus can collect them directly on [Prometheus status page](http://localhost:9090/targets)
|
||||
|
||||
## Monitoring Windmill servers and workers
|
||||
|
||||
Metrics need to be enabled on Windmill server and workers. They can be enabled only on Windmill Enterprise Edition by setting the environment variable `METRICS_ADDR` to `1` on each container or toggle on "Expose Metrics" in instance settings -> core (additional "debug-level" metrics can be enabled in the instance settings > debug menu).
|
||||
|
||||
Once metrics are enabled, Prometheus needs to discover Windmill service containers. Using docker-compose, it requires a few adjustments:
|
||||
- Each Windmill containers need to expose the metrics port (`8001` by default) so that Prometheus discovery knows which ports to scrap. This is done by adding the value `8001` to the `expose` block to both `windmill_server` and `windmill_worker`.
|
||||
- Each Windmill container needs to be labelled so that Prometheus service discovery filters out other services. Here we're using the `prometheus-job=windmill_server` and `prometheus-job=windmill_worker` in the docker compose to differentiate between server and worker
|
||||
- The Prometheus container needs to have access to the docker socket to discover other containers. To achieve this, Prometheus container needs to be run as root and the docker socket needs to be mounted on the container (see `user: root` and `/var/run/docker.sock:/var/run/docker.sock` in the `prometheus` block of docker-compose.yml)
|
||||
- Finally, the following block should be added to `prometheus.yml` config file:
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: "windmill_server"
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_label_prometheus_job]
|
||||
regex: windmill_server
|
||||
action: keep
|
||||
scrape_interval: 1s
|
||||
|
||||
- job_name: "windmill_worker"
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_label_prometheus_job]
|
||||
regex: windmill_worker
|
||||
action: keep
|
||||
scrape_interval: 1s
|
||||
```
|
||||
|
||||
With all this in place, Windmill servers and workers metrics will be scrapped by Prometheus and can be visualized in Grafana. We've added a simple dashboard in [grafana/dashboards/](./grafana/dashboards) showing the most interesting metrics.
|
||||
|
||||

|
||||
|
||||
Note: If your Windmill containers are split onto multiple docker daemon, the above still works. Instead of pointing to the docker socket in Prometheus' `docker_sd_config` block, simply expose docker daemon to http or https and point to the remote docker daemon host: `http://remote_docker_daemon:2375`. Multiple remote docker daemons can be listed.
|
||||
|
||||
## Producing metrics from individual Windmill jobs
|
||||
|
||||
Here is an example of how a job can "push" its own metrics to Prometheus via Prometheus Gateway
|
||||
|
||||
### Windmill script
|
||||
|
||||
Now we will create a script in Windmill that pushes a value to the Prometheus Gateway. We will do it in Python using [prometheus-client](https://github.com/prometheus/client_python), but other clients are available for [Golang](https://github.com/prometheus/client_golang) or [Typescript](https://github.com/siimon/prom-client), and for bash metrics can be pushed via [simple CURL commands](https://github.com/prometheus/pushgateway?tab=readme-ov-file#command-line).
|
||||
|
||||
In Windmill, create a new Python script (named `u/admin/random_number_metric_script` in this tutorial) with the following content:
|
||||
|
||||
```python
|
||||
import os
|
||||
import random
|
||||
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
|
||||
|
||||
PROMETHEUS_GATEWAY_URL = "prometheus_gateway:9091"
|
||||
|
||||
def main():
|
||||
job_path = os.environ.get("WM_JOB_PATH")
|
||||
registry = CollectorRegistry()
|
||||
gauge = Gauge(
|
||||
"job_records_processed",
|
||||
"Number of records processed for {}".format(job_path),
|
||||
registry=registry,
|
||||
)
|
||||
val = random.randint(0, 100)
|
||||
print("Storing metrics value: ", val)
|
||||
gauge.set(val)
|
||||
push_to_gateway(PROMETHEUS_GATEWAY_URL, job=job_path, registry=registry)
|
||||
```
|
||||
|
||||
It is quite simple. It generate a random number and pushed it via a Gauge metric named `job_records_processed` to the Prometheus Gateway. Note that we use the script path as the `job` label, so that all runs of this script will push to the same metric. Other labels (such as the job ID for example) can also be used via the `grouping_key` argument that `push_to_gateway` accepts.
|
||||
|
||||
Once the script is created, you can create a schedule that runs is every 5 seconds for example, such that a new value is pushed regularly.
|
||||
|
||||
### Visualizing the metric
|
||||
|
||||
If you chose to not run Grafana, the raw metric value can be seen in Prometheus UI directly. Go to http://localhost:9090/graph and input the following in the Expression bar:
|
||||
```promql
|
||||
job_records_processed{exported_job="u/admin/random_number_metric_script"}
|
||||
```
|
||||
|
||||

|
||||
|
||||
In Grafana, a similar visualization can be achieved. We've added a dashboard JSON in [grafana/dashboards](./grafana/dashboards/) and the easiest is to import it directly in your Grafana.
|
||||
|
||||
The first panel is a simple representation of the `job_records_processed` metric. We've adjusted the `Min Step` to 5 seconds because we know our job runs every 5 seconds and we ideally want to display all the measurement points.
|
||||
|
||||

|
||||
|
||||
The second panel is here to highlight a limitation to be aware of when using the Prometheus Gateway. It displays the instant value of the `job_records_processed` metric. The main issue with using the Gateway is that the value pushed to it will persist until a new value is pushed. Which means that if the schedule is somehow stopped, or if the script starts failing and stops pushing to Prometheus Gateway, the value will remain constant. It might not be ideal to spot any kind of undesired behavior. Instead what would be nice is to have the metric drop to a default value, like `0`. Thankfully the Gateway comes with a built-in metric called `push_time_seconds` which stores the timestamp of the last successful push for any given metric and label. You can combine the 2 metrics using basic functions to easily create an "instant" representation of signal. This is what we've done in the second panel:
|
||||
|
||||

|
||||
|
||||
We simply multiply the `job_records_processed` with the `irate` of the `push_time_seconds` metric for the same label, and we set the `Min Step` to `1s` for the `irate` to properly work.
|
||||
|
||||
We obtain the following simple yet useful dashboard:
|
||||
|
||||

|
||||
|
||||
In between the 2 light blue vertical lines, we manually stopped the Windmill schedule to show that the raw metric (at the top) remains constant, while its instant representation drops to zero.
|
||||
@@ -0,0 +1,100 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:14
|
||||
volumes:
|
||||
- db_data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_DB: windmill
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
windmill_server:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
deploy:
|
||||
replicas: 1
|
||||
labels:
|
||||
- prometheus-job=windmill_server
|
||||
expose:
|
||||
- 8001
|
||||
ports:
|
||||
- 8000:8000
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:changeme@db/windmill?sslmode=disable
|
||||
- MODE=server
|
||||
- METRICS_ADDR=1
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
windmill_worker:
|
||||
image: ghcr.io/windmill-labs/windmill-ee:main
|
||||
pull_policy: always
|
||||
deploy:
|
||||
replicas: 3
|
||||
labels:
|
||||
- prometheus-job=windmill_worker
|
||||
expose:
|
||||
- 8001
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- DATABASE_URL=postgres://postgres:changeme@db/windmill?sslmode=disable
|
||||
- MODE=worker
|
||||
- WORKER_GROUP=default
|
||||
- METRICS_ADDR=1
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus
|
||||
user: root
|
||||
volumes:
|
||||
- prometheus_data:/prometheus
|
||||
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro # for service discovery
|
||||
ports:
|
||||
- 9090:9090
|
||||
environment:
|
||||
POSTGRES_PASSWORD: changeme
|
||||
POSTGRES_DB: windmill
|
||||
command:
|
||||
- "--config.file=/etc/prometheus/prometheus.yml"
|
||||
- "--storage.tsdb.path=/prometheus"
|
||||
- "--storage.tsdb.retention.time=365d"
|
||||
|
||||
prometheus_gateway:
|
||||
image: prom/pushgateway
|
||||
ports:
|
||||
- 9091:9091
|
||||
command:
|
||||
- "--persistence.file=/data/persistence.dat"
|
||||
volumes:
|
||||
- prometheus_gateway_data:/data
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana
|
||||
container_name: grafana
|
||||
environment:
|
||||
- GF_PATHS_CONFIG=/etc/grafana/grafana.ini
|
||||
deploy:
|
||||
replicas: 1
|
||||
depends_on:
|
||||
- prometheus
|
||||
ports:
|
||||
- 3030:3000
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./grafana/datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
|
||||
- ./grafana/grafana.ini:/etc/grafana/grafana.ini
|
||||
|
||||
volumes:
|
||||
db_data: null
|
||||
prometheus_data: null
|
||||
prometheus_gateway_data: null
|
||||
grafana_data: null
|
||||
+319
@@ -0,0 +1,319 @@
|
||||
{
|
||||
"__inputs": [
|
||||
{
|
||||
"name": "DS_PROMETHEUS",
|
||||
"label": "Prometheus",
|
||||
"description": "",
|
||||
"type": "datasource",
|
||||
"pluginId": "prometheus",
|
||||
"pluginName": "Prometheus"
|
||||
}
|
||||
],
|
||||
"__elements": {},
|
||||
"__requires": [
|
||||
{
|
||||
"type": "grafana",
|
||||
"id": "grafana",
|
||||
"name": "Grafana",
|
||||
"version": "10.2.3"
|
||||
},
|
||||
{
|
||||
"type": "datasource",
|
||||
"id": "prometheus",
|
||||
"name": "Prometheus",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "timeseries",
|
||||
"name": "Time series",
|
||||
"version": ""
|
||||
}
|
||||
],
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"disableTextWrap": false,
|
||||
"editorMode": "builder",
|
||||
"expr": "job_records_processed{exported_job=\"$script_path\"}",
|
||||
"fullMetaSearch": false,
|
||||
"includeNullMetadata": true,
|
||||
"instant": false,
|
||||
"interval": "5s",
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "A",
|
||||
"useBackend": false
|
||||
}
|
||||
],
|
||||
"title": "Processed records (raw)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisBorderShow": false,
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 100,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": true,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 17,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 2,
|
||||
"interval": "15s",
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"disableTextWrap": false,
|
||||
"editorMode": "code",
|
||||
"expr": "irate(push_time_seconds{exported_job=\"$script_path\"}[$__interval]) * job_records_processed{exported_job=\"$script_path\"} / irate(push_time_seconds{exported_job=\"$script_path\"}[$__interval])",
|
||||
"fullMetaSearch": false,
|
||||
"hide": false,
|
||||
"includeNullMetadata": true,
|
||||
"instant": true,
|
||||
"interval": "1s",
|
||||
"legendFormat": "__auto",
|
||||
"range": true,
|
||||
"refId": "B",
|
||||
"useBackend": false
|
||||
}
|
||||
],
|
||||
"title": "Processed records (instant)",
|
||||
"transformations": [],
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 39,
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": "Prometheus",
|
||||
"value": "PBFA97CFB590B2093"
|
||||
},
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"multi": false,
|
||||
"name": "prometheus",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"queryValue": "",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"current": {},
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "${DS_PROMETHEUS}"
|
||||
},
|
||||
"definition": "label_values(exported_job)",
|
||||
"hide": 1,
|
||||
"includeAll": false,
|
||||
"multi": false,
|
||||
"name": "script_path",
|
||||
"options": [],
|
||||
"query": {
|
||||
"qryType": 1,
|
||||
"query": "label_values(exported_job)",
|
||||
"refId": "PrometheusVariableQueryEditor-VariableQuery"
|
||||
},
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Job metrics Dashboard",
|
||||
"uid": "a31424a8-5eaa-4d26-bded-19570145b7bd",
|
||||
"version": 8,
|
||||
"weekStart": ""
|
||||
}
|
||||
+1135
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
@@ -0,0 +1,3 @@
|
||||
[security]
|
||||
admin_user = windmill
|
||||
admin_password = changeme
|
||||
@@ -0,0 +1,28 @@
|
||||
global:
|
||||
scrape_interval: 1s
|
||||
external_labels:
|
||||
env: "infrastructure"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: "windmill_server"
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_label_prometheus_job]
|
||||
regex: windmill_server
|
||||
action: keep
|
||||
scrape_interval: 1s
|
||||
|
||||
- job_name: "windmill_worker"
|
||||
docker_sd_configs:
|
||||
- host: unix:///var/run/docker.sock
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_docker_container_label_prometheus_job]
|
||||
regex: windmill_worker
|
||||
action: keep
|
||||
scrape_interval: 1s
|
||||
|
||||
- job_name: "prometheus_gateway"
|
||||
static_configs:
|
||||
- targets: ["prometheus_gateway:9091"]
|
||||
scrape_interval: 1s
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
Reference in New Issue
Block a user