mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
468 lines
18 KiB
Jsonnet
468 lines
18 KiB
Jsonnet
// 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 },
|
|
}
|