KubeSense Dashboards
Build a dashboard preset — the same shape the Export button produces, so an exported dashboard can be edited and re-imported.
Two ways to deliver it:
create-dashboard MCP tool |
Preset JSON | |
|---|---|---|
| Applied by | the agent (write tool, needs approval) | the user, via Dashboards → Import |
| Use when | the user asked you to create one | they asked for the JSON, or want to review first |
| Shape to emit | the preset object itself | the import envelope, with preset stringified |
Both go through the same server-side schema check, so a preset that passes
validate-dashboard-json is one that both paths accept.
When to Use
- "Create a dashboard for…" / "Build a dashboard with panels for…" →
create-dashboard - "Give me the dashboard JSON for…" → preset JSON
- "Generate a dashboard preset" → preset JSON
Always Validate Before You Finish
Call validate-dashboard-json on the preset before creating it or handing it over.
It stores nothing, so call it as often as you need.
# valid=false findings=2
path rule message
/panels/0/queries/0/selectedMode shape value must be one of 'logs', 'metrics', 'traces', 'formula'
/gridLayout/0 shape must have required property 'h'
Each path is a JSON Pointer to the exact value to fix. Repeat until valid=true.
Pass the preset object as document — not the import envelope with the stringified
preset field. Same for create-dashboard's preset argument.
Validation checks shape, not whether the data exists. A query against a metric nobody collects is well-formed and will render empty, so discover names first.
Discover Fields First
If the KubeSense MCP server is connected, discover real names before writing queries — never guess a metric, group-by, or filter field.
| To find | Tool |
|---|---|
| Metric names | get-available-metrics |
Metric labels (for by) |
get-metric-labels |
| Log/trace fields | get-trace-or-log-fields |
Dashboard queries use storage-level field names, not the MCP catalog labels — the webapp posts these directly. Take field names from the discovery output's storage column where they differ, and tell the user to confirm on the panel preview. See kubesense-mcp for the label/storage distinction.
Without MCP, fall back to user-provided names and say they need verifying.
Output Format
{
"name": "<dashboard name>",
"description": "",
"preset": "<stringified JSON of the preset object>"
}
[!IMPORTANT] In this envelope,
presetis a JSON string, not a nested object. The validator declarespreset: z.string()and only runs the preset schema afterJSON.parse(preset). Emitting an object fails with "Preset must be a valid JSON string that matches the dashboard preset schema".This applies to the import envelope only.
create-dashboardandvalidate-dashboard-jsontake the preset object itself — passing a stringified blob to them works too, but do not wrap it in this envelope.
name is required and non-empty. description is optional.
The object you stringify:
{
"gridLayout": [],
"panels": [],
"variables": [],
"subGrids": [],
"subGridLayout": []
}
Only gridLayout and panels are required; the rest default to []. A sixth optional
key, publicDashboardPath (string), also validates.
Minimal Valid Dashboard
Verified against the live validator:
{
"name": "Minimal",
"description": "",
"preset": "{\"gridLayout\":[{\"i\":\"0\",\"x\":0,\"y\":0,\"w\":12,\"h\":4}],\"panels\":[{\"name\":\"Up\",\"panelType\":\"timeSeries\",\"queries\":[{\"selectedMode\":\"metrics\",\"label\":\"A\",\"selectedMetric\":\"up\",\"functions\":[]}],\"config\":{}}],\"variables\":[],\"subGrids\":[],\"subGridLayout\":[]}"
}
What Hard-Fails Import
Get these wrong and import is rejected with an error:
presetnot a string.- Missing or empty top-level
name. - Missing
gridLayoutorpanels. - A
gridLayoutitem missing any ofi,x,y,w,h, or with a wrong type ("6"instead of6). - A panel missing
name, orname: "". - A panel missing the
queriesarray (it may be[], but the key must exist). - A query whose
selectedModeis notmetrics|logs|traces|formula. - A logs/traces query missing
columnFields— the single hard-required field on those queries. - A
columnFields[]entry withfield: "". - A formula missing
expression, or one containing lowercase letters or a decimal point. - A formula referencing an undefined label, referencing itself, placed before the queries it references, or with a multi-character label.
What Silently Destroys Your Data
More dangerous than hard failures — these validate cleanly and then discard what you sent.
Almost every field carries a .catch(), so a single bad value resets its whole array or
object to the default.
| Field | On a bad value |
|---|---|
functions (metrics query) |
The entire array is wiped to [] — one malformed entry loses every function |
variables |
All variables deleted — one invalid variable empties the array |
subGrids |
All sub-grids dropped |
subGrids[].panels |
That row's panels emptied |
aggregation |
Resets to {"function": "row_count"} |
panelType |
Resets to "timeSeries" |
chart_type |
Resets to "table" |
yAxisLabelFormatter |
Resets to "auto" |
colorScheme |
Resets to {"type":"palette","palette":"default"} |
step |
Non-integer (e.g. 60.5) resets to "auto" |
pagination |
page_size > 500 resets the whole object to {page:1,page_size:50} |
Because of this, prefer omitting an optional field over guessing its value — an omitted field takes the default; a wrong one can take out its siblings.
[!WARNING]
gridLayoutmust have at least as many entries aspanels. Panels and layout are matched positionally by array index, not by theivalue. A shortgridLayoutpasses validation and then throws a TypeError when the dashboard renders. Theistring is only used to identify sub-grid rows (via asg-prefix); real exports set it to the index ("0","1", …).
Panel
{
"name": "CPU by namespace",
"description": "",
"panelType": "timeSeries",
"queries": [],
"config": {}
}
panelType — 9 values: timeSeries, stat, table, list, bar, pie, topList,
spl, sql. Note camelCase timeSeries.
config can never fail import (the whole object catches), so {} is always safe and
inherits every default. For the full field list, defaults, and the colorScheme /
thresholds / alignColumns / columnFormatting / visualFormattingRules shapes, read
references/panel-config.md.
For a table panel, per-column display names, visibility, cell type (number / bar),
and threshold/range colouring go in config.columnFormatting[] — see the reference.
For a top-list panel, config.topListDisplayMode is flat (default) or stacked, and
stacked needs two or more groupBy entries — the first is the row, the rest are the
blocks stacked inside it. With one dimension the renderer draws flat whatever the field
says. Value-driven colouring goes in config.visualFormattingRules[], which supersedes
thresholds for this panel type. Both are in the reference.
Three config fields the old format got wrong:
thresholdDisplayMode, notenableThresholds. Values:off(default),lines,lines_dashed,filled_regions,filled_regions_and_lines,filled_regions_and_lines_dashed.colorScheme, notcolorPalette. A 5-variant union; the palette variant is{"type":"palette","palette":"default"}and the only palette keys aredefault,success,warning,error.mergeTablesisz.literal(true)and lives in the defaults — it is alwaystrueand cannot be disabled. Sendingfalsesilently becomestrue. Just omit it.
Y-axis units
yAxisLabelFormatter accepts 199 values — the same enum as fieldConfig.unit. Common
ones: auto, number, percentage, bytes, bytes/sec, nanoseconds, milliseconds,
seconds, mCPU, CPU. Use nanoseconds for any panel aggregating trace duration —
the formatter auto-scales ns → µs/ms/s. Full list in
references/panel-config.md.
[!WARNING]
percent,percent_unit,short,ops,bps,celsius,fahrenheit, andnonedo not exist and silently becomeauto. Usepercentage,CPU/mCPU,bytes/sec,numberinstead.
Queries
Discriminated on selectedMode: metrics | logs | traces | formula. Note spl is
not a valid selectedMode — use panelType: "spl" with a logs/traces query and
filterMode: "SPL".
Metrics query
{
"selectedMode": "metrics",
"label": "A",
"selectedMetric": "container_cpu_usage_seconds_total",
"functions": [],
"filters": {},
"queryMode": "builder",
"promql": "",
"visible": true,
"labelOptions": { "type": "auto" },
"pagination": { "page": 1, "page_size": 50 },
"fieldConfig": {}
}
Every field is optional. queryMode is builder or code — use code with promql
set, builder with selectedMetric + functions.
variables is auto-derived from filters and promql ($name references) — anything you
supply is overwritten. Don't bother setting it.
Logs / traces query
{
"selectedMode": "logs",
"label": "A",
"columnFields": [],
"filters": {},
"queryMode": "builder",
"filterMode": "MFD",
"aggregation": { "function": "row_count" },
"groupBy": [],
"chart_type": "table",
"visible": true,
"sorting": {
"sortBy": { "field": "", "type": "string", "is_attribute": false },
"sortOrder": "ASC"
},
"query": "",
"pagination": { "page": 1, "page_size": 50 },
"fieldConfig": {}
}
[!IMPORTANT]
columnFieldsis required — include it on every logs/traces query, even as[]. Omitting it is a hard import failure. Entry shape:{"field": "namespace", "type": "string", "is_attribute": false}with optionalaliasandlabel.fieldmust be non-empty.
chart_type— 7 values:table,stat,bar,pie,topList,timeseries,list. Lowercasetimeseries—timeSeriessilently becomestable. This is a query-level field, separate from the panel-levelpanelType.filterMode—MFD,ADVANCED_QUERY,SPL,SQL. UseADVANCED_QUERYwith aquerystring for anything MFD's equality-only filters can't express (e.g. a latency threshold).groupByentries use the same shape ascolumnFields.list(array) andvalue(string) are the SPL/SQL column fields. There is notopListLabel/topListValue.
aggregation is strict about type:
{ "function": "p99", "fields": [ { "field": "duration", "type": "float", "is_attribute": false } ] }
row_count— nofields.unique_count—fieldsrequired,typeis"float"or"string".avg,sum,max,min,p99,p95,p90,p75,p50—fieldsrequired andtypemust be literally"float".
Get any of that wrong — missing fields, type: "string" on a numeric aggregation, or
function: "count" (not a valid name) — and the whole aggregation silently resets to
row_count, giving you a row count where you asked for a percentile.
Formula query
{ "selectedMode": "formula", "label": "C", "expression": "A/B", "visible": true, "fieldConfig": {} }
expression must match ^[A-Z0-9+/*()-]+$ after whitespace is stripped. Consequences:
- Uppercase only —
a+bfails. - No decimal point —
A*1.5fails. UseA*3/2. - Labels are single letters A–Z; a two-character label breaks the dependency check.
- The formula must appear after the queries it references in the
queriesarray. - It cannot reference itself, or a label that doesn't exist.
Filters
filters is Record<string, string[]>:
{ "namespace": ["production"], "level": ["ERROR", "FATAL"] }
- Attribute keys are prefixed
@_@, e.g."@_@user.id": ["abc"]. - Exclusion is a
-prefix on the value:{"namespace": ["-kube-system"]}. - A
$namevalue is a dashboard-variable reference.
Metrics Query Functions
functions is an ordered pipeline. One malformed entry wipes the entire array, and
argument counts are exact tuples — so build these carefully.
| type | names | arguments |
|---|---|---|
range |
rate, increase, resets |
[{arg_name:"over", arg_value:"5m"}] — any string |
aggregations |
sum, avg, max, min, count, No_Aggregations |
[{arg_name:"by", arg_value:["namespace"]}] — must be an array |
top_bottom |
top, bottom |
[{arg_name:"k",arg_value:5},{arg_name:"by",arg_value:"max"}] — by ∈ max|min|avg|median|last |
rollup |
avg_over_time, sum_over_time, max_over_time, min_over_time, count_over_time, last_over_time, absent_over_time, present_over_time, increases_over_time, range_over_time, quantile_over_time |
[{arg_name:"over", arg_value:"5m"}] — restricted to 30s|1m|5m|30m|1h|1d |
comparison |
greater, lesser, greater_than_or_equal, less_than_or_equal, equal, not_equal |
[{arg_name:"than"|"to", arg_value:100}] |
transform |
clamp (min+max), clamp_max (max), clamp_min (min), round (to_nearest), histogram_quantile (quantile), abs / sort / sort_desc (none) |
arg names in parens; the arguments key is still required — use [] for the zero-arg ones |
[!WARNING]
range.overaccepts any string, butrollup.overonly accepts30s,1m,5m,30m,1h,1d. Arollupwithover: "7m"silently deletes every function on that query.
Typical time-series pipeline: rate then aggregations.
"functions": [
{ "type": "range", "name": "rate", "arguments": [ { "arg_name": "over", "arg_value": "5m" } ] },
{ "type": "aggregations", "name": "sum", "arguments": [ { "arg_name": "by", "arg_value": ["namespace"] } ] }
]
Grid Layout
{ "i": "0", "x": 0, "y": 0, "w": 6, "h": 4 }
All five keys required. 12-column grid, rowHeight 100px, panel minH 2. No bounds are
validated — w: 99 is accepted and renders broken.
x: 0–11. Side by side:0, 6for two columns;0, 4, 8for three.y: increment by the previous row's height.i: the array index as a string. Matching is positional, so keepgridLayoutin the same order aspanels, and at least as long.
Variables and Rows
Both are optional and most dashboards need neither — omit variables, subGrids, and
subGridLayout and they default to [].
If the dashboard needs a template variable (a dropdown feeding $name into query
filters) or rows (collapsible panel groups), read
references/variables-and-rows.md for the schemas.
Two things to carry into that file: a variable's description is required (use ""), and
one invalid variable silently deletes every variable.
Delivering It
Creating it directly
validate-dashboard-json → valid=true → create-dashboard
create-dashboard takes name, preset (the object, or a JSON string containing it) and
an optional description. It returns the dashboard id and its UI path — quote that path so
the user can open it.
It is a write tool: only call it when the user has clearly asked for a dashboard to be created, and say what you are about to create before calling. If it refuses, the response names the JSON Pointer for every problem; fix them and retry rather than falling back to handing over JSON.
Handing over JSON to import
- Copy the JSON to a
.jsonfile (the import UI requiresapplication/json). - KubeSense → Dashboards → Import.
- Upload and review, then save.
On this path never claim the dashboard was created — the user imports and confirms it.
Rules
Rule 0: run validate-dashboard-json before creating or handing over, and fix what it
reports. Everything in What Hard-Fails Import is machine-checked
— the validator names the JSON Pointer, so it needs no checklist here.
This checklist is for what the validator is blind to. Every rule below passes validation and then silently discards your data or crashes at render. A green validation is not a working dashboard — check these by hand before you hand it over.
gridLayoutmust be at least as long aspanels, in the same order — matching is positional, and a short layout throws a TypeError when the dashboard renders.- Numeric aggregations need
fields[].type: "float"exactly, or the whole aggregation resets torow_count— a row count where you asked for a percentile. chart_typeis lowercasetimeseries;panelTypeis camelCasetimeSeries. Each resets to its own default (table/timeSeries) on a mismatch.rollup.overonly accepts30s/1m/5m/30m/1h/1d; a bad value wipes every function on that query.- Variables need
description(use"") and a regex-validname≤ 20 chars — one bad variable deletes them all. - Use real y-axis units (
percentage,mCPU,bytes/sec) —percent,short,nonebecomeauto. - Don't emit
enableThresholds,colorPalette,mergeTables: false,topListLabel, ortopListValue— none exist, and unknown keys are stripped without comment. On a top list, value colouring goes invisualFormattingRules, notthresholds. - Prefer omitting an optional field to guessing it: an omitted field takes its default, a wrong one can reset its siblings.
- Validation checks shape, not existence. Discover metric and field names with MCP before writing queries, and tell the user to confirm on the panel preview.
The one rule that spans both: preset is a stringified JSON string in the import envelope,
but the plain object when passed to create-dashboard or validate-dashboard-json.