mixin: add a dashboard per tab next to the full dashboard (#2678)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-11 16:29:56 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent b3d043d9dc
commit 1cdea1f42a
29 changed files with 34450 additions and 196 deletions
+15 -23
View File
@@ -1,42 +1,34 @@
# Keep exceptions scoped to the existing sample dashboard and panels.
# Exceptions apply to all sample dashboards; entries without a dashboard
# match every dashboard title.
exclusions:
target-promql-rule:
reason: dashboard-linter 0.3.0's v2 adapter omits custom variable current values and options, so the numeric process_top value becomes a vector identifier. These queries are validated against live Prometheus with limits 5, 10 and 20.
entries:
- dashboard: Windows Exporter
panel: Process CPU usage
- dashboard: Windows Exporter
panel: Process working set
- dashboard: Windows Exporter
panel: Process private bytes
- dashboard: Windows Exporter
panel: Process I/O throughput
- dashboard: Windows Exporter
panel: Process threads
- dashboard: Windows Exporter
panel: Process handles
- dashboard: Windows Exporter
panel: Process I/O operations
- dashboard: Windows Exporter
panel: Process page faults
- panel: Process CPU usage
- panel: Process working set
- panel: Process private bytes
- panel: Process I/O throughput
- panel: Process threads
- panel: Process handles
- panel: Process I/O operations
- panel: Process page faults
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: Automatic services not running
+46 -24
View File
@@ -202,16 +202,22 @@ Fork users should override `runbookURLPattern` to match their published URL.
## Dashboard
`grafanaDashboards` contains `windows-exporter.json`, the sample dashboard in
[dashboard/windows-exporter-dashboard.json](../../dashboard/windows-exporter-dashboard.json).
It is a Grafana v2 dashboard with tabs and needs Grafana 13 or later. See the
[dashboard documentation](../../dashboard/README.md) for its content.
`grafanaDashboards` contains `windows-exporter.json`, the full sample dashboard
with all tabs in
[dashboard/windows-exporter-dashboard.json](../../dashboard/windows-exporter-dashboard.json),
and a shorter dashboard per tab, such as `windows-exporter-cpu.json`, in
[dashboard](../../dashboard). They are Grafana v2 dashboards and need Grafana 13
or later. See the [dashboard documentation](../../dashboard/README.md) for their
content.
The dashboard uses additive builders with [grafonnet](https://github.com/grafana/grafonnet),
pinned in `jsonnetfile.json`:
- `dashboards/windows-exporter.libsonnet` composes the dashboard, with one source
file per tab for its panels, queries, rows, and grid positions.
- `dashboards/windows-exporter.libsonnet` composes the full dashboard and the
dashboard of each tab, with one source file per tab for its panels, queries,
rows, and grid positions. A tab dashboard keeps only the variables its panels
reference, directly or through other variables, and the annotations of those
variables.
- `dashboards/builders.libsonnet` uses Grafonnet's native `apps.dashboard.v2`
builders for the dashboard and layout and wraps its Prometheus query builders
into the v2 panel/query-group schema.
@@ -242,41 +248,56 @@ b.panel.new(200, 'CPU usage', 'timeseries')
+ b.panel.withDefaults({ unit: 'percentunit', min: 0, max: 1 })
```
Regenerate the committed file after changing the dashboard source:
Regenerate the committed files after changing the dashboard source:
```sh
mise run dashboard
```
This runs `jb install` and writes `../../dashboard/windows-exporter-dashboard.json`.
This runs `jb install` and writes `../../dashboard/windows-exporter-*.json`.
`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
The checked-in JSON remains the reference for the rendered dashboards, including
queries, IDs, variables, annotations, transformations, and tab/row layout.
`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
`mise run check-dashboards` regenerates them, marks new dashboard files with
`git add --intent-to-add` and runs `git diff --exit-code`, displaying any drift. CI runs that check before the mixin tests. The dashboard uses its own
variables for selectors; from `_config` it reads only `collectors`,
`enableFleetTab`, `enableOverviewTab` and the dashboard metadata below.
`enableFleetTab`, `enableOverviewTab`, `enableTabDashboards` and the dashboard
metadata below.
### Metadata
`dashboardUID`, `dashboardTitle`, `dashboardDescription` and `dashboardTags`
set the dashboard's UID (`metadata.name`), title, description and tags. Give a
customized dashboard its own UID so it can be imported next to the sample
dashboard instead of replacing it:
Every dashboard has a readable UID (`metadata.name`) built from
`dashboardUIDPrefix`, and a title built from `dashboardTitlePrefix`:
| Dashboard | UID | Title |
| --- | --- | --- |
| Full, all tabs | `windows-exporter-full` | Windows Exporter / Full |
| One tab | `windows-exporter-<tab>`, for example `windows-exporter-cpu` or `windows-exporter-scheduled-tasks` | Windows Exporter / `<tab>` |
`dashboardUID`, `dashboardTitle` and `dashboardDescription` override the UID,
title and description of the full dashboard. `dashboardTags` sets the tags of
all dashboards; each tab dashboard links to the others through a dropdown of
dashboards with these tags, keeping the time range and variables. Set
`enableTabDashboards: false` to render only the full dashboard. Give customized
dashboards their own UIDs so they can be imported next to the sample dashboards
instead of replacing them:
```jsonnet
(import 'mixin.libsonnet') {
_config+:: {
dashboardUID: 'windows-compact',
dashboardTitle: 'Windows (compact)',
dashboardUIDPrefix: 'windows-compact',
dashboardTitlePrefix: 'Windows (compact)',
dashboardTags+: ['compact'],
},
}.grafanaDashboards['windows-exporter.json']
}.grafanaDashboards
```
In the Fleet dashboard, a hostname links to the Overview dashboard; in the full
dashboard, it links to the Overview tab.
### Collectors
`_config.collectors` lists the windows_exporter collectors by their exporter
@@ -309,7 +330,7 @@ dashboard with only the collector tabs and the Exporter tab, turn them off:
}.grafanaDashboards['windows-exporter.json']
```
The committed sample dashboard enables every collector. The Fleet hosts table
The committed sample dashboards enable every collector. The Fleet hosts table
always queries the default collectors; its columns stay empty for collectors
that are turned off. Tab sources are
functions of `on(collector)`; return `null` instead of a row, a `[id, width,
@@ -325,8 +346,9 @@ Install Python 3, run `mise install`, then run:
mise run test
```
This checks Jsonnet formatting, checks that the committed dashboard is up to
date, runs `dashboard-linter lint --strict`, validates generated rules with
This checks Jsonnet formatting, runs `jsonnet-lint` on the Jsonnet sources to
find unused variables and references to missing fields, checks that the
committed dashboards are up to 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
@@ -347,8 +369,8 @@ committed dashboard JSON or its own workflow file, and can also be started
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
Run `mise run lint-dashboards` to lint just the committed dashboards. `.dashboard-lint.yaml`
documents exceptions for the sample dashboards' single-host selection, fleet
queries, editable state, and the service table without numeric units. All panels
include descriptions; missing titles or descriptions fail validation, including
warnings.
+12 -4
View File
@@ -11,11 +11,19 @@
// Requires the time collector with its ntp subcollector enabled.
enableTime: false,
// Grafana dashboard metadata. dashboardUID is the v2 metadata.name; change
// it to import the dashboard next to the sample one.
dashboardUID: 'Kdaassddw',
dashboardTitle: 'Windows Exporter',
// Grafana dashboard metadata. A UID is the v2 metadata.name; change the
// UIDs to import the dashboards next to the sample ones.
// Every tab also renders as a dashboard of its own with the UID
// <dashboardUIDPrefix>-<tab> and the title "<dashboardTitlePrefix> / <tab>",
// for example windows-exporter-cpu and "Windows Exporter / CPU".
enableTabDashboards: true,
dashboardUIDPrefix: 'windows-exporter',
dashboardTitlePrefix: 'Windows Exporter',
// The full dashboard has all tabs.
dashboardUID: self.dashboardUIDPrefix + '-full',
dashboardTitle: self.dashboardTitlePrefix + ' / Full',
dashboardDescription: 'Fleet overview and per-host details for Windows hosts monitored by windows_exporter.',
// The dashboards link to each other through their tags.
dashboardTags: ['prometheus', 'windows', 'windows_exporter'],
// Dashboard tabs that don't belong to a single collector. Turn them off
+9 -4
View File
@@ -1,8 +1,13 @@
// Renders dashboard/windows-exporter-dashboard.json. Run with jsonnet -J vendor -S.
// The sample dashboard shows every collector; mixin users select theirs with
// _config.collectors.
// Renders the sample dashboards into ../../dashboard. Run with
// jsonnet -J vendor -S -m <directory>. The sample dashboards show every
// collector; mixin users select theirs with _config.collectors.
local manifest = import 'lib/manifest.libsonnet';
local mixin = (import 'mixin.libsonnet') + {
_config+:: { collectors: { [collector]: true for collector in std.objectFields(super.collectors) } },
};
(import 'lib/manifest.libsonnet')(mixin.grafanaDashboards['windows-exporter.json'])
// The full dashboard keeps the file name it had before the tab dashboards.
{
[if name == 'windows-exporter.json' then 'windows-exporter-dashboard.json' else name]: manifest(mixin.grafanaDashboards[name])
for name in std.objectFields(mixin.grafanaDashboards)
}
@@ -1,5 +1,4 @@
{
grafanaDashboards+:: {
'windows-exporter.json': (import 'windows-exporter.libsonnet')($._config),
},
local dashboards = (import 'windows-exporter.libsonnet')($._config),
grafanaDashboards+:: { 'windows-exporter.json': dashboards.full } + dashboards.tabs,
}
+24 -18
View File
@@ -8,7 +8,9 @@ local volumeFree = '100 * windows_logical_disk_free_bytes{job=~"$job", volume!~"
local attention = [{ color: 'green', value: null }, { color: 'orange', value: 1 }];
// config supplies the alert thresholds, so a counter turns orange when the matching alert condition holds.
function(on, config)
// Hostnames link to the Overview tab of this dashboard, or to the dashboard with
// the UID overview when it is given. Without the Overview tab, they link nowhere.
function(on, config, overview=null)
b.tab(
'Fleet',
b.rows([
@@ -171,23 +173,27 @@ function(on, config)
options: 'hostname',
},
properties: [
{
id: 'links',
value: [
{
title: 'Show details for ${__data.fields.hostname}',
url: '/d/${__dashboard.uid}?${__url_time_range}&${datasource:queryparam}&${job:queryparam}&var-hostname=$__all&var-instance=${__data.fields.instance}&dtab=Overview',
},
],
},
{
id: 'custom.width',
value: 180,
},
{
id: 'custom.filterable',
value: true,
},
property
for property in [
if config.enableOverviewTab then {
id: 'links',
value: [
{
title: 'Show details for ${__data.fields.hostname}',
url: '/d/' + (if overview == null then '${__dashboard.uid}' else overview) + '?${__url_time_range}&${datasource:queryparam}&${job:queryparam}&var-hostname=$__all&var-instance=${__data.fields.instance}' + (if overview == null then '&dtab=Overview' else ''),
},
],
},
{
id: 'custom.width',
value: 180,
},
{
id: 'custom.filterable',
value: true,
},
]
if property != null
],
},
{
@@ -70,9 +70,6 @@ function(on) [variable for variable in [
new('instance', 'label_values(windows_os_hostname{job=~"$job", hostname=~"$hostname"}, instance)')
+ query.spec.withLabel('Instance')
,
new('show_hostname', 'label_values(windows_os_hostname{job=~"$job", instance="$instance"}, hostname)')
+ query.spec.withHide('hideVariable')
,
if on('logical_disk') then new('volume', 'label_values(windows_logical_disk_size_bytes{job=~"$job", instance="$instance"}, volume)')
+ query.spec.withCurrent({
text: [
@@ -1,10 +1,42 @@
local b = import 'builders.libsonnet';
local dashboard = b.dashboard;
// config.collectors selects the collectors the dashboard renders,
// Descriptions of the dashboards of single tabs, by tab title.
local descriptions = {
Fleet: 'Health, utilization and stopped services of all Windows hosts monitored by windows_exporter.',
Overview: 'Operating system, uptime, CPU, memory, volumes, disk and network throughput of one Windows host.',
CPU: 'CPU utilization by mode and core, scheduling, system calls and frequency of one Windows host.',
Memory: 'Physical memory, commit charge, paging and kernel memory of one Windows host.',
Disk: 'Volumes, physical disks, drives and Storage Spaces of one Windows host.',
Network: 'Interface throughput, errors and discards, TCP and UDP of one Windows host.',
SMB: 'SMB shares served and used by one Windows host.',
Processes: 'CPU, memory, I/O, threads and handles per process of one Windows host.',
Services: 'Service states and automatic services that are not running on one Windows host.',
'Scheduled tasks': 'Scheduled task states, results and missed runs of one Windows host.',
Updates: 'Pending Windows updates and the last update check of one Windows host.',
GPU: 'GPU utilization, memory, sensors and top processes of one Windows host.',
'Hyper-V': 'Hyper-V virtual machines, virtual disks, switches and network adapters of one Windows host.',
Time: 'Clock synchronization, NTP sources and clock offset of one Windows host.',
Exporter: 'Scrape status, collector health and resource usage of windows_exporter on one host.',
};
local wordChar(c) = std.member('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_', c);
// The leading identifier of text.
local identifier(text) =
local chars = std.stringChars(std.substr(text, 0, 32));
local length = std.foldl(function(acc, c) if acc.done || !wordChar(c) then acc { done: true } else acc { n+: 1 }, chars, { n: 0, done: false }).n;
std.substr(text, 0, length);
// The variable names that value references as $name or ${name...}.
local references(value) = std.set([
identifier(if std.startsWith(part, '{') then std.substr(part, 1, 33) else part)
for part in std.split(std.manifestJsonMinified(value), '$')[1:]
]);
// config.collectors selects the collectors the dashboards render,
// config.enableFleetTab and config.enableOverviewTab the tabs across
// collectors, and config.dashboard* its UID, title, description and tags, see
// config.libsonnet.
// collectors, and config.dashboard* their UIDs, titles, description and tags,
// see config.libsonnet. Returns the full dashboard with all tabs and, with
// config.enableTabDashboards, a dashboard per tab by its file name.
function(config)
local known = std.objectFields((import '../config.libsonnet')._config.collectors);
local unknown = std.setDiff(std.objectFields(config.collectors), known);
@@ -15,10 +47,14 @@ function(config)
std.get(config.collectors, collector, false);
// All variables select hosts through windows_os_hostname.
assert on('os') : 'The dashboard requires the os collector';
local tabs = [
local slug(title) = std.strReplace(std.asciiLower(title), ' ', '-');
local uid(title) = config.dashboardUIDPrefix + '-' + slug(title);
// overview is the UID of the Overview dashboard that Fleet links hosts to,
// or null for the Overview tab of the same dashboard.
local tabs(overview) = [
tab
for tab in [
if config.enableFleetTab then (import 'fleet.libsonnet')(on, config),
if config.enableFleetTab then (import 'fleet.libsonnet')(on, config, overview),
if config.enableOverviewTab then (import 'overview.libsonnet')(on),
(import 'cpu.libsonnet')(on),
(import 'memory.libsonnet')(on),
@@ -36,51 +72,113 @@ function(config)
]
if tab != null && !tab.empty
];
local fullTabs = tabs(null);
local panelNames = std.flattenArrays([std.objectFields(tab.elements) for tab in tabs]);
local panelNames = std.flattenArrays([std.objectFields(tab.elements) for tab in fullTabs]);
assert std.length(panelNames) == std.length(std.set(panelNames)) : 'Panel IDs must be unique across tabs';
dashboard.new(config.dashboardUID, config.dashboardTitle)
+ dashboard.spec.withCursorSync('Crosshair')
+ dashboard.spec.withDescription(config.dashboardDescription)
+ dashboard.spec.withEditable(true)
+ dashboard.spec.withLinks([
{
asDropdown: false,
icon: 'doc',
includeVars: false,
keepTime: false,
tags: [],
targetBlank: true,
title: 'windows_exporter',
tooltip: '',
type: 'link',
url: 'https://github.com/prometheus-community/windows_exporter',
},
])
+ dashboard.spec.withLiveNow(false)
+ dashboard.spec.withPreload(false)
+ dashboard.spec.withTags(config.dashboardTags)
+ dashboard.spec.withTimeSettings({
autoRefresh: '1m',
autoRefreshIntervals: [
'5s',
'10s',
'30s',
'1m',
'5m',
'15m',
'30m',
'1h',
'2h',
'1d',
],
fiscalYearStartMonth: 0,
from: 'now-3h',
hideTimepicker: false,
to: 'now',
})
+ dashboard.spec.withAnnotations((import 'annotations.libsonnet')(on))
+ dashboard.spec.withVariables((import 'variables.libsonnet')(on))
+ dashboard.spec.withElements(std.foldl(function(elements, tab) elements + tab.elements, tabs, {}))
+ dashboard.spec.withLayout(b.tabs([tab.layout for tab in tabs]))
local variables = (import 'variables.libsonnet')(on);
local annotations = (import 'annotations.libsonnet')(on);
// Tab dashboards link to each other through a dropdown of the dashboards
// with the same tags; the full dashboard has the tabs instead.
local new(uid, title, description, elements, layout, dropdown=true) =
// Grafana rejects longer dashboard UIDs.
assert std.length(uid) <= 40 : 'Dashboard UID ' + uid + ' is longer than 40 characters; shorten dashboardUIDPrefix or dashboardUID';
// Keeps the variables that the panels reference, directly or through
// other variables, and the annotations whose variables are kept.
// Variables only reference variables defined before them.
local kept = std.foldl(
function(used, variable)
if std.setMember(variable.spec.name, used)
then std.setUnion(used, references(variable))
else used,
std.reverse(variables),
references(elements),
);
local dropped = std.set([variable.spec.name for variable in variables if !std.setMember(variable.spec.name, kept)]);
dashboard.new(uid, title)
+ dashboard.spec.withCursorSync('Crosshair')
+ dashboard.spec.withDescription(description)
+ dashboard.spec.withEditable(true)
+ dashboard.spec.withLinks([
link
for link in [
if dropdown then {
asDropdown: true,
icon: 'external link',
includeVars: true,
keepTime: true,
tags: config.dashboardTags,
targetBlank: false,
title: config.dashboardTitlePrefix,
tooltip: 'Dashboards for windows_exporter',
type: 'dashboards',
url: '',
},
{
asDropdown: false,
icon: 'doc',
includeVars: false,
keepTime: false,
tags: [],
targetBlank: true,
title: 'windows_exporter',
tooltip: '',
type: 'link',
url: 'https://github.com/prometheus-community/windows_exporter',
},
]
if link != null
])
+ dashboard.spec.withLiveNow(false)
+ dashboard.spec.withPreload(false)
+ dashboard.spec.withTags(config.dashboardTags)
+ dashboard.spec.withTimeSettings({
autoRefresh: '1m',
autoRefreshIntervals: [
'5s',
'10s',
'30s',
'1m',
'5m',
'15m',
'30m',
'1h',
'2h',
'1d',
],
fiscalYearStartMonth: 0,
from: 'now-3h',
hideTimepicker: false,
to: 'now',
})
+ dashboard.spec.withAnnotations([
annotation
for annotation in annotations
if std.length(std.setInter(references(annotation), dropped)) == 0
])
+ dashboard.spec.withVariables([variable for variable in variables if std.setMember(variable.spec.name, kept)])
+ dashboard.spec.withElements(elements)
+ dashboard.spec.withLayout(layout);
{
full: new(
config.dashboardUID,
config.dashboardTitle,
config.dashboardDescription,
std.foldl(function(elements, tab) elements + tab.elements, fullTabs, {}),
b.tabs([tab.layout for tab in fullTabs]),
dropdown=false,
),
tabs: if config.enableTabDashboards then {
['windows-exporter-' + slug(tab.layout.spec.title) + '.json']: new(
uid(tab.layout.spec.title),
config.dashboardTitlePrefix + ' / ' + tab.layout.spec.title,
descriptions[tab.layout.spec.title],
tab.elements,
tab.layout.spec.layout,
)
for tab in tabs(if config.enableOverviewTab then uid('Overview'))
} else {},
}
+32 -12
View File
@@ -8,7 +8,7 @@
[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"
dashboards = "../../dashboard"
[tasks.generate]
description = "Generate Prometheus rules, alerts, and the dashboard"
@@ -27,24 +27,35 @@ description = "Install dashboard Jsonnet libraries"
run = "jb install"
[tasks.dashboard]
description = "Regenerate the committed Grafana dashboard"
description = "Regenerate the committed Grafana dashboards"
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}}
out=$(mktemp -d)
trap 'rm -rf "$out"' EXIT
jsonnet -J vendor -S -m "$out" dashboard.jsonnet > /dev/null
rm -f {{vars.dashboards}}/windows-exporter-*.json
cp "$out"/*.json {{vars.dashboards}}/
'''
[tasks.check-dashboards]
description = "Regenerate the dashboard and fail on uncommitted changes"
description = "Regenerate the dashboards and fail on uncommitted changes"
depends = ["dashboard"]
run = "git diff --exit-code -- {{vars.dashboard}}"
run = '''
set -eu
git add --intent-to-add -- {{vars.dashboards}}/windows-exporter-*.json
git diff --exit-code -- {{vars.dashboards}}
'''
[tasks.lint-dashboards]
description = "Lint the committed dashboard in strict mode"
run = "dashboard-linter lint --strict --config .dashboard-lint.yaml {{vars.dashboard}}"
description = "Lint the committed dashboards in strict mode"
run = '''
set -eu
for file in {{vars.dashboards}}/windows-exporter-*.json; do
dashboard-linter lint --strict --config .dashboard-lint.yaml "$file"
done
'''
[tasks.fmt]
description = "Format all mixin Jsonnet sources"
@@ -59,15 +70,24 @@ run = [
]
[tasks.lint]
description = "Check Jsonnet formatting, dashboard drift, and Prometheus rules"
description = "Check Jsonnet formatting and lint, 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" -
jsonnet-lint -J vendor "$file"
done
out=$(mktemp -d)
trap 'rm -rf "$out"' EXIT
jsonnet -J vendor -S -m "$out" dashboard.jsonnet > /dev/null
(cd "$out" && ls windows-exporter-*.json) > "$out/generated"
(cd {{vars.dashboards}} && ls windows-exporter-*.json) | diff -u "$out/generated" - \
|| { echo "{{vars.dashboards}} has missing or stale dashboards; run mise run dashboard"; exit 1; }
for file in $(cat "$out/generated"); do
diff -u {{vars.dashboards}}/"$file" "$out/$file" \
|| { echo "{{vars.dashboards}}/$file is out of date; run mise run dashboard"; exit 1; }
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
+57 -3
View File
@@ -1,6 +1,7 @@
// Renders the dashboard for several collector selections and checks that every
// layout reference has an element, every element is placed and no tab or row is
// empty. Run with jsonnet -J vendor tests/dashboard.test.jsonnet.
// Renders the dashboards for several collector selections and checks that every
// layout reference has an element, every element is placed, no tab or row is
// empty and every tab dashboard matches its tab. Run with
// jsonnet -J vendor tests/dashboard.test.jsonnet.
local mixin = import '../mixin.libsonnet';
local render(collectors, config={}) =
@@ -55,6 +56,59 @@ assert metadata.spec.title == 'Custom title' : 'metadata: got title ' + metadata
assert metadata.spec.description == 'Custom description' : 'metadata: got description ' + metadata.spec.description;
assert metadata.spec.tags == ['prometheus', 'windows', 'windows_exporter', 'custom'] : 'metadata: got tags ' + std.manifestJson(metadata.spec.tags);
// Every tab renders as a dashboard of its own with the panels and rows of that
// tab and only the variables its panels use.
local dashboards(collectors, config={}) = (mixin { _config+:: { collectors: collectors(super.collectors) } + config }).grafanaDashboards;
local defaults = dashboards(function(collectors) collectors);
local full = defaults['windows-exporter.json'];
assert full.metadata.name == 'windows-exporter-full' : 'full: got UID ' + full.metadata.name;
assert full.spec.title == 'Windows Exporter / Full' : 'full: got title ' + full.spec.title;
assert std.objectFields(defaults) == [
'windows-exporter-cpu.json',
'windows-exporter-disk.json',
'windows-exporter-exporter.json',
'windows-exporter-fleet.json',
'windows-exporter-memory.json',
'windows-exporter-network.json',
'windows-exporter-overview.json',
'windows-exporter-services.json',
'windows-exporter.json',
] : 'tab dashboards: got ' + std.join(', ', std.objectFields(defaults));
assert std.all([
local tab = std.filter(function(tab) tab.spec.title == dashboard.spec.title[std.length('Windows Exporter / '):], full.spec.layout.spec.tabs)[0];
dashboard.spec.layout == tab.spec.layout
&& std.objectFields(dashboard.spec.elements) == std.set(placed(tab.spec.layout))
&& std.startsWith(dashboard.metadata.name, 'windows-exporter-')
for dashboard in [defaults[name] for name in std.objectFields(defaults) if name != 'windows-exporter.json']
]) : 'A tab dashboard differs from its tab';
local linkTypes(dashboard) = [link.type for link in dashboard.spec.links];
assert linkTypes(full) == ['link'] : 'full: got links ' + std.join(', ', linkTypes(full));
assert linkTypes(defaults['windows-exporter-cpu.json']) == ['dashboards', 'link'] : 'cpu: got links ' + std.join(', ', linkTypes(defaults['windows-exporter-cpu.json']));
local variableNames(dashboard) = [variable.spec.name for variable in dashboard.spec.variables];
local annotationNames(dashboard) = [annotation.spec.name for annotation in dashboard.spec.annotations];
local fleet = defaults['windows-exporter-fleet.json'];
assert variableNames(fleet) == ['datasource', 'job', 'hostname'] : 'fleet: got variables ' + std.join(', ', variableNames(fleet));
assert annotationNames(fleet) == ['Annotations & Alerts'] : 'fleet: got annotations ' + std.join(', ', annotationNames(fleet));
assert std.length(std.findSubstr('/d/windows-exporter-overview?', std.manifestJson(fleet.spec.elements))) == 1 : 'fleet: hosts do not link to the Overview dashboard';
assert std.length(std.findSubstr('dtab=Overview', std.manifestJson(full.spec.elements))) == 1 : 'full: hosts do not link to the Overview tab';
// Without the Overview tab, hostnames in Fleet link nowhere.
local withoutOverview = dashboards(function(collectors) collectors, { enableOverviewTab: false });
assert std.all([
std.length(std.findSubstr('Show details for', std.manifestJson(dashboard.spec.elements))) == 0
for dashboard in [withoutOverview['windows-exporter.json'], withoutOverview['windows-exporter-fleet.json']]
]) : 'fleet without overview: hostnames link to a missing Overview';
assert !std.objectHas(withoutOverview, 'windows-exporter-overview.json') : 'fleet without overview: got an Overview dashboard';
local cpu = defaults['windows-exporter-cpu.json'];
assert variableNames(cpu) == ['datasource', 'job', 'hostname', 'instance'] : 'cpu: got variables ' + std.join(', ', variableNames(cpu));
assert annotationNames(cpu) == ['Annotations & Alerts', 'Reboots'] : 'cpu: got annotations ' + std.join(', ', annotationNames(cpu));
local processes = dashboards(only({ process: true }))['windows-exporter-processes.json'];
assert variableNames(processes) == ['datasource', 'job', 'hostname', 'instance', 'process', 'process_top'] : 'processes: got variables ' + std.join(', ', variableNames(processes));
local custom = dashboards(function(collectors) collectors, { dashboardUIDPrefix: 'win', dashboardTitlePrefix: 'Windows' });
assert custom['windows-exporter-cpu.json'].metadata.name == 'win-cpu' : 'custom prefix: got UID ' + custom['windows-exporter-cpu.json'].metadata.name;
assert custom['windows-exporter-cpu.json'].spec.title == 'Windows / CPU' : 'custom prefix: got title ' + custom['windows-exporter-cpu.json'].spec.title;
assert custom['windows-exporter.json'].metadata.name == 'win-full' : 'custom prefix: got full UID ' + custom['windows-exporter.json'].metadata.name;
assert std.objectFields(dashboards(function(collectors) collectors, { enableTabDashboards: false })) == ['windows-exporter.json'] : 'enableTabDashboards: false renders tab dashboards';
assert std.length(std.objectFields(results.all)) == 15 : 'all: got ' + std.join(', ', std.objectFields(results.all));
assert expect('defaults', {
CPU: ['Summary', 'Utilization', 'Scheduling', 'System calls and frequency'],