dashboard: factor Grafonnet builders into tab modules (#2595)

Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
This commit is contained in:
Jan-Otto Kröpke
2026-10-09 10:36:45 +02:00
committed by GitHub
parent e8f9f33eb1
commit ad8f84d4cf
21 changed files with 4059 additions and 1826 deletions
+3
View File
@@ -53,6 +53,9 @@ jobs:
if: steps.tools-cache.outputs.cache-hit != 'true' if: steps.tools-cache.outputs.cache-hit != 'true'
run: make tools run: make tools
- name: Build dashboard and check generated changes
run: make check-dashboards
- name: Lint and test rules - name: Lint and test rules
run: | run: |
make test PROMTOOL="docker run --rm --entrypoint promtool -v \"$PWD:/mixin:ro\" -w /mixin prom/prometheus:v3.15.0" make test PROMTOOL="docker run --rm --entrypoint promtool -v \"$PWD:/mixin:ro\" -w /mixin prom/prometheus:v3.15.0"
+10 -3
View File
@@ -17,9 +17,11 @@ DASHBOARD := ../../dashboard/windows-exporter-dashboard.json
.DEFAULT_GOAL := generate .DEFAULT_GOAL := generate
.DELETE_ON_ERROR: .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 tools: jsonnet-tools
go install github.com/cloudflare/pint/cmd/pint@$(PINT_VERSION) go install github.com/cloudflare/pint/cmd/pint@$(PINT_VERSION)
@@ -50,10 +52,15 @@ dashboard: vendor
$(JSONNET) -J vendor -S dashboard.jsonnet > $(DASHBOARD).tmp $(JSONNET) -J vendor -S dashboard.jsonnet > $(DASHBOARD).tmp
mv $(DASHBOARD).tmp $(DASHBOARD) mv $(DASHBOARD).tmp $(DASHBOARD)
dashboards: dashboard
check-dashboards: dashboard
git diff --exit-code -- $(DASHBOARD)
fmt: fmt:
$(JSONNETFMT) $(FMT_FLAGS) -i $(SOURCES) $(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 \ @set -e; for file in $(SOURCES); do \
$(JSONNETFMT) $(FMT_FLAGS) "$$file" | diff -u "$$file" -; \ $(JSONNETFMT) $(FMT_FLAGS) "$$file" | diff -u "$$file" -; \
done done
+34 -6
View File
@@ -13,7 +13,7 @@ and [pint](https://github.com/cloudflare/pint) tools:
cd contrib/mixin cd contrib/mixin
make tools make tools
export PATH="$(go env GOPATH)/bin:$PATH" export PATH="$(go env GOPATH)/bin:$PATH"
make generate make rules
``` ```
Only the dashboard depends on Jsonnet libraries, so `jb install` is not required 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 It is a Grafana v2 dashboard with tabs and needs Grafana 13 or later. See the
[dashboard documentation](../../dashboard/README.md) for its content. [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`: pinned in `jsonnetfile.json`:
- `dashboards/windows-exporter.libsonnet` defines the tabs, panels and queries. - `dashboards/windows-exporter.libsonnet` composes the dashboard, with one source
- `lib/dashboard.libsonnet` wraps grafonnet panel options and queries into v2 file per tab for its panels, queries, rows, and grid positions.
panels, and lays them out in tabs, rows and grids. - `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 - `lib/manifest.libsonnet` renders JSON with the four-space indentation of the
committed file. 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: Regenerate the committed file after changing the dashboard source:
```sh ```sh
@@ -217,7 +237,15 @@ make dashboard
``` ```
This runs `jb install` and writes `../../dashboard/windows-exporter-dashboard.json`. 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 ## Validate
@@ -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}')),
]
+107
View File
@@ -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 }),
},
}
+188
View File
@@ -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',
},
},
],
},
]),
]
)
+319
View File
@@ -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',
},
],
},
]),
]
)
+659
View File
@@ -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',
},
}),
]
)
+710
View File
@@ -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',
},
}),
]
)
+279
View File
@@ -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,
},
}),
]
)
+380
View File
@@ -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',
},
],
},
]),
]
)
+135
View File
@@ -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',
}),
]
)
+108
View File
@@ -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',
}),
]
)
+490
View File
@@ -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',
},
],
},
]),
]
)
+134
View File
@@ -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',
},
],
}),
]
)
+197
View File
@@ -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',
},
},
},
}
+108
View File
@@ -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',
}),
]
)
@@ -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).+$/'),
]
File diff suppressed because it is too large Load Diff
-467
View File
@@ -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-<number>) 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 },
}
+4 -2
View File
@@ -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): 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. - [dashboards/windows-exporter.libsonnet](../contrib/mixin/dashboards/windows-exporter.libsonnet) composes the dashboard from one file per tab.
- [lib/dashboard.libsonnet](../contrib/mixin/lib/dashboard.libsonnet) holds the panel and layout helpers. - [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. The generator writes the v2 schema directly, so it needs neither Grafana nor Prometheus.
Install the pinned tools and regenerate the file in `contrib/mixin`: Install 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 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 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. 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) ![Screenshot of the Fleet tab.](dashboard-fleet.png)