Subchapter 37.5
references/cloudwatch-omni/dashboards.mdMarkdown73 KBView on GitHub
Use this when building, saving, reading back, or fixing a CloudWatch Omni dashboard — “build me a dashboard for checkout”, “show what’s most critical for this service”, “lay this out”, “which chart for this signal?”, “why is that panel empty?”, “why does that field have no effect after save?”, “why was my save rejected?” — and for the dashboard API itself ( and its siblings, ).
CreateOmniDashboardaws cloudwatchomni *-omni-dashboard*A CloudWatch Omni dashboard is a set of panels laid out on a fixed 60-column
grid, most of them charts over CloudWatch Omni queries. The dashboard body is
JSON — a panels[] array (plus an optional scope) that the Omni UI renders. It
belongs to a Space and is stored, as a JSON string, in the body field of a
dashboard resource you create and update through the API (section 7). The
deliverable is therefore two things: a valid, grounded body, and the API call that
saves it.
Two rules hold across every step:
ALWAYS STATE — how a dashboard body fails. It fails in several ways, and only one of them errors — the rest fail silently, so call the right one out whenever you author, explain, or debug a body (full detail in “API save semantics”, section 4):
ValidationException).
The root and panel objects are schema-validated (additionalProperties off), so this
covers an unknown or misspelled key there (a top-level refreshInterval, a per-panel
id, a misspelled title), a bad variant enum, a missing or non-array panels, a
panel missing its required type or layout, and an out-of-bounds coordinate FIELD
(x/y below 0, w below 1). The save does NOT succeed — none of these is silently
dropped. Validate the structure and every root/panel key against section 4.variant (a bad one
is rejected at 400 — previous bullet); visualization, panel type, and alerts
state values are unchecked. An unknown visualization VALUE blanks THAT panel
while its siblings still render. Everything else it does not catch blanks
the WHOLE dashboard (every sibling included): the cross-field x + w > 60 overflow
(per-field bounds are validated, their sum is not), a w > 60, a bad h unit
(80vh), an unrecognised panel type VALUE, a bad alerts state, or a
now-30s/now-1y time bound. Either way there is no error, and a 200 is not proof
of renderability; nothing enforces the x + w <= 60 sum.config is accepted and silently ignored. config (and its
nested objects like chartOptions) is permissive: a misspelled or unknown key there
is stored, comes back on read, and never takes effect. This — not root/panel keys —
is where “a field I set doesn’t take effect, with no error” actually happens.queryLanguage must be set explicitly per panel; a mismatch fails silently.
Set each explore panel’s config.queryLanguage to 'sql' or 'promql' yourself
— it is not inferred from the query text. (Metrics → promql; log/trace
content → sql.)Work sections 2–5 in order — each depends on the one before — then save with the API in section 7.
Both are called “a CloudWatch dashboard” in ordinary speech, but they are different resources with different bodies, different APIs, and different query languages:
| CloudWatch Omni dashboard (this file) | CloudWatch dashboard | |
|---|---|---|
| Lives in | An Omni Space (spaceId on every call) | The AWS account/Region |
| API / CLI | CreateOmniDashboard etc. — aws cloudwatchomni … | PutDashboard etc. — aws cloudwatch … |
| Body | { "panels": [...], "scope": {...} } — panel type, config, layout {x,y,w,h} | { "widgets": [...] } — widget type, properties, x/y/width/height |
| Grid | 60 columns; y/h in pixels | 24 columns; all four in grid units |
| Panel queries | Omni SQL (logs/traces) and PromQL (metrics), one query per panel | CloudWatch metric definitions, Metrics Insights, Logs Insights |
| Identity | dashboardId (system-minted UUID); the name is metadata | The dashboard name |
The two bodies are not interchangeable — a CloudWatch widgets[] body pasted into
CreateOmniDashboard is rejected, and an Omni panels[] body means nothing to
PutDashboard. CloudWatch dashboards are documented in
../cloudwatch/dashboards.md.
Confirm Omni is enabled before acting. The Omni dashboard API is only reachable
once a Space exists in the target Region. Before you create, update, or delete a
dashboard, probe with aws cloudwatchomni list-spaces in that Region. If there is
no Space, say so plainly and route the same need to the CloudWatch path — that is the
correct fallback, not a wrong answer. A knowledge question (“does Omni have a
dashboard API?”, “what’s the panel schema?”) is answered from this file directly;
do not gate it behind the probe. See concepts.md for what a Space is.
Author and ground the panels, and surface the items below that are relevant to the task —
each is a save-time or render-time trap the API does not fully catch (only structural,
root/panel key, coordinate-field, and variant errors are loud; the rest are silent).
When the task is choosing a panel’s chart type, surface the visualization and panel-shape
items; when saving or reading a dashboard back, surface the save-boundary behavior for
their config. Each item points to the section that details it; state what fits the task,
not a re-paste of every detail.
visualization value from §4’s supported set deliberately, there is no safe
default. Any value outside that set, plus a missing or empty value, saves 200 and
silently blanks that one panel at render, with no fallback that draws.visualization and queryLanguage are both REQUIRED on every explore (data)
panel, and a queryLanguage mismatch is a silent blank rather than an error. They
are config fields of the explore panel type only. The non-explore panel types
(such as markdown, divider, alerts, spans, and application-map) carry
neither. Set each explore panel’s config.queryLanguage
to sql or promql yourself. It is never inferred from the query text (§4 preamble,
§4 “explore”, §4 “API save semantics”).number, not table; a top-N ranking (ORDER BY … LIMIT / topk) → table, not
line (§4, “Visualizations”).number-tile COUNT(*) product bug. A COUNT(*)-shaped scalar in a number
panel renders the result-set ROW COUNT (usually 1), not the count value; put the
COUNT(*) in a table panel instead, and for any other scalar aggregate make it the
first selected column so the number tile reads the value rather than the row count
(§4, “Visualizations”, “number tile trap”).config is stored, round-trips on read,
and is silently ignored; and a cross-field x + w > 60 overflow blanks the whole
canvas. LOUD (REJECTED at save with HTTP 400 ValidationException): a structural,
top-level or per-panel key, per-field coordinate-BOUND (negative x/y, w < 1), or
variant violation. And GetOmniDashboard returns the body byte-for-byte (§4, “API
save semantics”).Pick the panels, order them into sections, and choose a visualization per signal.
number panels). Four is also the natural grid
fit — 15 × 4 = 60 columns — so a fifth tile forces an awkward wrap.Every dashboard reads top-to-bottom as summary → trend → breakdown → raw:
number panels with the headline figures (availability,
error rate, p99 latency, request rate). The fast “is it healthy?” read.line / area / bar time series for the golden signals.table (top-N by the primary signal) so the worst
offenders surface.Open a logically distinct group with a divider, and open an on-call / incident
dashboard with a short markdown runbook panel.
When the request names a known resource TYPE — an EC2 fleet, a Lambda function, a Kubernetes/EKS workload, an RDS/database instance, an ECS/web service, or an API Gateway API — don’t classify from scratch. Start from that archetype’s ready panel set in the Appendix, then ground and lay it out.
The panel content for a resource type — which signals, how each is read
(gauge vs. counter, max for a 0/1 check, min for a credit balance), which exist
only under a configuration (an installed agent, a burstable instance type, Container
Insights), and the identity label to group by — is owned by the per-service catalog in
promql-metrics.md (its “Service-health question — facts you
MUST surface” section and section 5). When the ask is which signals or panels a named
resource type needs, state those catalog facts as the proposed panel set; this file then
supplies the body and the grid. Do not decide the content by enumerating resources
through the service’s control plane.
<service>“ / “how is <service> doing?”. Use RED
(rate/errors/duration) for a request-serving service, USE (utilization,
saturation, errors) for an infrastructure resource. CORE (~6–8): a KPI row
(availability %, error rate, p99 latency, request rate); a request-rate line;
an errors bar (stacked) or area; a latency line (p50/p90/p99) when a real
percentile source exists (section 3); and a top-N table. Build each family
from the service’s OWN grounded metric first, falling back to a generic OTel
metric, then a span / log derivation, only when no higher tier grounds it.number; a stacked bar/area of the
status-code or fault/error breakdown over time; a table of top error sources.
Offer a recent-error log panel and — if the space has alerts — an alerts panel
filtered to CRITICAL (or WARNING) as a follow-up.number; a line of p50/p90/p99 together; a table
of the slowest operations. If the service exposes no percentile-capable source,
chart the average with an honest title — never a fake p99.number; a line over time
grouped by the identity label — read the aggregation off the counter’s
temporality (sum_over_time for a delta Sum, rate() / increase() for a
cumulative Sum), not a uniform rate(); for a queue, pair inbound vs. outbound
counters and add the backlog gauges (depth, age).solidgauge for a bounded %
against a threshold, line for a trend): CPU, memory, disk/queue depth, credit
balances; a table of the resources closest to their limit. An empty series
often means “not applicable,” not zero.markdown summary, set a
fixed scope.timeRange (absolute start and end) on the dashboard so every
panel covers the incident, then golden-signal charts plus a spans panel — or
a dashboard panel with source.id: "trace-details" — for the offending path.
Find the offending trace with a span query per
sql-logs-traces.md and pin its traceId in that
panel’s source.params.traceConfig.number KPI plus a small line — under a divider per service, or a single
wide table ranking all services by health. Prefer a table or topk(...) over
a line with more than ~25 series.One query per explore panel. To compare two signals, emit two side-by-side
panels (section 5), never one overlaid panel. If the intent is genuinely one
number, a single number panel is a complete answer — don’t pad it.
Ground each data panel’s query against live telemetry before you write the body. The query languages themselves are deferred to sql-logs-traces.md and promql-metrics.md; this section is the discipline.
The grounding loop, per data panel:
EXPLAIN (ANALYZE_FIELDS) or SELECT@record… LIMIT 10,
narrowed to the service and telemetry type (see Schema Discovery in
sql-logs-traces.md). Interactive discovery queries
need a `@timestamp` bound — only the final panel query is windowless
(below).requests_total, http_requests_total, *_total,
*_seconds, *_count, or *_bucket; a PromQL selector MUST name a metric from
the grounded list verbatim. If the list lacks the metric you wanted, it does not
exist here — use a fallback surface (step 4), never a guessed name.
Pick the SOURCE per signal by a strict 3-tier priority — reach for a lower
tier only when no higher tier grounds the signal: (1) the service’s OWN domain
metric it emits (a business counter / gauge / histogram) FIRST; (2) a generic
OTel metric (e.g. Count / Errors) as a fallback; (3) a trace-span / log
derivation (span durationNano percentiles, log counts) LAST.queryLanguage to match. Metrics → PromQL; log
or trace content → SQL. Metrics execute as NATIVE PromQL
(queryLanguage: "promql"), NEVER SQL — there is no metrics.default SQL
table; a SQL FROM is only default, logs.default, or traces.default. Set
the panel’s queryLanguage explicitly — it is not inferred from the query
text, and a mismatch is a silent failure.queryString against the grounded lists. If a reference cannot be
confirmed, re-ground; if it still cannot — on any surface — drop that panel
rather than shipping an invented query. Optionally run the final windowless
query once with a temporary time bound added, to confirm it returns rows.Bind panels to the grounded metric identity. Vended AWS metrics carry an
instrumentation scope (@instrumentation.@name="cloudwatch.aws/<service>") plus a
datapoint attribute (e.g. FunctionName); OTLP-native signals carry
@resource.* labels. Select the metric by name — use an {__name__="<exact>"}
matcher for a name that starts with a digit or contains %, ., or spaces (e.g.
4xxErrors, traces.span.metrics.*); a plain non-dotted name may be used bare.
A bare Prometheus job= / service= selector is an ungrounded reference — drop
panels that use one. The per-service scope + datapoint-attribute matrix lives in
promql-metrics.md.
Pick the aggregation from the metric’s type. Read it off the grounded
instrument type and temporality — sum_over_time(<m>[w]) for a DELTA Sum,
rate() / increase() for a CUMULATIVE Sum, avg() / max() for a Gauge, and
histogram_quantile(0.99, rate(<base>[5m])) for a Histogram (use the BASE metric
name — NO _bucket, NO by (le)). Never label an average as a percentile, and
never chart a counter as a raw value.
Two grounding cautions:
queryString — no `@timestamp` bound, no NOW() - INTERVAL, no PromQL
range that pins an absolute time. The panel (or dashboard) scope drives the
time range; a hard-coded window fights the time picker and freezes the panel.
(This is the one place a dashboard query differs from an interactive one, which
requires a time bound — so don’t copy an interactive query verbatim.)This is the round-tripping JSON schema — the format a user hand-edits in the
source editor and the one the UI persists. Every field here survives save → reopen.
In a body you author now, a key that is not in the schema at the root or panel
level is rejected at save (see “API save semantics”), not dropped; a key inside
config is accepted but ignored. Only a pre-existing legacy body (stored before
the schema was enforced) has non-schema keys dropped or rewritten on read. The whole
object is what you serialize into the API’s body string (section 7).
{
"panels": [ /* Panel[] — required, may be empty */ ],
"scope": { "timeRange": { "start": "now-3h", "end": "now" } } // optional
}panels is the only required key. Panels sit on a fixed 60-column grid;
the column count is not a field and is not configurable.scope.timeRange is the window the whole dashboard opens under. Omit it to
inherit whatever range is already active.There is no
views, nolayout: { columns: 60 }, no per-panelid, norefreshInterval, and no top-levelowner. The root and panel objects are schema-validated withadditionalPropertiesoff, so sending one of these is not silently dropped — it fails the save with aValidationException(see “API save semantics”). Author new bodies in thepanels[]form only. A body authored under the current schema round-trips exactly:GetOmniDashboardreturns itsbodybyte-for-byte (stored verbatim as an opaque string; whitespace and key order preserved). Only a pre-existing legacy body is upgraded on read (see the carve-out above), so those do not round-trip byte-for-byte.
{
"type": "explore", // required — selects the panel and its config shape
"layout": { "x": 0, "y": 0, "w": 30, "h": 320 }, // required — rejected at save if absent (section 5)
"title": "Read IOPS", // optional
"description": "…", // optional, one line
"config": { /* shape depends on type */ },
"scope": { "timeRange": { "start": "now-7d", "end": "now" } }, // optional per-panel override
"variant": "transparent" // optional — drop the card frame (headings, spacers, prose)
}Both type and layout are required — a panel missing either is rejected at save
with a ValidationException (panels[N].type / panels[N].layout required). The
renderer does not reflow overlaps (section 5), so give every panel a deliberate
layout to keep placement deterministic. An untitled panel takes
a heading from its content. variant accepts "transparent" (drops the card frame)
or "default" (explicit no-op) — omit the key for the default card frame. variant
is a panel-level enum, so any other value is rejected at save with a
ValidationException (see “API save semantics” below), not silently blanked; other
variant values you read back in product-authored bodies are private — do not author
them.
scope.timeRange has the same shape at the dashboard and panel level; a panel’s
scope overrides the dashboard’s for that panel only (e.g. a 7-day baseline beside a
1-hour detail view).
"timeRange": { "start": "now-1h", "end": "now" } // rolling window
"timeRange": { "start": "2026-01-01T00:00:00Z", "end": "now" } // growing window (since incident)
"timeRange": { "start": "2026-01-01T00:00:00Z",
"end": "2026-02-01T00:00:00Z" } // fixed windowEach bound is independently an ISO-8601 instant, the literal now, or a relative
now-<N><unit>. Units: m minutes, h hours, d days, w weeks, mo months.
There is no seconds or years unit and no rounding syntax — use an absolute instant
for a calendar boundary. The scope drives a query’s time range; never embed a
window in a queryString (section 3).
alerts · explore · markdown · divider · spans · application-map ·
dashboards-list · investigation-list · dashboard · agent-kpi-strip ·
recent-error-traces-table · agent-health-table · agent-playground. Author
only these thirteen — this is the public authoring contract. dashboard and
agent-playground are full-bleed surfaces (public and hand-authorable, but not
offered for free composition; a dashboard panel is always x: 0, w: 60). There
is no navigation type. alert-detail and trace-details are withdrawn: a
saved body that already holds one still reads back, but do not author them in a
new body — see below for the dashboard-panel replacement. Other product-authored
types you read back are private; leave them as they are and do not author new ones.
A missing type is rejected at save (panels[N].type required, HTTP 400). An
unrecognised type VALUE is not caught by the schema — it saves with HTTP 200 and
blanks the canvas at render (see “API save semantics” below).
"config": {
"queryString": "sum by (FunctionName) (rate({__name__=\"Invocations\", \"@instrumentation.@name\"=\"cloudwatch.aws/lambda\"}[5m]))",
"queryLanguage": "promql", // 'sql' | 'promql' — REQUIRED; SET IT EXPLICITLY; not inferred; a mismatch silently blanks the panel
"visualization": "line", // REQUIRED; one of the nine below; missing or unknown silently blanks the panel (no 'table' fallback)
"autoRun": true, // run on open — set this on any data panel
"promqlQueryOptions": { "step": 300 }, // PromQL step in SECONDS (default 60)
"chartOptions": { /* appearance — see below */ }
}queryLanguage explicitly — it is not inferred from the query
text; a mismatch is a silent failure. The telemetry source is the query’s FROM
clause (SQL) or metric selector (PromQL); there is no separate source field.autoRun: true on a panel meant to show data without a click —
otherwise it opens showing its query, not its result.step to the range vector. A [5m] window sampled at the default 60s
over-samples 5×; set promqlQueryOptions.step to 300 (SECONDS).inputMode. Omit for a chart tile (the default). Set "inputMode": "editor"
only when authoring a full Explore-surface panel — that is the one accepted
value, and it is load-bearing on read (a saved Explore surface with inputMode
dropped reopens as a tile).@-labels are double-quoted PromQL selectors, so they are JSON-escaped
(\") once more inside the queryString; and because the whole body is itself
passed to the API as a JSON string, it is escaped a third time on the wire —
build the body as an object and let your JSON library serialize it (section 7)
rather than hand-escaping."config": { "content": "## On call\n\n1. Check error rate.\n2. Open the failing trace." }Pair with "h": "auto" and "variant": "transparent" for prose that reads as
part of the canvas.
"config": { "title": "Service health", "collapsed": false }Always full width (x: 0, w: 60); give it a small fixed height (40). Groups the
panels beneath it, down to the next divider, into a collapsible section.
{ "type": "alerts", "config": { "state": "CRITICAL" } } // OK | WARNING | CRITICAL | NODATA (the Omni alert states); omit for all. NOT the classic CloudWatch alarm states `ALARM` / `INSUFFICIENT_DATA` (and `ALERT` is not a state at all) — an unknown state value silently blanks the canvasOne alert’s detail is not a panel type of its own any more — alert-detail is
withdrawn (readable, not authorable). Author a dashboard panel instead:
{ "type": "dashboard",
"source": { "kind": "system", "id": "alert-details", "version": 1,
"params": { "alertName": "checkout-error-rate-high", "alertId": "6c89…" } }, // alertName required; alertId optional
"layout": { "x": 0, "y": 0, "w": 60, "h": "auto" } }The required parameter is the alert’s name, but alert names are not unique
within a space — two alerts can share one name. The stable identity is the
alertId (see alerts.md); pass it too so the panel is unambiguous.
Resolve a name to its alertIds with ListAlerts (filterCriteria.names) first,
and rename one via UpdateAlert if two collide.
Both take a public config; either also renders correctly with no config and
picks up the dashboard’s time range.
{ "type": "application-map", "config": { "focusServiceName": "checkout" } } // opens focused on one service; a one-time opening intent the console drops on its next save
{ "type": "spans", "config": { "lens": "application", // application | agent (default application)
"grain": "traces", // application lens: traces | spans (default traces)
"agentGrain": "traces", // agent lens: traces | sessions (default traces)
"agentName": "…" } } // agent lens only: scope to one agent; absent = allAn absent or unrecognised selector falls back to its default. Everything else — service/operation/status/duration/trace-id filters, camera, node selection, rail state — is per-user console state, not saved.
"config": {} — a list of the Space’s saved dashboards.
trace-details is a withdrawn panel type: a saved body that holds one still
reads back, but do not author it in a new body — like any unrecognised type it
saves with HTTP 200 and blanks the canvas. Author a dashboard panel whose
source.id is trace-details:
{ "type": "dashboard",
"source": { "kind": "system", "id": "trace-details", "version": 1,
"params": { "traceConfig": {
"traceId": "…", // required
"startTime": 1730000000000, "endTime": 1730000600000,
"initialMode": "waterfall", // waterfall | graph | flame | raw (`flamegraph` is a legacy alias read as `graph`)
"focusSpanId": "…" } } }, // optional — deep-link to a specific span
"layout": { "x": 0, "y": 0, "w": 60, "h": "auto" } }startTime/endTime are epoch milliseconds in traceConfig (not the panel
scope), fixed at author time. A pinned trace is useful only while it is still
retained — prefer a spans panel for a durable dashboard.
line · area · bar · scatter · pie · table · number · solidgauge ·
heatmap.
stacked-bar,single-metric, anddonutare not valid — usebarwithchartOptions.plotOptions.style.barOptions.stacked: true(there is noplotOptions.stacked; see Chart options),number, andpierespectively.visualizationis REQUIRED and there is no fallback default that renders: a missing value, an unknown value such asstacked-bar, or the empty string""saves with HTTP 200 but silently blanks the panel at render.
number tile trap (known product bug). A number panel driven by a
SELECT count(*) AS n (or similar COUNT(*)) query currently renders the ROW
COUNT of the result set — usually 1 when the query aggregates to one row —
instead of the value in the aliased column. Author the query so the aggregate
value is the first column of the first row (e.g. SELECT <aggregate_expression>),
and reserve COUNT(*)-shaped queries for table panels until the bug is fixed.
Pick by signal kind:
| Signal | Visualization |
|---|---|
| Time series, ≤ ~25 series, comparable units | line |
| Additive series / discrete buckets (2xx/4xx/5xx, per-AZ) | bar (stacked) or area |
| Correlation point cloud (no connecting line) | scatter |
| One number, with a delta | number |
| Resources ranked by a metric | table (default sort: descending on the primary signal) |
| Genuine part-of-whole | pie |
| A bounded ratio against thresholds (availability %) | solidgauge |
Two shapes authors most often get wrong — check them explicitly: a scalar
aggregate (one number, no by / no GROUP BY over time) should be a number
tile, not a table; a top-N ranking (ORDER BY … LIMIT / topk(...)) should be
a table, not a line.
Anti-patterns: no line past ~25 series (use topk(...) or a table); don’t
stack a bar whose series go negative; don’t put two units on one chart — split
into two w: 30 panels. heatmap does not yet ingest histogram-bucket output —
for a latency distribution use line with a histogram_quantile.
chartOptions sets appearance and is a discriminated union keyed on view, which
must match the panel’s visualization (when both are present, view wins — so
setting only visualization is the simpler, recommended form).
"chartOptions": {
"view": "line",
"title": { "text": "Requests/sec", "show": true },
"plotOptions": {
"legend": { "show": true, "position": "bottom" },
"yAxis": [ { "min": 0, "title": "req/s" } ] // TUPLE of 1 or 2 axes (2nd = right-hand)
}
}line/area/bar/scatter): legend, stacking, xAxis, and
yAxis as a tuple of one or two axes (the second is the right-hand axis). Use
type: 'datetime' for a time axis (not 'time').solidgauge: yAxis is an object { min, max } (not a tuple), plus
plotBands: [{ from, to, color }].heatmap: xAxis/yAxis with categories, and colorScale.table: hiddenColumns, summaryColumns (MIN/MAX/SUM/AVG),
layout (horizontal/vertical), stickySummary, showTimeSeriesData,
formatJson (true | 'raw' | 'raw-single-line'). Column order is not a
chart option — the console does not save it. number and pie take little
beyond the base fields.line/area, set
plotOptions.stacking: "normal" (or "percent") — an enum, not a boolean,
keyed directly on plotOptions; it wins over the equivalent
plotOptions.style.lineOptions.stacked: true. A bar panel does not read
stacking at all — the only stacking field the bar renderer reads is
plotOptions.style.barOptions.stacked: true. There is no plotOptions.stacked
field; a flag placed there is an unknown config-level key — it saves without error
but is ignored at render (it survives read-back yet never takes effect), so the
chart will not stack.The save API schema-validates the body’s structure, its root/panel keys, each
coordinate FIELD’s bounds, and the variant enum — but not cross-field invariants or
the other enum values (visualization, panel type, alerts state). A body
fails in three distinct ways, and only the first errors:
ValidationException). The root and panel objects
have additionalProperties off, so an unknown or misspelled key there — a
top-level refreshInterval / owner / layout, a per-panel id, a misspelled
title — fails with does not satisfy the 'additionalProperties' constraint. The
schema also rejects: a bad variant value (enum constraint); a missing or
non-array panels (required / type); a panel missing its required type or
layout (required); and an out-of-bounds coordinate FIELD — layout.x or
layout.y below 0, or layout.w below 1 (minimum). The dashboard is not
created or updated.200 and a dashboardId, then fails at render — the blast radius depends on the kind:
an unknown visualization VALUE blanks that panel while its siblings render;
everything else the schema does not catch — an unrecognised panel type VALUE, a bad
alerts state, the cross-field x + w > 60 overflow (each field is in bounds, but
their sum is not validated), a w > 60 (no maximum on w), a bad h unit (80vh, a
fraction), or a now-30s / now-1y bound (no seconds/years unit) — blanks the
WHOLE dashboard (valid siblings included). A 200
proves only that the body parsed and passed the schema, not that it renders.config (or its nested objects like chartOptions) is
stored, comes back on read, and simply never takes effect — this is the one place
the “a field I set doesn’t take effect, with no error” behavior lives (e.g. a
British visualisation, or a plotOptions.stacked that should be
plotOptions.stacking for line/area or plotOptions.style.<view>Options.stacked
for bar).Consequences:
200 + a dashboardId proves only that the JSON
parsed and passed the schema (structure, keys, and per-field bounds), not that it
renders.variant errors are loud; other enum-value and
cross-field errors are silent. The schema catches missing structure, unknown keys,
per-field coordinate bounds, and the variant enum with a ValidationException; it
does NOT catch an unknown visualization value — which blanks that one panel — or a
bad panel type /
alerts state, the x + w <= 60 sum, and bad h units — which blank the whole
canvas — all only at render.layout is required on every panel — a panel with no layout is rejected at
save (panels[N].layout required), not drawn full-width.body is stored
verbatim as an opaque string; GetOmniDashboard returns it exactly as written
(whitespace and key order preserved), so it round-trips exactly. (Only a pre-existing
legacy body is upgraded on read and so does not round-trip; section 6.)queryLanguage mismatches are silent failures, not errors. Set it per explore panel.name, not part of the body) must be 1–256 chars
matching ^[a-zA-Z0-9_.@~()-]+$. Nothing is trimmed: surrounding whitespace,
spaces, /, :, #, +, accented letters and emoji all fail the pattern, and
257+ chars fails the length check. The body itself must be 1–1,048,576 bytes
once serialized.Assign each panel’s layout: { x, y, w, h } so panels tile cleanly with no overlap
and no overflow. This is the single most error-prone part of a body.
ALWAYS STATE when laying out a grid — two different failure modes. (1) A per-field bound violation is rejected at save (
ValidationException, HTTP 400): a negativexory, or awbelow 1, trips the schema’sminimumconstraint and the save does not succeed. (2) The cross-fieldx + w > 60overflow, aw > 60, and a badh(a fraction or avh/%unit) are NOT validated: they save with HTTP 200 and adashboardIdand then render the entire dashboard — every otherwise-valid sibling panel included — as a blank page, with no error. Nothing enforces thex + w <= 60sum, and the renderer does not reflow or auto-arrange overlaps. Keepx + w <= 60on every panel yourself; a200is not proof the layout renders.
x/w and y/h do not share a unit:
| Field | Unit | Rule |
|---|---|---|
x | grid column | integer 0–59; x: 0 is the left edge. A negative x is rejected at save (minimum, HTTP 400) |
w | grid columns | integer, w >= 1 is enforced at save (w: 0 is rejected, minimum, HTTP 400); there is no enforced maximum (w: 61 saves). x + w must not exceed 60, but that sum is not enforced — an overflow saves with HTTP 200 and blanks the canvas |
y | pixels | integer pixels from the top, not a row index; a negative y is rejected at save (minimum, HTTP 400) |
h | pixels or 'auto' | integer pixels within roughly 100–2000 (divider 40 is the sanctioned sub-100 row), or the literal 'auto'; never fractional and never a relative CSS unit (vh / % / vw) — those are not rejected at save, they save with HTTP 200 and blank the canvas at render |
Mixing the units (treating y as a row index, or w as pixels) is the most common
authoring bug. Prefer 'auto' for content-sized panels (markdown, lists, nested
dashboard panels) and a fixed pixel height where the content needs one; a relative
CSS unit such as '80vh' is an unknown value that blanks the canvas.
Every row MUST match one of the width-per-count patterns below. Author the correct shape yourself — nothing re-shapes it for you.
| Row count | Width per panel (w) | Use for |
|---|---|---|
| 1 panel | 60 | full-width strip (hero chart, table, status strip, divider, application-map, dashboard) |
| 2 panels | 30 each | two halves side-by-side (most time-series pairs) |
| 3 panels | 20 each | three thirds (read/write/idle, 2xx/4xx/5xx) |
| 4 panels | 15 each | four quarters (KPI row) |
Hard rules — all MUST:
x as the cumulative sum from 0
(0, 30; 0, 20, 40; 0, 15, 30, 45).y AND the same h.w: 60 hero, row 2 is w: 30×2 or w: 20×3 supporting
panels — never side-by-side with the hero.line chart and a table on the same
row read badly — split them into two rows (chart row above, table row below).y is the pixel offset from the top and it orders rows. Give successive rows
increasing y equal to the running sum of prior row heights:
h: 120) at y: 0h: 320) at y: 120y: 440, and so on.When a row uses 'auto' heights, still give the next row a larger y; the renderer
resolves final positions from each row’s height, so approximate but
monotonically-increasing y values are fine.
| Panel kind | Suggested h |
|---|---|
number KPI tile | 120 (short — one figure) |
Time-series (line/area/bar/scatter) | ~320 |
table | ~320–400, taller for many rows |
pie / solidgauge | ~280–320 (roughly square reads best) |
divider | 40 (always full width, x: 0, w: 60) |
dashboard (nested system dashboard) | 'auto' (always full width, x: 0, w: 60) |
markdown runbook | 'auto' (pair with variant: 'transparent') |
application-map / spans | full width, tall (~480) |
{ x: 0, y: <row>, w: 60, h: <h> }.{ x: 0, …, w: 30 } and { x: 30, …, w: 30 }, same y.x: 0, x: 20, x: 40, each w: 20, same y.y: 0, w: 15,
x: 0/15/30/45, h: 120; one chart at y: 120, x: 0, w: 60, h: 320.divider (h: 40) at the row’s y, then
the section’s panels at a larger y beneath it.x + w > 60) is never reflowed into what you
meant — it saves with HTTP 200 and blanks the whole canvas (section 4, API save
semantics). Keep x + w <= 60 on every panel yourself.y must not share
columns — give them non-overlapping [x, x+w) ranges. Follow the
width-per-count patterns above and give each row a larger y and you will never
overlap.Work down this list in order; the first match is usually the cause.
queryString; a *_total / *_seconds Prometheus-convention name, or a
bare job= / service= selector, is the CloudWatch tell.queryLanguage mismatch. A PromQL selector under "queryLanguage": "sql"
(or SQL under promql) fails silently. Check the language against the text.`@timestamp` bound, a
NOW() - INTERVAL, or an absolute PromQL range pins the panel to a window the
time picker no longer covers. Remove it; the scope drives the range.scope excludes the data. A fixed incident-window
scope, or a per-panel override, can legitimately show nothing outside its range.autoRun is not true. The panel opens showing its query, not its result —
it looks empty until clicked.@instrumentation.@name scope, or the wrong datapoint attribute
(FunctionName vs. ApiName), matches nothing. Compare against the archetype
identity forms and promql-metrics.md.IteratorAge for a non-stream consumer, RunningTaskCount without Container
Insights, burst-credit metrics on a non-burstable instance) return nothing when
the condition is absent. Say so, and either drop the panel or retitle it.step far coarser than the window. With a very large
promqlQueryOptions.step over a short scope there may be no evaluation point.config (e.g.
plotOptions.stacked, which should be plotOptions.stacking: "normal" or
plotOptions.style.<view>Options.stacked) saves without error and is ignored at
render — that is the “field has no effect” case. A wrong key at the root or
panel level (layout.columns, per-panel id, refreshInterval, owner,
views) does NOT reach this state — it is rejected at save with a
ValidationException (section 4), so its symptom is a failed save, not a
silently-ignored field.inputMode was omitted from an Explore-surface panel. It reopens as a tile.x + w > 60 overflow or a w > 60, a bad h (fractional
or a vh unit), an unrecognised panel type VALUE, a bad alerts state, a
timeRange bound with a seconds/years unit, a source on a non-dashboard panel, or
a dashboard panel without one. No sibling renders until it is fixed — validate each
panel against section 4. (An unknown visualization VALUE instead blanks only its own
panel; see the next bullet. A missing/non-array panels, a panel missing type or
layout, or a negative x/y or w: 0 is rejected at save with a
ValidationException, not this silent-blank symptom.)visualization VALUE
(stacked-bar, donut, single-metric, ""; there is no table fallback) blanks
that panel alone while its siblings render.views became panels and old relative ranges became
{ start, end } on read — that is the read-time upgrade of grandfathered bodies,
not data loss. It does NOT apply to a body you author now: sending views (or any
unknown root/panel key) in a new body is rejected at save with a ValidationException
(see “API save semantics”), not upgraded.omniDashboard.body is a JSON string —
parse it before inspecting it. From a paste, strip any surrounding prose.panels must be an array; note any scope.timeRange
(a fixed absolute window means an incident dashboard).type is one of the thirteen authorable types; layout uses the two units correctly
and stays inside 60 columns; rows share y and h; for explore,
queryLanguage matches the query text, visualization is one of the nine, the
query is windowless, and every name in it is plausible for the grounded catalog;
chartOptions.view matches visualization; no unknown keys.config that is stored
but ignored at render; queryLanguage not inferred) whenever you find an instance
of either, because the user will not have seen an error. An unknown root- or
panel-level key cannot appear in a read-back body from a current-schema save — it
would have been rejected at write with a ValidationException.Five operations manage dashboards. They are exposed by the CloudWatch Omni service
(endpoint prefix cloudwatch-omni, SigV4 signing name cloudwatch — see
programmatic-access.md) and reachable from the AWS CLI as
aws cloudwatchomni <kebab-case-operation>, from every AWS SDK, or through an AWS
MCP aws___call_aws tool with the same operation and parameter names.
Common to all five:
spaceId (the Space’s UUID) is required on every call. There
is no account-wide list of dashboards — you list per Space.dashboardId, a system-minted lowercase UUID
(^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$). The
caller-supplied name is metadata, is not unique, and is not part of the
id — renaming never changes the id or the ARN.arn:<partition>:cloudwatch:<region>:<account-id>:omni-dashboard/<dashboardId>
— the cloudwatch vendor namespace with an omni-dashboard/ resource type, the
same convention as alerts. Neither the Space nor the name appears in it.body is a string, 1–1,048,576 bytes, containing the serialized JSON from
section 4. The API schema-validates the root and per-panel objects on write
(additionalProperties off) and stores the string verbatim, returning a
current-schema body byte-for-byte on read (only pre-existing legacy bodies are
upgraded on read; section 6). An API ValidationException on body can therefore be a
length/type problem OR a body-schema one — an unknown root/panel key, a bad variant
enum, a missing/non-array panels, a panel missing type/layout, or an
out-of-bounds coordinate field (negative x/y, w: 0) (section 4). What is NOT
enforced at the API — a bad enum VALUE (visualization, panel type), the
x + w > 60 sum, or unknown config keys — surfaces only when the UI renders.name: 1–256 chars, ^[a-zA-Z0-9_.@~()-]+$ (section 4’s rule).
description: 1–1024 chars, optional.ValidationException,
AccessDeniedException, InternalServerException, ThrottlingException.
Per-operation additions are noted below.AccessDeniedException usually means the caller
has no grant on the Space — check that before suspecting the request. The IAM
action names are the operation names (cloudwatch:CreateOmniDashboard, etc.).Creates a dashboard in a Space. Returns the full omniDashboard — read
dashboardId and arn from it; you do not need a follow-up GetOmniDashboard.
| Input | Required | Notes |
|---|---|---|
spaceId | ✔ | The Space to create the dashboard in |
name | ✔ | Display name, 1–256 chars, ^[a-zA-Z0-9_.@~()-]+$. Not unique |
body | ✔ | The serialized { "panels": [...] } JSON, 1–1,048,576 bytes |
description | 1–1024 chars | |
tags | Key/value map | |
clientToken | Idempotency token — a retry with the same token returns the original result instead of creating a duplicate. SDKs and the CLI fill it automatically |
Errors beyond the common set: ServiceQuotaExceededException (the Space’s
dashboard limit), ResourceNotFoundException (unknown Space), ConflictException.
# body.json holds the panels[] object from section 4; file:// passes its text as the string value
aws cloudwatchomni create-omni-dashboard \
--region us-east-1 \
--space-id 3f2c1e9a-7b4d-4c0e-9a1b-2d3e4f5a6b7c \
--name checkout-service-health \
--description "RED golden signals for checkout" \
--body file://body.jsonBuild body.json with a JSON library from the panel objects — do not hand-escape
the nested \" in PromQL selectors a second time; the CLI/SDK serializes the string
for the wire.
Retrieves one dashboard including its body. This is the only way to read a saved body — list results do not carry it.
| Input | Required |
|---|---|
spaceId | ✔ |
dashboardId | ✔ |
Returns omniDashboard: dashboardId, arn, name, body (a JSON string —
parse it), createdBy (the creating principal), description, tags, createdAt,
updatedAt. Read-only. Errors beyond the common set: ResourceNotFoundException.
aws cloudwatchomni get-omni-dashboard --region us-east-1 \
--space-id <spaceId> --dashboard-id <dashboardId> \
--query 'omniDashboard.body' --output text > body.jsonEnumerates the dashboards in a Space, optionally filtered by name prefix, with pagination.
| Input | Notes |
|---|---|
spaceId | Required |
namePrefix | 1–256 chars, same character set as name; matches names that start with it |
maxResults | 1–100 per page |
nextToken | Pagination token |
Read the summaries from items and page with nextToken — items is the only
list member the response carries. Each entry is an
OmniDashboardSummary: dashboardId, arn, name, createdBy, description,
tags, createdAt, updatedAt — no body. Because names are not unique, a
namePrefix (or an exact-name match on the client side) can return several
dashboards; this is the usual way to resolve a name to a dashboardId before
Get/Update/Delete. Follow nextToken rather than treating the first page as
the complete set. Errors beyond the common set: ResourceNotFoundException
(unknown Space).
aws cloudwatchomni list-omni-dashboards --region us-east-1 \
--space-id <spaceId> --name-prefix checkout- --max-results 50Modifies a dashboard in place, addressed by dashboardId. PATCH semantics:
only the fields you send change; an omitted field is left unchanged. Returns the
full updated omniDashboard, so you can confirm the result without a second
call. @idempotent.
| Input | Required | Notes |
|---|---|---|
spaceId | ✔ | |
dashboardId | ✔ | The dashboard to update |
body | Full replacement body, 1–1,048,576 bytes. There is no partial-panel patch — send the complete panels[] | |
name | Renaming is supported; the id and ARN do not change | |
description | 1–1024 chars |
tags cannot be changed through this operation. Errors beyond the common set:
ResourceNotFoundException, ConflictException, ServiceQuotaExceededException.
To edit one panel: GetOmniDashboard → parse body → change the panel →
serialize → UpdateOmniDashboard with the whole body. Because a body is replaced,
not merged, an update built from a stale copy silently discards someone else’s
edits — fetch immediately before you write.
Confirm before updating. Draft the exact change, state that body replaces the
whole panel set, and confirm with the user before calling.
aws cloudwatchomni update-omni-dashboard --region us-east-1 \
--space-id <spaceId> --dashboard-id <dashboardId> \
--body file://body.jsonPermanently removes a dashboard. Returns an empty response.
| Input | Required |
|---|---|
spaceId | ✔ |
dashboardId | ✔ |
Idempotent — deleting a dashboard that has already been removed succeeds
without error (the operation does not return ResourceNotFoundException), so a
successful response does not prove the dashboard existed. Deletion cannot be undone;
if the body might be wanted later, GetOmniDashboard and keep it first.
Confirm before deleting. This is destructive and irreversible. Name the specific dashboard (id and name) and confirm with the user before calling — never delete on inference, and because a success does not tell you whether you deleted the one you meant, confirm the target up front rather than checking after.
aws cloudwatchomni delete-omni-dashboard --region us-east-1 \
--space-id <spaceId> --dashboard-id <dashboardId>aws cloudwatchomni list-spaces — pick the spaceId (section 1).panels[] object.body.json; create-omni-dashboard with a valid name.get-omni-dashboard, parse body, and run the read-back checks
in section 6 — the API schema-checks only root/panel keys and per-field bounds,
so this is where a config-level slip (bad enum value, unknown key) would surface.dashboardId, arn, and a one-line description of each panel.CLI says cloudwatchomni is not a valid choice. The local AWS CLI predates
Omni’s service model; upgrade it. The failure is client-side argument parsing and
says nothing about whether Omni is enabled. aws cloudwatch <omni-operation>
will never work at any version — CloudWatch is a different service that
shares only the signing name.
When the ask names a known resource TYPE, start from its ready panel set below,
then ground every query (section 3) and lay it out (section 5). Build the
CORE subset by default and offer the optional-depth panels as a follow-up “add
section.” Look up each panel’s real metric and reading (gauge vs. counter, derived
formulas, whether a real percentile source exists) in
promql-metrics.md; the query strings are TEMPLATES — the
identity form is canonical, but the metric name and statistic MUST be grounded. If
a signal cannot be grounded, drop that panel and re-sequence y so no blank row
is left.
Every template binds to the @-label identity (section 3) — vended AWS metrics on
@instrumentation.@name="cloudwatch.aws/<svc>" + datapoint attribute, OTLP /
span-RED on @resource.*. Set queryLanguage explicitly, autoRun: true on every
data panel, and promqlQueryOptions.step to match the range vector; keep every
query windowless. … in a template stands for the identity labels shown in that
archetype’s first row.
Infrastructure resource — USE / saturation; scope cloudwatch.aws/ec2 by
InstanceId (the CWAgent guest-OS metrics share the identity). Reading rules from the
catalog: the vended EC2 metrics reach this surface as delta exponential histograms
(CPUUtilization, StatusCheckFailed*, CPUCreditBalance) and delta Sums
(NetworkIn/NetworkOut), so a max is histogram_quantile(1, …), a min
histogram_quantile(0, …), an average histogram_sum(…) / histogram_count(…), and a
network volume sum_over_time(…[5m]) — bare max()/avg() and rate() render empty
panels here. Every StatusCheckFailed* metric is a per-period 0/1 flag read as a max
(an average hides a failed check), and _System (AWS hardware) is a different fault from
_Instance (the guest OS or network) — show them side by side; CPUCreditBalance exists
only on burstable (T-family) instances, and near zero at baseline CPU means the instance is
being throttled; mem_used_percent / disk_used_percent exist only where the CloudWatch
agent is installed (an empty panel means “no agent”, so ground it and drop it if absent);
DiskRead* / DiskWrite* are instance-store only — EBS volume I/O is the AWS/EBS
family keyed by VolumeId, a separate row if the fleet’s disks matter. __name__ takes no
regex, so a panel that shows several check types is one selector per name joined with or
and tagged with label_replace. S below stands for "@instrumentation.@name"="cloudwatch.aws/ec2".
CORE (~8 panels — the default build; every query below returns series on an enriched Space, except the two configuration-gated tiles noted inline):
| Panel | Viz | Query template | Layout |
|---|---|---|---|
| Instances failing any check | number | count(histogram_quantile(1, {__name__="StatusCheckFailed", S}) == 1) or vector(0) — count() over an empty match is itself empty, so without or vector(0) a healthy fleet renders blank; with it the tile reads 0 both when the fleet is healthy and when the selector matches no instance, so confirm the bare selector returns series before trusting a 0 | {x:0,y:0,w:15,h:120} |
| Peak CPU % | number | max(histogram_quantile(1, {__name__="CPUUtilization", S})) | {x:15,y:0,w:15,h:120} |
| Lowest CPU credits | number | min(histogram_quantile(0, {__name__="CPUCreditBalance", S})) — burstable instances only; drop if empty | {x:30,y:0,w:15,h:120} |
| Peak memory % | number | max({__name__="mem_used_percent", S}) — CloudWatch agent only, a plain gauge (switch to histogram_quantile(1, …) only if its __type__ is ExponentialHistogram); drop if empty | {x:45,y:0,w:15,h:120} |
| CPU % by instance | line | histogram_quantile(1, {__name__="CPUUtilization", S}) — one series per InstanceId | {x:0,y:120,w:30,h:320} |
| Status checks by instance and type | line | label_replace(histogram_quantile(1, {__name__="StatusCheckFailed_System", S}), "check", "System", "", "") or label_replace(histogram_quantile(1, {__name__="StatusCheckFailed_Instance", S}), "check", "Instance", "", "") or label_replace(histogram_quantile(1, {__name__="StatusCheckFailed_AttachedEBS", S}), "check", "AttachedEBS", "", "") — one 0/1 series per instance and check type, never averaged | {x:30,y:120,w:30,h:320} |
| Network in/out by instance | line | label_replace(sum by (InstanceId) (sum_over_time({__name__="NetworkIn", S}[5m])), "direction", "in", "", "") or label_replace(sum by (InstanceId) (sum_over_time({__name__="NetworkOut", S}[5m])), "direction", "out", "", "") — bytes per 5 minutes; append / 300 for bytes per second | {x:0,y:440,w:60,h:320} |
| Hottest instances | table | topk(10, histogram_quantile(1, {__name__="CPUUtilization", S})) | {x:0,y:760,w:60,h:360} |
Optional depth (follow-up “infra / USE health”): CPUCreditBalance by instance
(histogram_quantile(0, …), line); disk_used_percent by instance (CWAgent); the EBS
volume side (VolumeQueueLength, VolumeAvgReadLatency / VolumeAvgWriteLatency on
cloudwatch.aws/ebs by VolumeId — check their __type__ first). Append two-up and
re-sequence y.
Serverless function — RED-shaped; scope cloudwatch.aws/lambda by FunctionName.
Duration is a gauge with no percentile label; the error-rate tile is the derived
Errors / Invocations.
CORE (~8 panels — the default build):
| Panel | Viz | Query template | Layout |
|---|---|---|---|
| Invocations/s | number | sum(rate({__name__="Invocations", "@instrumentation.@name"="cloudwatch.aws/lambda", FunctionName="<fn>"}[5m])) | {x:0,y:0,w:15,h:120} |
| Error rate | number | sum(rate({__name__="Errors", …, FunctionName="<fn>"}[5m])) / sum(rate({__name__="Invocations", …, FunctionName="<fn>"}[5m])) | {x:15,y:0,w:15,h:120} |
| Throttles/s | number | sum(rate({__name__="Throttles", …, FunctionName="<fn>"}[5m])) | {x:30,y:0,w:15,h:120} |
| Concurrency | number | max({__name__="ConcurrentExecutions", …, FunctionName="<fn>"}) | {x:45,y:0,w:15,h:120} |
| Invocation rate | line | sum by (FunctionName) (rate({__name__="Invocations", …, FunctionName="<fn>"}[5m])) | {x:0,y:120,w:30,h:320} |
| Errors/s | line | sum by (FunctionName) (rate({__name__="Errors", …, FunctionName="<fn>"}[5m])) | {x:30,y:120,w:30,h:320} |
| Duration (ms) | line | {__name__="Duration", …, FunctionName="<fn>"} | {x:0,y:440,w:60,h:320} |
| Recent errors | table (SQL) | scope to /aws/lambda/<fn> with a grounded log-group filter; queryLanguage:'sql' | {x:0,y:760,w:60,h:360} |
Optional depth (follow-up): Throttles line; Concurrent executions line;
Stream-consumer lag line (IteratorAge — only for a stream/event-source
consumer, empty otherwise). Append two-up (w:30,h:320) and re-sequence y.
OTLP-native — no vended scope. RED from the span metrics grouped by
@resource.service.name; USE from the OTLP Kubernetes resource metrics grouped
by @resource.k8s.*. The span-metric names, span-status attribute, and k8s.*
resource-metric names are ingestion-path-dependent — ground them against the live
catalog (section 3).
CORE (~8 panels — the default build):
| Panel | Viz | Query template | Layout |
|---|---|---|---|
| Request rate | number | sum(rate({__name__="traces.span.metrics.calls", "@resource.service.name"="<svc>"}[5m])) | {x:0,y:0,w:15,h:120} |
| Error rate | number | ratio of …calls, "@status.code"="ERROR" to all calls | {x:15,y:0,w:15,h:120} |
| p99 latency | number | histogram_quantile(0.99, rate({__name__="traces.span.metrics.duration", …}[5m])) | {x:30,y:0,w:15,h:120} |
| Running pods | number | count({"k8s.pod.phase", "@resource.k8s.namespace.name"="<ns>", "@resource.k8s.deployment.name"="<deploy>"}) | {x:45,y:0,w:15,h:120} |
| Request rate | line | sum by ("@resource.service.name") (rate({__name__="traces.span.metrics.calls", …}[5m])) | {x:0,y:120,w:30,h:320} |
| Errors by status | bar | sum by ("@status.code") (rate({__name__="traces.span.metrics.calls", …}[5m])) | {x:30,y:120,w:30,h:320} |
| Latency p50/p90/p99 | line | histogram_quantile(0.99, rate({__name__="traces.span.metrics.duration", …}[5m])) — one series per quantile (0.50, 0.90, 0.99) | {x:0,y:440,w:60,h:320} |
| Top services by errors | table | topk(10, sum by ("@resource.service.name") (rate({__name__="traces.span.metrics.calls", "@status.code"="ERROR"}[5m]))) | {x:0,y:760,w:60,h:360} |
Optional depth (follow-up “infra / USE health”): Pod CPU usage, Pod memory
usage, Container restarts — grouped by @resource.k8s.* (k8s.pod.cpu.usage,
k8s.pod.memory.usage, increase(k8s.container.restarts[15m])). Append two-up and
re-sequence y.
Infrastructure resource — USE/saturation; scope cloudwatch.aws/rds by
DBInstanceIdentifier. Reading rules: IOPS is already per-second → never rate();
latency is in seconds → × 1000 for ms; mind FreeStorageSpace / FreeableMemory
polarity (higher is healthier).
CORE (~7 panels — the default build):
| Panel | Viz | Query template | Layout |
|---|---|---|---|
| CPU % | number | max({__name__="CPUUtilization", "@instrumentation.@name"="cloudwatch.aws/rds", DBInstanceIdentifier="<db>"}) | {x:0,y:0,w:15,h:120} |
| Connections | number | max({__name__="DatabaseConnections", …, DBInstanceIdentifier="<db>"}) | {x:15,y:0,w:15,h:120} |
| Free storage | number | min({__name__="FreeStorageSpace", …, DBInstanceIdentifier="<db>"}) | {x:30,y:0,w:15,h:120} |
| Read latency (ms) | number | max({__name__="ReadLatency", …, DBInstanceIdentifier="<db>"}) * 1000 | {x:45,y:0,w:15,h:120} |
| CPU % | line | {__name__="CPUUtilization", …, DBInstanceIdentifier="<db>"} | {x:0,y:120,w:30,h:320} |
| Connections | line | {__name__="DatabaseConnections", …, DBInstanceIdentifier="<db>"} | {x:30,y:120,w:30,h:320} |
| Top instances by CPU | table | topk(10, max by (DBInstanceIdentifier) ({__name__="CPUUtilization", …})) | {x:0,y:440,w:60,h:360} |
Optional depth (follow-up “infra / USE health”): Read IOPS line; Read latency
(ms) line; add the WriteIOPS / WriteLatency counterparts for a full
read+write view. Append two-up and re-sequence y.
Cross-signal — USE from the ECS service tier combined with RED from the
load balancer in front (or from Application Signals). Identity + reading rules: ECS
scopes by ClusterName + a non-empty ServiceName; ALB TargetResponseTime is a
gauge in seconds with no percentile label; the target status-code metrics carry the
_Count suffix.
CORE (~7 panels — the default build; this archetype has no ranked table):
| Panel | Viz | Query template | Layout |
|---|---|---|---|
| Request rate | number | sum(rate({__name__="RequestCount", "@instrumentation.@name"="cloudwatch.aws/applicationelb", LoadBalancer="<lb>"}[5m])) | {x:0,y:0,w:15,h:120} |
| 5xx rate | number | sum(rate({__name__="HTTPCode_Target_5XX_Count", …, LoadBalancer="<lb>"}[5m])) | {x:15,y:0,w:15,h:120} |
| Peak response (ms) | number | max({__name__="TargetResponseTime", …, LoadBalancer="<lb>"}) * 1000 | {x:30,y:0,w:15,h:120} |
| Service CPU % | number | max({__name__="CPUUtilization", "@instrumentation.@name"="cloudwatch.aws/ecs", ClusterName="<cluster>", ServiceName="<svc>"}) | {x:45,y:0,w:15,h:120} |
| Request rate | line | sum(rate({__name__="RequestCount", …, LoadBalancer="<lb>"}[5m])) | {x:0,y:120,w:30,h:320} |
| Status codes | bar | sum by (__name__) (rate({__name__=~"HTTPCode_Target_..._Count", …, LoadBalancer="<lb>"}[5m])) | {x:30,y:120,w:30,h:320} |
| Response time (ms) | line | {__name__="TargetResponseTime", …, LoadBalancer="<lb>"} * 1000 | {x:0,y:440,w:60,h:320} |
Optional depth (follow-up “infra / USE health”): Memory util % line; Running
tasks line (RunningTaskCount — needs ECS Container Insights or OTel enrichment,
empty otherwise). If the space runs Application Signals, swap the RED panels for
Service/Fault/Error and the availability formula (1 − Fault/Total) × 100.
Append two-up and re-sequence y.
Request-serving — RED. Identity split: REST v1 on ApiName with 4XXError /
5XXError, HTTP v2 on ApiId with 4xx / 5xx; gateway overhead is the formula
Latency − IntegrationLatency.
CORE (~8 panels — the default build):
| Panel | Viz | Query template | Layout |
|---|---|---|---|
| Request count/s | number | sum(rate({__name__="Count", "@instrumentation.@name"="cloudwatch.aws/apigateway", ApiName="<api>"}[5m])) | {x:0,y:0,w:15,h:120} |
| 5xx rate | number | sum(rate({__name__="5XXError", …, ApiName="<api>"}[5m])) | {x:15,y:0,w:15,h:120} |
| 4xx rate | number | sum(rate({__name__="4XXError", …, ApiName="<api>"}[5m])) | {x:30,y:0,w:15,h:120} |
| Peak latency (ms) | number | max({__name__="Latency", …, ApiName="<api>"}) | {x:45,y:0,w:15,h:120} |
| Request count | line | sum(rate({__name__="Count", …, ApiName="<api>"}[5m])) | {x:0,y:120,w:30,h:320} |
| Errors 4xx/5xx | bar | sum by (__name__) (rate({__name__=~"[45]XXError", …, ApiName="<api>"}[5m])) | {x:30,y:120,w:30,h:320} |
| Latency (ms) | line | {__name__="Latency", …, ApiName="<api>"} | {x:0,y:440,w:60,h:320} |
| Top resources by 5xx | table | topk(10, sum by (Resource) (rate({__name__="5XXError", …, ApiName="<api>"}[5m]))) | {x:0,y:760,w:60,h:360} |
Optional depth (follow-up “per-service latency”): Integration latency (ms)
line (IntegrationLatency) — chart it beside Latency so the gap reads as
gateway overhead. Append two-up and re-sequence y.
This file