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
+10 -3
View File
@@ -17,9 +17,11 @@ DASHBOARD := ../../dashboard/windows-exporter-dashboard.json
.DEFAULT_GOAL := generate
.DELETE_ON_ERROR:
.PHONY: generate dashboard tools jsonnet-tools fmt lint test check-runbooks clean
.PHONY: generate rules dashboard dashboards check-dashboards tools jsonnet-tools fmt lint test check-runbooks clean
generate: windows_rules.yaml windows_alerts.yaml
generate: rules dashboard
rules: windows_rules.yaml windows_alerts.yaml
tools: jsonnet-tools
go install github.com/cloudflare/pint/cmd/pint@$(PINT_VERSION)
@@ -50,10 +52,15 @@ dashboard: vendor
$(JSONNET) -J vendor -S dashboard.jsonnet > $(DASHBOARD).tmp
mv $(DASHBOARD).tmp $(DASHBOARD)
dashboards: dashboard
check-dashboards: dashboard
git diff --exit-code -- $(DASHBOARD)
fmt:
$(JSONNETFMT) $(FMT_FLAGS) -i $(SOURCES)
lint: generate tests/custom_rules.yaml tests/custom_alerts.yaml vendor
lint: rules tests/custom_rules.yaml tests/custom_alerts.yaml vendor
@set -e; for file in $(SOURCES); do \
$(JSONNETFMT) $(FMT_FLAGS) "$$file" | diff -u "$$file" -; \
done
+34 -6
View File
@@ -13,7 +13,7 @@ and [pint](https://github.com/cloudflare/pint) tools:
cd contrib/mixin
make tools
export PATH="$(go env GOPATH)/bin:$PATH"
make generate
make rules
```
Only the dashboard depends on Jsonnet libraries, so `jb install` is not required
@@ -201,15 +201,35 @@ Fork users should override `runbookURLPattern` to match their published URL.
It is a Grafana v2 dashboard with tabs and needs Grafana 13 or later. See the
[dashboard documentation](../../dashboard/README.md) for its content.
The dashboard is built with [grafonnet](https://github.com/grafana/grafonnet),
The dashboard uses additive builders with [grafonnet](https://github.com/grafana/grafonnet),
pinned in `jsonnetfile.json`:
- `dashboards/windows-exporter.libsonnet` defines the tabs, panels and queries.
- `lib/dashboard.libsonnet` wraps grafonnet panel options and queries into v2
panels, and lays them out in tabs, rows and grids.
- `dashboards/windows-exporter.libsonnet` composes the dashboard, with one source
file per tab for its panels, queries, rows, and grid positions.
- `dashboards/builders.libsonnet` uses Grafonnet's native `apps.dashboard.v2`
builders for the dashboard and layout and wraps its Prometheus query builders
into the v2 panel/query-group schema.
- `dashboards/styles.libsonnet` shares the visualization defaults; individual
panels add their differences using `withDefaults`, `withOptions`, and
`withOverrides`. Explicit nulls are preserved and arrays are replaced whole.
- `dashboards/variables.libsonnet` and `dashboards/annotations.libsonnet` use
native Grafonnet v2 builders for variables and annotations.
- `lib/manifest.libsonnet` renders JSON with the four-space indentation of the
committed file.
For example, a panel can be built with:
```jsonnet
local b = import 'dashboards/builders.libsonnet';
b.panel.new(200, 'CPU usage', 'timeseries')
+ b.panel.withQueries([
b.query.new('windows:cpu_usage:ratio{instance="$instance"}')
+ b.query.withLegendFormat('CPU'),
])
+ b.panel.withDefaults({ unit: 'percentunit', min: 0, max: 1 })
```
Regenerate the committed file after changing the dashboard source:
```sh
@@ -217,7 +237,15 @@ make dashboard
```
This runs `jb install` and writes `../../dashboard/windows-exporter-dashboard.json`.
The dashboard uses its own variables and does not read `_config`.
`make dashboards` is an alias. `make generate` builds both the rules and the
dashboard, while `make rules` builds only the rules. The same commands work
inside WSL with the Go tool binaries on `PATH`.
The checked-in JSON remains the reference for the rendered dashboard, including
queries, IDs, variables, annotations, transformations, and tab/row layout.
`make check-dashboards` regenerates it and runs `git diff --exit-code`, displaying
any drift. CI runs that check before the mixin tests. The dashboard uses its own
variables and does not read `_config`.
## Validate
@@ -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 },
}