zfs_exporter.jsonnet (25100B)
1 // ZFS Exporter dashboard (pdf/zfs_exporter) 2 3 local g = import 'github.com/grafana/grafonnet/gen/grafonnet-latest/main.libsonnet'; 4 5 local dashboard = g.dashboard; 6 local var = dashboard.variable; 7 local ts = g.panel.timeSeries; 8 local stat = g.panel.stat; 9 local gauge = g.panel.gauge; 10 local barGauge = g.panel.barGauge; 11 local table = g.panel.table; 12 local row = g.panel.row; 13 local prometheus = g.query.prometheus; 14 15 // --------------------------------------------------------------------------- 16 // Shared selectors and datasource reference 17 // --------------------------------------------------------------------------- 18 local dsRef = { type: 'prometheus', uid: '${datasource}' }; 19 local poolSel = '{instance=~"$instance",pool=~"$pool"}'; 20 local dsSel = '{type="filesystem",instance=~"$instance",pool=~"$pool",name=~"$dataset"}'; 21 22 // --------------------------------------------------------------------------- 23 // Threshold step sets and value mappings 24 // --------------------------------------------------------------------------- 25 local greenOnly = [{ color: 'green', value: null }]; 26 local usedSteps = [ 27 { color: 'green', value: null }, 28 { color: 'yellow', value: 75 }, 29 { color: 'orange', value: 85 }, 30 { color: 'red', value: 90 }, 31 ]; 32 local healthyStep = [{ color: 'red', value: null }, { color: 'green', value: 1 }]; 33 local fragSteps = [ 34 { color: 'green', value: null }, 35 { color: 'yellow', value: 0.4 }, 36 { color: 'red', value: 0.7 }, 37 ]; 38 local compSteps = [ 39 { color: 'blue', value: null }, 40 { color: 'green', value: 1.1 }, 41 { color: 'super-light-green', value: 2 }, 42 ]; 43 44 local healthMap = [{ 45 type: 'value', 46 options: { 47 '0': { text: 'ONLINE', color: 'green', index: 0 }, 48 '1': { text: 'DEGRADED', color: 'orange', index: 1 }, 49 '2': { text: 'FAULTED', color: 'red', index: 2 }, 50 '3': { text: 'OFFLINE', color: 'red', index: 3 }, 51 '4': { text: 'UNAVAIL', color: 'red', index: 4 }, 52 '5': { text: 'REMOVED', color: 'red', index: 5 }, 53 '6': { text: 'SUSPENDED', color: 'red', index: 6 }, 54 }, 55 }]; 56 local okFailMap = [{ 57 type: 'value', 58 options: { 59 '0': { text: 'FAILED', color: 'red', index: 0 }, 60 '1': { text: 'OK', color: 'green', index: 1 }, 61 }, 62 }]; 63 local rwroMap = [{ 64 type: 'value', 65 options: { 66 '0': { text: 'read-write', color: 'green', index: 0 }, 67 '1': { text: 'read-only', color: 'orange', index: 1 }, 68 }, 69 }]; 70 local upDownMap = [{ 71 type: 'value', 72 options: { 73 '0': { text: 'DOWN', color: 'red', index: 0 }, 74 '1': { text: 'UP', color: 'green', index: 1 }, 75 }, 76 }]; 77 78 // --------------------------------------------------------------------------- 79 // Query helper 80 // --------------------------------------------------------------------------- 81 local prom(expr, legend=null, refId='A', instant=false) = 82 prometheus.new('${datasource}', expr) 83 + prometheus.withRefId(refId) 84 + prometheus.withEditorMode('code') 85 + (if legend != null then prometheus.withLegendFormat(legend) else {}) 86 + ( 87 if instant then 88 prometheus.withInstant(true) 89 + prometheus.withFormat('table') 90 + prometheus.withRange(false) 91 else prometheus.withRange(true) 92 ); 93 94 // --------------------------------------------------------------------------- 95 // Panel helpers 96 // --------------------------------------------------------------------------- 97 local tsPanel(title, gp, targets, unit='bytes', steps=greenOnly, mappings=[], min=null, max=null, decimals=null, desc=null, fill=10, stack='none', interp='smooth', threshStyle='off', calcs=['lastNotNull', 'max', 'mean']) = 98 ts.new(title) 99 + ts.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y) 100 + { datasource: dsRef } 101 + ts.queryOptions.withTargets(targets) 102 + ts.standardOptions.withUnit(unit) 103 + ts.standardOptions.color.withMode('palette-classic') 104 + ts.standardOptions.thresholds.withMode('absolute') 105 + ts.standardOptions.thresholds.withSteps(steps) 106 + ts.standardOptions.withMappings(mappings) 107 + ts.fieldConfig.defaults.custom.withDrawStyle('line') 108 + ts.fieldConfig.defaults.custom.withFillOpacity(fill) 109 + ts.fieldConfig.defaults.custom.withGradientMode('opacity') 110 + ts.fieldConfig.defaults.custom.withLineWidth(2) 111 + ts.fieldConfig.defaults.custom.withLineInterpolation(interp) 112 + ts.fieldConfig.defaults.custom.withShowPoints('never') 113 + ts.fieldConfig.defaults.custom.withSpanNulls(false) 114 + ts.fieldConfig.defaults.custom.withStacking({ mode: stack, group: 'A' }) 115 + ts.fieldConfig.defaults.custom.thresholdsStyle.withMode(threshStyle) 116 + ts.options.legend.withDisplayMode('table') 117 + ts.options.legend.withPlacement('bottom') 118 + ts.options.legend.withShowLegend(true) 119 + ts.options.legend.withCalcs(calcs) 120 + ts.options.tooltip.withMode('multi') 121 + ts.options.tooltip.withSort('desc') 122 + (if min != null then ts.standardOptions.withMin(min) else {}) 123 + (if max != null then ts.standardOptions.withMax(max) else {}) 124 + (if decimals != null then ts.standardOptions.withDecimals(decimals) else {}) 125 + (if desc != null then ts.panelOptions.withDescription(desc) else {}); 126 127 local statPanel(title, gp, targets, unit='none', mappings=[], steps=greenOnly, colorMode='value', textMode='auto', graphMode='area', decimals=null, calc='lastNotNull', desc=null) = 128 stat.new(title) 129 + stat.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y) 130 + { datasource: dsRef } 131 + stat.queryOptions.withTargets(targets) 132 + stat.standardOptions.withUnit(unit) 133 + stat.standardOptions.withMappings(mappings) 134 + stat.standardOptions.color.withMode('thresholds') 135 + stat.standardOptions.thresholds.withMode('absolute') 136 + stat.standardOptions.thresholds.withSteps(steps) 137 + stat.options.withColorMode(colorMode) 138 + stat.options.withGraphMode(graphMode) 139 + stat.options.withTextMode(textMode) 140 + stat.options.withJustifyMode('auto') 141 + stat.options.withOrientation('auto') 142 + stat.options.reduceOptions.withCalcs([calc]) 143 + stat.options.reduceOptions.withFields('') 144 + stat.options.reduceOptions.withValues(false) 145 + (if decimals != null then stat.standardOptions.withDecimals(decimals) else {}) 146 + (if desc != null then stat.panelOptions.withDescription(desc) else {}); 147 148 local gaugePanel(title, gp, targets, unit='percent', steps=usedSteps, min=0, max=100, desc=null) = 149 gauge.new(title) 150 + gauge.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y) 151 + { datasource: dsRef } 152 + gauge.queryOptions.withTargets(targets) 153 + gauge.standardOptions.withUnit(unit) 154 + gauge.standardOptions.withMin(min) 155 + gauge.standardOptions.withMax(max) 156 + gauge.standardOptions.color.withMode('thresholds') 157 + gauge.standardOptions.thresholds.withMode('absolute') 158 + gauge.standardOptions.thresholds.withSteps(steps) 159 + gauge.options.withShowThresholdLabels(false) 160 + gauge.options.withShowThresholdMarkers(true) 161 + gauge.options.withOrientation('auto') 162 + gauge.options.reduceOptions.withCalcs(['lastNotNull']) 163 + gauge.options.reduceOptions.withFields('') 164 + gauge.options.reduceOptions.withValues(false) 165 + (if desc != null then gauge.panelOptions.withDescription(desc) else {}); 166 167 local barGaugePanel(title, gp, targets, unit='bytes', colorMode='continuous-GrYlRd', steps=greenOnly, display='gradient', orientation='horizontal', min=null, max=null, decimals=null, desc=null) = 168 barGauge.new(title) 169 + barGauge.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y) 170 + { datasource: dsRef } 171 + barGauge.queryOptions.withTargets(targets) 172 + barGauge.standardOptions.withUnit(unit) 173 + barGauge.standardOptions.color.withMode(colorMode) 174 + barGauge.standardOptions.thresholds.withMode('absolute') 175 + barGauge.standardOptions.thresholds.withSteps(steps) 176 + barGauge.options.withDisplayMode(display) 177 + barGauge.options.withOrientation(orientation) 178 + barGauge.options.withShowUnfilled(true) 179 + barGauge.options.withValueMode('color') 180 + barGauge.options.reduceOptions.withCalcs(['lastNotNull']) 181 + barGauge.options.reduceOptions.withFields('') 182 + barGauge.options.reduceOptions.withValues(false) 183 + (if min != null then barGauge.standardOptions.withMin(min) else {}) 184 + (if max != null then barGauge.standardOptions.withMax(max) else {}) 185 + (if decimals != null then barGauge.standardOptions.withDecimals(decimals) else {}) 186 + (if desc != null then barGauge.panelOptions.withDescription(desc) else {}); 187 188 local tablePanel(title, gp, targets, transformations, overrides, sortBy, footer, desc) = 189 table.new(title) 190 + table.panelOptions.withGridPos(gp.h, gp.w, gp.x, gp.y) 191 + { datasource: dsRef } 192 + table.panelOptions.withDescription(desc) 193 + table.queryOptions.withTargets(targets) 194 + table.queryOptions.withTransformations(transformations) 195 + table.standardOptions.withUnit('bytes') 196 + table.standardOptions.color.withMode('thresholds') 197 + table.standardOptions.thresholds.withMode('absolute') 198 + table.standardOptions.thresholds.withSteps(greenOnly) 199 + table.standardOptions.withOverrides(overrides) 200 + { fieldConfig+: { defaults+: { custom+: { 201 align: 'auto', 202 cellOptions: { type: 'auto' }, 203 filterable: true, 204 inspect: false, 205 } } } } 206 + table.options.withShowHeader(true) 207 + table.options.withCellHeight('sm') 208 + table.options.withFooter(footer) 209 + table.options.withSortBy(sortBy); 210 211 local rowPanel(title, y) = 212 row.new(title) 213 + row.withGridPos(y) 214 + row.withCollapsed(false); 215 216 local override(name, properties) = { 217 matcher: { id: 'byName', options: name }, 218 properties: properties, 219 }; 220 221 // --------------------------------------------------------------------------- 222 // Variables 223 // --------------------------------------------------------------------------- 224 local datasourceVar = 225 var.datasource.new('datasource', 'prometheus') 226 + var.datasource.generalOptions.withLabel('Data source') 227 + var.datasource.withRegex('') 228 + { current: { selected: false, text: 'VictoriaMetrics', value: 'DS_PROMETHEUS' } }; 229 230 local queryVar(name, label, expr, valueLabel) = 231 var.query.new(name) 232 + var.query.withDatasourceFromVariable(datasourceVar) 233 + var.query.queryTypes.withLabelValues(valueLabel, expr) 234 + var.query.generalOptions.withLabel(label) 235 + var.query.selectionOptions.withMulti(true) 236 + var.query.selectionOptions.withIncludeAll(true) 237 + var.query.withRefresh('time') 238 + var.query.withSort(1); 239 240 local instanceVar = queryVar('instance', 'Instance', 'zfs_pool_health', 'instance'); 241 local poolVar = queryVar('pool', 'Pool', 'zfs_pool_health{instance=~"$instance"}', 'pool'); 242 local datasetVar = queryVar('dataset', 'Dataset', 'zfs_dataset_used_bytes{type="filesystem",instance=~"$instance",pool=~"$pool"}', 'name'); 243 244 // --------------------------------------------------------------------------- 245 // Panels 246 // --------------------------------------------------------------------------- 247 local panels = [ 248 //=== Pool Overview ======================================================= 249 rowPanel('Pool Overview', 0), 250 251 statPanel('Pools', { h: 4, w: 3, x: 0, y: 1 }, 252 [prom('count(zfs_pool_health%s)' % poolSel)], 253 unit='none', graphMode='none', 254 desc='Number of ZFS pools matched by the current filters.'), 255 256 statPanel('Pools Healthy', { h: 4, w: 3, x: 3, y: 1 }, 257 [prom('count(zfs_pool_health%s == 0) or vector(0)' % poolSel)], 258 unit='none', graphMode='none', steps=healthyStep, 259 desc='Pools currently in the ONLINE (healthy) state.'), 260 261 statPanel('Total Capacity', { h: 4, w: 3, x: 6, y: 1 }, 262 [prom('sum(zfs_pool_size_bytes%s)' % poolSel)], 263 unit='bytes', graphMode='none'), 264 265 statPanel('Allocated', { h: 4, w: 3, x: 9, y: 1 }, 266 [prom('sum(zfs_pool_allocated_bytes%s)' % poolSel)], 267 unit='bytes'), 268 269 statPanel('Free', { h: 4, w: 3, x: 12, y: 1 }, 270 [prom('sum(zfs_pool_free_bytes%s)' % poolSel)], 271 unit='bytes'), 272 273 gaugePanel('Used %', { h: 4, w: 3, x: 15, y: 1 }, 274 [prom('sum(zfs_pool_allocated_bytes%s) / sum(zfs_pool_size_bytes%s) * 100' % [poolSel, poolSel])], 275 desc='Aggregate space used across all matched pools.'), 276 277 statPanel('Datasets', { h: 4, w: 3, x: 18, y: 1 }, 278 [prom('count(zfs_dataset_used_bytes{type="filesystem",instance=~"$instance",pool=~"$pool"})')], 279 unit='none', graphMode='none'), 280 281 statPanel('Exporter', { h: 4, w: 3, x: 21, y: 1 }, 282 [prom('min(up{job="zfs_exporter",instance=~"$instance"})')], 283 unit='none', graphMode='none', mappings=upDownMap, 284 colorMode='background', textMode='value', 285 desc='Scrape state of the zfs_exporter target(s).'), 286 287 //=== Pool Health & Capacity ============================================== 288 rowPanel('Pool Health & Capacity', 5), 289 290 tablePanel( 291 'Pool Status', { h: 9, w: 15, x: 0, y: 6 }, 292 [ 293 prom('max by (pool) (zfs_pool_health%s)' % poolSel, refId='A', instant=true), 294 prom('max by (pool) (zfs_pool_size_bytes%s)' % poolSel, refId='B', instant=true), 295 prom('max by (pool) (zfs_pool_allocated_bytes%s)' % poolSel, refId='C', instant=true), 296 prom('max by (pool) (zfs_pool_free_bytes%s)' % poolSel, refId='D', instant=true), 297 prom('max by (pool) (zfs_pool_allocated_bytes%s / zfs_pool_size_bytes%s * 100)' % [poolSel, poolSel], refId='E', instant=true), 298 prom('max by (pool) (zfs_pool_fragmentation_ratio%s)' % poolSel, refId='F', instant=true), 299 prom('max by (pool) (zfs_pool_deduplication_ratio%s)' % poolSel, refId='G', instant=true), 300 prom('max by (pool) (zfs_pool_readonly%s)' % poolSel, refId='H', instant=true), 301 ], 302 [ 303 { id: 'joinByField', options: { byField: 'pool', mode: 'outer' } }, 304 { id: 'organize', options: { 305 excludeByName: { Time: true, 'Time 1': true, 'Time 2': true, 'Time 3': true, 'Time 4': true, 'Time 5': true, 'Time 6': true, 'Time 7': true, 'Time 8': true }, 306 renameByName: { 307 pool: 'Pool', 308 'Value #A': 'Health', 309 'Value #B': 'Size', 310 'Value #C': 'Allocated', 311 'Value #D': 'Free', 312 'Value #E': 'Used %', 313 'Value #F': 'Fragmentation', 314 'Value #G': 'Dedup', 315 'Value #H': 'Read-only', 316 }, 317 indexByName: { pool: 0, 'Value #A': 1, 'Value #B': 2, 'Value #C': 3, 'Value #D': 4, 'Value #E': 5, 'Value #F': 6, 'Value #G': 7, 'Value #H': 8 }, 318 } }, 319 ], 320 [ 321 override('Health', [ 322 { id: 'mappings', value: healthMap }, 323 { id: 'custom.cellOptions', value: { type: 'color-text' } }, 324 { id: 'custom.width', value: 110 }, 325 ]), 326 override('Used %', [ 327 { id: 'unit', value: 'percent' }, 328 { id: 'decimals', value: 1 }, 329 { id: 'custom.cellOptions', value: { type: 'gauge', mode: 'gradient' } }, 330 { id: 'min', value: 0 }, 331 { id: 'max', value: 100 }, 332 { id: 'thresholds', value: { mode: 'absolute', steps: usedSteps } }, 333 ]), 334 override('Fragmentation', [ 335 { id: 'unit', value: 'percentunit' }, 336 { id: 'decimals', value: 1 }, 337 { id: 'custom.cellOptions', value: { type: 'color-text' } }, 338 { id: 'thresholds', value: { mode: 'absolute', steps: fragSteps } }, 339 ]), 340 override('Dedup', [ 341 { id: 'unit', value: 'none' }, 342 { id: 'decimals', value: 2 }, 343 ]), 344 override('Read-only', [ 345 { id: 'mappings', value: rwroMap }, 346 { id: 'custom.cellOptions', value: { type: 'color-text' } }, 347 ]), 348 ], 349 [{ displayName: 'Used %', desc: true }], 350 { show: false, reducer: ['sum'], countRows: false, fields: '' }, 351 'Per-pool health, capacity, fragmentation and dedup at a glance.' 352 ), 353 354 barGaugePanel( 355 'Capacity Used per Pool', { h: 9, w: 9, x: 15, y: 6 }, 356 [prom('zfs_pool_allocated_bytes%s / zfs_pool_size_bytes%s * 100' % [poolSel, poolSel], legend='{{pool}}')], 357 unit='percent', colorMode='thresholds', steps=usedSteps, display='lcd', 358 min=0, max=100, decimals=1, 359 desc='Fill percentage per pool. ZFS performance degrades sharply above ~80%.' 360 ), 361 362 //=== Capacity Trends ===================================================== 363 rowPanel('Capacity Trends', 15), 364 365 tsPanel('Pool Used % Over Time', { h: 8, w: 12, x: 0, y: 16 }, 366 [prom('zfs_pool_allocated_bytes%s / zfs_pool_size_bytes%s * 100' % [poolSel, poolSel], legend='{{pool}}')], 367 unit='percent', min=0, max=100, steps=usedSteps, threshStyle='dashed', 368 desc='Capacity fill trend with the 90% danger threshold marked.'), 369 370 tsPanel('Pool Free Space Trend', { h: 8, w: 12, x: 12, y: 16 }, 371 [ 372 prom('zfs_pool_free_bytes%s' % poolSel, legend='{{pool}} free', refId='A'), 373 prom('zfs_pool_allocated_bytes%s' % poolSel, legend='{{pool}} allocated', refId='B'), 374 ], 375 unit='bytes', 376 desc='Absolute free vs. allocated space — useful for eyeballing time-to-full.'), 377 378 tsPanel('Fragmentation', { h: 7, w: 8, x: 0, y: 24 }, 379 [prom('zfs_pool_fragmentation_ratio%s' % poolSel, legend='{{pool}}')], 380 unit='percentunit', min=0, max=1, 381 desc='Free-space fragmentation ratio per pool.'), 382 383 tsPanel('Deduplication Ratio', { h: 7, w: 8, x: 8, y: 24 }, 384 [prom('zfs_pool_deduplication_ratio%s' % poolSel, legend='{{pool}}')], 385 unit='none', decimals=2, 386 desc='Dedup ratio (1.00 = no deduplication).'), 387 388 tsPanel('Freeing & Leaked Bytes', { h: 7, w: 8, x: 16, y: 24 }, 389 [ 390 prom('zfs_pool_freeing_bytes%s' % poolSel, legend='{{pool}} freeing', refId='A'), 391 prom('zfs_pool_leaked_bytes%s' % poolSel, legend='{{pool}} leaked', refId='B'), 392 ], 393 unit='bytes', 394 desc='Space pending free after destroys, and leaked (unaccounted) bytes — both should trend to 0.'), 395 396 //=== Datasets ============================================================ 397 rowPanel('Datasets', 31), 398 399 tablePanel( 400 'Dataset Usage', { h: 11, w: 15, x: 0, y: 32 }, 401 [ 402 prom('max by (name, pool) (zfs_dataset_used_bytes%s)' % dsSel, refId='A', instant=true), 403 prom('max by (name) (zfs_dataset_available_bytes%s)' % dsSel, refId='B', instant=true), 404 prom('max by (name) (zfs_dataset_referenced_bytes%s)' % dsSel, refId='C', instant=true), 405 prom('max by (name) (zfs_dataset_logical_used_bytes%s)' % dsSel, refId='D', instant=true), 406 prom('max by (name) (zfs_dataset_quota_bytes%s)' % dsSel, refId='E', instant=true), 407 prom('max by (name) (zfs_dataset_used_bytes%s / (zfs_dataset_used_bytes%s + zfs_dataset_available_bytes%s) * 100)' % [dsSel, dsSel, dsSel], refId='F', instant=true), 408 prom('max by (name) (zfs_dataset_logical_used_bytes%s / zfs_dataset_used_by_dataset_bytes%s)' % [dsSel, dsSel], refId='G', instant=true), 409 ], 410 [ 411 { id: 'joinByField', options: { byField: 'name', mode: 'outer' } }, 412 { id: 'organize', options: { 413 excludeByName: { Time: true, 'Time 1': true, 'Time 2': true, 'Time 3': true, 'Time 4': true, 'Time 5': true, 'Time 6': true, 'Time 7': true }, 414 renameByName: { 415 name: 'Dataset', 416 pool: 'Pool', 417 'Value #A': 'Used', 418 'Value #B': 'Available', 419 'Value #C': 'Referenced', 420 'Value #D': 'Logical Used', 421 'Value #E': 'Quota', 422 'Value #F': 'Fill %', 423 'Value #G': 'Compression', 424 }, 425 indexByName: { name: 0, pool: 1, 'Value #A': 2, 'Value #F': 3, 'Value #B': 4, 'Value #C': 5, 'Value #D': 6, 'Value #E': 7, 'Value #G': 8 }, 426 } }, 427 { id: 'sortBy', options: { fields: '', sort: [{ field: 'Used', desc: true }] } }, 428 ], 429 [ 430 override('Dataset', [{ id: 'custom.width', value: 240 }]), 431 override('Pool', [{ id: 'custom.width', value: 80 }]), 432 override('Used', [ 433 { id: 'unit', value: 'bytes' }, 434 { id: 'custom.cellOptions', value: { type: 'gauge', mode: 'gradient', valueDisplayMode: 'text' } }, 435 ]), 436 override('Fill %', [ 437 { id: 'unit', value: 'percent' }, 438 { id: 'decimals', value: 1 }, 439 { id: 'custom.cellOptions', value: { type: 'color-background', mode: 'gradient' } }, 440 { id: 'thresholds', value: { mode: 'absolute', steps: usedSteps } }, 441 ]), 442 override('Compression', [ 443 { id: 'unit', value: 'none' }, 444 { id: 'decimals', value: 2 }, 445 { id: 'custom.cellOptions', value: { type: 'color-text' } }, 446 { id: 'thresholds', value: { mode: 'absolute', steps: [{ color: 'text', value: null }, { color: 'green', value: 1.1 }] } }, 447 ]), 448 ], 449 [{ displayName: 'Used', desc: true }], 450 { show: true, reducer: ['sum'], countRows: false, fields: ['Used', 'Logical Used'] }, 451 'Per-dataset space accounting, quota fill and compression ratio.' 452 ), 453 454 barGaugePanel( 455 'Top Datasets by Used Space', { h: 11, w: 9, x: 15, y: 32 }, 456 [prom('topk(15, max by (name) (zfs_dataset_used_bytes%s))' % dsSel, legend='{{name}}')], 457 unit='bytes', display='gradient', 458 desc='Largest datasets by space consumed (incl. descendants).' 459 ), 460 461 //=== Dataset Trends ====================================================== 462 rowPanel('Dataset Trends', 43), 463 464 tsPanel('Dataset Used Over Time', { h: 8, w: 12, x: 0, y: 44 }, 465 [prom('topk(20, max by (name) (zfs_dataset_used_bytes%s))' % dsSel, legend='{{name}}')], 466 unit='bytes', 467 desc='Growth of the top datasets over the selected window.'), 468 469 tsPanel('Quota Fill', { h: 8, w: 12, x: 12, y: 44 }, 470 [prom('zfs_dataset_used_bytes%s / (zfs_dataset_quota_bytes%s > 0) * 100' % [dsSel, dsSel], legend='{{name}}')], 471 unit='percent', min=0, max=100, steps=usedSteps, threshStyle='dashed', 472 desc='Used space as a percentage of quota, for datasets that have a quota set.'), 473 474 barGaugePanel( 475 'Compression Ratio (logical / physical)', { h: 8, w: 12, x: 0, y: 52 }, 476 [prom('topk(15, max by (name) (zfs_dataset_logical_used_bytes%s / zfs_dataset_used_by_dataset_bytes%s))' % [dsSel, dsSel], legend='{{name}}')], 477 unit='none', colorMode='thresholds', steps=compSteps, display='gradient', decimals=2, 478 desc="How much each dataset's data shrinks on disk. 1.00 = incompressible." 479 ), 480 481 tsPanel('Written Since Last Snapshot', { h: 8, w: 12, x: 12, y: 52 }, 482 [prom('topk(15, max by (name) (zfs_dataset_written_bytes%s))' % dsSel, legend='{{name}}')], 483 unit='bytes', fill=25, interp='linear', 484 desc='Referenced bytes written since the previous snapshot — a proxy for recent write activity (resets on snapshot).'), 485 486 //=== Exporter Health ===================================================== 487 rowPanel('Exporter Health', 60), 488 489 statPanel('Collectors Healthy', { h: 5, w: 4, x: 0, y: 61 }, 490 [prom('count(zfs_scrape_collector_success{instance=~"$instance"} == 1) or vector(0)')], 491 unit='none', graphMode='none', steps=healthyStep, 492 desc='ZFS exporter sub-collectors reporting success.'), 493 494 statPanel('Collector Status', { h: 5, w: 8, x: 4, y: 61 }, 495 [prom('zfs_scrape_collector_success{instance=~"$instance"}', legend='{{collector}}')], 496 unit='none', graphMode='none', mappings=okFailMap, 497 colorMode='background', textMode='value_and_name', 498 desc='Success/failure per exporter collector.'), 499 500 tsPanel('Collector Scrape Duration', { h: 5, w: 12, x: 12, y: 61 }, 501 [prom('zfs_scrape_collector_duration_seconds{instance=~"$instance"}', legend='{{collector}}')], 502 unit='s', 503 desc='Time each collector takes to gather metrics.'), 504 505 statPanel('Goroutines', { h: 4, w: 4, x: 0, y: 66 }, 506 [prom('go_goroutines{job="zfs_exporter",instance=~"$instance"}')], 507 unit='none'), 508 509 statPanel('Heap In Use', { h: 4, w: 4, x: 4, y: 66 }, 510 [prom('go_memstats_heap_inuse_bytes{job="zfs_exporter",instance=~"$instance"}')], 511 unit='bytes'), 512 513 statPanel('Go Version', { h: 4, w: 4, x: 8, y: 66 }, 514 [prom('go_info{job="zfs_exporter",instance=~"$instance"}', legend='{{version}}')], 515 unit='none', graphMode='none', textMode='name', colorMode='none'), 516 517 tsPanel('Exporter Memory', { h: 4, w: 12, x: 12, y: 66 }, 518 [ 519 prom('go_memstats_heap_inuse_bytes{job="zfs_exporter",instance=~"$instance"}', legend='heap in-use', refId='A'), 520 prom('go_memstats_stack_inuse_bytes{job="zfs_exporter",instance=~"$instance"}', legend='stack in-use', refId='B'), 521 prom('go_memstats_sys_bytes{job="zfs_exporter",instance=~"$instance"}', legend='sys', refId='C'), 522 ], 523 unit='bytes', calcs=['lastNotNull', 'max']), 524 ]; 525 526 // Assign stable, unique panel IDs. 527 local panelsWithIds = std.mapWithIndex(function(i, p) p + { id: i + 1 }, panels); 528 529 // --------------------------------------------------------------------------- 530 // Dashboard 531 // --------------------------------------------------------------------------- 532 dashboard.new('ZFS Exporter') 533 + dashboard.withUid('zfs-exporter') 534 + dashboard.withTags(['zfs', 'storage', 'freebsd']) 535 + dashboard.withRefresh('30s') 536 + dashboard.withTimezone('browser') 537 + dashboard.withVariables([datasourceVar, instanceVar, poolVar, datasetVar]) 538 + dashboard.withPanels(panelsWithIds) 539 + { graphTooltip: 1, time: { from: 'now-24h', to: 'now' } }