From 122eb272dd8c98cd4bad26dd628cc7b5d04f739a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 Oct 2026 12:13:51 +0200 Subject: [PATCH] mixin: lint dashboards and migrate CI tasks to mise (#2608) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Jan-Otto Kröpke --- .github/workflows/mixin.yml | 21 +++---- .github/workflows/pr-check.yaml | 23 ++++++-- .github/workflows/runbooks.yml | 16 ++---- contrib/mixin/.dashboard-lint.yaml | 68 ++++++++++++++++++++++ contrib/mixin/Makefile | 81 --------------------------- contrib/mixin/README.md | 52 ++++++++++------- contrib/mixin/mise.toml | 90 ++++++++++++++++++++++++++++++ dashboard/README.md | 15 ++--- 8 files changed, 227 insertions(+), 139 deletions(-) create mode 100644 contrib/mixin/.dashboard-lint.yaml delete mode 100644 contrib/mixin/Makefile create mode 100644 contrib/mixin/mise.toml diff --git a/.github/workflows/mixin.yml b/.github/workflows/mixin.yml index 00b96205..7537b45f 100644 --- a/.github/workflows/mixin.yml +++ b/.github/workflows/mixin.yml @@ -25,7 +25,7 @@ concurrency: jobs: rules: - name: Validate Prometheus rules + name: Validate mixin rules and dashboard runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: @@ -42,20 +42,13 @@ jobs: with: go-version-file: go.mod - - name: Cache mixin tools - id: tools-cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - name: Install and cache pinned mixin tools + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: - path: ~/go/bin - key: ${{ runner.os }}-${{ runner.arch }}-mixin-tools-${{ hashFiles('contrib/mixin/Makefile', 'go.mod') }} - - - name: Install pinned tools - if: steps.tools-cache.outputs.cache-hit != 'true' - run: make tools + working_directory: contrib/mixin - name: Build dashboard and check generated changes - run: make check-dashboards + run: mise run check-dashboards - - name: Lint and test rules - run: | - make test PROMTOOL="docker run --rm --entrypoint promtool -v \"$PWD:/mixin:ro\" -w /mixin prom/prometheus:v3.15.0" + - name: Lint dashboard and test rules + run: mise run test diff --git a/.github/workflows/pr-check.yaml b/.github/workflows/pr-check.yaml index 4b43166d..04969420 100644 --- a/.github/workflows/pr-check.yaml +++ b/.github/workflows/pr-check.yaml @@ -44,13 +44,26 @@ jobs: - name: check run: | - PR_TITLE_PREFIX=$(echo "$PR_TITLE" | cut -d':' -f1) - if [[ -d "internal/collector/$PR_TITLE_PREFIX" ]] || [[ -d "internal/$PR_TITLE_PREFIX" ]] || [[ -d "pkg/$PR_TITLE_PREFIX" ]] || [[ -d "$PR_TITLE_PREFIX" ]] || [[ "$PR_TITLE_PREFIX" == "docs" ]] || [[ "$PR_TITLE_PREFIX" == "ci" ]] || [[ "$PR_TITLE_PREFIX" == "revert" ]] || [[ "$PR_TITLE_PREFIX" == "fix" ]] || [[ "$PR_TITLE_PREFIX" == "fix(deps)" ]] || [[ "$PR_TITLE_PREFIX" == "feat" ]] || [[ "$PR_TITLE_PREFIX" == "chore" ]] || [[ "$PR_TITLE_PREFIX" == "chore(docs)" ]] || [[ "$PR_TITLE_PREFIX" == "chore(github-actions)" ]] || [[ "$PR_TITLE_PREFIX" == "chore(deps)" ]] || [[ "$PR_TITLE_PREFIX" == "*" ]] || [[ "$PR_TITLE_PREFIX" == "Release"* ]] || [[ "$PR_TITLE_PREFIX" == "Synchronize common files from prometheus/prometheus" ]] || [[ "$PR_TITLE_PREFIX" == "[0."* ]] || [[ "$PR_TITLE_PREFIX" == "[1."* ]]; then - exit 0 + PR_TITLE_PREFIX="${PR_TITLE%%:*}" + SCOPED_PREFIX_PATTERN='^([^():]+)\(.*\)(:|$)' + if [[ "$PR_TITLE" =~ $SCOPED_PREFIX_PATTERN ]]; then + PR_TITLE_PREFIX="${BASH_REMATCH[1]}" fi - echo "PR title must start with an name of an collector package" - echo "Example: 'logical_disk: description'" + for directory in internal/collector internal pkg .; do + if [[ -d "$directory/$PR_TITLE_PREFIX" ]]; then + exit 0 + fi + done + + case "$PR_TITLE_PREFIX" in + docs|ci|revert|fix|feat|chore|mixin|main|'*'|Release*|'Synchronize common files from prometheus/prometheus'|'[0.'*|'[1.'*) + exit 0 + ;; + esac + + echo "PR title must start with a package name or an allowed prefix, optionally followed by a scope" + echo "Examples: 'logical_disk: description', 'mixin(dashboard): description', 'chore(github-action): description'" exit 1 env: PR_TITLE: ${{ github.event.pull_request.title }} diff --git a/.github/workflows/runbooks.yml b/.github/workflows/runbooks.yml index 197b3dd7..604fb2b0 100644 --- a/.github/workflows/runbooks.yml +++ b/.github/workflows/runbooks.yml @@ -60,21 +60,13 @@ jobs: with: go-version-file: go.mod - - name: Cache Jsonnet tools - id: jsonnet-cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - name: Install and cache pinned mixin tools + uses: jdx/mise-action@2d8d4cafcbd33be2ea37d2b6f5ad595363d1f1ca # v5.1.1 with: - path: | - ~/go/bin/jsonnet - ~/go/bin/jsonnetfmt - key: ${{ runner.os }}-${{ runner.arch }}-runbooks-jsonnet-${{ hashFiles('contrib/mixin/Makefile', 'go.mod') }} - - - name: Install Jsonnet tools - if: steps.jsonnet-cache.outputs.cache-hit != 'true' - run: make -C contrib/mixin jsonnet-tools + working_directory: contrib/mixin - name: Check alert runbook coverage - run: make -C contrib/mixin check-runbooks + run: mise -C contrib/mixin run check-runbooks - name: Set up Pages id: pages diff --git a/contrib/mixin/.dashboard-lint.yaml b/contrib/mixin/.dashboard-lint.yaml new file mode 100644 index 00000000..3b809ed8 --- /dev/null +++ b/contrib/mixin/.dashboard-lint.yaml @@ -0,0 +1,68 @@ +# Keep exceptions scoped to the existing sample dashboard and panels. +exclusions: + target-instance-rule: + reason: Fleet panels intentionally span instances; per-host panels use an exact matcher for the single-select instance variable. + entries: + - dashboard: Windows Exporter + template-instance-rule: + reason: Per-host tabs require one instance, so the instance variable has neither multi-select nor an All value. + entries: + - dashboard: Windows Exporter + template-job-rule: + reason: Grafana expands All to the discovered job values rather than using a custom .+ wildcard. + entries: + - dashboard: Windows Exporter + uneditable-dashboard: + reason: This sample dashboard is editable so users can adapt it to their environment. + entries: + - dashboard: Windows Exporter + panel-units-rule: + reason: This table displays service names and states; the numeric query value is hidden. + entries: + - dashboard: Windows Exporter + panel: Automatic services not running + panel-title-description-rule: + reason: These existing panels have titles but no descriptions; new panels must include both. + entries: + - dashboard: Windows Exporter + panel: Operating system + - dashboard: Windows Exporter + panel: CPUs + - dashboard: Windows Exporter + panel: RAM + - dashboard: Windows Exporter + panel: CPU busy + - dashboard: Windows Exporter + panel: Memory used + - dashboard: Windows Exporter + panel: CPU utilization + - dashboard: Windows Exporter + panel: Volumes + - dashboard: Windows Exporter + panel: Context switches and interrupts + - dashboard: Windows Exporter + panel: Processes and threads + - dashboard: Windows Exporter + panel: System calls and exceptions + - dashboard: Windows Exporter + panel: Volume usage + - dashboard: Windows Exporter + panel: Network errors and discards + - dashboard: Windows Exporter + panel: Running services + - dashboard: Windows Exporter + panel: Disabled services + - dashboard: Windows Exporter + panel: VMs in critical health + - dashboard: Windows Exporter + panel: Logical processors + - dashboard: Windows Exporter + panel: Virtual switch dropped packets + - dashboard: Windows Exporter + panel: Clock sync source + - dashboard: Windows Exporter + panel: Clock offset and NTP round-trip delay + - dashboard: Windows Exporter + panel: Samples per scrape + - dashboard: Windows Exporter + panel: Collectors diff --git a/contrib/mixin/Makefile b/contrib/mixin/Makefile deleted file mode 100644 index 1fc56550..00000000 --- a/contrib/mixin/Makefile +++ /dev/null @@ -1,81 +0,0 @@ -JSONNET ?= jsonnet -JSONNETFMT ?= jsonnetfmt -JB ?= jb -PROMTOOL ?= promtool -PINT ?= pint -PYTHON ?= python3 - -# renovate: go=github.com/google/go-jsonnet -JSONNET_VERSION := v0.22.0 -# renovate: go=github.com/jsonnet-bundler/jsonnet-bundler -JB_VERSION := v0.6.0 -# renovate: go=github.com/cloudflare/pint -PINT_VERSION := v0.89.2 -SOURCES := $(wildcard *.libsonnet *.jsonnet lib/*.libsonnet rules/*.libsonnet alerts/*.libsonnet dashboards/*.libsonnet tests/*.libsonnet tests/*.jsonnet) -FMT_FLAGS := -n 2 --max-blank-lines 2 --string-style s --comment-style s -DASHBOARD := ../../dashboard/windows-exporter-dashboard.json - -.DEFAULT_GOAL := generate -.DELETE_ON_ERROR: -.PHONY: generate rules dashboard dashboards check-dashboards tools jsonnet-tools fmt lint test check-runbooks clean - -generate: rules dashboard - -rules: windows_rules.yaml windows_alerts.yaml - -tools: jsonnet-tools - go install github.com/cloudflare/pint/cmd/pint@$(PINT_VERSION) - -jsonnet-tools: - go install github.com/google/go-jsonnet/cmd/jsonnet@$(JSONNET_VERSION) - go install github.com/google/go-jsonnet/cmd/jsonnetfmt@$(JSONNET_VERSION) - go install github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb@$(JB_VERSION) - -# Only the dashboard needs the Jsonnet libraries (grafonnet). -vendor: jsonnetfile.json jsonnetfile.lock.json - $(JB) install - @touch $@ - -windows_rules.yaml: $(SOURCES) - $(JSONNET) -S rules.jsonnet > $@ - -windows_alerts.yaml: $(SOURCES) - $(JSONNET) -S alerts.jsonnet > $@ - -tests/custom_rules.yaml: $(SOURCES) - $(JSONNET) -S -e 'std.manifestYamlDoc((import "tests/config.libsonnet").prometheusRules)' > $@ - -tests/custom_alerts.yaml: $(SOURCES) - $(JSONNET) -S -e 'std.manifestYamlDoc((import "tests/config.libsonnet").prometheusAlerts)' > $@ - -dashboard: vendor - $(JSONNET) -J vendor -S dashboard.jsonnet > $(DASHBOARD).tmp - mv $(DASHBOARD).tmp $(DASHBOARD) - -dashboards: dashboard - -check-dashboards: dashboard - git diff --exit-code -- $(DASHBOARD) - -fmt: - $(JSONNETFMT) $(FMT_FLAGS) -i $(SOURCES) - -lint: rules tests/custom_rules.yaml tests/custom_alerts.yaml vendor - @set -e; for file in $(SOURCES); do \ - $(JSONNETFMT) $(FMT_FLAGS) "$$file" | diff -u "$$file" -; \ - done - @$(JSONNET) -J vendor -S dashboard.jsonnet | diff -u $(DASHBOARD) - \ - || { echo "$(DASHBOARD) is out of date; run make dashboard"; exit 1; } - $(PROMTOOL) check rules windows_rules.yaml windows_alerts.yaml tests/custom_rules.yaml tests/custom_alerts.yaml - $(PINT) --offline lint windows_rules.yaml windows_alerts.yaml - $(PINT) --offline lint tests/custom_rules.yaml tests/custom_alerts.yaml - -check-runbooks: - $(JSONNET) -e '(import "mixin.libsonnet") { _config+:: { enableActiveDirectory: true, enableTime: true } }.prometheusAlerts' | $(PYTHON) tests/check_runbooks.py ../../docs/runbooks - -test: lint check-runbooks - $(JSONNET) tests/runbooks.test.jsonnet - $(PROMTOOL) test rules tests/*.test.yaml - -clean: - rm -f windows_rules.yaml windows_alerts.yaml tests/custom_rules.yaml tests/custom_alerts.yaml diff --git a/contrib/mixin/README.md b/contrib/mixin/README.md index bc8b710f..4430b807 100644 --- a/contrib/mixin/README.md +++ b/contrib/mixin/README.md @@ -6,16 +6,22 @@ source of the sample dashboard. The source uses Jsonnet and exposes the standard ## Generate rules -Install the pinned Jsonnet, [jsonnet-bundler](https://github.com/jsonnet-bundler/jsonnet-bundler) -and [pint](https://github.com/cloudflare/pint) tools: +Install [mise](https://mise.jdx.dev/) and Go, then install the pinned Jsonnet, +[jsonnet-bundler](https://github.com/jsonnet-bundler/jsonnet-bundler), +[pint](https://github.com/cloudflare/pint), +[dashboard-linter](https://github.com/grafana/dashboard-linter), and Prometheus +tools from `mise.toml`. Tasks use a POSIX shell; on Windows, run them in WSL. ```sh cd contrib/mixin -make tools -export PATH="$(go env GOPATH)/bin:$PATH" -make rules +mise trust +mise install +mise run rules ``` +Mise installs release binaries with checksum verification and builds Jsonnet +Bundler using Go. `mise run` makes the pinned tools available automatically. + Only the dashboard depends on Jsonnet libraries, so `jb install` is not required to generate rules. Generated rule files are ignored by Git: @@ -233,50 +239,56 @@ b.panel.new(200, 'CPU usage', 'timeseries') Regenerate the committed file after changing the dashboard source: ```sh -make dashboard +mise run dashboard ``` This runs `jb install` and writes `../../dashboard/windows-exporter-dashboard.json`. -`make dashboards` is an alias. `make generate` builds both the rules and the -dashboard, while `make rules` builds only the rules. The same commands work -inside WSL with the Go tool binaries on `PATH`. +`mise run dashboards` is an alias. `mise run generate` builds both the rules and the +dashboard, while `mise run rules` builds only the rules. The same commands work +inside WSL; mise supplies the tool binaries on `PATH`. The checked-in JSON remains the reference for the rendered dashboard, including queries, IDs, variables, annotations, transformations, and tab/row layout. -`make check-dashboards` regenerates it and runs `git diff --exit-code`, displaying +`mise run check-dashboards` regenerates it and runs `git diff --exit-code`, displaying any drift. CI runs that check before the mixin tests. The dashboard uses its own variables and does not read `_config`. ## Validate -Install Python 3 and `promtool` from a [Prometheus release](https://github.com/prometheus/prometheus/releases), -then run: +Install Python 3, run `mise install`, then run: ```sh -make test +mise run test ``` This checks Jsonnet formatting, checks that the committed dashboard is up to -date, validates generated rules with `promtool`, runs -`pint --offline lint`, and evaluates the rules against synthetic time series. +date, runs `dashboard-linter lint --strict`, validates generated rules with +`promtool`, runs `pint --offline lint`, and evaluates the rules against synthetic +time series. Tests cover all 25 recordings, alert timing and recovery, threshold boundaries, counter resets, zero denominators, disk forecasts, target-label preservation, and custom selectors/thresholds with Active Directory and clock alerts enabled. Runbook checks include opt-in alerts and verify that their pages and URLs exist. CI uses Prometheus 3.15.0 and the same pinned tool versions as -`make tools`. +`mise install`. -If you prefer to run `promtool` through Docker: +To list the available tasks: ```sh -make test PROMTOOL="docker run --rm --entrypoint promtool -v \"$PWD:/mixin:ro\" -w /mixin prom/prometheus:v3.15.0" +mise tasks ls ``` The dedicated `Mixin` workflow runs on changes to `contrib/mixin/**`, the committed dashboard JSON or its own workflow file, and can also be started -manually. It caches pinned tool binaries and runs the same `make test` checks +manually. It caches pinned tool binaries and runs the same `mise run test` checks used locally. +Run `mise run lint-dashboards` to lint just the committed dashboard. `.dashboard-lint.yaml` +documents exceptions for the sample dashboard's single-host selection, fleet +queries, editable state, and existing panels without descriptions or numeric +units. Other findings fail validation, including warnings. Remove panel +exceptions when adding the missing descriptions. + `.pint.hcl` requires a non-empty severity label, summary/description annotations, and an HTTP(S) runbook URL on alerts, in addition to pint's built-in checks. CI uses offline mode without a Prometheus endpoint. To validate metric @@ -293,7 +305,7 @@ Then run `pint --config /path/to/local.pint.hcl lint windows_rules.yaml windows_ without `--offline`. See the [pint documentation](https://cloudflare.github.io/pint/) for available checks. -Run `make fmt` after source changes and `make clean` to remove generated rule +Run `mise run fmt` after source changes and `mise run clean` to remove generated rule files. For downstream composition with Jsonnet Bundler, import `github.com/prometheus-community/windows_exporter/contrib/mixin/mixin.libsonnet` and extend `_config`. See the [monitoring mixin documentation](https://github.com/monitoring-mixins/docs) diff --git a/contrib/mixin/mise.toml b/contrib/mixin/mise.toml new file mode 100644 index 00000000..ace2399d --- /dev/null +++ b/contrib/mixin/mise.toml @@ -0,0 +1,90 @@ +[tools] +"github:google/go-jsonnet" = "0.22.0" +"go:github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb" = "0.6.0" +"github:cloudflare/pint" = "0.89.2" +"github:grafana/dashboard-linter" = "0.3.0" +"github:prometheus/prometheus" = "3.15.0" + +[vars] +sources = "*.libsonnet *.jsonnet lib/*.libsonnet rules/*.libsonnet alerts/*.libsonnet dashboards/*.libsonnet tests/*.libsonnet tests/*.jsonnet" +fmt_flags = "-n 2 --max-blank-lines 2 --string-style s --comment-style s" +dashboard = "../../dashboard/windows-exporter-dashboard.json" + +[tasks.generate] +description = "Generate Prometheus rules, alerts, and the dashboard" +alias = "default" +depends = ["rules", "dashboard"] + +[tasks.rules] +description = "Generate Prometheus rules and alerts" +run = [ + "jsonnet -S rules.jsonnet > windows_rules.yaml", + "jsonnet -S alerts.jsonnet > windows_alerts.yaml", +] + +[tasks.vendor] +description = "Install dashboard Jsonnet libraries" +run = "jb install" + +[tasks.dashboard] +description = "Regenerate the committed Grafana dashboard" +alias = "dashboards" +depends = ["vendor"] +run = ''' +set -eu +trap 'rm -f {{vars.dashboard}}.tmp' EXIT +jsonnet -J vendor -S dashboard.jsonnet > {{vars.dashboard}}.tmp +mv {{vars.dashboard}}.tmp {{vars.dashboard}} +''' + +[tasks.check-dashboards] +description = "Regenerate the dashboard and fail on uncommitted changes" +depends = ["dashboard"] +run = "git diff --exit-code -- {{vars.dashboard}}" + +[tasks.lint-dashboards] +description = "Lint the committed dashboard in strict mode" +run = "dashboard-linter lint --strict --config .dashboard-lint.yaml {{vars.dashboard}}" + +[tasks.fmt] +description = "Format all mixin Jsonnet sources" +run = "jsonnetfmt {{vars.fmt_flags}} -i {{vars.sources}}" + +[tasks.custom-rules] +description = "Generate rules and alerts with the test configuration" +hide = true +run = [ + '''jsonnet -S -e 'std.manifestYamlDoc((import "tests/config.libsonnet").prometheusRules)' > tests/custom_rules.yaml''', + '''jsonnet -S -e 'std.manifestYamlDoc((import "tests/config.libsonnet").prometheusAlerts)' > tests/custom_alerts.yaml''', +] + +[tasks.lint] +description = "Check Jsonnet formatting, dashboard drift, and Prometheus rules" +depends = ["rules", "custom-rules", "vendor", "lint-dashboards"] +run = ''' +set -eu +for file in {{vars.sources}}; do + jsonnetfmt {{vars.fmt_flags}} "$file" | diff -u "$file" - +done +jsonnet -J vendor -S dashboard.jsonnet | diff -u {{vars.dashboard}} - \ + || { echo "{{vars.dashboard}} is out of date; run mise run dashboard"; exit 1; } +promtool check rules windows_rules.yaml windows_alerts.yaml tests/custom_rules.yaml tests/custom_alerts.yaml +pint --offline lint windows_rules.yaml windows_alerts.yaml +pint --offline lint tests/custom_rules.yaml tests/custom_alerts.yaml +''' + +[tasks.check-runbooks] +description = "Check runbook coverage for all alerts, including opt-in alerts" +run = '''jsonnet -e '(import "mixin.libsonnet") { _config+:: { enableActiveDirectory: true, enableTime: true } }.prometheusAlerts' | python3 tests/check_runbooks.py ../../docs/runbooks''' + +[tasks.test] +description = "Run all mixin lint, runbook, and rule tests" +depends = ["lint", "check-runbooks"] +run = [ + "jsonnet tests/runbooks.test.jsonnet", + "promtool test rules tests/*.test.yaml", +] + +[tasks.clean] +description = "Remove generated rule and alert files" +run = "rm -f windows_rules.yaml windows_alerts.yaml tests/custom_rules.yaml tests/custom_alerts.yaml" diff --git a/dashboard/README.md b/dashboard/README.md index 0b266e19..ef076f19 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -53,18 +53,19 @@ Don't edit the JSON file by hand. It is generated from Jsonnet with [grafonnet]( - [dashboards/styles.libsonnet](../contrib/mixin/dashboards/styles.libsonnet) shares the visualization defaults. The generator writes the v2 schema directly, so it needs neither Grafana nor Prometheus. -Install the pinned tools and regenerate the file in `contrib/mixin`: +Install [mise](https://mise.jdx.dev/) and Go, then install the pinned tools and +regenerate the file in `contrib/mixin` (use WSL on Windows): ```sh cd contrib/mixin -make tools -export PATH="$(go env GOPATH)/bin:$PATH" -make dashboard +mise trust +mise install +mise run dashboard ``` -`make dashboard` runs `jb install` to fetch grafonnet into `contrib/mixin/vendor` and renders the dashboard. -`make lint` fails if the committed JSON file differs from the generated one. -`make check-dashboards` rebuilds it and runs `git diff --exit-code`, as CI does before the mixin tests. +`mise run dashboard` runs `jb install` to fetch grafonnet into `contrib/mixin/vendor` and renders the dashboard. +`mise run lint` fails if the committed JSON file differs from the generated one. +`mise run check-dashboards` rebuilds it and runs `git diff --exit-code`, as CI does before the mixin tests. Check new or changed queries in Grafana before you open a pull request, for example by importing the file into a local Grafana 13. ![Screenshot of the Fleet tab.](dashboard-fleet.png)