From ad8f84d4cf4b7bf2ccb13235e8eefd0d9cbb9e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan-Otto=20Kr=C3=B6pke?= Date: Fri, 9 Oct 2026 10:36:45 +0200 Subject: [PATCH] dashboard: factor Grafonnet builders into tab modules (#2595) 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 | 3 + contrib/mixin/Makefile | 13 +- contrib/mixin/README.md | 40 +- .../mixin/dashboards/annotations.libsonnet | 30 + contrib/mixin/dashboards/builders.libsonnet | 107 ++ contrib/mixin/dashboards/cpu.libsonnet | 188 +++ contrib/mixin/dashboards/disk.libsonnet | 319 ++++ contrib/mixin/dashboards/exporter.libsonnet | 659 ++++++++ contrib/mixin/dashboards/fleet.libsonnet | 710 +++++++++ contrib/mixin/dashboards/gpu.libsonnet | 279 ++++ contrib/mixin/dashboards/hyper_v.libsonnet | 380 +++++ contrib/mixin/dashboards/memory.libsonnet | 135 ++ contrib/mixin/dashboards/network.libsonnet | 108 ++ contrib/mixin/dashboards/overview.libsonnet | 490 ++++++ contrib/mixin/dashboards/services.libsonnet | 134 ++ contrib/mixin/dashboards/styles.libsonnet | 197 +++ contrib/mixin/dashboards/time.libsonnet | 108 ++ contrib/mixin/dashboards/variables.libsonnet | 102 ++ .../dashboards/windows-exporter.libsonnet | 1410 +---------------- contrib/mixin/lib/dashboard.libsonnet | 467 ------ dashboard/README.md | 6 +- 21 files changed, 4059 insertions(+), 1826 deletions(-) create mode 100644 contrib/mixin/dashboards/annotations.libsonnet create mode 100644 contrib/mixin/dashboards/builders.libsonnet create mode 100644 contrib/mixin/dashboards/cpu.libsonnet create mode 100644 contrib/mixin/dashboards/disk.libsonnet create mode 100644 contrib/mixin/dashboards/exporter.libsonnet create mode 100644 contrib/mixin/dashboards/fleet.libsonnet create mode 100644 contrib/mixin/dashboards/gpu.libsonnet create mode 100644 contrib/mixin/dashboards/hyper_v.libsonnet create mode 100644 contrib/mixin/dashboards/memory.libsonnet create mode 100644 contrib/mixin/dashboards/network.libsonnet create mode 100644 contrib/mixin/dashboards/overview.libsonnet create mode 100644 contrib/mixin/dashboards/services.libsonnet create mode 100644 contrib/mixin/dashboards/styles.libsonnet create mode 100644 contrib/mixin/dashboards/time.libsonnet create mode 100644 contrib/mixin/dashboards/variables.libsonnet delete mode 100644 contrib/mixin/lib/dashboard.libsonnet diff --git a/.github/workflows/mixin.yml b/.github/workflows/mixin.yml index ad39f460..00b96205 100644 --- a/.github/workflows/mixin.yml +++ b/.github/workflows/mixin.yml @@ -53,6 +53,9 @@ jobs: if: steps.tools-cache.outputs.cache-hit != 'true' run: make tools + - name: Build dashboard and check generated changes + run: make 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" diff --git a/contrib/mixin/Makefile b/contrib/mixin/Makefile index fef34f4c..1fc56550 100644 --- a/contrib/mixin/Makefile +++ b/contrib/mixin/Makefile @@ -17,9 +17,11 @@ DASHBOARD := ../../dashboard/windows-exporter-dashboard.json .DEFAULT_GOAL := generate .DELETE_ON_ERROR: -.PHONY: generate dashboard tools jsonnet-tools fmt lint test check-runbooks clean +.PHONY: generate rules dashboard dashboards check-dashboards tools jsonnet-tools fmt lint test check-runbooks clean -generate: windows_rules.yaml windows_alerts.yaml +generate: rules dashboard + +rules: windows_rules.yaml windows_alerts.yaml tools: jsonnet-tools go install github.com/cloudflare/pint/cmd/pint@$(PINT_VERSION) @@ -50,10 +52,15 @@ 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: generate tests/custom_rules.yaml tests/custom_alerts.yaml vendor +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 diff --git a/contrib/mixin/README.md b/contrib/mixin/README.md index 3fe36d6b..bc8b710f 100644 --- a/contrib/mixin/README.md +++ b/contrib/mixin/README.md @@ -13,7 +13,7 @@ and [pint](https://github.com/cloudflare/pint) tools: cd contrib/mixin make tools export PATH="$(go env GOPATH)/bin:$PATH" -make generate +make rules ``` Only the dashboard depends on Jsonnet libraries, so `jb install` is not required @@ -201,15 +201,35 @@ Fork users should override `runbookURLPattern` to match their published URL. It is a Grafana v2 dashboard with tabs and needs Grafana 13 or later. See the [dashboard documentation](../../dashboard/README.md) for its content. -The dashboard is built with [grafonnet](https://github.com/grafana/grafonnet), +The dashboard uses additive builders with [grafonnet](https://github.com/grafana/grafonnet), pinned in `jsonnetfile.json`: -- `dashboards/windows-exporter.libsonnet` defines the tabs, panels and queries. -- `lib/dashboard.libsonnet` wraps grafonnet panel options and queries into v2 - panels, and lays them out in tabs, rows and grids. +- `dashboards/windows-exporter.libsonnet` composes the dashboard, with one source + file per tab for its panels, queries, rows, and grid positions. +- `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. +- `dashboards/styles.libsonnet` shares the visualization defaults; individual + panels add their differences using `withDefaults`, `withOptions`, and + `withOverrides`. Explicit nulls are preserved and arrays are replaced whole. +- `dashboards/variables.libsonnet` and `dashboards/annotations.libsonnet` use + native Grafonnet v2 builders for variables and annotations. - `lib/manifest.libsonnet` renders JSON with the four-space indentation of the committed file. +For example, a panel can be built with: + +```jsonnet +local b = import 'dashboards/builders.libsonnet'; + +b.panel.new(200, 'CPU usage', 'timeseries') ++ b.panel.withQueries([ + b.query.new('windows:cpu_usage:ratio{instance="$instance"}') + + b.query.withLegendFormat('CPU'), +]) ++ b.panel.withDefaults({ unit: 'percentunit', min: 0, max: 1 }) +``` + Regenerate the committed file after changing the dashboard source: ```sh @@ -217,7 +237,15 @@ make dashboard ``` This runs `jb install` and writes `../../dashboard/windows-exporter-dashboard.json`. -The dashboard uses its own variables and does not read `_config`. +`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`. + +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 +any drift. CI runs that check before the mixin tests. The dashboard uses its own +variables and does not read `_config`. ## Validate diff --git a/contrib/mixin/dashboards/annotations.libsonnet b/contrib/mixin/dashboards/annotations.libsonnet new file mode 100644 index 00000000..a89bfe57 --- /dev/null +++ b/contrib/mixin/dashboards/annotations.libsonnet @@ -0,0 +1,30 @@ +local b = import 'builders.libsonnet'; +local annotation = b.dashboard.spec.annotations; + +[ + annotation.withKind() + + annotation.spec.withBuiltIn(true) + + annotation.spec.withEnable(true) + + annotation.spec.withHide(true) + + annotation.spec.withIconColor('rgba(0, 211, 255, 1)') + + annotation.spec.withLegacyOptions({ + type: 'dashboard', + }) + + annotation.spec.withName('Annotations & Alerts') + + annotation.spec.withQuery(b.dataQuery({}, 'grafana', '-- Grafana --')) + , + annotation.withKind() + + annotation.spec.withEnable(true) + + annotation.spec.withHide(false) + + annotation.spec.withIconColor('orange') + + annotation.spec.withLegacyOptions({ + expr: 'windows_system_boot_time_timestamp{job=~"$job", instance="$instance"} * 1000 > $__from < $__to', + step: '1m', + tagKeys: 'instance', + textFormat: '', + titleFormat: 'Reboot', + useValueForTime: 'on', + }) + + annotation.spec.withName('Reboots') + + annotation.spec.withQuery(b.dataQuery({}, 'prometheus', '${datasource}')), +] diff --git a/contrib/mixin/dashboards/builders.libsonnet b/contrib/mixin/dashboards/builders.libsonnet new file mode 100644 index 00000000..01b21a41 --- /dev/null +++ b/contrib/mixin/dashboards/builders.libsonnet @@ -0,0 +1,107 @@ +local g = import 'github.com/grafana/grafonnet/gen/grafonnet-v13.0.0/main.libsonnet'; +local v2 = g.apps.dashboard.v2; +local styles = import 'styles.libsonnet'; +local grid = v2.spec.layout.GridLayoutKind; +local rows = v2.spec.layout.RowsLayoutKind; +local tabs = v2.spec.layout.TabsLayoutKind; +local item = grid.spec.items; +local row = rows.spec.rows; +local tab = tabs.spec.tabs; +local prometheus = g.query.prometheus; + +// Unlike std.mergePatch, this keeps explicit nulls (notably threshold steps). +local merge(base, patch) = base + { + [key]: if std.objectHas(base, key) && std.isObject(base[key]) && std.isObject(patch[key]) + then merge(base[key], patch[key]) else patch[key] + for key in std.objectFields(patch) +}; + +{ + dashboard: v2, + + // Grafonnet's v2 dashboard accepts elements but has no Panel/QueryGroup constructor. + // Keep that small schema boundary here; tab sources use the same additive builders. + panel: { + new(id, title, visualization): { + kind: 'Panel', + spec: { + id: id, + title: title, + description: '', + links: [], + data: { + kind: 'QueryGroup', + spec: { queries: [], queryOptions: {}, transformations: [] }, + }, + vizConfig: { + kind: 'VizConfig', + group: visualization, + version: '', + spec: { + fieldConfig: { defaults: styles[visualization].defaults, overrides: [] }, + options: styles[visualization].options, + }, + }, + }, + }, + withDescription(value): { spec+: { description: value } }, + withLinks(value): { spec+: { links: value } }, + withQueries(value): { spec+: { data+: { spec+: { queries: value } } } }, + withQueryOptions(value): { spec+: { data+: { spec+: { queryOptions: value } } } }, + withTransformations(value): { spec+: { data+: { spec+: { transformations: value } } } }, + withDefaults(value): { + spec+: { vizConfig+: { spec+: { fieldConfig+: { defaults: merge(super.defaults, value) } } } }, + }, + withOptions(value): { spec+: { vizConfig+: { spec+: { options: merge(super.options, value) } } } }, + withOverrides(value): { spec+: { vizConfig+: { spec+: { fieldConfig+: { overrides: value } } } } }, + }, + + dataQuery(spec, group='prometheus', datasource='${datasource}'): { + kind: 'DataQuery', + group: group, + version: 'v0', + datasource: { name: datasource }, + spec: spec, + }, + + query: { + new(expr, refId='A'): { + kind: 'PanelQuery', + spec: { + hidden: false, + refId: refId, + query: $.dataQuery( + prometheus.withExpr(expr) + + prometheus.withEditorMode('code') + + prometheus.withInstant(false) + + prometheus.withRange(true) + + prometheus.withLegendFormat('__auto') + ), + }, + }, + withLegendFormat(value): { + spec+: { query+: { spec+: prometheus.withLegendFormat(value) } }, + }, + withInstant(value=true): { + spec+: { query+: { spec+: prometheus.withInstant(value) + prometheus.withRange(!value) } }, + }, + withRange(value): { spec+: { query+: { spec+: prometheus.withRange(value) } } }, + withFormat(value): { spec+: { query+: { spec+: prometheus.withFormat(value) } } }, + }, + + grid(items): (grid.withKind() + grid.spec.withItems(items)).spec.layout, + rows(rows_): (rows.withKind() + rows.spec.withRows(rows_)).spec.layout, + tabs(tabs_): (tabs.withKind() + tabs.spec.withTabs(tabs_)).spec.layout, + place(id, x, y, width, height): + item.withKind() + + item.spec.withElement({ kind: 'ElementReference', name: 'panel-' + id }) + + item.spec.withX(x) + + item.spec.withY(y) + + item.spec.withWidth(width) + + item.spec.withHeight(height), + row(title, layout): row.withKind() + row.withSpec({ title: title, collapse: false, layout: layout }), + tab(title, layout, panels): { + elements: { ['panel-' + panel.spec.id]: panel for panel in panels }, + layout: tab.withKind() + tab.withSpec({ title: title, layout: layout }), + }, +} diff --git a/contrib/mixin/dashboards/cpu.libsonnet b/contrib/mixin/dashboards/cpu.libsonnet new file mode 100644 index 00000000..cc3a14af --- /dev/null +++ b/contrib/mixin/dashboards/cpu.libsonnet @@ -0,0 +1,188 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'CPU', + b.grid([ + b.place(24, 0, 0, 12, 10), + b.place(25, 12, 0, 12, 10), + b.place(26, 0, 10, 8, 8), + b.place(27, 8, 10, 8, 8), + b.place(28, 16, 10, 8, 8), + b.place(29, 0, 18, 12, 8), + b.place(30, 12, 18, 12, 8), + ]), + [ + b.panel.new(24, 'CPU utilization by mode', 'timeseries') + + b.panel.withDescription('Share of total CPU time across all logical processors.') + + b.panel.withQueries([ + b.query.new('sum by (mode) (rate(windows_cpu_time_total{job=~"$job", instance="$instance", mode!="idle"}[$__rate_interval])) / scalar(count(windows_cpu_time_total{job=~"$job", instance="$instance", mode="idle"}))', 'A') + + b.query.withLegendFormat('{{mode}}'), + ]) + + b.panel.withDefaults({ + custom: { + fillOpacity: 40, + stacking: { + mode: 'normal', + }, + }, + max: 1, + min: 0, + }), + + b.panel.new(25, 'CPU utilization per core', 'state-timeline') + + b.panel.withDescription('Busy time of each logical processor, labelled processor group,core. Green is idle, red is fully busy.') + + b.panel.withQueries([ + b.query.new('label_replace(clamp(1 - rate(windows_cpu_time_total{job=~"$job", instance="$instance", mode="idle"}[$__rate_interval]), 0, 1), "core", "$1,0$2", "core", "(\\\\d+),(\\\\d)")', 'A') + + b.query.withLegendFormat('{{core}}'), + ]) + + b.panel.withDefaults({ + color: { + mode: 'continuous-GrYlRd', + }, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + unit: 'percentunit', + }), + + b.panel.new(26, 'Processor queue length', 'timeseries') + + b.panel.withDescription('Threads that are ready to run but waiting for a CPU. A sustained value above 2 per core indicates CPU contention.') + + b.panel.withQueries([ + b.query.new('windows_system_processor_queue_length{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('queue length'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'short', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + + b.panel.new(27, 'Context switches and interrupts', 'timeseries') + + b.panel.withQueries([ + b.query.new('rate(windows_system_context_switches_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('context switches'), + b.query.new('sum(rate(windows_cpu_interrupts_total{job=~"$job", instance="$instance"}[$__rate_interval]))', 'B') + + b.query.withLegendFormat('interrupts'), + b.query.new('sum(rate(windows_cpu_dpcs_total{job=~"$job", instance="$instance"}[$__rate_interval]))', 'C') + + b.query.withLegendFormat('DPCs'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'ops', + }), + + b.panel.new(28, 'Processes and threads', 'timeseries') + + b.panel.withQueries([ + b.query.new('windows_system_processes{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('processes'), + b.query.new('windows_system_threads{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('threads'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'short', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'threads', + }, + properties: [ + { + id: 'custom.axisPlacement', + value: 'right', + }, + ], + }, + ]), + + b.panel.new(29, 'System calls and exceptions', 'timeseries') + + b.panel.withQueries([ + b.query.new('rate(windows_system_system_calls_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('system calls'), + b.query.new('rate(windows_system_exception_dispatches_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('exception dispatches'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'ops', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'exception dispatches', + }, + properties: [ + { + id: 'custom.axisPlacement', + value: 'right', + }, + ], + }, + ]), + + b.panel.new(30, 'CPU frequency', 'timeseries') + + b.panel.withDescription('Effective frequency from the APERF/MPERF counters, averaged and maximum across logical processors, against the nominal frequency. Values above nominal mean turbo boost.') + + b.panel.withQueries([ + b.query.new('avg(1e4 * windows_cpu_core_frequency_mhz{job=~"$job", instance="$instance"} * rate(windows_cpu_processor_performance_total{job=~"$job", instance="$instance"}[$__rate_interval]) / rate(windows_cpu_processor_mperf_total{job=~"$job", instance="$instance"}[$__rate_interval]))', 'A') + + b.query.withLegendFormat('average'), + b.query.new('max(1e4 * windows_cpu_core_frequency_mhz{job=~"$job", instance="$instance"} * rate(windows_cpu_processor_performance_total{job=~"$job", instance="$instance"}[$__rate_interval]) / rate(windows_cpu_processor_mperf_total{job=~"$job", instance="$instance"}[$__rate_interval]))', 'B') + + b.query.withLegendFormat('maximum'), + b.query.new('avg(windows_cpu_core_frequency_mhz{job=~"$job", instance="$instance"}) * 1e6', 'C') + + b.query.withLegendFormat('nominal'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'hertz', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'nominal', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'text', + mode: 'fixed', + }, + }, + { + id: 'custom.fillOpacity', + value: 0, + }, + { + id: 'custom.lineStyle', + value: { + dash: [ + 10, + 10, + ], + fill: 'dash', + }, + }, + ], + }, + ]), + ] +) diff --git a/contrib/mixin/dashboards/disk.libsonnet b/contrib/mixin/dashboards/disk.libsonnet new file mode 100644 index 00000000..2fe8f525 --- /dev/null +++ b/contrib/mixin/dashboards/disk.libsonnet @@ -0,0 +1,319 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Disk', + b.grid([ + b.place(37, 0, 0, 8, 8), + b.place(38, 8, 0, 8, 8), + b.place(39, 16, 0, 8, 8), + b.place(40, 0, 8, 12, 8), + b.place(41, 12, 8, 12, 8), + b.place(42, 0, 16, 12, 8), + b.place(43, 12, 16, 12, 8), + ]), + [ + b.panel.new(37, 'Volumes', 'table') + + b.panel.withQueries([ + b.query.new('windows_logical_disk_size_bytes{job=~"$job", instance="$instance", volume=~"$volume"}', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('windows_logical_disk_free_bytes{job=~"$job", instance="$instance", volume=~"$volume"}', 'B') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('100 * (1 - windows_logical_disk_free_bytes{job=~"$job", instance="$instance", volume=~"$volume"} / windows_logical_disk_size_bytes{job=~"$job", instance="$instance", volume=~"$volume"})', 'C') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withTransformations([ + { + group: 'joinByField', + kind: 'Transformation', + spec: { + options: { + byField: 'volume', + mode: 'outer', + }, + }, + }, + { + group: 'organize', + kind: 'Transformation', + spec: { + options: { + excludeByName: { + Time: true, + 'Time 1': true, + 'Time 2': true, + 'Time 3': true, + __name__: true, + instance: true, + 'instance 1': true, + 'instance 2': true, + 'instance 3': true, + job: true, + 'job 1': true, + 'job 2': true, + 'job 3': true, + port: true, + 'port 1': true, + 'port 2': true, + 'port 3': true, + }, + includeByName: {}, + indexByName: { + 'Value #A': 1, + 'Value #B': 2, + 'Value #C': 3, + volume: 0, + }, + renameByName: { + 'Value #A': 'Size', + 'Value #B': 'Free', + 'Value #C': 'Used', + volume: 'Volume', + }, + }, + }, + }, + ]) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'Value #A', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #B', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #C', + }, + properties: [ + { + id: 'unit', + value: 'percent', + }, + { + id: 'min', + value: 0, + }, + { + id: 'max', + value: 100, + }, + { + id: 'thresholds', + value: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + }, + { + id: 'color', + value: { + mode: 'continuous-GrYlRd', + }, + }, + { + id: 'custom.cellOptions', + value: { + mode: 'basic', + type: 'gauge', + valueDisplayMode: 'text', + }, + }, + { + id: 'decimals', + value: 1, + }, + ], + }, + ]), + + b.panel.new(38, 'Volume usage', 'timeseries') + + b.panel.withQueries([ + b.query.new('100 * (1 - windows_logical_disk_free_bytes{job=~"$job", instance="$instance", volume=~"$volume"} / windows_logical_disk_size_bytes{job=~"$job", instance="$instance", volume=~"$volume"})', 'A') + + b.query.withLegendFormat('{{volume}}'), + ]) + + b.panel.withDefaults({ + custom: { + thresholdsStyle: { + mode: 'dashed', + }, + }, + max: 100, + min: 0, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + unit: 'percent', + }) + + b.panel.withOptions({ + legend: { + calcs: [ + 'min', + 'max', + 'lastNotNull', + ], + }, + }), + + b.panel.new(39, 'Volume busy time', 'timeseries') + + b.panel.withDescription('Share of time the volume was servicing requests (1 - idle time).') + + b.panel.withQueries([ + b.query.new('1 - clamp_max(rate(windows_logical_disk_idle_seconds_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval]), 1)', 'A') + + b.query.withLegendFormat('{{volume}}'), + ]) + + b.panel.withDefaults({ + max: 1, + min: 0, + }), + + b.panel.new(40, 'Disk throughput', 'timeseries') + + b.panel.withDescription('Reads are drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_logical_disk_write_bytes_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{volume}} write'), + b.query.new('rate(windows_logical_disk_read_bytes_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{volume}} read'), + ]) + + b.panel.withDefaults({ + unit: 'Bps', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* read$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(41, 'Disk IOPS', 'timeseries') + + b.panel.withDescription('Reads are drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_logical_disk_writes_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{volume}} write'), + b.query.new('rate(windows_logical_disk_reads_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{volume}} read'), + ]) + + b.panel.withDefaults({ + unit: 'iops', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* read$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(42, 'Disk latency', 'timeseries') + + b.panel.withDescription('Average time per read and write operation. Reads are drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_logical_disk_write_seconds_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval]) / rate(windows_logical_disk_writes_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{volume}} write'), + b.query.new('rate(windows_logical_disk_read_seconds_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval]) / rate(windows_logical_disk_reads_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{volume}} read'), + ]) + + b.panel.withDefaults({ + unit: 's', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* read$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(43, 'Disk queue length', 'timeseries') + + b.panel.withDescription('Average number of read and write requests queued for the volume. Reads are drawn below the axis. A queue that stays high while latency rises indicates a disk bottleneck. The avg_*_requests_queued metrics grow like counters, so the panel takes their rate.') + + b.panel.withQueries([ + b.query.new('rate(windows_logical_disk_avg_write_requests_queued{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{volume}} write'), + b.query.new('rate(windows_logical_disk_avg_read_requests_queued{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{volume}} read'), + ]) + + b.panel.withDefaults({ + unit: 'short', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* read$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + ] +) diff --git a/contrib/mixin/dashboards/exporter.libsonnet b/contrib/mixin/dashboards/exporter.libsonnet new file mode 100644 index 00000000..2b7d4ec6 --- /dev/null +++ b/contrib/mixin/dashboards/exporter.libsonnet @@ -0,0 +1,659 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Exporter', + b.rows([ + b.row('Scrape', b.grid([ + b.place(90, 0, 0, 4, 4), + b.place(91, 4, 0, 4, 4), + b.place(92, 8, 0, 4, 4), + b.place(93, 12, 0, 4, 4), + b.place(94, 16, 0, 4, 4), + b.place(95, 20, 0, 4, 4), + b.place(96, 0, 4, 12, 8), + b.place(97, 12, 4, 12, 8), + ])), + b.row('Collectors', b.grid([ + b.place(99, 0, 0, 8, 8), + b.place(100, 8, 0, 16, 8), + b.place(101, 0, 8, 12, 9), + b.place(102, 12, 8, 12, 9), + ])), + b.row('Process and Go runtime', b.grid([ + b.place(104, 0, 0, 8, 8), + b.place(105, 8, 0, 8, 8), + b.place(106, 16, 0, 8, 8), + b.place(107, 0, 8, 8, 8), + b.place(108, 8, 8, 8, 8), + b.place(109, 16, 8, 8, 8), + b.place(110, 0, 16, 12, 8), + b.place(111, 12, 16, 12, 8), + ])), + ]), + [ + b.panel.new(90, 'Scrape status', 'stat') + + b.panel.withDescription('Result of the last scrape by Prometheus.') + + b.panel.withQueries([ + b.query.new('up{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + mappings: [ + { + options: { + '0': { + index: 0, + text: 'down', + }, + '1': { + index: 1, + text: 'up', + }, + }, + type: 'value', + }, + ], + thresholds: { + steps: [ + { + color: 'red', + value: null, + }, + { + color: 'green', + value: 1, + }, + ], + }, + unit: '', + }), + + b.panel.new(91, 'Exporter version', 'stat') + + b.panel.withDescription('Builds without a version show the first seven characters of the Git revision.') + + b.panel.withQueries([ + b.query.new('windows_exporter_build_info{job=~"$job", instance="$instance", version!=""} or on (instance) label_replace(windows_exporter_build_info{job=~"$job", instance="$instance"}, "version", "$1", "revision", "(.{7}).*")', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withDefaults({ + unit: '', + }) + + b.panel.withOptions({ + colorMode: 'none', + reduceOptions: { + fields: '/^version$/', + }, + textMode: 'value', + }), + + b.panel.new(92, 'Exporter uptime', 'stat') + + b.panel.withDescription('Time since the exporter process started. A short uptime on a host with a long uptime means the exporter restarted.') + + b.panel.withQueries([ + b.query.new('time() - process_start_time_seconds{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'orange', + value: null, + }, + { + color: 'text', + value: 3600, + }, + ], + }, + unit: 'dtdurations', + }), + + b.panel.new(93, 'Failed collectors', 'stat') + + b.panel.withDescription('Collectors that failed at least once in the selected time range.') + + b.panel.withQueries([ + b.query.new('count(min_over_time(windows_exporter_collector_success{job=~"$job", instance="$instance"}[$__range]) == 0) or vector(0)', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 1, + }, + ], + }, + }), + + b.panel.new(94, 'Timed-out collectors', 'stat') + + b.panel.withDescription('Collectors that hit the collector timeout at least once in the selected time range.') + + b.panel.withQueries([ + b.query.new('count(max_over_time(windows_exporter_collector_timeout{job=~"$job", instance="$instance"}[$__range]) == 1) or vector(0)', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 1, + }, + ], + }, + }), + + b.panel.new(95, 'Samples per scrape', 'stat') + + b.panel.withQueries([ + b.query.new('scrape_samples_scraped{job=~"$job", instance="$instance"}', 'A'), + ]) + + b.panel.withDefaults({ + unit: 'short', + }) + + b.panel.withOptions({ + graphMode: 'area', + }), + + b.panel.new(96, 'Scrape duration', 'timeseries') + + b.panel.withDescription('Scrape duration measured by Prometheus and by the exporter. It must stay below the scrape timeout of the job.') + + b.panel.withQueries([ + b.query.new('scrape_duration_seconds{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('Prometheus'), + b.query.new('windows_exporter_scrape_duration_seconds{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('exporter'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 's', + }), + + b.panel.new(97, 'HTTP requests to /metrics', 'timeseries') + + b.panel.withDescription('Scrape requests by HTTP status code, and errors while gathering or encoding metrics. More requests than one per scrape interval mean several jobs or Prometheus servers scrape this exporter.') + + b.panel.withQueries([ + b.query.new('sum by (code) (rate(promhttp_metric_handler_requests_total{job=~"$job", instance="$instance"}[$__rate_interval])) > 0', 'A') + + b.query.withLegendFormat('HTTP {{code}}'), + b.query.new('sum by (cause) (rate(promhttp_metric_handler_errors_total{job=~"$job", instance="$instance"}[$__rate_interval])) > 0', 'B') + + b.query.withLegendFormat('{{cause}} errors'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'reqps', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* errors', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'red', + mode: 'fixed', + }, + }, + ], + }, + ]), + + b.panel.new(99, 'Collectors', 'table') + + b.panel.withQueries([ + b.query.new('windows_exporter_collector_success{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('windows_exporter_collector_timeout{job=~"$job", instance="$instance"}', 'B') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('windows_exporter_collector_duration_seconds{job=~"$job", instance="$instance"}', 'C') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withTransformations([ + { + group: 'joinByField', + kind: 'Transformation', + spec: { + options: { + byField: 'collector', + mode: 'outer', + }, + }, + }, + { + group: 'organize', + kind: 'Transformation', + spec: { + options: { + excludeByName: { + Time: true, + 'Time 1': true, + 'Time 2': true, + 'Time 3': true, + __name__: true, + '__name__ 1': true, + '__name__ 2': true, + '__name__ 3': true, + instance: true, + 'instance 1': true, + 'instance 2': true, + 'instance 3': true, + job: true, + 'job 1': true, + 'job 2': true, + 'job 3': true, + port: true, + 'port 1': true, + 'port 2': true, + 'port 3': true, + }, + includeByName: {}, + indexByName: { + 'Value #A': 1, + 'Value #B': 2, + 'Value #C': 3, + collector: 0, + }, + renameByName: { + 'Value #A': 'Status', + 'Value #B': 'Timeout', + 'Value #C': 'Duration', + collector: 'Collector', + }, + }, + }, + }, + ]) + + b.panel.withOptions({ + sortBy: [ + { + desc: true, + displayName: 'Duration', + }, + ], + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'Value #A', + }, + properties: [ + { + id: 'mappings', + value: [ + { + options: { + '0': { + color: 'red', + index: 0, + text: 'failed', + }, + '1': { + color: 'green', + index: 1, + text: 'ok', + }, + }, + type: 'value', + }, + ], + }, + { + id: 'custom.cellOptions', + value: { + type: 'color-text', + }, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #B', + }, + properties: [ + { + id: 'mappings', + value: [ + { + options: { + '0': { + color: 'green', + index: 0, + text: 'no', + }, + '1': { + color: 'red', + index: 1, + text: 'yes', + }, + }, + type: 'value', + }, + ], + }, + { + id: 'custom.cellOptions', + value: { + type: 'color-text', + }, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #C', + }, + properties: [ + { + id: 'unit', + value: 's', + }, + ], + }, + ]), + + b.panel.new(100, 'Collector duration', 'timeseries') + + b.panel.withDescription('Time each collector spent per scrape. Compare against the scrape timeout.') + + b.panel.withQueries([ + b.query.new('windows_exporter_collector_duration_seconds{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('{{collector}}'), + ]) + + b.panel.withDefaults({ + custom: { + fillOpacity: 40, + stacking: { + mode: 'normal', + }, + }, + min: 0, + unit: 's', + }) + + b.panel.withOptions({ + legend: { + sortBy: 'Mean', + sortDesc: true, + }, + }), + + b.panel.new(101, 'Collector failures', 'state-timeline') + + b.panel.withDescription('Collectors that failed at least once in the selected time range.') + + b.panel.withQueries([ + b.query.new('windows_exporter_collector_success{job=~"$job", instance="$instance"} and on (collector) (min_over_time(windows_exporter_collector_success{job=~"$job", instance="$instance"}[$__range] @ end()) == 0)', 'A') + + b.query.withLegendFormat('{{collector}}'), + ]) + + b.panel.withDefaults({ + mappings: [ + { + options: { + '0': { + color: 'red', + index: 0, + text: 'failed', + }, + '1': { + color: 'green', + index: 1, + text: 'ok', + }, + }, + type: 'value', + }, + ], + }), + + b.panel.new(102, 'Collector timeouts', 'state-timeline') + + b.panel.withDescription('Collectors that hit the collector timeout at least once in the selected time range.') + + b.panel.withQueries([ + b.query.new('1 - windows_exporter_collector_timeout{job=~"$job", instance="$instance"} and on (collector) (max_over_time(windows_exporter_collector_timeout{job=~"$job", instance="$instance"}[$__range] @ end()) == 1)', 'A') + + b.query.withLegendFormat('{{collector}}'), + ]) + + b.panel.withDefaults({ + mappings: [ + { + options: { + '0': { + color: 'red', + index: 0, + text: 'timed out', + }, + '1': { + color: 'green', + index: 1, + text: 'in time', + }, + }, + type: 'value', + }, + ], + }), + + b.panel.new(104, 'Exporter CPU usage', 'timeseries') + + b.panel.withDescription('CPU time used by the exporter process, as a share of one core. CPU usage grows with the number of scrapes, for example when two Prometheus jobs scrape the same exporter; CPU per scrape (right axis) does not.') + + b.panel.withQueries([ + b.query.new('rate(process_cpu_seconds_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('CPU'), + b.query.new('rate(process_cpu_seconds_total{job=~"$job", instance="$instance"}[$__rate_interval]) / on (instance) sum by (instance) (rate(promhttp_metric_handler_requests_total{job=~"$job", instance="$instance"}[$__rate_interval]))', 'B') + + b.query.withLegendFormat('CPU per scrape'), + ]) + + b.panel.withDefaults({ + min: 0, + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'CPU per scrape', + }, + properties: [ + { + id: 'unit', + value: 's', + }, + { + id: 'custom.axisPlacement', + value: 'right', + }, + ], + }, + ]), + + b.panel.new(105, 'Exporter memory', 'timeseries') + + b.panel.withDescription('Working set and private bytes of the exporter process, and the Go heap in use. Steady growth points to a leak.') + + b.panel.withQueries([ + b.query.new('process_resident_memory_bytes{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('working set'), + b.query.new('process_virtual_memory_bytes{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('private bytes'), + b.query.new('go_memstats_heap_inuse_bytes{job=~"$job", instance="$instance"}', 'C') + + b.query.withLegendFormat('Go heap in use'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'bytes', + }), + + b.panel.new(106, 'Exporter handles', 'timeseries') + + b.panel.withDescription('Open handles of the exporter process (process_open_fds). Steady growth points to a handle leak.') + + b.panel.withQueries([ + b.query.new('process_open_fds{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('open handles'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'short', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + + b.panel.new(107, 'Goroutines by state', 'timeseries') + + b.panel.withDescription('Steady growth of goroutines points to a goroutine leak. Goroutines that stay "not in Go" sit in a system call, for example a collector waiting on WMI or PDH. The states need an exporter build with the Go scheduler metrics; the total line works with every build.') + + b.panel.withQueries([ + b.query.new('go_sched_goroutines_running_goroutines{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('running'), + b.query.new('go_sched_goroutines_runnable_goroutines{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('runnable'), + b.query.new('go_sched_goroutines_waiting_goroutines{job=~"$job", instance="$instance"}', 'C') + + b.query.withLegendFormat('waiting'), + b.query.new('go_sched_goroutines_not_in_go_goroutines{job=~"$job", instance="$instance"}', 'D') + + b.query.withLegendFormat('not in Go'), + b.query.new('go_goroutines{job=~"$job", instance="$instance"}', 'E') + + b.query.withLegendFormat('total'), + ]) + + b.panel.withDefaults({ + custom: { + fillOpacity: 40, + stacking: { + mode: 'normal', + }, + }, + min: 0, + unit: 'short', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'total', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'text', + mode: 'fixed', + }, + }, + { + id: 'custom.stacking', + value: { + group: 'B', + mode: 'none', + }, + }, + { + id: 'custom.fillOpacity', + value: 0, + }, + { + id: 'custom.lineStyle', + value: { + dash: [ + 10, + 10, + ], + fill: 'dash', + }, + }, + ], + }, + ]), + + b.panel.new(108, 'Goroutines created and threads', 'timeseries') + + b.panel.withDescription('Goroutines started per second, and OS threads owned by the Go runtime. Goroutines created needs an exporter build with the Go scheduler metrics.') + + b.panel.withQueries([ + b.query.new('rate(go_sched_goroutines_created_goroutines_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('goroutines created/s'), + b.query.new('go_threads{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('OS threads'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'short', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'OS threads', + }, + properties: [ + { + id: 'custom.axisPlacement', + value: 'right', + }, + ], + }, + ]), + + b.panel.new(109, 'Scheduler latency', 'timeseries') + + b.panel.withDescription('Time goroutines wait in the runnable state before they run. High values mean the exporter does not get enough CPU. Needs an exporter build with the Go scheduler metrics.') + + b.panel.withQueries([ + b.query.new('histogram_quantile(0.99, sum by (le) (rate(go_sched_latencies_seconds_bucket{job=~"$job", instance="$instance"}[$__rate_interval])))', 'A') + + b.query.withLegendFormat('p99'), + b.query.new('histogram_quantile(0.5, sum by (le) (rate(go_sched_latencies_seconds_bucket{job=~"$job", instance="$instance"}[$__rate_interval])))', 'B') + + b.query.withLegendFormat('p50'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 's', + }), + + b.panel.new(110, 'Garbage collection', 'timeseries') + + b.panel.withDescription('Average GC pause and GC runs per second.') + + b.panel.withQueries([ + b.query.new('rate(go_gc_duration_seconds_sum{job=~"$job", instance="$instance"}[$__rate_interval]) / rate(go_gc_duration_seconds_count{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('average pause'), + b.query.new('rate(go_gc_duration_seconds_count{job=~"$job", instance="$instance"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('GC runs'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 's', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'GC runs', + }, + properties: [ + { + id: 'unit', + value: 'ops', + }, + { + id: 'custom.axisPlacement', + value: 'right', + }, + ], + }, + ]), + + b.panel.new(111, 'Allocation rate', 'timeseries') + + b.panel.withDescription('Bytes allocated on the Go heap per second. Spikes line up with expensive collectors.') + + b.panel.withQueries([ + b.query.new('rate(go_memstats_alloc_bytes_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('allocated'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'Bps', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + ] +) diff --git a/contrib/mixin/dashboards/fleet.libsonnet b/contrib/mixin/dashboards/fleet.libsonnet new file mode 100644 index 00000000..f3b82380 --- /dev/null +++ b/contrib/mixin/dashboards/fleet.libsonnet @@ -0,0 +1,710 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Fleet', + b.grid([ + b.place(2, 0, 0, 24, 10), + b.place(3, 0, 10, 8, 8), + b.place(4, 8, 10, 8, 8), + b.place(5, 16, 10, 8, 8), + b.place(6, 0, 18, 8, 8), + b.place(7, 8, 18, 8, 8), + b.place(8, 16, 18, 8, 8), + ]), + [ + b.panel.new(2, 'Hosts', 'table') + + b.panel.withDescription('One row per scraped host. Click a hostname to open its details. Services up counts running services; Auto stopped counts services with start mode auto that are not running.') + + b.panel.withQueries([ + b.query.new('max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"})', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(max by (instance, product, version) (windows_os_info{job=~"$job"})) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'B') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(max by (instance) (time() - windows_system_boot_time_timestamp{job=~"$job"})) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'C') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(max by (instance) (windows_cpu_logical_processor{job=~"$job"})) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'D') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(100 * (1 - avg by (instance) (clamp_max(rate(windows_cpu_time_total{job=~"$job", mode="idle"}[$__rate_interval]), 1)))) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'E') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(max by (instance) (windows_memory_physical_total_bytes{job=~"$job"})) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'F') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(max by (instance) (100 * (1 - windows_memory_physical_free_bytes{job=~"$job"} / windows_memory_physical_total_bytes{job=~"$job"}))) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'G') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(max by (instance) (100 * windows_memory_committed_bytes{job=~"$job"} / windows_memory_commit_limit{job=~"$job"})) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'H') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(max by (instance) (100 * (1 - windows_logical_disk_free_bytes{job=~"$job", volume!~"HarddiskVolume.*"} / windows_logical_disk_size_bytes{job=~"$job", volume!~"HarddiskVolume.*"}))) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'I') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(sum by (instance) (windows_service_state{job=~"$job", state="running"})) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'J') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(count by (instance) (windows_service_state{job=~"$job", state="running"} == 0 and on (instance, name) windows_service_start_mode{job=~"$job", start_mode="auto"} == 1)) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'K') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('(max by (instance) (windows_system_processes{job=~"$job"})) and on (instance) windows_os_hostname{job=~"$job", hostname=~"$hostname"}', 'L') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withTransformations([ + { + group: 'joinByField', + kind: 'Transformation', + spec: { + options: { + byField: 'instance', + mode: 'outer', + }, + }, + }, + { + group: 'organize', + kind: 'Transformation', + spec: { + options: { + excludeByName: { + Time: true, + 'Time 1': true, + 'Time 10': true, + 'Time 11': true, + 'Time 12': true, + 'Time 2': true, + 'Time 3': true, + 'Time 4': true, + 'Time 5': true, + 'Time 6': true, + 'Time 7': true, + 'Time 8': true, + 'Time 9': true, + 'Value #A': true, + 'Value #B': true, + }, + includeByName: {}, + indexByName: { + 'Value #C': 4, + 'Value #D': 5, + 'Value #E': 6, + 'Value #F': 7, + 'Value #G': 8, + 'Value #H': 9, + 'Value #I': 10, + 'Value #J': 11, + 'Value #K': 12, + 'Value #L': 13, + hostname: 0, + instance: 1, + product: 2, + version: 3, + }, + renameByName: { + 'Value #C': 'Uptime', + 'Value #D': 'CPUs', + 'Value #E': 'CPU', + 'Value #F': 'Memory', + 'Value #G': 'Memory used', + 'Value #H': 'Commit used', + 'Value #I': 'Fullest volume', + 'Value #J': 'Services up', + 'Value #K': 'Auto stopped', + 'Value #L': 'Processes', + hostname: 'Hostname', + instance: 'Instance', + product: 'OS', + version: 'Version', + }, + }, + }, + }, + ]) + + b.panel.withOptions({ + footer: { + fields: [ + 'CPUs', + 'Memory', + 'Processes', + ], + show: true, + }, + sortBy: [ + { + desc: false, + displayName: 'Hostname', + }, + ], + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + 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, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #C', + }, + properties: [ + { + id: 'unit', + value: 'dtdurations', + }, + { + id: 'custom.width', + value: 90, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'instance', + }, + properties: [ + { + id: 'custom.width', + value: 140, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'product', + }, + properties: [ + { + id: 'custom.width', + value: 160, + }, + { + id: 'custom.filterable', + value: true, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'version', + }, + properties: [ + { + id: 'custom.width', + value: 100, + }, + { + id: 'custom.filterable', + value: true, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #D', + }, + properties: [ + { + id: 'unit', + value: 'none', + }, + { + id: 'custom.width', + value: 60, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #E', + }, + properties: [ + { + id: 'unit', + value: 'percent', + }, + { + id: 'min', + value: 0, + }, + { + id: 'max', + value: 100, + }, + { + id: 'thresholds', + value: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + }, + { + id: 'color', + value: { + mode: 'continuous-GrYlRd', + }, + }, + { + id: 'custom.cellOptions', + value: { + mode: 'basic', + type: 'gauge', + valueDisplayMode: 'text', + }, + }, + { + id: 'decimals', + value: 1, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #F', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + { + id: 'custom.width', + value: 90, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #G', + }, + properties: [ + { + id: 'unit', + value: 'percent', + }, + { + id: 'min', + value: 0, + }, + { + id: 'max', + value: 100, + }, + { + id: 'thresholds', + value: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + }, + { + id: 'color', + value: { + mode: 'continuous-GrYlRd', + }, + }, + { + id: 'custom.cellOptions', + value: { + mode: 'basic', + type: 'gauge', + valueDisplayMode: 'text', + }, + }, + { + id: 'decimals', + value: 1, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #H', + }, + properties: [ + { + id: 'unit', + value: 'percent', + }, + { + id: 'min', + value: 0, + }, + { + id: 'max', + value: 100, + }, + { + id: 'thresholds', + value: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + }, + { + id: 'color', + value: { + mode: 'continuous-GrYlRd', + }, + }, + { + id: 'custom.cellOptions', + value: { + mode: 'basic', + type: 'gauge', + valueDisplayMode: 'text', + }, + }, + { + id: 'decimals', + value: 1, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #I', + }, + properties: [ + { + id: 'unit', + value: 'percent', + }, + { + id: 'min', + value: 0, + }, + { + id: 'max', + value: 100, + }, + { + id: 'thresholds', + value: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + }, + { + id: 'color', + value: { + mode: 'continuous-GrYlRd', + }, + }, + { + id: 'custom.cellOptions', + value: { + mode: 'basic', + type: 'gauge', + valueDisplayMode: 'text', + }, + }, + { + id: 'decimals', + value: 1, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #J', + }, + properties: [ + { + id: 'unit', + value: 'none', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #K', + }, + properties: [ + { + id: 'unit', + value: 'none', + }, + { + id: 'thresholds', + value: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'orange', + value: 1, + }, + ], + }, + }, + { + id: 'custom.cellOptions', + value: { + type: 'color-text', + }, + }, + { + id: 'noValue', + value: '0', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #L', + }, + properties: [ + { + id: 'unit', + value: 'none', + }, + { + id: 'custom.width', + value: 90, + }, + ], + }, + ]), + + b.panel.new(3, 'CPU utilization', 'timeseries') + + b.panel.withDescription('Shows the 25 highest hosts at each point in time.') + + b.panel.withQueries([ + b.query.new('topk(25, (100 * (1 - avg by (instance) (clamp_max(rate(windows_cpu_time_total{job=~"$job", mode="idle"}[$__rate_interval]), 1)))) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"}))', 'A') + + b.query.withLegendFormat('{{hostname}}'), + ]) + + b.panel.withDefaults({ + max: 100, + min: 0, + unit: 'percent', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + + b.panel.new(4, 'Memory utilization', 'timeseries') + + b.panel.withDescription('Shows the 25 highest hosts at each point in time.') + + b.panel.withQueries([ + b.query.new('topk(25, (max by (instance) (100 * (1 - windows_memory_physical_free_bytes{job=~"$job"} / windows_memory_physical_total_bytes{job=~"$job"}))) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"}))', 'A') + + b.query.withLegendFormat('{{hostname}}'), + ]) + + b.panel.withDefaults({ + max: 100, + min: 0, + unit: 'percent', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + + b.panel.new(5, 'Fullest volume', 'timeseries') + + b.panel.withDescription('Usage of the fullest volume per host. Shows the 25 highest hosts at each point in time.') + + b.panel.withQueries([ + b.query.new('topk(25, (max by (instance) (100 * (1 - windows_logical_disk_free_bytes{job=~"$job", volume!~"HarddiskVolume.*"} / windows_logical_disk_size_bytes{job=~"$job", volume!~"HarddiskVolume.*"}))) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"}))', 'A') + + b.query.withLegendFormat('{{hostname}}'), + ]) + + b.panel.withDefaults({ + custom: { + thresholdsStyle: { + mode: 'dashed', + }, + }, + max: 100, + min: 0, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + unit: 'percent', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + + b.panel.new(6, 'Network throughput', 'timeseries') + + b.panel.withDescription('Sum over all interfaces. Received is drawn below the axis. Shows the 25 highest hosts at each point in time.') + + b.panel.withQueries([ + b.query.new('topk(25, (sum by (instance) (rate(windows_net_bytes_sent_total{job=~"$job"}[$__rate_interval]) * 8)) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"}))', 'A') + + b.query.withLegendFormat('{{hostname}} sent'), + b.query.new('topk(25, (sum by (instance) (rate(windows_net_bytes_received_total{job=~"$job"}[$__rate_interval]) * 8)) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"}))', 'B') + + b.query.withLegendFormat('{{hostname}} received'), + ]) + + b.panel.withDefaults({ + unit: 'bps', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* received$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(7, 'Disk throughput', 'timeseries') + + b.panel.withDescription('Sum over all volumes. Read is drawn below the axis. Shows the 25 highest hosts at each point in time.') + + b.panel.withQueries([ + b.query.new('topk(25, (sum by (instance) (rate(windows_logical_disk_write_bytes_total{job=~"$job", volume!~"HarddiskVolume.*"}[$__rate_interval]))) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"}))', 'A') + + b.query.withLegendFormat('{{hostname}} write'), + b.query.new('topk(25, (sum by (instance) (rate(windows_logical_disk_read_bytes_total{job=~"$job", volume!~"HarddiskVolume.*"}[$__rate_interval]))) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"}))', 'B') + + b.query.withLegendFormat('{{hostname}} read'), + ]) + + b.panel.withDefaults({ + unit: 'Bps', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* read$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(8, 'Network errors and discards', 'timeseries') + + b.panel.withDescription('Sum over all interfaces of received and outbound errors and discards. Shows the 25 highest hosts at each point in time.') + + b.panel.withQueries([ + b.query.new('topk(25, (sum by (instance) (rate(windows_net_packets_received_errors_total{job=~"$job"}[$__rate_interval]) + rate(windows_net_packets_received_discarded_total{job=~"$job"}[$__rate_interval]) + rate(windows_net_packets_outbound_errors_total{job=~"$job"}[$__rate_interval]) + rate(windows_net_packets_outbound_discarded_total{job=~"$job"}[$__rate_interval]))) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{job=~"$job", hostname=~"$hostname"}))', 'A') + + b.query.withLegendFormat('{{hostname}}'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'pps', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + ] +) diff --git a/contrib/mixin/dashboards/gpu.libsonnet b/contrib/mixin/dashboards/gpu.libsonnet new file mode 100644 index 00000000..a89ed8aa --- /dev/null +++ b/contrib/mixin/dashboards/gpu.libsonnet @@ -0,0 +1,279 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'GPU', + b.grid([ + b.place(57, 0, 0, 24, 5), + b.place(58, 0, 5, 12, 8), + b.place(59, 12, 5, 12, 8), + b.place(60, 0, 13, 12, 8), + b.place(61, 12, 13, 12, 8), + ]), + [ + b.panel.new(57, 'GPUs', 'table') + + b.panel.withDescription('Needs the gpu collector, which is not enabled by default. Utilization is the busiest engine of the GPU.') + + b.panel.withQueries([ + b.query.new('max by (luid, name) (windows_gpu_info{job=~"$job", instance="$instance"})', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('clamp_max(max by (luid) (sum by (luid, eng, engtype) (rate(windows_gpu_engine_time_seconds{job=~"$job", instance="$instance"}[$__rate_interval]))), 1)', 'B') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('max by (luid) (windows_gpu_adapter_memory_dedicated_bytes{job=~"$job", instance="$instance"})', 'C') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('max by (luid) (windows_gpu_dedicated_video_memory_size_bytes{job=~"$job", instance="$instance"})', 'D') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('max by (luid) (windows_gpu_adapter_memory_shared_bytes{job=~"$job", instance="$instance"})', 'E') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('max by (luid) (windows_gpu_shared_system_memory_size_bytes{job=~"$job", instance="$instance"})', 'F') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withTransformations([ + { + group: 'joinByField', + kind: 'Transformation', + spec: { + options: { + byField: 'luid', + mode: 'outer', + }, + }, + }, + { + group: 'organize', + kind: 'Transformation', + spec: { + options: { + excludeByName: { + Time: true, + 'Time 1': true, + 'Time 2': true, + 'Time 3': true, + 'Time 4': true, + 'Time 5': true, + 'Time 6': true, + 'Value #A': true, + luid: true, + }, + includeByName: {}, + indexByName: { + 'Value #B': 1, + 'Value #C': 2, + 'Value #D': 3, + 'Value #E': 4, + 'Value #F': 5, + name: 0, + }, + renameByName: { + 'Value #B': 'Utilization', + 'Value #C': 'Dedicated memory used', + 'Value #D': 'Dedicated memory', + 'Value #E': 'Shared memory used', + 'Value #F': 'Shared memory', + name: 'GPU', + }, + }, + }, + }, + ]) + + b.panel.withOptions({ + sortBy: [], + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'Value #B', + }, + properties: [ + { + id: 'unit', + value: 'percentunit', + }, + { + id: 'min', + value: 0, + }, + { + id: 'max', + value: 1, + }, + { + id: 'thresholds', + value: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + }, + { + id: 'color', + value: { + mode: 'continuous-GrYlRd', + }, + }, + { + id: 'custom.cellOptions', + value: { + mode: 'basic', + type: 'gauge', + valueDisplayMode: 'text', + }, + }, + { + id: 'decimals', + value: 1, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #C', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #D', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #E', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #F', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + ], + }, + ]), + + b.panel.new(58, 'GPU utilization by engine type', 'timeseries') + + b.panel.withDescription('Busiest engine of each engine type, as in Task Manager.') + + b.panel.withQueries([ + b.query.new('clamp_max(max by (luid, engtype) (sum by (luid, eng, engtype) (rate(windows_gpu_engine_time_seconds{job=~"$job", instance="$instance"}[$__rate_interval]))), 1) * on (luid) group_left (name) max by (luid, name) (windows_gpu_info{job=~"$job", instance="$instance"})', 'A') + + b.query.withLegendFormat('{{name}} {{engtype}}'), + ]) + + b.panel.withDefaults({ + max: 1, + min: 0, + }), + + b.panel.new(59, 'GPU memory', 'timeseries') + + b.panel.withDescription('Dedicated (video) and shared (system) memory in use, against the size of each pool.') + + b.panel.withQueries([ + b.query.new('max by (luid) (windows_gpu_adapter_memory_dedicated_bytes{job=~"$job", instance="$instance"}) * on (luid) group_left (name) max by (luid, name) (windows_gpu_info{job=~"$job", instance="$instance"})', 'A') + + b.query.withLegendFormat('{{name}} dedicated used'), + b.query.new('max by (luid) (windows_gpu_dedicated_video_memory_size_bytes{job=~"$job", instance="$instance"}) * on (luid) group_left (name) max by (luid, name) (windows_gpu_info{job=~"$job", instance="$instance"})', 'B') + + b.query.withLegendFormat('{{name}} dedicated size'), + b.query.new('max by (luid) (windows_gpu_adapter_memory_shared_bytes{job=~"$job", instance="$instance"}) * on (luid) group_left (name) max by (luid, name) (windows_gpu_info{job=~"$job", instance="$instance"})', 'C') + + b.query.withLegendFormat('{{name}} shared used'), + b.query.new('max by (luid) (windows_gpu_shared_system_memory_size_bytes{job=~"$job", instance="$instance"}) * on (luid) group_left (name) max by (luid, name) (windows_gpu_info{job=~"$job", instance="$instance"})', 'D') + + b.query.withLegendFormat('{{name}} shared size'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'bytes', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* size', + }, + properties: [ + { + id: 'custom.fillOpacity', + value: 0, + }, + { + id: 'custom.lineStyle', + value: { + dash: [ + 10, + 10, + ], + fill: 'dash', + }, + }, + ], + }, + ]), + + b.panel.new(60, 'Top 10 processes by GPU utilization', 'timeseries') + + b.panel.withDescription('Busiest GPU engine used by each process. Process names need the process collector; otherwise only the PID is shown.') + + b.panel.withQueries([ + b.query.new('topk(10, (max by (process_id) (sum by (process_id, luid, eng) (rate(windows_gpu_engine_time_seconds{job=~"$job", instance="$instance"}[$__rate_interval]))) * on (process_id) group_left (process) max by (process_id, process) (windows_process_info{job=~"$job", instance="$instance"})) or on (process_id) max by (process_id) (sum by (process_id, luid, eng) (rate(windows_gpu_engine_time_seconds{job=~"$job", instance="$instance"}[$__rate_interval]))))', 'A') + + b.query.withLegendFormat('{{process}} ({{process_id}})'), + ]) + + b.panel.withDefaults({ + min: 0, + }) + + b.panel.withOptions({ + legend: { + sortBy: 'Mean', + sortDesc: true, + }, + }), + + b.panel.new(61, 'Top 10 processes by dedicated GPU memory', 'timeseries') + + b.panel.withDescription('Process names need the process collector; otherwise only the PID is shown.') + + b.panel.withQueries([ + b.query.new('topk(10, (sum by (process_id) (windows_gpu_process_memory_dedicated_bytes{job=~"$job", instance="$instance"}) * on (process_id) group_left (process) max by (process_id, process) (windows_process_info{job=~"$job", instance="$instance"})) or on (process_id) sum by (process_id) (windows_gpu_process_memory_dedicated_bytes{job=~"$job", instance="$instance"}))', 'A') + + b.query.withLegendFormat('{{process}} ({{process_id}})'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'bytes', + }) + + b.panel.withOptions({ + legend: { + sortBy: 'Mean', + sortDesc: true, + }, + }), + ] +) diff --git a/contrib/mixin/dashboards/hyper_v.libsonnet b/contrib/mixin/dashboards/hyper_v.libsonnet new file mode 100644 index 00000000..6f439f34 --- /dev/null +++ b/contrib/mixin/dashboards/hyper_v.libsonnet @@ -0,0 +1,380 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Hyper-V', + b.rows([ + b.row('Host', b.grid([ + b.place(63, 0, 0, 4, 4), + b.place(64, 4, 0, 4, 4), + b.place(65, 8, 0, 4, 4), + b.place(66, 12, 0, 4, 4), + b.place(67, 16, 0, 4, 4), + b.place(68, 20, 0, 4, 4), + b.place(69, 0, 4, 12, 8), + b.place(70, 12, 4, 12, 8), + ])), + b.row('Memory', b.grid([ + b.place(72, 0, 0, 8, 8), + b.place(73, 8, 0, 8, 8), + b.place(74, 16, 0, 8, 8), + ])), + b.row('Storage', b.grid([ + b.place(76, 0, 0, 8, 8), + b.place(77, 8, 0, 8, 8), + b.place(78, 16, 0, 8, 8), + ])), + b.row('Network', b.grid([ + b.place(80, 0, 0, 8, 8), + b.place(81, 8, 0, 8, 8), + b.place(82, 16, 0, 8, 8), + ])), + ]), + [ + b.panel.new(63, 'Virtual machines', 'stat') + + b.panel.withDescription('Needs the hyperv collector, which is not enabled by default.') + + b.panel.withQueries([ + b.query.new('sum(windows_hyperv_virtual_machine_health_total_count{job=~"$job", instance="$instance"})', 'A') + + b.query.withInstant(), + ]), + + b.panel.new(64, 'VMs in critical health', 'stat') + + b.panel.withQueries([ + b.query.new('sum(windows_hyperv_virtual_machine_health_total_count{job=~"$job", instance="$instance", state="critical"})', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 1, + }, + ], + }, + }), + + b.panel.new(65, 'Hyper-V WMI', 'stat') + + b.panel.withDescription('Whether the Hyper-V WMI namespace answers. When it stops answering, Hyper-V Manager and Failover Cluster Manager usually cannot manage the host.') + + b.panel.withQueries([ + b.query.new('windows_hyperv_wmi_health{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + mappings: [ + { + options: { + '0': { + index: 0, + text: 'not responding', + }, + '1': { + index: 1, + text: 'ok', + }, + }, + type: 'value', + }, + ], + thresholds: { + steps: [ + { + color: 'red', + value: null, + }, + { + color: 'green', + value: 1, + }, + ], + }, + unit: '', + }), + + b.panel.new(66, 'Logical processors', 'stat') + + b.panel.withQueries([ + b.query.new('windows_hyperv_host_logical_processor_count{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]), + + b.panel.new(67, 'Virtual processors', 'stat') + + b.panel.withDescription('Virtual processors assigned to all VMs.') + + b.panel.withQueries([ + b.query.new('windows_hyperv_total_vm_processor_count{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]), + + b.panel.new(68, 'vCPU per logical CPU', 'stat') + + b.panel.withDescription('Virtual processors assigned to VMs per logical processor of the host.') + + b.panel.withQueries([ + b.query.new('windows_hyperv_total_vm_processor_count{job=~"$job", instance="$instance"} / windows_hyperv_host_logical_processor_count{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + decimals: 2, + }), + + b.panel.new(69, 'Host CPU time by state', 'timeseries') + + b.panel.withDescription('Share of all logical processors spent running guest code (VMs and the root partition) and in the hypervisor. With Hyper-V enabled, the CPU tab only sees the root partition.') + + b.panel.withQueries([ + b.query.new('sum by (state) (rate(windows_hyperv_hypervisor_logical_processor_time_total{job=~"$job", instance="$instance", state!="idle"}[$__rate_interval])) / scalar(count(windows_hyperv_hypervisor_logical_processor_time_total{job=~"$job", instance="$instance", state="idle"}))', 'A') + + b.query.withLegendFormat('{{state}}'), + ]) + + b.panel.withDefaults({ + custom: { + fillOpacity: 40, + stacking: { + mode: 'normal', + }, + }, + max: 1, + min: 0, + }), + + b.panel.new(70, 'CPU usage per VM', 'timeseries') + + b.panel.withDescription('Virtual processor run time of each VM as a share of all logical processors of the host.') + + b.panel.withQueries([ + b.query.new('sum by (vm) (rate(windows_hyperv_hypervisor_virtual_processor_run_time_total{job=~"$job", instance="$instance"}[$__rate_interval])) / scalar(max(windows_hyperv_host_logical_processor_count{job=~"$job", instance="$instance"}))', 'A') + + b.query.withLegendFormat('{{vm}}'), + ]) + + b.panel.withDefaults({ + custom: { + fillOpacity: 40, + stacking: { + mode: 'normal', + }, + }, + min: 0, + }) + + b.panel.withOptions({ + legend: { + sortBy: 'Mean', + sortDesc: true, + }, + }), + + b.panel.new(72, 'Memory assigned to VMs', 'timeseries') + + b.panel.withDescription('Physical memory currently assigned to each VM with dynamic memory.') + + b.panel.withQueries([ + b.query.new('windows_hyperv_dynamic_memory_vm_physical_bytes{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('{{vm}}'), + ]) + + b.panel.withDefaults({ + custom: { + fillOpacity: 40, + stacking: { + mode: 'normal', + }, + }, + min: 0, + unit: 'bytes', + }) + + b.panel.withOptions({ + legend: { + sortBy: 'Mean', + sortDesc: true, + }, + }), + + b.panel.new(73, 'VM memory pressure', 'timeseries') + + b.panel.withDescription('Memory a VM needs as a share of the memory it has. Above 100% the VM needs more memory than it is assigned.') + + b.panel.withQueries([ + b.query.new('windows_hyperv_dynamic_memory_vm_pressure_current_ratio{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('{{vm}}'), + ]) + + b.panel.withDefaults({ + custom: { + thresholdsStyle: { + mode: 'dashed', + }, + }, + min: 0, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 1, + }, + ], + }, + }), + + b.panel.new(74, 'Memory available for VMs', 'timeseries') + + b.panel.withDescription('Memory the dynamic memory balancer can still hand out to VMs.') + + b.panel.withQueries([ + b.query.new('windows_hyperv_dynamic_memory_balancer_available_memory_bytes{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('{{balancer}}'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'bytes', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + + b.panel.new(76, 'Virtual disk throughput', 'timeseries') + + b.panel.withDescription('Reads are drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_hyperv_virtual_storage_device_bytes_written{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{device}} write'), + b.query.new('rate(windows_hyperv_virtual_storage_device_bytes_read{job=~"$job", instance="$instance"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{device}} read'), + ]) + + b.panel.withDefaults({ + unit: 'Bps', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* read$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(77, 'Virtual disk IOPS', 'timeseries') + + b.panel.withDescription('Reads are drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_hyperv_virtual_storage_device_operations_written_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{device}} write'), + b.query.new('rate(windows_hyperv_virtual_storage_device_operations_read_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{device}} read'), + ]) + + b.panel.withDefaults({ + unit: 'iops', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* read$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(78, 'Virtual disk latency and errors', 'timeseries') + + b.panel.withDescription('Average I/O latency of each virtual disk. windows_hyperv_virtual_storage_device_latency_seconds holds the raw, growing counter in 100 ns ticks, so the panel divides its rate by the rate of I/O operations. Errors use the right axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_hyperv_virtual_storage_device_latency_seconds{job=~"$job", instance="$instance"}[$__rate_interval]) / (rate(windows_hyperv_virtual_storage_device_operations_read_total{job=~"$job", instance="$instance"}[$__rate_interval]) + rate(windows_hyperv_virtual_storage_device_operations_written_total{job=~"$job", instance="$instance"}[$__rate_interval])) / 1e7', 'A') + + b.query.withLegendFormat('{{device}} latency'), + b.query.new('rate(windows_hyperv_virtual_storage_device_error_count_total{job=~"$job", instance="$instance"}[$__rate_interval]) > 0', 'B') + + b.query.withLegendFormat('{{device}} errors'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 's', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* errors', + }, + properties: [ + { + id: 'unit', + value: 'ops', + }, + { + id: 'custom.axisPlacement', + value: 'right', + }, + { + id: 'color', + value: { + fixedColor: 'red', + mode: 'fixed', + }, + }, + ], + }, + ]), + + b.panel.new(80, 'Virtual switch throughput', 'timeseries') + + b.panel.withDescription('Received traffic is drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_hyperv_vswitch_bytes_sent_total{job=~"$job", instance="$instance"}[$__rate_interval]) * 8', 'A') + + b.query.withLegendFormat('{{vswitch}} sent'), + b.query.new('rate(windows_hyperv_vswitch_bytes_received_total{job=~"$job", instance="$instance"}[$__rate_interval]) * 8', 'B') + + b.query.withLegendFormat('{{vswitch}} received'), + ]) + + b.panel.withDefaults({ + unit: 'bps', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* received$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(81, 'Virtual switch dropped packets', 'timeseries') + + b.panel.withQueries([ + b.query.new('rate(windows_hyperv_vswitch_dropped_packets_incoming_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{vswitch}} incoming'), + b.query.new('rate(windows_hyperv_vswitch_dropped_packets_outcoming_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{vswitch}} outgoing'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'pps', + }), + + b.panel.new(82, 'VM network adapter throughput', 'timeseries') + + b.panel.withDescription('Received traffic is drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_hyperv_virtual_network_adapter_sent_bytes_total{job=~"$job", instance="$instance"}[$__rate_interval]) * 8', 'A') + + b.query.withLegendFormat('{{adapter}} sent'), + b.query.new('rate(windows_hyperv_virtual_network_adapter_received_bytes_total{job=~"$job", instance="$instance"}[$__rate_interval]) * 8', 'B') + + b.query.withLegendFormat('{{adapter}} received'), + ]) + + b.panel.withDefaults({ + unit: 'bps', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* received$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + ] +) diff --git a/contrib/mixin/dashboards/memory.libsonnet b/contrib/mixin/dashboards/memory.libsonnet new file mode 100644 index 00000000..376b8dd1 --- /dev/null +++ b/contrib/mixin/dashboards/memory.libsonnet @@ -0,0 +1,135 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Memory', + b.grid([ + b.place(32, 0, 0, 12, 8), + b.place(33, 12, 0, 12, 8), + b.place(34, 0, 8, 12, 8), + b.place(35, 12, 8, 12, 8), + ]), + [ + b.panel.new(32, 'Physical memory', 'timeseries') + + b.panel.withDescription('Available memory is the standby (cache), free and zero page lists.') + + b.panel.withQueries([ + b.query.new('windows_memory_physical_total_bytes{job=~"$job", instance="$instance"} - windows_memory_physical_free_bytes{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('used'), + b.query.new('windows_memory_physical_free_bytes{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('available'), + ]) + + b.panel.withDefaults({ + custom: { + fillOpacity: 40, + stacking: { + mode: 'normal', + }, + }, + min: 0, + unit: 'bytes', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'used', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'orange', + mode: 'fixed', + }, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'available', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'green', + mode: 'fixed', + }, + }, + ], + }, + ]), + + b.panel.new(33, 'Commit charge', 'timeseries') + + b.panel.withDescription('Committed virtual memory against the commit limit (physical memory + page files).') + + b.panel.withQueries([ + b.query.new('windows_memory_committed_bytes{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('committed'), + b.query.new('windows_memory_commit_limit{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('limit'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'bytes', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'limit', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'red', + mode: 'fixed', + }, + }, + { + id: 'custom.fillOpacity', + value: 0, + }, + { + id: 'custom.lineStyle', + value: { + dash: [ + 10, + 10, + ], + fill: 'dash', + }, + }, + ], + }, + ]), + + b.panel.new(34, 'Paging', 'timeseries') + + b.panel.withDescription('Hard page faults read from or written to disk. Sustained page reads indicate memory pressure.') + + b.panel.withQueries([ + b.query.new('rate(windows_memory_swap_page_reads_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('page reads'), + b.query.new('rate(windows_memory_swap_page_writes_total{job=~"$job", instance="$instance"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('page writes'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'ops', + }), + + b.panel.new(35, 'Kernel pools and cache', 'timeseries') + + b.panel.withDescription('A steadily growing nonpaged pool usually means a driver leaks memory.') + + b.panel.withQueries([ + b.query.new('windows_memory_pool_nonpaged_bytes{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('nonpaged pool'), + b.query.new('windows_memory_pool_paged_bytes{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('paged pool'), + b.query.new('windows_memory_cache_bytes{job=~"$job", instance="$instance"}', 'C') + + b.query.withLegendFormat('system cache'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'bytes', + }), + ] +) diff --git a/contrib/mixin/dashboards/network.libsonnet b/contrib/mixin/dashboards/network.libsonnet new file mode 100644 index 00000000..9ce5357b --- /dev/null +++ b/contrib/mixin/dashboards/network.libsonnet @@ -0,0 +1,108 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Network', + b.grid([ + b.place(45, 0, 0, 12, 8), + b.place(46, 12, 0, 12, 8), + b.place(47, 0, 8, 12, 8), + b.place(48, 12, 8, 12, 8), + ]), + [ + b.panel.new(45, 'Network throughput', 'timeseries') + + b.panel.withDescription('Received traffic is drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_net_bytes_sent_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval]) * 8', 'A') + + b.query.withLegendFormat('{{nic}} sent'), + b.query.new('rate(windows_net_bytes_received_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval]) * 8', 'B') + + b.query.withLegendFormat('{{nic}} received'), + ]) + + b.panel.withDefaults({ + unit: 'bps', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* received$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(46, 'Network utilization', 'timeseries') + + b.panel.withDescription('Sent plus received traffic relative to the link speed reported by the adapter. Interfaces without a link speed are not shown.') + + b.panel.withQueries([ + b.query.new('rate(windows_net_bytes_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval]) / (windows_net_current_bandwidth_bytes{job=~"$job", instance="$instance", nic=~"$nic"} > 0)', 'A') + + b.query.withLegendFormat('{{nic}}'), + ]) + + b.panel.withDefaults({ + min: 0, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + }), + + b.panel.new(47, 'Packets', 'timeseries') + + b.panel.withDescription('Received packets are drawn below the axis.') + + b.panel.withQueries([ + b.query.new('rate(windows_net_packets_sent_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{nic}} sent'), + b.query.new('rate(windows_net_packets_received_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{nic}} received'), + ]) + + b.panel.withDefaults({ + unit: 'pps', + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: '.* received$', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(48, 'Network errors and discards', 'timeseries') + + b.panel.withQueries([ + b.query.new('rate(windows_net_packets_received_errors_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])', 'A') + + b.query.withLegendFormat('{{nic}} received errors'), + b.query.new('rate(windows_net_packets_received_discarded_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])', 'B') + + b.query.withLegendFormat('{{nic}} received discarded'), + b.query.new('rate(windows_net_packets_outbound_errors_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])', 'C') + + b.query.withLegendFormat('{{nic}} outbound errors'), + b.query.new('rate(windows_net_packets_outbound_discarded_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])', 'D') + + b.query.withLegendFormat('{{nic}} outbound discarded'), + b.query.new('rate(windows_net_packets_received_unknown_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])', 'E') + + b.query.withLegendFormat('{{nic}} received unknown protocol'), + ]) + + b.panel.withDefaults({ + min: 0, + unit: 'pps', + }), + ] +) diff --git a/contrib/mixin/dashboards/overview.libsonnet b/contrib/mixin/dashboards/overview.libsonnet new file mode 100644 index 00000000..ac93ac48 --- /dev/null +++ b/contrib/mixin/dashboards/overview.libsonnet @@ -0,0 +1,490 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Overview', + b.grid([ + b.place(10, 0, 0, 4, 4), + b.place(11, 4, 0, 3, 4), + b.place(12, 7, 0, 2, 4), + b.place(13, 9, 0, 2, 4), + b.place(14, 11, 0, 3, 4), + b.place(15, 14, 0, 3, 4), + b.place(16, 17, 0, 3, 4), + b.place(17, 20, 0, 4, 4), + b.place(18, 0, 4, 12, 8), + b.place(19, 12, 4, 12, 8), + b.place(20, 0, 12, 8, 8), + b.place(21, 8, 12, 8, 8), + b.place(22, 16, 12, 8, 8), + ]), + [ + b.panel.new(10, 'Operating system', 'stat') + + b.panel.withQueries([ + b.query.new('windows_os_info{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withDefaults({ + unit: '', + }) + + b.panel.withOptions({ + colorMode: 'none', + reduceOptions: { + fields: '/^product$/', + }, + textMode: 'value', + }), + + b.panel.new(11, 'Uptime', 'stat') + + b.panel.withDescription('Time since the last boot.') + + b.panel.withQueries([ + b.query.new('time() - windows_system_boot_time_timestamp{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'orange', + value: null, + }, + { + color: 'text', + value: 3600, + }, + ], + }, + unit: 'dtdurations', + }), + + b.panel.new(12, 'CPUs', 'stat') + + b.panel.withQueries([ + b.query.new('windows_cpu_logical_processor{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]), + + b.panel.new(13, 'RAM', 'stat') + + b.panel.withQueries([ + b.query.new('windows_memory_physical_total_bytes{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + decimals: 1, + unit: 'bytes', + }), + + b.panel.new(14, 'CPU busy', 'stat') + + b.panel.withQueries([ + b.query.new('100 * (1 - avg by (instance) (clamp_max(rate(windows_cpu_time_total{job=~"$job", instance="$instance", mode="idle"}[$__rate_interval]), 1)))', 'A'), + ]) + + b.panel.withDefaults({ + decimals: 1, + max: 100, + min: 0, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + unit: 'percent', + }) + + b.panel.withOptions({ + graphMode: 'area', + }), + + b.panel.new(15, 'Memory used', 'stat') + + b.panel.withQueries([ + b.query.new('100 * (1 - windows_memory_physical_free_bytes{job=~"$job", instance="$instance"} / windows_memory_physical_total_bytes{job=~"$job", instance="$instance"})', 'A'), + ]) + + b.panel.withDefaults({ + decimals: 1, + max: 100, + min: 0, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + unit: 'percent', + }) + + b.panel.withOptions({ + graphMode: 'area', + }), + + b.panel.new(16, 'Commit used', 'stat') + + b.panel.withDescription('Committed virtual memory as a share of the commit limit (physical memory + page files). Allocations fail at 100%.') + + b.panel.withQueries([ + b.query.new('100 * windows_memory_committed_bytes{job=~"$job", instance="$instance"} / windows_memory_commit_limit{job=~"$job", instance="$instance"}', 'A'), + ]) + + b.panel.withDefaults({ + decimals: 1, + max: 100, + min: 0, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + unit: 'percent', + }) + + b.panel.withOptions({ + graphMode: 'area', + }), + + b.panel.new(17, 'Stopped auto services', 'stat') + + b.panel.withDescription('Services with start mode "auto" that are not running. See the Services tab for names.') + + b.panel.withQueries([ + b.query.new('count(windows_service_state{job=~"$job", instance="$instance", state="running"} == 0 and on (name) windows_service_start_mode{job=~"$job", instance="$instance", start_mode="auto"} == 1) or vector(0)', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 1, + }, + ], + }, + }), + + b.panel.new(18, 'CPU utilization', 'timeseries') + + b.panel.withQueries([ + b.query.new('100 * (1 - avg by (instance) (clamp_max(rate(windows_cpu_time_total{job=~"$job", instance="$instance", mode="idle"}[$__rate_interval]), 1)))', 'A') + + b.query.withLegendFormat('CPU busy'), + ]) + + b.panel.withDefaults({ + max: 100, + min: 0, + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + unit: 'percent', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }), + + b.panel.new(19, 'Physical memory', 'timeseries') + + b.panel.withDescription('Available memory is the standby (cache), free and zero page lists.') + + b.panel.withQueries([ + b.query.new('windows_memory_physical_total_bytes{job=~"$job", instance="$instance"} - windows_memory_physical_free_bytes{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('used'), + b.query.new('windows_memory_physical_free_bytes{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('available'), + ]) + + b.panel.withDefaults({ + custom: { + fillOpacity: 40, + stacking: { + mode: 'normal', + }, + }, + min: 0, + unit: 'bytes', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'used', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'orange', + mode: 'fixed', + }, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'available', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'green', + mode: 'fixed', + }, + }, + ], + }, + ]), + + b.panel.new(20, 'Volumes', 'table') + + b.panel.withQueries([ + b.query.new('windows_logical_disk_size_bytes{job=~"$job", instance="$instance", volume=~"$volume"}', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('windows_logical_disk_free_bytes{job=~"$job", instance="$instance", volume=~"$volume"}', 'B') + + b.query.withInstant() + + b.query.withFormat('table'), + b.query.new('100 * (1 - windows_logical_disk_free_bytes{job=~"$job", instance="$instance", volume=~"$volume"} / windows_logical_disk_size_bytes{job=~"$job", instance="$instance", volume=~"$volume"})', 'C') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withTransformations([ + { + group: 'joinByField', + kind: 'Transformation', + spec: { + options: { + byField: 'volume', + mode: 'outer', + }, + }, + }, + { + group: 'organize', + kind: 'Transformation', + spec: { + options: { + excludeByName: { + Time: true, + 'Time 1': true, + 'Time 2': true, + 'Time 3': true, + __name__: true, + instance: true, + 'instance 1': true, + 'instance 2': true, + 'instance 3': true, + job: true, + 'job 1': true, + 'job 2': true, + 'job 3': true, + port: true, + 'port 1': true, + 'port 2': true, + 'port 3': true, + }, + includeByName: {}, + indexByName: { + 'Value #A': 1, + 'Value #B': 2, + 'Value #C': 3, + volume: 0, + }, + renameByName: { + 'Value #A': 'Size', + 'Value #B': 'Free', + 'Value #C': 'Used', + volume: 'Volume', + }, + }, + }, + }, + ]) + + b.panel.withOverrides([ + { + matcher: { + id: 'byName', + options: 'Value #A', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #B', + }, + properties: [ + { + id: 'unit', + value: 'bytes', + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'Value #C', + }, + properties: [ + { + id: 'unit', + value: 'percent', + }, + { + id: 'min', + value: 0, + }, + { + id: 'max', + value: 100, + }, + { + id: 'thresholds', + value: { + mode: 'absolute', + steps: [ + { + color: 'green', + }, + { + color: 'orange', + value: 80, + }, + { + color: 'red', + value: 90, + }, + ], + }, + }, + { + id: 'color', + value: { + mode: 'continuous-GrYlRd', + }, + }, + { + id: 'custom.cellOptions', + value: { + mode: 'basic', + type: 'gauge', + valueDisplayMode: 'text', + }, + }, + { + id: 'decimals', + value: 1, + }, + ], + }, + ]), + + b.panel.new(21, 'Disk throughput', 'timeseries') + + b.panel.withDescription('Sum over the selected volumes. Reads are drawn below the axis.') + + b.panel.withQueries([ + b.query.new('sum(rate(windows_logical_disk_write_bytes_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval]))', 'A') + + b.query.withLegendFormat('write'), + b.query.new('sum(rate(windows_logical_disk_read_bytes_total{job=~"$job", instance="$instance", volume=~"$volume"}[$__rate_interval]))', 'B') + + b.query.withLegendFormat('read'), + ]) + + b.panel.withDefaults({ + unit: 'Bps', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: 'read', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + + b.panel.new(22, 'Network throughput', 'timeseries') + + b.panel.withDescription('Sum over the selected interfaces. Received traffic is drawn below the axis.') + + b.panel.withQueries([ + b.query.new('sum(rate(windows_net_bytes_sent_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])) * 8', 'A') + + b.query.withLegendFormat('sent'), + b.query.new('sum(rate(windows_net_bytes_received_total{job=~"$job", instance="$instance", nic=~"$nic"}[$__rate_interval])) * 8', 'B') + + b.query.withLegendFormat('received'), + ]) + + b.panel.withDefaults({ + unit: 'bps', + }) + + b.panel.withOptions({ + legend: { + calcs: [], + displayMode: 'list', + }, + }) + + b.panel.withOverrides([ + { + matcher: { + id: 'byRegexp', + options: 'received', + }, + properties: [ + { + id: 'custom.transform', + value: 'negative-Y', + }, + ], + }, + ]), + ] +) diff --git a/contrib/mixin/dashboards/services.libsonnet b/contrib/mixin/dashboards/services.libsonnet new file mode 100644 index 00000000..ebd28195 --- /dev/null +++ b/contrib/mixin/dashboards/services.libsonnet @@ -0,0 +1,134 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Services', + b.grid([ + b.place(50, 0, 0, 6, 4), + b.place(51, 6, 0, 6, 4), + b.place(52, 12, 0, 6, 4), + b.place(53, 18, 0, 6, 4), + b.place(54, 0, 4, 12, 10), + b.place(55, 12, 4, 12, 10), + ]), + [ + b.panel.new(50, 'Running services', 'stat') + + b.panel.withQueries([ + b.query.new('sum(windows_service_state{job=~"$job", instance="$instance", state="running"})', 'A') + + b.query.withInstant(), + ]), + + b.panel.new(51, 'Stopped auto services', 'stat') + + b.panel.withDescription('Services with start mode "auto" that are not running.') + + b.panel.withQueries([ + b.query.new('count(windows_service_state{job=~"$job", instance="$instance", state="running"} == 0 and on (name) windows_service_start_mode{job=~"$job", instance="$instance", start_mode="auto"} == 1) or vector(0)', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 1, + }, + ], + }, + }), + + b.panel.new(52, 'Pending services', 'stat') + + b.panel.withDescription('Services in a start, stop, pause or continue pending state. A service that stays pending is hung.') + + b.panel.withQueries([ + b.query.new('sum(windows_service_state{job=~"$job", instance="$instance", state=~".* pending"}) or vector(0)', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 1, + }, + ], + }, + }), + + b.panel.new(53, 'Disabled services', 'stat') + + b.panel.withQueries([ + b.query.new('sum(windows_service_start_mode{job=~"$job", instance="$instance", start_mode="disabled"})', 'A') + + b.query.withInstant(), + ]), + + b.panel.new(54, 'Automatic services not running', 'table') + + b.panel.withDescription('Services configured to start automatically that are not in the running state. Delayed-start and trigger-start services may show up here until they start.') + + b.panel.withQueries([ + b.query.new('max by (name, state) (windows_service_state{job=~"$job", instance="$instance"} == 1) and on (name) (windows_service_state{job=~"$job", instance="$instance", state="running"} == 0) and on (name) (windows_service_start_mode{job=~"$job", instance="$instance", start_mode="auto"} == 1)', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withTransformations([ + { + group: 'organize', + kind: 'Transformation', + spec: { + options: { + excludeByName: { + Time: true, + 'Time 1': true, + Value: true, + }, + includeByName: {}, + indexByName: { + name: 0, + state: 1, + }, + renameByName: { + name: 'Service', + state: 'State', + }, + }, + }, + }, + ]) + + b.panel.withOptions({ + sortBy: [ + { + desc: false, + displayName: 'Service', + }, + ], + }), + + b.panel.new(55, 'Service state changes', 'state-timeline') + + b.panel.withDescription('Services that started or stopped in the selected time range.') + + b.panel.withQueries([ + b.query.new('windows_service_state{job=~"$job", instance="$instance", state="running"} and on (name) (changes(windows_service_state{job=~"$job", instance="$instance", state="running"}[$__range] @ end()) > 0)', 'A') + + b.query.withLegendFormat('{{name}}'), + ]) + + b.panel.withDefaults({ + mappings: [ + { + options: { + '0': { + color: 'red', + index: 0, + text: 'not running', + }, + '1': { + color: 'green', + index: 1, + text: 'running', + }, + }, + type: 'value', + }, + ], + }), + ] +) diff --git a/contrib/mixin/dashboards/styles.libsonnet b/contrib/mixin/dashboards/styles.libsonnet new file mode 100644 index 00000000..84b2484b --- /dev/null +++ b/contrib/mixin/dashboards/styles.libsonnet @@ -0,0 +1,197 @@ +// Shared visualization defaults from the reference dashboard. Arrays are replaced whole. +{ + timeseries: { + defaults: { + color: { + mode: 'palette-classic', + }, + custom: { + axisBorderShow: false, + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 10, + gradientMode: 'opacity', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + insertNulls: false, + lineInterpolation: 'linear', + lineWidth: 1, + pointSize: 4, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'never', + spanNulls: false, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + unit: 'percentunit', + }, + options: { + legend: { + calcs: [ + 'mean', + 'max', + 'lastNotNull', + ], + displayMode: 'table', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + hideZeros: false, + mode: 'multi', + sort: 'desc', + }, + }, + }, + stat: { + defaults: { + color: { + mode: 'thresholds', + }, + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'text', + value: null, + }, + ], + }, + unit: 'none', + }, + options: { + colorMode: 'value', + graphMode: 'none', + justifyMode: 'auto', + orientation: 'auto', + percentChangeColorMode: 'standard', + reduceOptions: { + calcs: [ + 'lastNotNull', + ], + fields: '', + values: false, + }, + showPercentChange: false, + textMode: 'auto', + wideLayout: true, + }, + }, + table: { + defaults: { + color: { + mode: 'thresholds', + }, + custom: { + align: 'auto', + cellOptions: { + type: 'auto', + }, + filterable: false, + inspect: false, + minWidth: 60, + }, + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + }, + options: { + cellHeight: 'sm', + enablePagination: false, + footer: { + countRows: false, + fields: '', + reducer: [ + 'sum', + ], + show: false, + }, + showHeader: true, + sortBy: [ + { + desc: true, + displayName: 'Used', + }, + ], + }, + }, + 'state-timeline': { + defaults: { + color: { + mode: 'thresholds', + }, + custom: { + fillOpacity: 90, + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + insertNulls: false, + lineWidth: 0, + spanNulls: false, + }, + max: 1, + min: 0, + thresholds: { + mode: 'absolute', + steps: [ + { + color: 'red', + value: null, + }, + { + color: 'green', + value: 1, + }, + ], + }, + unit: 'none', + }, + options: { + alignValue: 'left', + legend: { + displayMode: 'list', + placement: 'bottom', + showLegend: false, + }, + mergeValues: false, + rowHeight: 1, + showValue: 'never', + tooltip: { + hideZeros: false, + mode: 'single', + sort: 'none', + }, + }, + }, +} diff --git a/contrib/mixin/dashboards/time.libsonnet b/contrib/mixin/dashboards/time.libsonnet new file mode 100644 index 00000000..d8291090 --- /dev/null +++ b/contrib/mixin/dashboards/time.libsonnet @@ -0,0 +1,108 @@ +local b = import 'builders.libsonnet'; + +b.tab( + 'Time', + b.grid([ + b.place(84, 0, 0, 6, 4), + b.place(85, 6, 0, 6, 4), + b.place(86, 12, 0, 6, 4), + b.place(87, 18, 0, 6, 4), + b.place(88, 0, 4, 24, 8), + ]), + [ + b.panel.new(84, 'Time zone', 'stat') + + b.panel.withDescription('Needs the time collector, which is not enabled by default.') + + b.panel.withQueries([ + b.query.new('windows_time_timezone{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withDefaults({ + unit: '', + }) + + b.panel.withOptions({ + colorMode: 'none', + reduceOptions: { + fields: '/^timezone$/', + }, + textMode: 'value', + }), + + b.panel.new(85, 'Clock sync source', 'stat') + + b.panel.withQueries([ + b.query.new('windows_time_clock_sync_source{job=~"$job", instance="$instance"} == 1', 'A') + + b.query.withInstant() + + b.query.withFormat('table'), + ]) + + b.panel.withDefaults({ + unit: '', + }) + + b.panel.withOptions({ + colorMode: 'none', + reduceOptions: { + fields: '/^type$/', + }, + textMode: 'value', + }), + + b.panel.new(86, 'NTP time sources', 'stat') + + b.panel.withDescription('Number of time sources the NTP client uses. 0 means the clock is not synchronized.') + + b.panel.withQueries([ + b.query.new('windows_time_ntp_client_time_sources{job=~"$job", instance="$instance"}', 'A') + + b.query.withInstant(), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'red', + value: null, + }, + { + color: 'green', + value: 1, + }, + ], + }, + }), + + b.panel.new(87, 'Clock offset', 'stat') + + b.panel.withDescription('Absolute offset between the system clock and the selected time source.') + + b.panel.withQueries([ + b.query.new('abs(windows_time_computed_time_offset_seconds{job=~"$job", instance="$instance"})', 'A'), + ]) + + b.panel.withDefaults({ + thresholds: { + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'orange', + value: 0.5, + }, + { + color: 'red', + value: 1, + }, + ], + }, + unit: 's', + }) + + b.panel.withOptions({ + graphMode: 'area', + }), + + b.panel.new(88, 'Clock offset and NTP round-trip delay', 'timeseries') + + b.panel.withQueries([ + b.query.new('windows_time_computed_time_offset_seconds{job=~"$job", instance="$instance"}', 'A') + + b.query.withLegendFormat('clock offset'), + b.query.new('windows_time_ntp_round_trip_delay_seconds{job=~"$job", instance="$instance"}', 'B') + + b.query.withLegendFormat('NTP round-trip delay'), + ]) + + b.panel.withDefaults({ + unit: 's', + }), + ] +) diff --git a/contrib/mixin/dashboards/variables.libsonnet b/contrib/mixin/dashboards/variables.libsonnet new file mode 100644 index 00000000..ec110ae3 --- /dev/null +++ b/contrib/mixin/dashboards/variables.libsonnet @@ -0,0 +1,102 @@ +local b = import 'builders.libsonnet'; +local variables = b.dashboard.spec.variables; +local query = variables.QueryVariableKind; +local datasource = variables.DatasourceVariableKind; +local common = query.withKind() + + query.spec.withAllowCustomValue(true) + + query.spec.withCurrent({ text: '', value: '' }) + + query.spec.withHide('dontHide') + + query.spec.withIncludeAll(false) + + query.spec.withMulti(false) + + query.spec.withOptions([]) + + query.spec.withRefresh('onTimeRangeChanged') + + query.spec.withRegex('') + + query.spec.withSkipUrlSync(false) + + query.spec.withSort('alphabeticalAsc'); +local new(name, definition) = common + + query.spec.withName(name) + + query.spec.withDefinition(definition) + + query.spec.withQuery(b.dataQuery({ + qryType: 1, + query: definition, + refId: 'PrometheusVariableQueryEditor-VariableQuery', + })); + +[ + datasource.withKind() + + datasource.spec.withAllowCustomValue(true) + + datasource.spec.withCurrent({ + text: '', + value: '', + }) + + datasource.spec.withHide('dontHide') + + datasource.spec.withIncludeAll(false) + + datasource.spec.withLabel('Data source') + + datasource.spec.withMulti(false) + + datasource.spec.withName('datasource') + + datasource.spec.withOptions([]) + + datasource.spec.withPluginId('prometheus') + + datasource.spec.withRefresh('onDashboardLoad') + + datasource.spec.withRegex('') + + datasource.spec.withSkipUrlSync(false) + , + new('job', 'label_values(windows_os_hostname, job)') + + query.spec.withCurrent({ + text: [ + 'All', + ], + value: [ + '$__all', + ], + }) + + query.spec.withIncludeAll(true) + + query.spec.withLabel('Job') + + query.spec.withMulti(true) + , + new('hostname', 'label_values(windows_os_hostname{job=~"$job"}, hostname)') + + query.spec.withCurrent({ + text: [ + 'All', + ], + value: [ + '$__all', + ], + }) + + query.spec.withIncludeAll(true) + + query.spec.withLabel('Hostname') + + query.spec.withMulti(true) + , + 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') + , + new('volume', 'label_values(windows_logical_disk_size_bytes{job=~"$job", instance="$instance"}, volume)') + + query.spec.withCurrent({ + text: [ + 'All', + ], + value: [ + '$__all', + ], + }) + + query.spec.withIncludeAll(true) + + query.spec.withLabel('Volume') + + query.spec.withMulti(true) + + query.spec.withRegex('/^(?!HarddiskVolume).+$/') + , + new('nic', 'label_values(windows_net_bytes_total{job=~"$job", instance="$instance"}, nic)') + + query.spec.withCurrent({ + text: [ + 'All', + ], + value: [ + '$__all', + ], + }) + + query.spec.withIncludeAll(true) + + query.spec.withLabel('Network interface') + + query.spec.withMulti(true) + + query.spec.withRegex('/^(?!isatap|Teredo|6to4).+$/'), +] diff --git a/contrib/mixin/dashboards/windows-exporter.libsonnet b/contrib/mixin/dashboards/windows-exporter.libsonnet index 63db2b9d..498e7c3e 100644 --- a/contrib/mixin/dashboards/windows-exporter.libsonnet +++ b/contrib/mixin/dashboards/windows-exporter.libsonnet @@ -1,1350 +1,64 @@ -// The sample dashboard in dashboard/windows-exporter-dashboard.json. See dashboard/README.md. -local d = import '../lib/dashboard.libsonnet'; +local b = import 'builders.libsonnet'; +local dashboard = b.dashboard; +local tabs = [ + import 'fleet.libsonnet', + import 'overview.libsonnet', + import 'cpu.libsonnet', + import 'memory.libsonnet', + import 'disk.libsonnet', + import 'network.libsonnet', + import 'services.libsonnet', + import 'gpu.libsonnet', + import 'hyper_v.libsonnet', + import 'time.libsonnet', + import 'exporter.libsonnet', +]; -local q = d.q; -local H = d.host; -local F = d.fleet; - -local cpuUtil(sel) = - '100 * (1 - avg by (instance) (clamp_max(rate(windows_cpu_time_total{%s, mode="idle"}[$__rate_interval]), 1)))' % sel; -local memUtil(sel) = - '100 * (1 - windows_memory_physical_free_bytes{%s} / windows_memory_physical_total_bytes{%s})' % [sel, sel]; -local volSelFleet = F + ', volume!~"HarddiskVolume.*"'; -local volSel = H + ', volume=~"$volume"'; -local nicSel = H + ', nic=~"$nic"'; - -// Fleet graphs show only the busiest hosts so they stay readable for large fleets. -local top(expr) = 'topk(25, ' + expr + ')'; -local topDesc = 'Shows the 25 highest hosts at each point in time.'; - -local pctSteps = d.pctSteps; -local steps = d.steps; -local byName = d.byName; -local byRegexp = d.byRegexp; -local prop = d.prop; - -local volumesTable = d.table( - 'Volumes', - width=8, - height=8, - targets=[ - q('windows_logical_disk_size_bytes{' + volSel + '}'), - q('windows_logical_disk_free_bytes{' + volSel + '}'), - q('100 * (1 - windows_logical_disk_free_bytes{' + volSel + '} / windows_logical_disk_size_bytes{' + volSel + '})'), - ], - joinBy='volume', - exclude=['instance', 'job', 'port', '__name__', 'instance 1', 'instance 2', 'instance 3', 'job 1', 'job 2', 'job 3', 'port 1', 'port 2', 'port 3'], - order={ volume: 0, 'Value #A': 1, 'Value #B': 2, 'Value #C': 3 }, - rename={ volume: 'Volume', 'Value #A': 'Size', 'Value #B': 'Free', 'Value #C': 'Used' }, - sortBy=[{ desc: true, displayName: 'Used' }], - overrides=[ - byName('Value #A', d.unitCell('bytes')), - byName('Value #B', d.unitCell('bytes')), - byName('Value #C', d.gaugeCell('percent', 100)), - ], -); - -local physicalMemory(legend) = d.timeseries( - 'Physical memory', - width=12, - height=8, - unit='bytes', - min=0, - stack=true, - fill=40, - legend=legend, - description='Available memory is the standby (cache), free and zero page lists.', - targets=[ - q('windows_memory_physical_total_bytes{' + H + '} - windows_memory_physical_free_bytes{' + H + '}', 'used'), - q('windows_memory_physical_free_bytes{' + H + '}', 'available'), - ], - overrides=[byName('used', [d.fixedColor('orange')]), byName('available', [d.fixedColor('green')])], -); - -local stoppedAutoServices = 'count(windows_service_state{' + H + ', state="running"} == 0 and on (name) windows_service_start_mode{' + H + ', start_mode="auto"} == 1) or vector(0)'; - -// ================================================================ Fleet -local fleet = d.tab('Fleet', [ - [ - d.table( - 'Hosts', - width=24, - height=10, - description='One row per scraped host. Click a hostname to open its details. Services up counts running services; Auto stopped counts services with start mode auto that are not running.', - targets=[ - q('max by (instance, hostname) (windows_os_hostname{' + F + ', hostname=~"$hostname"})'), - q(d.fleetFilter('max by (instance, product, version) (windows_os_info{' + F + '})')), - q(d.fleetFilter('max by (instance) (time() - windows_system_boot_time_timestamp{' + F + '})')), - q(d.fleetFilter('max by (instance) (windows_cpu_logical_processor{' + F + '})')), - q(d.fleetFilter(cpuUtil(F))), - q(d.fleetFilter('max by (instance) (windows_memory_physical_total_bytes{' + F + '})')), - q(d.fleetFilter('max by (instance) (' + memUtil(F) + ')')), - q(d.fleetFilter('max by (instance) (100 * windows_memory_committed_bytes{' + F + '} / windows_memory_commit_limit{' + F + '})')), - q(d.fleetFilter('max by (instance) (100 * (1 - windows_logical_disk_free_bytes{' + volSelFleet + '} / windows_logical_disk_size_bytes{' + volSelFleet + '}))')), - q(d.fleetFilter('sum by (instance) (windows_service_state{' + F + ', state="running"})')), - q(d.fleetFilter('count by (instance) (windows_service_state{' + F + ', state="running"} == 0 and on (instance, name) windows_service_start_mode{' + F + ', start_mode="auto"} == 1)')), - q(d.fleetFilter('max by (instance) (windows_system_processes{' + F + '})')), - ], - joinBy='instance', - order={ - hostname: 0, - instance: 1, - product: 2, - version: 3, - 'Value #C': 4, - 'Value #D': 5, - 'Value #E': 6, - 'Value #F': 7, - 'Value #G': 8, - 'Value #H': 9, - 'Value #I': 10, - 'Value #J': 11, - 'Value #K': 12, - 'Value #L': 13, - }, - exclude=['Value #A', 'Value #B'], - rename={ - hostname: 'Hostname', - instance: 'Instance', - product: 'OS', - version: 'Version', - 'Value #C': 'Uptime', - 'Value #D': 'CPUs', - 'Value #E': 'CPU', - 'Value #F': 'Memory', - 'Value #G': 'Memory used', - 'Value #H': 'Commit used', - 'Value #I': 'Fullest volume', - 'Value #J': 'Services up', - 'Value #K': 'Auto stopped', - 'Value #L': 'Processes', - }, - sortBy=[{ desc: false, displayName: 'Hostname' }], - footer=['CPUs', 'Memory', 'Processes'], - overrides=[ - byName('hostname', [ - prop('links', [{ - 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', - }]), - prop('custom.width', 180), - prop('custom.filterable', true), - ]), - byName('Value #C', [prop('unit', 'dtdurations'), prop('custom.width', 90)]), - byName('instance', [prop('custom.width', 140)]), - byName('product', [prop('custom.width', 160), prop('custom.filterable', true)]), - byName('version', [prop('custom.width', 100), prop('custom.filterable', true)]), - byName('Value #D', [prop('unit', 'none'), prop('custom.width', 60)]), - byName('Value #E', d.gaugeCell('percent', 100)), - byName('Value #F', [prop('unit', 'bytes'), prop('custom.width', 90)]), - byName('Value #G', d.gaugeCell('percent', 100)), - byName('Value #H', d.gaugeCell('percent', 100)), - byName('Value #I', d.gaugeCell('percent', 100)), - byName('Value #J', d.unitCell('none')), - byName('Value #K', [ - prop('unit', 'none'), - prop('thresholds', d.overrideSteps([['green', null], ['orange', 1]])), - prop('custom.cellOptions', { type: 'color-text' }), - prop('noValue', '0'), - ]), - byName('Value #L', [prop('unit', 'none'), prop('custom.width', 90)]), - ], - ), - ], - [ - d.timeseries( - 'CPU utilization', - width=8, - height=8, - description=topDesc, - unit='percent', - min=0, - max=100, - legend='list', - targets=[q(top(d.fleetJoin(cpuUtil(F))), '{{hostname}}')], - ), - d.timeseries( - 'Memory utilization', - width=8, - height=8, - description=topDesc, - unit='percent', - min=0, - max=100, - legend='list', - targets=[q(top(d.fleetJoin('max by (instance) (' + memUtil(F) + ')')), '{{hostname}}')], - ), - d.timeseries( - 'Fullest volume', - width=8, - height=8, - description='Usage of the fullest volume per host. ' + topDesc, - unit='percent', - min=0, - max=100, - legend='list', - thresholds=pctSteps, - showThresholds=true, - targets=[q(top(d.fleetJoin('max by (instance) (100 * (1 - windows_logical_disk_free_bytes{' + volSelFleet + '} / windows_logical_disk_size_bytes{' + volSelFleet + '}))')), '{{hostname}}')], - ), - ], - [ - d.timeseries( - 'Network throughput', - width=8, - height=8, - description='Sum over all interfaces. Received is drawn below the axis. ' + topDesc, - unit='bps', - legend='list', - targets=[ - q(top(d.fleetJoin('sum by (instance) (rate(windows_net_bytes_sent_total{' + F + '}[$__rate_interval]) * 8)')), '{{hostname}} sent'), - q(top(d.fleetJoin('sum by (instance) (rate(windows_net_bytes_received_total{' + F + '}[$__rate_interval]) * 8)')), '{{hostname}} received'), - ], - overrides=[d.negativeY('.* received$')], - ), - d.timeseries( - 'Disk throughput', - width=8, - height=8, - description='Sum over all volumes. Read is drawn below the axis. ' + topDesc, - unit='Bps', - legend='list', - targets=[ - q(top(d.fleetJoin('sum by (instance) (rate(windows_logical_disk_write_bytes_total{' + volSelFleet + '}[$__rate_interval]))')), '{{hostname}} write'), - q(top(d.fleetJoin('sum by (instance) (rate(windows_logical_disk_read_bytes_total{' + volSelFleet + '}[$__rate_interval]))')), '{{hostname}} read'), - ], - overrides=[d.negativeY('.* read$')], - ), - d.timeseries( - 'Network errors and discards', - width=8, - height=8, - description='Sum over all interfaces of received and outbound errors and discards. ' + topDesc, - unit='pps', - min=0, - legend='list', - targets=[q(top(d.fleetJoin('sum by (instance) (rate(windows_net_packets_received_errors_total{' + F + '}[$__rate_interval]) + rate(windows_net_packets_received_discarded_total{' + F + '}[$__rate_interval]) + rate(windows_net_packets_outbound_errors_total{' + F + '}[$__rate_interval]) + rate(windows_net_packets_outbound_discarded_total{' + F + '}[$__rate_interval]))')), '{{hostname}}')], - ), - ], -]); - -// ================================================================ Overview -local overview = d.tab('Overview', [ - [ - d.stat('Operating system', 'windows_os_info{' + H + '}', width=4, height=4, text='product', colorMode='none'), - d.stat( - 'Uptime', - 'time() - windows_system_boot_time_timestamp{' + H + '}', - width=3, - height=4, - unit='dtdurations', - description='Time since the last boot.', - thresholds=steps([['orange', null], ['text', 3600]]), - ), - d.stat('CPUs', 'windows_cpu_logical_processor{' + H + '}', width=2, height=4, unit='none'), - d.stat('RAM', 'windows_memory_physical_total_bytes{' + H + '}', width=2, height=4, unit='bytes', decimals=1), - d.stat( - 'CPU busy', - cpuUtil(H), - width=3, - height=4, - unit='percent', - min=0, - max=100, - graph=true, - thresholds=pctSteps, - decimals=1, - ), - d.stat( - 'Memory used', - memUtil(H), - width=3, - height=4, - unit='percent', - min=0, - max=100, - graph=true, - thresholds=pctSteps, - decimals=1, - ), - d.stat( - 'Commit used', - '100 * windows_memory_committed_bytes{' + H + '} / windows_memory_commit_limit{' + H + '}', - width=3, - height=4, - description='Committed virtual memory as a share of the commit limit (physical memory + page files). Allocations fail at 100%.', - unit='percent', - min=0, - max=100, - graph=true, - thresholds=pctSteps, - decimals=1, - ), - d.stat( - 'Stopped auto services', - stoppedAutoServices, - width=4, - height=4, - description='Services with start mode "auto" that are not running. See the Services tab for names.', - unit='none', - thresholds=steps([['green', null], ['orange', 1]]), - ), - ], - [ - d.timeseries( - 'CPU utilization', - width=12, - height=8, - unit='percent', - min=0, - max=100, - legend='list', - thresholds=pctSteps, - targets=[q(cpuUtil(H), 'CPU busy')], - ), - physicalMemory('list'), - ], - [ - volumesTable, - d.timeseries( - 'Disk throughput', - width=8, - height=8, - unit='Bps', - description='Sum over the selected volumes. Reads are drawn below the axis.', - legend='list', - targets=[ - q('sum(rate(windows_logical_disk_write_bytes_total{' + volSel + '}[$__rate_interval]))', 'write'), - q('sum(rate(windows_logical_disk_read_bytes_total{' + volSel + '}[$__rate_interval]))', 'read'), - ], - overrides=[d.negativeY('read')], - ), - d.timeseries( - 'Network throughput', - width=8, - height=8, - unit='bps', - description='Sum over the selected interfaces. Received traffic is drawn below the axis.', - legend='list', - targets=[ - q('sum(rate(windows_net_bytes_sent_total{' + nicSel + '}[$__rate_interval])) * 8', 'sent'), - q('sum(rate(windows_net_bytes_received_total{' + nicSel + '}[$__rate_interval])) * 8', 'received'), - ], - overrides=[d.negativeY('received')], - ), - ], -]); - -// ================================================================ CPU and system -local cpu = d.tab('CPU', [ - [ - d.timeseries( - 'CPU utilization by mode', - width=12, - height=10, - description='Share of total CPU time across all logical processors.', - unit='percentunit', - min=0, - max=1, - stack=true, - fill=40, - targets=[q( - 'sum by (mode) (rate(windows_cpu_time_total{' + H + ', mode!="idle"}[$__rate_interval])) / scalar(count(windows_cpu_time_total{' + H + ', mode="idle"}))', - '{{mode}}', - )], - ), - // Single-digit core numbers are padded ("0,2" -> "0,02") so the rows sort numerically. - d.stateTimeline( - 'CPU utilization per core', - 'Busy time of each logical processor, labelled processor group,core. Green is idle, red is fully busy.', - 'percentunit', - 1, - q(@'label_replace(clamp(1 - rate(windows_cpu_time_total{' + H + @', mode="idle"}[$__rate_interval]), 0, 1), "core", "$1,0$2", "core", "(\\d+),(\\d)")', '{{core}}'), - width=12, - height=10, - ), - ], - [ - d.timeseries( - 'Processor queue length', - width=8, - height=8, - unit='short', - min=0, - legend='list', - description='Threads that are ready to run but waiting for a CPU. A sustained value above 2 per core indicates CPU contention.', - targets=[q('windows_system_processor_queue_length{' + H + '}', 'queue length')], - ), - d.timeseries( - 'Context switches and interrupts', - width=8, - height=8, - unit='ops', - min=0, - targets=[ - q('rate(windows_system_context_switches_total{' + H + '}[$__rate_interval])', 'context switches'), - q('sum(rate(windows_cpu_interrupts_total{' + H + '}[$__rate_interval]))', 'interrupts'), - q('sum(rate(windows_cpu_dpcs_total{' + H + '}[$__rate_interval]))', 'DPCs'), - ], - ), - d.timeseries( - 'Processes and threads', - width=8, - height=8, - unit='short', - min=0, - targets=[ - q('windows_system_processes{' + H + '}', 'processes'), - q('windows_system_threads{' + H + '}', 'threads'), - ], - overrides=[byName('threads', [prop('custom.axisPlacement', 'right')])], - ), - ], - [ - d.timeseries( - 'System calls and exceptions', - width=12, - height=8, - unit='ops', - min=0, - targets=[ - q('rate(windows_system_system_calls_total{' + H + '}[$__rate_interval])', 'system calls'), - q('rate(windows_system_exception_dispatches_total{' + H + '}[$__rate_interval])', 'exception dispatches'), - ], - overrides=[byName('exception dispatches', [prop('custom.axisPlacement', 'right')])], - ), - d.timeseries( - 'CPU frequency', - width=12, - height=8, - unit='hertz', - min=0, - legend='list', - description='Effective frequency from the APERF/MPERF counters, averaged and maximum across logical processors, against the nominal frequency. Values above nominal mean turbo boost.', - targets=[ - q('avg(1e4 * windows_cpu_core_frequency_mhz{' + H + '} * rate(windows_cpu_processor_performance_total{' + H + '}[$__rate_interval]) / rate(windows_cpu_processor_mperf_total{' + H + '}[$__rate_interval]))', 'average'), - q('max(1e4 * windows_cpu_core_frequency_mhz{' + H + '} * rate(windows_cpu_processor_performance_total{' + H + '}[$__rate_interval]) / rate(windows_cpu_processor_mperf_total{' + H + '}[$__rate_interval]))', 'maximum'), - q('avg(windows_cpu_core_frequency_mhz{' + H + '}) * 1e6', 'nominal'), - ], - overrides=[byName('nominal', [d.fixedColor('text')] + d.dashed)], - ), - ], -]); - -// ================================================================ Memory -local memory = d.tab('Memory', [ - [ - physicalMemory('table'), - d.timeseries( - 'Commit charge', - width=12, - height=8, - unit='bytes', - min=0, - description='Committed virtual memory against the commit limit (physical memory + page files).', - targets=[ - q('windows_memory_committed_bytes{' + H + '}', 'committed'), - q('windows_memory_commit_limit{' + H + '}', 'limit'), - ], - overrides=[byName('limit', [d.fixedColor('red')] + d.dashed)], - ), - ], - [ - d.timeseries( - 'Paging', - width=12, - height=8, - unit='ops', - min=0, - description='Hard page faults read from or written to disk. Sustained page reads indicate memory pressure.', - targets=[ - q('rate(windows_memory_swap_page_reads_total{' + H + '}[$__rate_interval])', 'page reads'), - q('rate(windows_memory_swap_page_writes_total{' + H + '}[$__rate_interval])', 'page writes'), - ], - ), - d.timeseries( - 'Kernel pools and cache', - width=12, - height=8, - unit='bytes', - min=0, - description='A steadily growing nonpaged pool usually means a driver leaks memory.', - targets=[ - q('windows_memory_pool_nonpaged_bytes{' + H + '}', 'nonpaged pool'), - q('windows_memory_pool_paged_bytes{' + H + '}', 'paged pool'), - q('windows_memory_cache_bytes{' + H + '}', 'system cache'), - ], - ), - ], -]); - -// ================================================================ Disk -local disk = d.tab('Disk', [ - [ - volumesTable, - d.timeseries( - 'Volume usage', - width=8, - height=8, - unit='percent', - min=0, - max=100, - thresholds=pctSteps, - showThresholds=true, - targets=[q('100 * (1 - windows_logical_disk_free_bytes{' + volSel + '} / windows_logical_disk_size_bytes{' + volSel + '})', '{{volume}}')], - calcs=['min', 'max', 'lastNotNull'], - ), - d.timeseries( - 'Volume busy time', - width=8, - height=8, - unit='percentunit', - min=0, - max=1, - description='Share of time the volume was servicing requests (1 - idle time).', - targets=[q('1 - clamp_max(rate(windows_logical_disk_idle_seconds_total{' + volSel + '}[$__rate_interval]), 1)', '{{volume}}')], - ), - ], - [ - d.timeseries( - 'Disk throughput', - width=12, - height=8, - unit='Bps', - description='Reads are drawn below the axis.', - targets=[ - q('rate(windows_logical_disk_write_bytes_total{' + volSel + '}[$__rate_interval])', '{{volume}} write'), - q('rate(windows_logical_disk_read_bytes_total{' + volSel + '}[$__rate_interval])', '{{volume}} read'), - ], - overrides=[d.negativeY('.* read$')], - ), - d.timeseries( - 'Disk IOPS', - width=12, - height=8, - unit='iops', - description='Reads are drawn below the axis.', - targets=[ - q('rate(windows_logical_disk_writes_total{' + volSel + '}[$__rate_interval])', '{{volume}} write'), - q('rate(windows_logical_disk_reads_total{' + volSel + '}[$__rate_interval])', '{{volume}} read'), - ], - overrides=[d.negativeY('.* read$')], - ), - ], - [ - d.timeseries( - 'Disk latency', - width=12, - height=8, - unit='s', - description='Average time per read and write operation. Reads are drawn below the axis.', - targets=[ - q('rate(windows_logical_disk_write_seconds_total{' + volSel + '}[$__rate_interval]) / rate(windows_logical_disk_writes_total{' + volSel + '}[$__rate_interval])', '{{volume}} write'), - q('rate(windows_logical_disk_read_seconds_total{' + volSel + '}[$__rate_interval]) / rate(windows_logical_disk_reads_total{' + volSel + '}[$__rate_interval])', '{{volume}} read'), - ], - overrides=[d.negativeY('.* read$')], - ), - d.timeseries( - 'Disk queue length', - width=12, - height=8, - unit='short', - description='Average number of read and write requests queued for the volume. Reads are drawn below the axis. A queue that stays high while latency rises indicates a disk bottleneck. The avg_*_requests_queued metrics grow like counters, so the panel takes their rate.', - targets=[ - q('rate(windows_logical_disk_avg_write_requests_queued{' + volSel + '}[$__rate_interval])', '{{volume}} write'), - q('rate(windows_logical_disk_avg_read_requests_queued{' + volSel + '}[$__rate_interval])', '{{volume}} read'), - ], - overrides=[d.negativeY('.* read$')], - ), - ], -]); - -// ================================================================ Network -local network = d.tab('Network', [ - [ - d.timeseries( - 'Network throughput', - width=12, - height=8, - unit='bps', - description='Received traffic is drawn below the axis.', - targets=[ - q('rate(windows_net_bytes_sent_total{' + nicSel + '}[$__rate_interval]) * 8', '{{nic}} sent'), - q('rate(windows_net_bytes_received_total{' + nicSel + '}[$__rate_interval]) * 8', '{{nic}} received'), - ], - overrides=[d.negativeY('.* received$')], - ), - d.timeseries( - 'Network utilization', - width=12, - height=8, - unit='percentunit', - min=0, - thresholds=pctSteps, - description='Sent plus received traffic relative to the link speed reported by the adapter. Interfaces without a link speed are not shown.', - targets=[q('rate(windows_net_bytes_total{' + nicSel + '}[$__rate_interval]) / (windows_net_current_bandwidth_bytes{' + nicSel + '} > 0)', '{{nic}}')], - ), - ], - [ - d.timeseries( - 'Packets', - width=12, - height=8, - unit='pps', - description='Received packets are drawn below the axis.', - targets=[ - q('rate(windows_net_packets_sent_total{' + nicSel + '}[$__rate_interval])', '{{nic}} sent'), - q('rate(windows_net_packets_received_total{' + nicSel + '}[$__rate_interval])', '{{nic}} received'), - ], - overrides=[d.negativeY('.* received$')], - ), - d.timeseries( - 'Network errors and discards', - width=12, - height=8, - unit='pps', - min=0, - targets=[ - q('rate(windows_net_packets_received_errors_total{' + nicSel + '}[$__rate_interval])', '{{nic}} received errors'), - q('rate(windows_net_packets_received_discarded_total{' + nicSel + '}[$__rate_interval])', '{{nic}} received discarded'), - q('rate(windows_net_packets_outbound_errors_total{' + nicSel + '}[$__rate_interval])', '{{nic}} outbound errors'), - q('rate(windows_net_packets_outbound_discarded_total{' + nicSel + '}[$__rate_interval])', '{{nic}} outbound discarded'), - q('rate(windows_net_packets_received_unknown_total{' + nicSel + '}[$__rate_interval])', '{{nic}} received unknown protocol'), - ], - ), - ], -]); - -// ================================================================ Services -local services = d.tab('Services', [ - [ - d.stat('Running services', 'sum(windows_service_state{' + H + ', state="running"})', width=6, height=4, unit='none'), - d.stat( - 'Stopped auto services', - stoppedAutoServices, - width=6, - height=4, - description='Services with start mode "auto" that are not running.', - unit='none', - thresholds=steps([['green', null], ['orange', 1]]), - ), - d.stat( - 'Pending services', - 'sum(windows_service_state{' + H + ', state=~".* pending"}) or vector(0)', - width=6, - height=4, - description='Services in a start, stop, pause or continue pending state. A service that stays pending is hung.', - unit='none', - thresholds=steps([['green', null], ['orange', 1]]), - ), - d.stat('Disabled services', 'sum(windows_service_start_mode{' + H + ', start_mode="disabled"})', width=6, height=4, unit='none'), - ], - [ - d.table( - 'Automatic services not running', - width=12, - height=10, - description='Services configured to start automatically that are not in the running state. Delayed-start and trigger-start services may show up here until they start.', - targets=[q('max by (name, state) (windows_service_state{' + H + '} == 1) and on (name) (windows_service_state{' + H + ', state="running"} == 0) and on (name) (windows_service_start_mode{' + H + ', start_mode="auto"} == 1)')], - exclude=['Value'], - order={ name: 0, state: 1 }, - rename={ name: 'Service', state: 'State' }, - sortBy=[{ desc: false, displayName: 'Service' }], - ), - d.statusTimeline( - 'Service state changes', - 'Services that started or stopped in the selected time range.', - q('windows_service_state{' + H + ', state="running"} and on (name) (changes(windows_service_state{' + H + ', state="running"}[$__range] @ end()) > 0)', '{{name}}'), - 'running', - 'not running', - width=12, - height=10, - ), - ], -]); - -// ================================================================ GPU (optional collector) -// Engine time is per process and engine. Like Task Manager, utilization of an engine type is -// the busiest engine of that type, and the GPU utilization is the busiest engine overall. -local gpuEngine = 'sum by (luid, eng, engtype) (rate(windows_gpu_engine_time_seconds{' + H + '}[$__rate_interval]))'; -local gpuName = 'max by (luid, name) (windows_gpu_info{' + H + '})'; -local gpuProc = 'max by (process_id) (sum by (process_id, luid, eng) (rate(windows_gpu_engine_time_seconds{' + H + '}[$__rate_interval])))'; -local gpuProcMem = 'sum by (process_id) (windows_gpu_process_memory_dedicated_bytes{' + H + '})'; -// withProcessName adds the process name when the process collector is enabled and keeps the PID otherwise. -local withProcessName(expr) = - '(' + expr + ' * on (process_id) group_left (process) max by (process_id, process) (windows_process_info{' + H + '})) or on (process_id) ' + expr; - -local gpu = d.tab('GPU', [ - [ - d.table( - 'GPUs', - width=24, - height=5, - description='Needs the gpu collector, which is not enabled by default. Utilization is the busiest engine of the GPU.', - targets=[ - q(gpuName), - q('clamp_max(max by (luid) (' + gpuEngine + '), 1)'), - q('max by (luid) (windows_gpu_adapter_memory_dedicated_bytes{' + H + '})'), - q('max by (luid) (windows_gpu_dedicated_video_memory_size_bytes{' + H + '})'), - q('max by (luid) (windows_gpu_adapter_memory_shared_bytes{' + H + '})'), - q('max by (luid) (windows_gpu_shared_system_memory_size_bytes{' + H + '})'), - ], - joinBy='luid', - exclude=['luid', 'Value #A'], - order={ name: 0, 'Value #B': 1, 'Value #C': 2, 'Value #D': 3, 'Value #E': 4, 'Value #F': 5 }, - rename={ - name: 'GPU', - 'Value #B': 'Utilization', - 'Value #C': 'Dedicated memory used', - 'Value #D': 'Dedicated memory', - 'Value #E': 'Shared memory used', - 'Value #F': 'Shared memory', - }, - overrides=[ - byName('Value #B', d.gaugeCell('percentunit', 1)), - byName('Value #C', d.unitCell('bytes')), - byName('Value #D', d.unitCell('bytes')), - byName('Value #E', d.unitCell('bytes')), - byName('Value #F', d.unitCell('bytes')), - ], - ), - ], - [ - d.timeseries( - 'GPU utilization by engine type', - width=12, - height=8, - unit='percentunit', - min=0, - max=1, - description='Busiest engine of each engine type, as in Task Manager.', - targets=[q('clamp_max(max by (luid, engtype) (' + gpuEngine + '), 1) * on (luid) group_left (name) ' + gpuName, '{{name}} {{engtype}}')], - ), - d.timeseries( - 'GPU memory', - width=12, - height=8, - unit='bytes', - min=0, - description='Dedicated (video) and shared (system) memory in use, against the size of each pool.', - targets=[ - q('max by (luid) (windows_gpu_adapter_memory_dedicated_bytes{' + H + '}) * on (luid) group_left (name) ' + gpuName, '{{name}} dedicated used'), - q('max by (luid) (windows_gpu_dedicated_video_memory_size_bytes{' + H + '}) * on (luid) group_left (name) ' + gpuName, '{{name}} dedicated size'), - q('max by (luid) (windows_gpu_adapter_memory_shared_bytes{' + H + '}) * on (luid) group_left (name) ' + gpuName, '{{name}} shared used'), - q('max by (luid) (windows_gpu_shared_system_memory_size_bytes{' + H + '}) * on (luid) group_left (name) ' + gpuName, '{{name}} shared size'), - ], - overrides=[byRegexp('.* size', d.dashed)], - ), - ], - [ - d.timeseries( - 'Top 10 processes by GPU utilization', - width=12, - height=8, - unit='percentunit', - min=0, - sortBy='Mean', - description='Busiest GPU engine used by each process. Process names need the process collector; otherwise only the PID is shown.', - targets=[q('topk(10, ' + withProcessName(gpuProc) + ')', '{{process}} ({{process_id}})')], - ), - d.timeseries( - 'Top 10 processes by dedicated GPU memory', - width=12, - height=8, - unit='bytes', - min=0, - sortBy='Mean', - description='Process names need the process collector; otherwise only the PID is shown.', - targets=[q('topk(10, ' + withProcessName(gpuProcMem) + ')', '{{process}} ({{process_id}})')], - ), - ], -]); - -// ================================================================ Hyper-V (optional collector) -local hyperv = d.tab('Hyper-V', rows=[ - d.row('Host', [ - [ - d.stat( - 'Virtual machines', - 'sum(windows_hyperv_virtual_machine_health_total_count{' + H + '})', - width=4, - height=4, - unit='none', - description='Needs the hyperv collector, which is not enabled by default.', - ), - d.stat( - 'VMs in critical health', - 'sum(windows_hyperv_virtual_machine_health_total_count{' + H + ', state="critical"})', - width=4, - height=4, - unit='none', - thresholds=steps([['green', null], ['red', 1]]), - ), - d.stat( - 'Hyper-V WMI', - 'windows_hyperv_wmi_health{' + H + '}', - width=4, - height=4, - thresholds=steps([['red', null], ['green', 1]]), - description='Whether the Hyper-V WMI namespace answers. When it stops answering, Hyper-V Manager and Failover Cluster Manager usually cannot manage the host.', - mappings=d.valueMapping({ text: 'not responding' }, { text: 'ok' }), - ), - d.stat('Logical processors', 'windows_hyperv_host_logical_processor_count{' + H + '}', width=4, height=4, unit='none'), - d.stat( - 'Virtual processors', - 'windows_hyperv_total_vm_processor_count{' + H + '}', - width=4, - height=4, - unit='none', - description='Virtual processors assigned to all VMs.', - ), - d.stat( - 'vCPU per logical CPU', - 'windows_hyperv_total_vm_processor_count{' + H + '} / windows_hyperv_host_logical_processor_count{' + H + '}', - width=4, - height=4, - unit='none', - decimals=2, - description='Virtual processors assigned to VMs per logical processor of the host.', - ), - ], - [ - d.timeseries( - 'Host CPU time by state', - width=12, - height=8, - unit='percentunit', - min=0, - max=1, - stack=true, - fill=40, - description='Share of all logical processors spent running guest code (VMs and the root partition) and in the hypervisor. With Hyper-V enabled, the CPU tab only sees the root partition.', - targets=[q( - 'sum by (state) (rate(windows_hyperv_hypervisor_logical_processor_time_total{' + H + ', state!="idle"}[$__rate_interval])) / scalar(count(windows_hyperv_hypervisor_logical_processor_time_total{' + H + ', state="idle"}))', - '{{state}}', - )], - ), - d.timeseries( - 'CPU usage per VM', - width=12, - height=8, - unit='percentunit', - min=0, - stack=true, - fill=40, - sortBy='Mean', - description='Virtual processor run time of each VM as a share of all logical processors of the host.', - targets=[q( - 'sum by (vm) (rate(windows_hyperv_hypervisor_virtual_processor_run_time_total{' + H + '}[$__rate_interval])) / scalar(max(windows_hyperv_host_logical_processor_count{' + H + '}))', - '{{vm}}', - )], - ), - ], - ]), - d.row('Memory', [[ - d.timeseries( - 'Memory assigned to VMs', - width=8, - height=8, - unit='bytes', - min=0, - stack=true, - fill=40, - sortBy='Mean', - description='Physical memory currently assigned to each VM with dynamic memory.', - targets=[q('windows_hyperv_dynamic_memory_vm_physical_bytes{' + H + '}', '{{vm}}')], - ), - d.timeseries( - 'VM memory pressure', - width=8, - height=8, - unit='percentunit', - min=0, - thresholds=steps([['green', null], ['red', 1]]), - showThresholds=true, - description='Memory a VM needs as a share of the memory it has. Above 100% the VM needs more memory than it is assigned.', - targets=[q('windows_hyperv_dynamic_memory_vm_pressure_current_ratio{' + H + '}', '{{vm}}')], - ), - d.timeseries( - 'Memory available for VMs', - width=8, - height=8, - unit='bytes', - min=0, - legend='list', - description='Memory the dynamic memory balancer can still hand out to VMs.', - targets=[q('windows_hyperv_dynamic_memory_balancer_available_memory_bytes{' + H + '}', '{{balancer}}')], - ), - ]]), - d.row('Storage', [[ - d.timeseries( - 'Virtual disk throughput', - width=8, - height=8, - unit='Bps', - description='Reads are drawn below the axis.', - targets=[ - q('rate(windows_hyperv_virtual_storage_device_bytes_written{' + H + '}[$__rate_interval])', '{{device}} write'), - q('rate(windows_hyperv_virtual_storage_device_bytes_read{' + H + '}[$__rate_interval])', '{{device}} read'), - ], - overrides=[d.negativeY('.* read$')], - ), - d.timeseries( - 'Virtual disk IOPS', - width=8, - height=8, - unit='iops', - description='Reads are drawn below the axis.', - targets=[ - q('rate(windows_hyperv_virtual_storage_device_operations_written_total{' + H + '}[$__rate_interval])', '{{device}} write'), - q('rate(windows_hyperv_virtual_storage_device_operations_read_total{' + H + '}[$__rate_interval])', '{{device}} read'), - ], - overrides=[d.negativeY('.* read$')], - ), - d.timeseries( - 'Virtual disk latency and errors', - width=8, - height=8, - unit='s', - min=0, - description='Average I/O latency of each virtual disk. windows_hyperv_virtual_storage_device_latency_seconds holds the raw, growing counter in 100 ns ticks, so the panel divides its rate by the rate of I/O operations. Errors use the right axis.', - targets=[ - q('rate(windows_hyperv_virtual_storage_device_latency_seconds{' + H + '}[$__rate_interval]) / (rate(windows_hyperv_virtual_storage_device_operations_read_total{' + H + '}[$__rate_interval]) + rate(windows_hyperv_virtual_storage_device_operations_written_total{' + H + '}[$__rate_interval])) / 1e7', '{{device}} latency'), - q('rate(windows_hyperv_virtual_storage_device_error_count_total{' + H + '}[$__rate_interval]) > 0', '{{device}} errors'), - ], - overrides=[byRegexp('.* errors', [prop('unit', 'ops'), prop('custom.axisPlacement', 'right'), d.fixedColor('red')])], - ), - ]]), - d.row('Network', [[ - d.timeseries( - 'Virtual switch throughput', - width=8, - height=8, - unit='bps', - description='Received traffic is drawn below the axis.', - targets=[ - q('rate(windows_hyperv_vswitch_bytes_sent_total{' + H + '}[$__rate_interval]) * 8', '{{vswitch}} sent'), - q('rate(windows_hyperv_vswitch_bytes_received_total{' + H + '}[$__rate_interval]) * 8', '{{vswitch}} received'), - ], - overrides=[d.negativeY('.* received$')], - ), - d.timeseries( - 'Virtual switch dropped packets', - width=8, - height=8, - unit='pps', - min=0, - targets=[ - q('rate(windows_hyperv_vswitch_dropped_packets_incoming_total{' + H + '}[$__rate_interval])', '{{vswitch}} incoming'), - q('rate(windows_hyperv_vswitch_dropped_packets_outcoming_total{' + H + '}[$__rate_interval])', '{{vswitch}} outgoing'), - ], - ), - d.timeseries( - 'VM network adapter throughput', - width=8, - height=8, - unit='bps', - description='Received traffic is drawn below the axis.', - targets=[ - q('rate(windows_hyperv_virtual_network_adapter_sent_bytes_total{' + H + '}[$__rate_interval]) * 8', '{{adapter}} sent'), - q('rate(windows_hyperv_virtual_network_adapter_received_bytes_total{' + H + '}[$__rate_interval]) * 8', '{{adapter}} received'), - ], - overrides=[d.negativeY('.* received$')], - ), - ]]), -]); - -// ================================================================ Time (optional collector) -local time = d.tab('Time', [ - [ - d.stat( - 'Time zone', - 'windows_time_timezone{' + H + '}', - width=6, - height=4, - description='Needs the time collector, which is not enabled by default.', - text='timezone', - colorMode='none', - ), - d.stat('Clock sync source', 'windows_time_clock_sync_source{' + H + '} == 1', width=6, height=4, text='type', colorMode='none'), - d.stat( - 'NTP time sources', - 'windows_time_ntp_client_time_sources{' + H + '}', - width=6, - height=4, - description='Number of time sources the NTP client uses. 0 means the clock is not synchronized.', - unit='none', - thresholds=steps([['red', null], ['green', 1]]), - ), - d.stat( - 'Clock offset', - 'abs(windows_time_computed_time_offset_seconds{' + H + '})', - width=6, - height=4, - description='Absolute offset between the system clock and the selected time source.', - unit='s', - graph=true, - thresholds=steps([['green', null], ['orange', 0.5], ['red', 1]]), - ), - ], - [ - d.timeseries( - 'Clock offset and NTP round-trip delay', - width=24, - height=8, - unit='s', - targets=[ - q('windows_time_computed_time_offset_seconds{' + H + '}', 'clock offset'), - q('windows_time_ntp_round_trip_delay_seconds{' + H + '}', 'NTP round-trip delay'), - ], - ), - ], -]); - -// ================================================================ Exporter -local exporter = d.tab('Exporter', rows=[ - d.row('Scrape', [ - [ - d.stat( - 'Scrape status', - 'up{' + H + '}', - width=4, - height=4, - description='Result of the last scrape by Prometheus.', - thresholds=steps([['red', null], ['green', 1]]), - mappings=d.valueMapping({ text: 'down' }, { text: 'up' }), - ), - // Development builds have no version label, so fall back to the short revision. - d.stat( - 'Exporter version', - 'windows_exporter_build_info{' + H + ', version!=""} or on (instance) label_replace(windows_exporter_build_info{' + H + '}, "version", "$1", "revision", "(.{7}).*")', - width=4, - height=4, - description='Builds without a version show the first seven characters of the Git revision.', - text='version', - colorMode='none', - ), - d.stat( - 'Exporter uptime', - 'time() - process_start_time_seconds{' + H + '}', - width=4, - height=4, - unit='dtdurations', - description='Time since the exporter process started. A short uptime on a host with a long uptime means the exporter restarted.', - thresholds=steps([['orange', null], ['text', 3600]]), - ), - d.stat( - 'Failed collectors', - 'count(min_over_time(windows_exporter_collector_success{' + H + '}[$__range]) == 0) or vector(0)', - width=4, - height=4, - unit='none', - thresholds=steps([['green', null], ['red', 1]]), - description='Collectors that failed at least once in the selected time range.', - ), - d.stat( - 'Timed-out collectors', - 'count(max_over_time(windows_exporter_collector_timeout{' + H + '}[$__range]) == 1) or vector(0)', - width=4, - height=4, - unit='none', - thresholds=steps([['green', null], ['red', 1]]), - description='Collectors that hit the collector timeout at least once in the selected time range.', - ), - d.stat('Samples per scrape', 'scrape_samples_scraped{' + H + '}', width=4, height=4, unit='short', graph=true), - ], - [ - d.timeseries( - 'Scrape duration', - width=12, - height=8, - unit='s', - min=0, - description='Scrape duration measured by Prometheus and by the exporter. It must stay below the scrape timeout of the job.', - targets=[ - q('scrape_duration_seconds{' + H + '}', 'Prometheus'), - q('windows_exporter_scrape_duration_seconds{' + H + '}', 'exporter'), - ], - ), - d.timeseries( - 'HTTP requests to /metrics', - width=12, - height=8, - unit='reqps', - min=0, - description='Scrape requests by HTTP status code, and errors while gathering or encoding metrics. More requests than one per scrape interval mean several jobs or Prometheus servers scrape this exporter.', - targets=[ - q('sum by (code) (rate(promhttp_metric_handler_requests_total{' + H + '}[$__rate_interval])) > 0', 'HTTP {{code}}'), - q('sum by (cause) (rate(promhttp_metric_handler_errors_total{' + H + '}[$__rate_interval])) > 0', '{{cause}} errors'), - ], - overrides=[byRegexp('.* errors', [d.fixedColor('red')])], - ), - ], - ]), - d.row('Collectors', [ - [ - d.table( - 'Collectors', - width=8, - height=8, - targets=[ - q('windows_exporter_collector_success{' + H + '}'), - q('windows_exporter_collector_timeout{' + H + '}'), - q('windows_exporter_collector_duration_seconds{' + H + '}'), - ], - joinBy='collector', - exclude=['instance', 'job', 'port', '__name__', 'instance 1', 'instance 2', 'instance 3', 'job 1', 'job 2', 'job 3', 'port 1', 'port 2', 'port 3', '__name__ 1', '__name__ 2', '__name__ 3'], - order={ collector: 0, 'Value #A': 1, 'Value #B': 2, 'Value #C': 3 }, - rename={ collector: 'Collector', 'Value #A': 'Status', 'Value #B': 'Timeout', 'Value #C': 'Duration' }, - sortBy=[{ desc: true, displayName: 'Duration' }], - overrides=[ - byName('Value #A', [ - prop('mappings', d.valueMapping({ color: 'red', text: 'failed' }, { color: 'green', text: 'ok' })), - prop('custom.cellOptions', { type: 'color-text' }), - ]), - byName('Value #B', [ - prop('mappings', d.valueMapping({ color: 'green', text: 'no' }, { color: 'red', text: 'yes' })), - prop('custom.cellOptions', { type: 'color-text' }), - ]), - byName('Value #C', d.unitCell('s')), - ], - ), - d.timeseries( - 'Collector duration', - width=16, - height=8, - unit='s', - min=0, - stack=true, - fill=40, - sortBy='Mean', - description='Time each collector spent per scrape. Compare against the scrape timeout.', - targets=[q('windows_exporter_collector_duration_seconds{' + H + '}', '{{collector}}')], - ), - ], - [ - d.statusTimeline( - 'Collector failures', - 'Collectors that failed at least once in the selected time range.', - q('windows_exporter_collector_success{' + H + '} and on (collector) (min_over_time(windows_exporter_collector_success{' + H + '}[$__range] @ end()) == 0)', '{{collector}}'), - 'ok', - 'failed', - width=12, - height=9, - ), - d.statusTimeline( - 'Collector timeouts', - 'Collectors that hit the collector timeout at least once in the selected time range.', - q('1 - windows_exporter_collector_timeout{' + H + '} and on (collector) (max_over_time(windows_exporter_collector_timeout{' + H + '}[$__range] @ end()) == 1)', '{{collector}}'), - 'in time', - 'timed out', - width=12, - height=9, - ), - ], - ]), - // The go_sched_* metrics need an exporter that enables the Go scheduler metrics; older builds show no data there. - d.row('Process and Go runtime', [ - [ - d.timeseries( - 'Exporter CPU usage', - width=8, - height=8, - unit='percentunit', - min=0, - legend='list', - description='CPU time used by the exporter process, as a share of one core. CPU usage grows with the number of scrapes, for example when two Prometheus jobs scrape the same exporter; CPU per scrape (right axis) does not.', - targets=[ - q('rate(process_cpu_seconds_total{' + H + '}[$__rate_interval])', 'CPU'), - q('rate(process_cpu_seconds_total{' + H + '}[$__rate_interval]) / on (instance) sum by (instance) (rate(promhttp_metric_handler_requests_total{' + H + '}[$__rate_interval]))', 'CPU per scrape'), - ], - overrides=[byName('CPU per scrape', [prop('unit', 's'), prop('custom.axisPlacement', 'right')])], - ), - d.timeseries( - 'Exporter memory', - width=8, - height=8, - unit='bytes', - min=0, - description='Working set and private bytes of the exporter process, and the Go heap in use. Steady growth points to a leak.', - targets=[ - q('process_resident_memory_bytes{' + H + '}', 'working set'), - q('process_virtual_memory_bytes{' + H + '}', 'private bytes'), - q('go_memstats_heap_inuse_bytes{' + H + '}', 'Go heap in use'), - ], - ), - d.timeseries( - 'Exporter handles', - width=8, - height=8, - unit='short', - min=0, - legend='list', - description='Open handles of the exporter process (process_open_fds). Steady growth points to a handle leak.', - targets=[q('process_open_fds{' + H + '}', 'open handles')], - ), - ], - [ - d.timeseries( - 'Goroutines by state', - width=8, - height=8, - unit='short', - min=0, - stack=true, - fill=40, - description='Steady growth of goroutines points to a goroutine leak. Goroutines that stay "not in Go" sit in a system call, for example a collector waiting on WMI or PDH. The states need an exporter build with the Go scheduler metrics; the total line works with every build.', - targets=[ - q('go_sched_goroutines_running_goroutines{' + H + '}', 'running'), - q('go_sched_goroutines_runnable_goroutines{' + H + '}', 'runnable'), - q('go_sched_goroutines_waiting_goroutines{' + H + '}', 'waiting'), - q('go_sched_goroutines_not_in_go_goroutines{' + H + '}', 'not in Go'), - q('go_goroutines{' + H + '}', 'total'), - ], - overrides=[byName('total', [d.fixedColor('text'), prop('custom.stacking', { group: 'B', mode: 'none' })] + d.dashed)], - ), - d.timeseries( - 'Goroutines created and threads', - width=8, - height=8, - unit='short', - min=0, - description='Goroutines started per second, and OS threads owned by the Go runtime. Goroutines created needs an exporter build with the Go scheduler metrics.', - targets=[ - q('rate(go_sched_goroutines_created_goroutines_total{' + H + '}[$__rate_interval])', 'goroutines created/s'), - q('go_threads{' + H + '}', 'OS threads'), - ], - overrides=[byName('OS threads', [prop('custom.axisPlacement', 'right')])], - ), - d.timeseries( - 'Scheduler latency', - width=8, - height=8, - unit='s', - min=0, - description='Time goroutines wait in the runnable state before they run. High values mean the exporter does not get enough CPU. Needs an exporter build with the Go scheduler metrics.', - targets=[ - q('histogram_quantile(0.99, sum by (le) (rate(go_sched_latencies_seconds_bucket{' + H + '}[$__rate_interval])))', 'p99'), - q('histogram_quantile(0.5, sum by (le) (rate(go_sched_latencies_seconds_bucket{' + H + '}[$__rate_interval])))', 'p50'), - ], - ), - ], - [ - d.timeseries( - 'Garbage collection', - width=12, - height=8, - unit='s', - min=0, - description='Average GC pause and GC runs per second.', - targets=[ - q('rate(go_gc_duration_seconds_sum{' + H + '}[$__rate_interval]) / rate(go_gc_duration_seconds_count{' + H + '}[$__rate_interval])', 'average pause'), - q('rate(go_gc_duration_seconds_count{' + H + '}[$__rate_interval])', 'GC runs'), - ], - overrides=[byName('GC runs', [prop('unit', 'ops'), prop('custom.axisPlacement', 'right')])], - ), - d.timeseries( - 'Allocation rate', - width=12, - height=8, - unit='Bps', - min=0, - legend='list', - description='Bytes allocated on the Go heap per second. Spikes line up with expensive collectors.', - targets=[q('rate(go_memstats_alloc_bytes_total{' + H + '}[$__rate_interval])', 'allocated')], - ), - ], - ]), -]); - -d.dashboard( - 'Kdaassddw', - 'Windows Exporter', - [fleet, overview, cpu, memory, disk, network, services, gpu, hyperv, time, exporter], - variables=[ - { - kind: 'DatasourceVariable', - spec: { - allowCustomValue: true, - current: { text: '', value: '' }, - hide: 'dontHide', - includeAll: false, - label: 'Data source', - multi: false, - name: 'datasource', - options: [], - pluginId: 'prometheus', - refresh: 'onDashboardLoad', - regex: '', - skipUrlSync: false, - }, - }, - d.variable('job', 'Job', 'label_values(windows_os_hostname, job)', multi=true, includeAll=true), - d.variable('hostname', 'Hostname', 'label_values(windows_os_hostname{job=~"$job"}, hostname)', multi=true, includeAll=true), - d.variable('instance', 'Instance', 'label_values(windows_os_hostname{job=~"$job", hostname=~"$hostname"}, instance)'), - d.variable('show_hostname', '', 'label_values(windows_os_hostname{job=~"$job", instance="$instance"}, hostname)', hide=true), - d.variable('volume', 'Volume', 'label_values(windows_logical_disk_size_bytes{job=~"$job", instance="$instance"}, volume)', multi=true, includeAll=true, regex='/^(?!HarddiskVolume).+$/'), - d.variable('nic', 'Network interface', 'label_values(windows_net_bytes_total{job=~"$job", instance="$instance"}, nic)', multi=true, includeAll=true, regex='/^(?!isatap|Teredo|6to4).+$/'), - ], - annotations=[ - { - kind: 'AnnotationQuery', - spec: { - builtIn: true, - enable: true, - hide: true, - iconColor: 'rgba(0, 211, 255, 1)', - legacyOptions: { type: 'dashboard' }, - name: 'Annotations & Alerts', - query: { datasource: { name: '-- Grafana --' }, group: 'grafana', kind: 'DataQuery', spec: {}, version: 'v0' }, - }, - }, - { - // The boot timestamp only changes on reboot, so each distinct value marks one reboot. - kind: 'AnnotationQuery', - spec: { - enable: true, - hide: false, - iconColor: 'orange', - legacyOptions: { - expr: 'windows_system_boot_time_timestamp{' + H + '} * 1000 > $__from < $__to', - step: '1m', - tagKeys: 'instance', - textFormat: '', - titleFormat: 'Reboot', - useValueForTime: 'on', - }, - name: 'Reboots', - query: { datasource: { name: '${datasource}' }, group: 'prometheus', kind: 'DataQuery', spec: {}, version: 'v0' }, - }, - }, - ], - spec={ - cursorSync: 'Crosshair', - description: 'Fleet overview and per-host details for Windows hosts monitored by windows_exporter.', - editable: true, - links: [{ - 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', - }], - liveNow: false, - preload: false, - tags: ['prometheus', 'windows', 'windows_exporter'], - timeSettings: { - autoRefresh: '1m', - autoRefreshIntervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], - fiscalYearStartMonth: 0, - from: 'now-3h', - hideTimepicker: false, - to: 'now', - }, +dashboard.new('Kdaassddw', 'Windows Exporter') ++ dashboard.spec.withCursorSync('Crosshair') ++ dashboard.spec.withDescription('Fleet overview and per-host details for Windows hosts monitored by windows_exporter.') ++ 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([ + 'prometheus', + 'windows', + 'windows_exporter', +]) ++ 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') ++ dashboard.spec.withVariables(import 'variables.libsonnet') ++ dashboard.spec.withElements(std.foldl(function(elements, tab) elements + tab.elements, tabs, {})) ++ dashboard.spec.withLayout(b.tabs([tab.layout for tab in tabs])) diff --git a/contrib/mixin/lib/dashboard.libsonnet b/contrib/mixin/lib/dashboard.libsonnet deleted file mode 100644 index 2fc2eafa..00000000 --- a/contrib/mixin/lib/dashboard.libsonnet +++ /dev/null @@ -1,467 +0,0 @@ -// Helpers to build a Grafana v2 dashboard (dashboard.grafana.app/v2) with tabs. -// -// Grafonnet builds the resource, the panel options and field config, and the Prometheus queries. -// The panel options and field config have the same shape in v1 panels and in v2 VizConfig. -// The helpers wrap them into v2 elements and lay the elements out in tabs, rows and grids. -local g = import 'github.com/grafana/grafonnet/gen/grafonnet-v13.0.0/main.libsonnet'; - -local prometheus = g.query.prometheus; -local stat = g.panel.stat; -local stateTimeline = g.panel.stateTimeline; -local table = g.panel.table; -local timeSeries = g.panel.timeSeries; - -local datasource = { name: '${datasource}' }; - -// steps builds absolute thresholds from [color, value] pairs. The first value is null (base). -local steps(pairs) = { - mode: 'absolute', - steps: [{ color: p[0], value: p[1] } for p in pairs], -}; - -// Thresholds inside field overrides leave out the null value of the base step. -local overrideSteps(pairs) = { - mode: 'absolute', - steps: [{ color: p[0] } + (if p[1] == null then {} else { value: p[1] }) for p in pairs], -}; - -local optional(value, fn) = if value == null then {} else fn(value); - -// vizConfig keeps the fields of a grafonnet panel that make up the v2 VizConfig spec. -local vizConfig(group, panel) = { - group: group, - kind: 'VizConfig', - spec: { fieldConfig: panel.fieldConfig, options: panel.options }, - version: '', -}; - -local query(q, refId) = { - kind: 'PanelQuery', - spec: { - hidden: false, - query: { - datasource: datasource, - group: 'prometheus', - kind: 'DataQuery', - spec: - prometheus.withEditorMode('code') - + prometheus.withExpr(q.expr) - + prometheus.withLegendFormat(if q.legend == '' then '__auto' else q.legend) - + prometheus.withInstant(q.instant) - + prometheus.withRange(!q.instant) - + (if q.table then prometheus.withFormat('table') else {}), - version: 'v0', - }, - refId: refId, - }, -}; - -// element is a v2 panel. The dashboard function adds its ID and grid position. -local element(title, description, queries, viz, width, height, transformations=[]) = { - width:: width, - height:: height, - kind: 'Panel', - spec: { - data: { - kind: 'QueryGroup', - spec: { - queries: [query(queries[i], std.char(std.codepoint('A') + i)) for i in std.range(0, std.length(queries) - 1)], - queryOptions: {}, - transformations: [ - { group: t.id, kind: 'Transformation', spec: { options: t.options } } - for t in transformations - ], - }, - }, - description: description, - links: [], - title: title, - vizConfig: viz, - }, -}; - -local gridItems(bands) = - local heights = [std.foldl(std.max, [p.height for p in band], 0) for band in bands]; - std.flattenArrays([ - local y = std.foldl(function(sum, h) sum + h, heights[0:b], 0); - [ - { - panel:: bands[b][i], - kind: 'GridLayoutItem', - spec: { - height: bands[b][i].height, - width: bands[b][i].width, - x: std.foldl(function(sum, p) sum + p.width, bands[b][0:i], 0), - y: y, - }, - } - for i in std.range(0, std.length(bands[b]) - 1) - ] - for b in std.range(0, std.length(bands) - 1) - ]); - -{ - // host selects the host chosen in the Instance variable. - host:: 'job=~"$job", instance="$instance"', - // fleet selects all hosts of the chosen jobs. Fleet queries add fleetJoin or fleetFilter for the Hostname variable. - fleet:: 'job=~"$job"', - - // fleetJoin adds the hostname label and restricts to the selected hostnames. - fleetJoin(expr):: - '(%s) * on (instance) group_left (hostname) max by (instance, hostname) (windows_os_hostname{%s, hostname=~"$hostname"})' % [expr, self.fleet], - - // fleetFilter restricts to the selected hostnames without adding labels (for tables). - fleetFilter(expr):: - '(%s) and on (instance) windows_os_hostname{%s, hostname=~"$hostname"}' % [expr, self.fleet], - - // q is a Prometheus query. An empty legend uses Grafana's automatic legend. - q(expr, legend=''):: { expr: expr, legend: legend, instant: false, table: false }, - - steps:: steps, - pctSteps:: steps([['green', null], ['orange', 80], ['red', 90]]), - plainSteps:: steps([['green', null]]), - textSteps:: steps([['text', null]]), - - prop(id, value):: { id: id, value: value }, - fixedColor(color):: self.prop('color', { fixedColor: color, mode: 'fixed' }), - dashed:: [self.prop('custom.fillOpacity', 0), self.prop('custom.lineStyle', { dash: [10, 10], fill: 'dash' })], - - byName(name, props):: timeSeries.fieldOverride.byName.new(name) + { properties: props }, - byRegexp(regex, props):: timeSeries.fieldOverride.byRegexp.new(regex) + { properties: props }, - - // negativeY mirrors series whose display name matches the regular expression below the X axis. - negativeY(regex):: self.byRegexp(regex, [self.prop('custom.transform', 'negative-Y')]), - - // valueMapping maps the values 0 and 1 to texts and colors. - valueMapping(zero, one):: [{ type: 'value', options: { '0': zero { index: 0 }, '1': one { index: 1 } } }], - - // gaugeCell shows a table cell as a gauge from 0 to maxValue. - gaugeCell(unit, maxValue):: [ - self.prop('unit', unit), - self.prop('min', 0), - self.prop('max', maxValue), - self.prop('thresholds', overrideSteps([['green', null], ['orange', 80], ['red', 90]])), - self.prop('color', { mode: 'continuous-GrYlRd' }), - self.prop('custom.cellOptions', { type: 'gauge', mode: 'basic', valueDisplayMode: 'text' }), - self.prop('decimals', 1), - ], - overrideSteps:: overrideSteps, - - unitCell(unit):: [self.prop('unit', unit)], - - // timeseries draws a time series panel. legend is table (with calcs), list or hidden. - // sortBy sorts the table legend by that column, descending. - timeseries( - title, - targets, - width, - height, - description='', - unit='', - stack=false, - min=null, - max=null, - decimals=null, - legend='table', - calcs=['mean', 'max', 'lastNotNull'], - overrides=[], - fill=10, - thresholds=self.plainSteps, - showThresholds=false, - sortBy='', - ):: - local custom = timeSeries.fieldConfig.defaults.custom; - local panel = - custom.withAxisBorderShow(false) - + custom.withAxisCenteredZero(false) - + custom.withAxisColorMode('text') - + custom.withAxisLabel('') - + custom.withAxisPlacement('auto') - + custom.withBarAlignment(0) - + custom.withDrawStyle('line') - + custom.withFillOpacity(fill) - + custom.withGradientMode('opacity') - + custom.hideFrom.withLegend(false) - + custom.hideFrom.withTooltip(false) - + custom.hideFrom.withViz(false) - + custom.withInsertNulls(false) - + custom.withLineInterpolation('linear') - + custom.withLineWidth(1) - + custom.withPointSize(4) - + custom.scaleDistribution.withType('linear') - + custom.withShowPoints('never') - + custom.withSpanNulls(false) - + custom.stacking.withGroup('A') - + custom.stacking.withMode(if stack then 'normal' else 'none') - + custom.thresholdsStyle.withMode(if showThresholds then 'dashed' else 'off') - + timeSeries.standardOptions.color.withMode('palette-classic') - + timeSeries.standardOptions.thresholds.withMode(thresholds.mode) - + timeSeries.standardOptions.thresholds.withSteps(thresholds.steps) - + timeSeries.standardOptions.withUnit(unit) - + optional(min, timeSeries.standardOptions.withMin) - + optional(max, timeSeries.standardOptions.withMax) - + optional(decimals, timeSeries.standardOptions.withDecimals) - + timeSeries.standardOptions.withOverrides(overrides) - + timeSeries.options.legend.withPlacement('bottom') - + ( - if legend == 'table' then - timeSeries.options.legend.withCalcs(calcs) - + timeSeries.options.legend.withDisplayMode('table') - + timeSeries.options.legend.withShowLegend(true) - + (if sortBy == '' then {} else timeSeries.options.legend.withSortBy(sortBy) + timeSeries.options.legend.withSortDesc(true)) - else - timeSeries.options.legend.withCalcs([]) - + timeSeries.options.legend.withDisplayMode('list') - + timeSeries.options.legend.withShowLegend(legend == 'list') - ) - + timeSeries.options.tooltip.withHideZeros(false) - + timeSeries.options.tooltip.withMode('multi') - + timeSeries.options.tooltip.withSort('desc'); - element(title, description, targets, vizConfig('timeseries', panel), width=width, height=height), - - // stat shows the last value of expr. With text, it shows that label of the result instead of the value. - stat( - title, - expr, - width, - height, - description='', - unit='', - thresholds=self.textSteps, - graph=false, - colorMode='value', - decimals=null, - min=null, - max=null, - text='', - mappings=[], - ):: - local panel = - stat.standardOptions.color.withMode('thresholds') - + (if mappings == [] then {} else stat.standardOptions.withMappings(mappings)) - + stat.standardOptions.thresholds.withMode(thresholds.mode) - + stat.standardOptions.thresholds.withSteps(thresholds.steps) - + stat.standardOptions.withUnit(unit) - + optional(decimals, stat.standardOptions.withDecimals) - + optional(min, stat.standardOptions.withMin) - + optional(max, stat.standardOptions.withMax) - + stat.standardOptions.withOverrides([]) - + stat.options.withColorMode(colorMode) - + stat.options.withGraphMode(if graph then 'area' else 'none') - + stat.options.withJustifyMode('auto') - + stat.options.withOrientation('auto') - + stat.options.withPercentChangeColorMode('standard') - + stat.options.reduceOptions.withCalcs(['lastNotNull']) - + stat.options.reduceOptions.withFields(if text == '' then '' else '/^' + text + '$/') - + stat.options.reduceOptions.withValues(false) - + stat.options.withShowPercentChange(false) - + stat.options.withTextMode(if text == '' then 'auto' else 'value') - + stat.options.withWideLayout(true); - // A label shown instead of the value needs the query result as a table. - local target = self.q(expr) { instant: !graph, table: text != '' }; - element(title, description, [target], vizConfig('stat', panel), width=width, height=height), - - // table joins instant queries into one table. The value columns are named "Value #A", "Value #B" and - // so on by query; rename, order and the overrides refer to those names. footer lists columns summed - // in the table footer. - table( - title, - targets, - width, - height, - description='', - joinBy='', - exclude=[], - rename={}, - order={}, - overrides=[], - sortBy=[], - transformations=[], - footer=null, - ):: - local excluded = { Time: true } + { [e]: true for e in exclude } + { - ['Time ' + (i + 1)]: true - for i in std.range(0, std.length(targets) - 1) - }; - local panel = - table.standardOptions.color.withMode('thresholds') - + table.fieldConfig.defaults.custom.withAlign('auto') - + table.fieldConfig.defaults.custom.withCellOptions({ type: 'auto' }) - + table.fieldConfig.defaults.custom.withFilterable(false) - + table.fieldConfig.defaults.custom.withInspect(false) - + table.fieldConfig.defaults.custom.withMinWidth(60) - + table.standardOptions.thresholds.withMode(self.plainSteps.mode) - + table.standardOptions.thresholds.withSteps(self.plainSteps.steps) - + table.standardOptions.withOverrides(overrides) - + table.options.withCellHeight('sm') - + table.options.withFooter({ - countRows: false, - fields: if footer == null then '' else footer, - reducer: ['sum'], - show: footer != null, - }) - + table.options.withShowHeader(true) - + table.options.withSortBy(sortBy) - + { options+: { enablePagination: false } }; - element( - title, - description, - [t { instant: true, table: true } for t in targets], - vizConfig('table', panel), - width=width, - height=height, - transformations=(if joinBy == '' then [] else [{ id: 'joinByField', options: { byField: joinBy, mode: 'outer' } }]) - + transformations - + [{ - id: 'organize', - options: { excludeByName: excluded, includeByName: {}, indexByName: order, renameByName: rename }, - }], - ), - - local timeline(title, description, target, defaults, width, height) = - local custom = stateTimeline.fieldConfig.defaults.custom; - local panel = - { fieldConfig+: { defaults+: defaults } } - + custom.withFillOpacity(90) - + custom.hideFrom.withLegend(false) - + custom.hideFrom.withTooltip(false) - + custom.hideFrom.withViz(false) - + custom.withInsertNulls(false) - + custom.withLineWidth(0) - + custom.withSpanNulls(false) - + stateTimeline.standardOptions.withMin(0) - + stateTimeline.standardOptions.withOverrides([]) - + stateTimeline.options.withAlignValue('left') - + stateTimeline.options.legend.withDisplayMode('list') - + stateTimeline.options.legend.withPlacement('bottom') - + stateTimeline.options.legend.withShowLegend(false) - + stateTimeline.options.withMergeValues(false) - + stateTimeline.options.withRowHeight(1) - + stateTimeline.options.withShowValue('never') - + stateTimeline.options.tooltip.withHideZeros(false) - + stateTimeline.options.tooltip.withMode('single') - + stateTimeline.options.tooltip.withSort('none'); - element(title, description, [target], vizConfig('state-timeline', panel), width=width, height=height), - - // stateTimeline draws one row per series, colored from green to red by value. It is used for per-core - // CPU load, where a line per core becomes unreadable. - stateTimeline(title, description, unit, maxValue, target, width, height):: - timeline(title, description, target, { - color: { mode: 'continuous-GrYlRd' }, - max: maxValue, - thresholds: $.plainSteps, - unit: unit, - }, width, height), - - // statusTimeline draws a 0/1 metric per series as a red/green timeline. - statusTimeline(title, description, target, okText, failText, width, height):: - timeline(title, description, target, { - color: { mode: 'thresholds' }, - mappings: $.valueMapping({ color: 'red', text: failText }, { color: 'green', text: okText }), - max: 1, - thresholds: steps([['red', null], ['green', 1]]), - unit: 'none', - }, width, height), - - // variable is a Prometheus query variable. - variable(name, label, query, multi=false, includeAll=false, hide=false, regex=''):: { - kind: 'QueryVariable', - spec: { - allowCustomValue: true, - current: if includeAll then { text: ['All'], value: ['$__all'] } else { text: '', value: '' }, - definition: query, - hide: if hide then 'hideVariable' else 'dontHide', - includeAll: includeAll, - [if label != '' then 'label']: label, - multi: multi, - name: name, - options: [], - query: { - datasource: datasource, - group: 'prometheus', - kind: 'DataQuery', - spec: { qryType: 1, query: query, refId: 'PrometheusVariableQueryEditor-VariableQuery' }, - version: 'v0', - }, - refresh: 'onTimeRangeChanged', - regex: regex, - skipUrlSync: false, - sort: 'alphabeticalAsc', - }, - }, - - // row is a row of a tab. bands is a list of panel lists; the panels of a band sit side by side - // and the bands are stacked. - row(title, bands):: { title: title, bands: bands }, - - // tab is a tab of the dashboard. A tab is either one grid of bands, or a list of rows. - tab(title, bands=[], rows=[]):: { title: title, rows: if rows == [] then [$.row('', bands)] else rows }, - - // dashboard builds the v2 dashboard from the tabs. Panels are numbered in order, and every row - // takes one number before its panels, as in the classic dashboard the layout was converted from. - // The element names (panel-) are taken from these numbers. - dashboard(name, title, tabs, variables=[], annotations=[], spec={}):: - local v2 = g.apps.dashboard.v2; - local rows = [ - tabs[t].rows[r] { tab:: t } - for t in std.range(0, std.length(tabs) - 1) - for r in std.range(0, std.length(tabs[t].rows) - 1) - ]; - local panelCount(row) = std.length(std.flattenArrays(row.bands)); - // The row itself takes the number before its first panel. - local firstID = [ - std.foldl(function(sum, r) sum + 1 + panelCount(r), rows[0:i], 0) + 2 - for i in std.range(0, std.length(rows) - 1) - ]; - local numbered = [ - local items = gridItems(rows[i].bands); - rows[i] { - items:: [ - items[j] { - id:: firstID[i] + j, - spec+: { element: { kind: 'ElementReference', name: 'panel-' + (firstID[i] + j) } }, - } - for j in std.range(0, std.length(items) - 1) - ], - } - for i in std.range(0, std.length(rows) - 1) - ]; - local grid(row) = { kind: 'GridLayout', spec: { items: row.items } }; - local tabRows(t) = [r for r in numbered if r.tab == t]; - v2.new(name, title) - + v2.spec.withAnnotations(annotations) - + v2.spec.withElements({ - [item.spec.element.name]: item.panel { spec+: { id: item.id } } - for row in numbered - for item in row.items - }) - + v2.spec.withLayout({ - kind: 'TabsLayout', - spec: { - tabs: [ - local tab = tabs[t]; - local r = tabRows(t); - { - kind: 'TabsLayoutTab', - spec: { - layout: - if std.length(r) == 1 && r[0].title == '' then grid(r[0]) - else { - kind: 'RowsLayout', - spec: { - rows: [ - { kind: 'RowsLayoutRow', spec: { collapse: false, layout: grid(x), title: x.title } } - for x in r - ], - }, - }, - title: tab.title, - }, - } - for t in std.range(0, std.length(tabs) - 1) - ], - }, - }) - + v2.spec.withVariables(variables) - + { spec+: spec }, -} diff --git a/dashboard/README.md b/dashboard/README.md index 6bd7373f..0b266e19 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -48,8 +48,9 @@ Click a hostname in the Fleet tab to open that host in the Overview tab. Don't edit the JSON file by hand. It is generated from Jsonnet with [grafonnet](https://github.com/grafana/grafonnet) as part of the [windows_exporter mixin](../contrib/mixin): -- [dashboards/windows-exporter.libsonnet](../contrib/mixin/dashboards/windows-exporter.libsonnet) defines the tabs, panels and queries. -- [lib/dashboard.libsonnet](../contrib/mixin/lib/dashboard.libsonnet) holds the panel and layout helpers. +- [dashboards/windows-exporter.libsonnet](../contrib/mixin/dashboards/windows-exporter.libsonnet) composes the dashboard from one file per tab. +- [dashboards/builders.libsonnet](../contrib/mixin/dashboards/builders.libsonnet) holds the panel, query, and layout builders. +- [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`: @@ -63,6 +64,7 @@ make 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. 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)