Generated banner for AWS Observability
Generated logo for AWS Observability

AWS Observability

AWSOfficialPublished by AWS

  • Skill
  • Tool integration
  • Cloud and DevOps
  • Apache-2.0
  • Low risk
  • Has scripts

From the author

Investigates root causes on the Omni context graph (service dependencies) and Intelligence insights; builds, configures, debugs, tunes AWS observability - operator-symptom questions, detecting Omni vs classic CloudWatch. CloudWatch on a reporting service: fleet health views, Log Insights, metric/composite/anomaly alarms, custom metrics/EMF, dashboards, X-Ray/ADOT tracing, canaries, CloudTrail, Dynamic Instrumentation (live breakpoints), and Application Signals service map. CloudWatch Omni on an existing Space: SQL over logs/traces, PromQL, Omni dashboards, alerts and Slack notifications, programmatic/IaC access (API/SDK/CLI/CFN), driving Omni from a coding agent or skills, AI agent quality from traces - on-demand and online evaluation of live traffic, incl. setting it up, readback, evaluators. For first-time setup - creating an Omni Space, granting access, ingestion, or ADOT instrumentation for Application Signals or Omni - use setting-up-cloudwatch-observability. Not for app logging or threat detection.
— the description by AWS

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides comprehensive domain expertise for AWS observability, covering both classic CloudWatch and CloudWatch Omni. It assists users in investigating service health, querying logs and traces, configuring alerts, and managing dashboards. The skill is designed to help operators distinguish between the two services, navigate their respective APIs, and perform advanced tasks like agent quality evaluation and context graph analysis. It does not handle initial service setup or onboarding, which are managed by a separate dedicated skill.

What it does

  • Investigates root causes using Omni context graphs and intelligence insights.
  • Queries logs, traces, and metrics using SQL, PromQL, and Log Insights.
  • Configures and tunes alerts, dashboards, and alarms for both CloudWatch and Omni.
  • Evaluates AI agent quality through trace scoring and online evaluation.
  • Provides methodology for service-health investigations and dependency analysis.
  • Supports programmatic access via AWS CLI, SDKs, and CloudFormation.

When to use it

  • When you need to investigate a service performance issue or root cause.
  • When you need to write or debug queries for logs, traces, or metrics.
  • When you need to create or update alerts and dashboards in CloudWatch or Omni.
  • When you need to evaluate the quality of an AI agent using trace telemetry.
  • When you need to understand service dependencies and blast radius.

Advantages

  • Provides clear routing between CloudWatch and Omni based on service signals.
  • Offers structured methodologies for complex observability tasks.
  • Includes specific checklists to ensure all required configuration facts are surfaced.
  • Maintains a strict scope to ensure accurate and relevant technical guidance.
  • Supports advanced observability features like dynamic instrumentation and agent evaluation.

Usage examples

“Why is my service experiencing high latency?”
The agent will guide you through a service-health investigation, starting with relevant telemetry signals and checking for configuration issues like timeouts.
“Create an Omni alert for high error rates.”
The agent will provide the necessary authoring guidance, ask for your specific threshold and evaluation period, and confirm the configuration before proceeding.
“How do I query my traces in Omni?”
The agent will explain the SQL syntax for traces.default and provide the necessary fields to identify slow or failed spans.
“Evaluate the quality of my AI agent's recent sessions.”
The agent will help you select an appropriate evaluator and guide you through the process of scoring traces to assess agent performance.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/aws/aws-observability/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip aws-observability.zip -d .agents/skills

Documentation by AWS

AWS Observability

Overview

Domain expertise for AWS observability across metrics, logs, and traces, for two products that share the CloudWatch name but are separate services with separate control planes, data models, and APIs:

CloudWatchCloudWatch Omni
What it isLog groups, metric namespaces, alarms, Log Insights, X-Ray, Application SignalsApplication Observability / Agent Observability. A Space per account per Region is the access boundary over the account's CloudWatch Dataset (OpenTelemetry logs, traces, and metrics); the Dataset is a CloudWatch resource the Space reads, not something the Space contains
Control planeaws cloudwatch, aws logs, aws xray, aws application-signalsaws cloudwatchomni (endpoint prefix cloudwatch-omni, signing name cloudwatch)
QueryLog Insights query language; GetMetricDataSQL over logs.default / traces.default; PromQL over metrics; named views
NotifyAlarms (metric, composite, anomaly)Alerts (SQL/PromQL rule, contributors, OK/WARNING/CRITICAL/NODATA)
TopologyApplication Signals service mapContext graph (GetContextGraph)
AccessIAM onlyDomain → Space → access grants and access profiles (set up in setting-up-cloudwatch-observability)
Only hereDynamic Instrumentation, Synthetics canaries, CloudTrail auditing, EMFAgent-quality evaluation, views, context graph
First-time setupApplication Signals onboarding — ADOT auto-instrumentation, the amazon-cloudwatch-observability add-on, ServiceEvents, CI/CD metadataDomain → Space → grants → telemetry in → plain-ADOT instrumentation
Referencesreferences/cloudwatch/references/cloudwatch-omni/

This skill covers using both products, never setting either one up. Both first-time setup paths above live in setting-up-cloudwatch-observability (same two-folder split); route there rather than describing an onboarding procedure from memory.

Enabling Omni does not replace CloudWatch; log groups, metrics, and alarms keep working, and most customers use both.

Works best with the AWS MCP server — enables running CLI commands, querying CloudWatch, and validating configurations directly. All guidance also works with standard AWS CLI access.

Note: Reference files contain specific runtime versions, quota values, and feature matrices that may change. When precision matters (e.g. deploying to production, choosing a runtime, or checking a quota), confirm values against current AWS documentation rather than relying solely on the values in these files.

Scope guard — is this an observability request?

This skill owns the scope decision for every request routed to it, and this section is its sole home.

If a request is NOT about AWS observability with CloudWatch or CloudWatch Omni (weather, trivia, general chit-chat, non-observability coding), decline it in one sentence: state plainly that it is out of scope, never fabricate an answer, and never claim a false capability limitation (no "no internet access", no "no weather data") — the reason is simply that it is out of scope. Then redirect by naming what this skill does cover: querying CloudWatch and CloudWatch Omni logs, traces, and metrics, building dashboards, configuring alerts, investigating a service, or agent evaluation.

Do not attempt the off-topic task, and do not call a tool or run a query in pursuit of it. Keep it brief: no lecture, no long refusal.

Step 0 — CloudWatch or CloudWatch Omni?

Decide this before routing. The natural wording ("set up an alert for high latency", "build a dashboard", "query my logs") does not say which product the customer means.

  1. The customer names the product — "Omni", "Application Observability", "Agent Observability", a Space, Domain, Dataset, access grant, spaceId, cloudwatch-omni, Omni SQL, PromQL, views, context graph, evaluators, agent evaluation / scoring traces / online or continuous evaluation / gen_ai.evaluation, and OTel span vocabulary (traces.default, logs.default, spans, durationNano, status.code, resource attributes, service.name) → Omni. "Log Insights", "log group", "metric namespace", "CloudWatch alarm", "metric alarm", "composite alarm", "anomaly alarm", "X-Ray", "Application Signals", "canary", "CloudTrail", "Dynamic Instrumentation" → CloudWatch. A "log group" named inside an agent-evaluation request is the online-evaluation data source to verify, not a CloudWatch signal. A bare "alarm" (or "alert") with no other product signal is ambiguous — fall through to rule 3 and probe: a Space → Omni alert (alerts.md); no Space → CloudWatch alarm (cloudwatch/alarms.md). Exception: a "PromQL alarm" is a CloudWatch alarm on OTel metrics (cloudwatch/alarms.md) — "alarm" wins over "PromQL".

  2. Knowledge or how-to question ("what is an Omni alert", "does Omni have an API", "how do alerts differ from alarms") → answer from the reference files directly. Do not probe the account, and do not divert to the other product. Whether a capability exists is a fact about the product, not the account. concepts.md carries the full feature-equivalence matrix. 2a. Authoring / how-to alert request — "create / set up / write an Omni alert that fires when X", "how should I alert on Y" — with no Space or Region supplied and no go-ahead to actually create it, is a HOW-TO request. Deliver the authoring guidance (the alert row of "Must-state checklists" below, then alerts.md) FIRST, from the reference files. Do NOT stall on a Region/Space clarifying question and do NOT fall back to a CloudWatch alarm for a request that explicitly says "Omni alert". Probe the account only once the user supplies a Space/Region or asks you to create it. 2b. A telemetry object is already in the prompt — the user pastes, or the UI passes as context, a span, trace, log record, or query result and asks what it means, what errors it has, or how long it took. This is not a live-data request: do not probe the account, run a query, or ask the user to fetch it again. Read it in place per the "Reading a span or trace you already have" section of query/sql-logs-traces.md and state every item in its must-state list. 2c. A query request whose live result is empty or unreachable ("show me the slowest spans", "which traces failed") is still answered with the methodology — the exact query and the field rules that make it correct (see the must-state callouts in query/sql-logs-traces.md). Zero rows, a wrong-Region Space, or an unreachable endpoint is reported as a finding alongside the query, never as the whole answer.

  3. Request that must act on live data, and the wording is ambiguous (not a Step 0.5 content question, which is answered from the catalog) → probe the target Region first:

    aws cloudwatchomni list-domains
    aws cloudwatchomni list-spaces --region <target-region> --query "items[?region=='<target-region>']"  # account-global; filter to the Region
    • list-spaces is account-global (the --region flag only selects the endpoint), so filter its result to the target Region as shown rather than reading a non-empty list as proof; a Space is one per account per Region. See references/cloudwatch-omni/concepts.md for the full Region-probe rationale.

    • A Domain and a Space in that Region (a non-empty filtered list) → Omni.

    • No Space in that Region (an empty filtered list) → CloudWatch: alarms → cloudwatch/alarms.md, dashboards → cloudwatch/dashboards.md, queries → cloudwatch/log-insights.md, metrics → cloudwatch/metrics.md. If the customer explicitly asked for Omni and has no Space, that is a first-time setup — see step 4.

    • The probe itself errors ("not yet supported", unknown service, endpoint does not resolve) → the CLI/SDK model in use lacks cloudwatchomni. That is not evidence Omni is absent and must not be reported as "Omni is unavailable". The customer's installed AWS CLI/SDK most likely predates the service. If the request carried any Omni signal, give the customer the upgrade command to run (AWS CLI v2 reinstall or brew upgrade awscli; pip install -U boto3 botocore) and have them re-run aws cloudwatchomni list-domains — exact steps in programmatic-access.md. Never run a package-manager upgrade or installer on the host yourself (brew, pip install -U, .pkg/MSI); it mutates the customer's machine beyond the request and can break unrelated tooling — hand over the command and continue with the guidance. Never substitute a CloudWatch or X-Ray command for an Omni request. If the request carried no Omni signal, do not block on the upgrade. When you are already going back to the customer for missing inputs (for example an alert's threshold and period), ask which product they want in that same question: say the Space probe could not run on this CLI, describe both options, and build neither until they answer. When the request is fully specified or says not to ask, proceed on the CloudWatch path (the pre-Omni default) and mention the upgrade only in passing.

    • Still inconclusive → ask the customer.

  4. First-time setup, on EITHER product → STOP and route to the setting-up-cloudwatch-observability skill. This skill covers a service or Space that already reports and holds no onboarding procedure for either product, so there is nothing here to fall back on: not Omni setup (creating a Domain or Space, granting access, ingestion, forwarding, Slack, instrumenting an app or AI agent for a Space) and not CloudWatch setup (onboarding a service to Application Signals — ADOT auto-instrumentation, the amazon-cloudwatch-observability add-on, monitored service, ServiceEvents, CI/CD metadata, the per-platform enablement guides). Any instrumentation / ADOT / collector request is a setup request either way; the setup skill owns the product decision too, so hand the whole request over rather than probing list-spaces yourself. Application Signals once it is reporting — service map, alarms on its metrics, Dynamic Instrumentation debugging — belongs here.

Under-specified alert requests: When an alert or alarm request names what to watch (a symptom or a service) but not the inputs it needs — the threshold value and the evaluation period — ask for those rather than inventing them. Notifications are optional (per alerts.md), so ask for a notification destination only if the user wants to be notified. This holds on both paths (an Omni alert or a CloudWatch alarm).

Under-specified dashboard requests: When a dashboard request names what to show but not which metrics, panels, or layout, ground those against the data and confirm the panel set rather than inventing panels; a dashboard has no threshold, period, or notification. For ANY dashboard authoring/save request, also open dashboards.md and surface its "Facts you MUST surface when building or saving an Omni dashboard" checklist (see "Must-state checklists" below). Which signals or panels a named resource type needs is a Step 0.5 catalog question, not a dashboards-file question.

Step 0.5 — Service-health investigation (routing)

A large share of real questions are phrased as an operator symptom, not as a tool: "is my <service> throttled / slow / erroring / unhealthy," "which of my <service>s are <symptom>," "what's the health of my <service>," "what does <service> depend on and which is broken," "what signals / what should be on a dashboard or view for <service>." These are observability-data questions. Start from the telemetry, and answer with the methodology (the correct signals, aggregation, scoping, and caveats) even when you also pull live numbers and even when no matching resource exists in the account. Telemetry shows the symptom. The cause is often a configured value, so when the question is why something fails, also read the configuration of the resources involved, for example an API Gateway integration timeout, an SQS visibility timeout, or a Lambda timeout or event source mapping. A read-only or "logs only" role can usually still call these Get and Describe APIs. Make the call and report AccessDenied if it happens.

Route by the symptom, then open the reference and surface every applicable item in its "facts you MUST surface" checklist — the checklist is the output contract, and it lives in the reference file, not here:

  • A metric symptom about one AWS service (throttling, latency, error rate, restarts, saturation, "what signals should it get", which signals or panels a named resource type needs on a dashboard or view) → query/promql-metrics.md. Open its "Service-health question — facts you MUST surface" section and surface every applicable item. Dashboard or view wording does not send this to either dashboards file (neither carries the signal facts) nor to rule 3's probe — it is a content question.

  • "What does <service> depend on / what's broken downstream" / blast radius / who is affected / which direction do I walk the graph / what do CALLS, ACCESSES, RUNS_ON mean → context-graph.md. Open its "Dependency / blast-radius question — facts you MUST surface" section and surface every applicable item. A slowness or error symptom phrased in terms of the graph, dependencies, or edge types routes here, not to the metric bullet above.

  • A health VERDICT for a service ("is <service> healthy", "health summary of <service>") or a dependency map WITH ROLES ("what does <service> depend on and what role does each play", "which services call <service>") → service-health.md. Open its "Service health & dependency question — facts you MUST surface" section and surface every applicable item — it owns the combined verdict and the role-labelled dependency map, and defers the per-resource signal catalog and blast radius elsewhere. A bare single-service metric symptom, or a "what signals / panels does <resource type> need" or fleet-health-view ask, routes to the promql-metrics bullet above; a pure blast-radius / direction / edge-semantics question routes to the context-graph bullet.

  • System status, a root-cause / triage symptom, or a past incident — "is anything wrong right now?", "what's wrong with <service>", "why is <service> slow or erroring", "what changed since <time>", "users reported errors between X and Y — which service is the single most likely root cause", or any mention of insights, observations, or anomalies (a cause ask, not a single-signal health verdict or a dependency list) → intelligence.md: start from Omni Intelligence insights and observations, then use the context graph and queries to confirm. Its own "Scope and neighbours" section states which asks it hands back to the bullets above.

  • Individual slow or failing spans / traces ("slowest spans for <service>", "which traces failed") are trace SQL, not a metric aggregate → query/sql-logs-traces.md. Open its "Span Duration" and "Finding failed spans" sections and state every item in their must-state callouts — a request about a service's latency or error rate (an aggregate signal) is the PromQL bullet above instead.

  • A failure being investigated from logs ("is this only failing for X?", "how many errors?", "what is the root cause?") → load the aws-investigation skill and read its references/completeness-and-root-cause.md. Read every page and every scope before counting, and check the root cause against every fact before reporting it.

Must-state checklists — the output contract

Four Omni tasks carry a checklist the answer text must carry, not just the plan. Open the reference and surface every item relevant to the request; the reference holds the per-item detail and is the source of truth, so do not restate it here.

TaskOpenSection to surface
Authoring or advising on an Omni alertalerts.md"Facts you MUST surface when authoring an alert"
Scoring traces, choosing an evaluator, reading stored scores, online/continuous evaluation, evaluation datasets, turning scores into agent changesagent-evaluation.mdevery "tell the user ALL of this" callout
Exploring traces — one trace by id, comparing runs, finding traces by their spans, slowest traces, trace fields, agent sessions, token cost, what changed between two windowsquery/trace-exploration.mdevery "state … in the answer" callout
Authoring, saving, reading back, or debugging an Omni dashboarddashboards.md"Facts you MUST surface when building or saving an Omni dashboard"

Routing — CloudWatch (references/cloudwatch/)

User needAction
Enabling/onboarding a service to Application Signals (auto-instrumentation, the amazon-cloudwatch-observability add-on, monitored service), propagating ServiceEvents git/deployment metadata through CI/CD, or the per-platform × per-language enablement stepsSTOP — this is first-time CloudWatch setup and no reference here covers it. Route to the setting-up-cloudwatch-observability skill
Writing Log Insights queries (pipe-delimited syntax: fields, filter, stats, sort, parse, display)Read log-insights.md. If StartQuery rejects the window as before the log group's creation time, read the same window with FilterLogEvents. See Known issues 11 there
Configuring alarms (metric, composite, anomaly)Read alarms.md. For an Omni alert, see the Omni table
Publishing custom metrics or using EMFRead metrics.md
X-Ray / ADOT tracing behaviour — X-Ray-SDK-vs-ADOT choice, trace and segment structure, annotations vs metadata, sampling rules, collector pipeline config, X-Ray→OTel migration trapsRead tracing.md. Onboarding an un-instrumented service is the setup skill's job (see the first row)
Building CloudWatch dashboards (widget mechanics; which signals a given AWS service needs is Step 0.5)Read dashboards.md
Debugging observability issuesRead troubleshooting.md — starts with the 5 most common fixes
Investigating a failure from logs: whether it affects one entity or the whole fleet, counts/totals/"no evidence of X", or deciding and reporting the root causeLoad the aws-investigation skill — completeness and scope first (references/completeness-and-root-cause.md), then root-cause analysis and the closing checklist
Debugging canary failuresRead synthetics.md — see Common failures table
CloudTrail operational auditingRead cloudtrail.md
Setting up Lambda monitoring with CDKUse alarm-template.ts as a starting point
Creating synthetic canariesRead synthetics.md
Configuring ADOT collectorUse otel-config.yaml as a starting point
Debugging a running service with breakpoints/snapshots — Dynamic Instrumentation (modifies live services and captures live data)Read dynamic-instrumentation.md in full before acting. Confirm with the user before any create/delete, and narrate before significant actions: observation → hypothesis → proposed action → expected result. Source inspection alone identifies hypotheses, not confirmed root causes; keep suspected causes tentative until runtime evidence confirms them.

Routing — CloudWatch Omni (references/cloudwatch-omni/)

Rows that act on live Space data assume Step 0 found a Space. Knowledge questions are answered from the file directly.

User needAction
Concepts. What Omni is, what a Domain / Space / Dataset / grant / profile / view / alert / context graph is, whether a feature is Omni or CloudWatch, where setup startsRead concepts.md
Query logs or traces — SQL (SELECT … FROM logs.default / traces.default / default), field access, schema discovery, slowest / failed spans (durationNano, status.code), TABLESAMPLERead query/sql-logs-traces.md. For slowest or failed spans, state every item in its "Span Duration" / "Finding failed spans" must-state callouts, even when the live result is empty
Explore traces — open one trace by id (where the time went, why it failed, span tree, inputs/outputs, tokens), compare two runs, why a trace scored badly, find traces by their spans (tool calls per trace, a repeated tool, failed tool calls), the slowest traces today, what fields a trace carries, list or walk an agent's sessions (a looping session), token cost, guardrail or deploy questions, what changed (agent, model or downstream)Read query/trace-exploration.md for the trace recipes and agent-span attributes, with query/sql-logs-traces.md for the SQL itself
A span, trace, or log record supplied in the prompt — "I have this span open, what errors are in it", a pasted telemetry objectRead the "Reading a span or trace you already have" section of query/sql-logs-traces.md. Answer from the object's own fields; do not probe, query, or ask the user to fetch it
Query metrics — PromQL, which metric answers which symptom per AWS service, why a metric is missing, gauge vs counterRead query/promql-metrics.md. Metrics are PromQL, never SQL; run them directly through the CloudWatch Prometheus-compatible API (monitoring endpoint) or the CloudWatch MCP server's PromQL tools, not through StartTelemetryQuery
Views — create, manage, or query named reusable SQL (FROM view.<name>)Read query/views.md
Dashboards in Omni — compose, ground panel queries, author panels[], lay out the grid, the API save semantics (an unknown root- or panel-level key, a missing type/layout, or a bad variant is REJECTED at save with a 400 ValidationException; a bad enum VALUE, x+w>60, or an unknown key inside config saves 200 and fails or is ignored at render; validate before save), fix an empty or blank panel, the *OmniDashboard APIsRead dashboards.md
Alerts in Omni — any mention of an Omni alert, CreateAlert / GetAlert / ListAlerts / UpdateAlert / DeleteAlert, a profileId, an alert ARN, or how alerts differ from alarms; create, tune, tag, list, delete; notificationsRead alerts.md. The alert API is real and first-class — do NOT redirect to CloudWatch alarms. For CloudWatch alarms when Omni is not enabled, read cloudwatch/alarms.md
Omni Intelligence — is anything wrong right now, status of a service, why is a service slow or erroring, what caused an incident between two times, root cause, past incidents, insights, observations, anomalies, ListInsights, ListObservationsRead intelligence.md; it links to the context graph for cause-vs-impact questions. ListInsights, ListObservations, and GetContextGraph are the public CloudWatch Omni operations it teaches (aws cloudwatchomni list-insights / list-observations / get-context-graph), with the Intelligence console view as a human-readable cross-check. It consults the intelligence layer, localizes on the context graph, confirms with telemetry, and names one root-cause service. For a health verdict or dependency roles with no single-cause ask, use service-health.md; for pure dependency / blast-radius topology and graph traversal, use context-graph.md
Context graph — why is service X slow or failing, what depends on it, upstream/downstream, which direction to walk, edge types CALLS / ACCESSES / RUNS_ON, blast radius, walking from an insight or anomaly to a root cause, GetContextGraphRead context-graph.md and state every applicable item in its "facts you MUST surface" section
Service health & dependency analysis — is <service> healthy, health summary, what does <service> depend on and what ROLE does each play (caller / callee / datastore / runs-on), which services call itRead service-health.md and state every applicable item in its "facts you MUST surface" section. It grounds the verdict in the context graph + RED/USE metrics + alarm state; for the resource-specific signal catalog and query mechanics it defers to query/promql-metrics.md (§5 per-service catalog), query/sql-logs-traces.md, and alerts.md, for traversal and blast radius to context-graph.md. A "what signals / panels does <resource type> need" or fleet-health-view ask goes to promql-metrics.md, not here
Agent evaluation — score traces on demand, choose an evaluator, read back stored gen_ai.evaluation.* scores ("which evaluators are doing worst", "which online evaluators are unhealthy / underperforming"), build datasets from traces, set up online evaluation, author a custom evaluator, audit whether an agent's traces are flowing, monitor quality, root-cause a quality regression, recommend prompt or tool-description changes from scores, verify a change against a baselineRead agent-evaluation.md and state every applicable item in its "tell the user ALL of this" callouts
Programmatic access — "is there an API or SDK for Omni", calling Omni from code, CI, IaC, or an AI coding agentRead programmatic-access.md. Omni has a real public SigV4 API; never answer that it has none, never substitute the CloudWatch or X-Ray CLI/SDK, and answer without probing for a Space. Endpoint cloudwatch-omni.<region>.api.aws, never .amazonaws.com; client cloudwatchomni, no hyphen. Give both authorization classes: a grant-mediated caller needs a matching grant or is denied; a direct IAM caller with none is not denied by the Space — its own IAM policy decides
AWS DevOps Agent — Investigations reports it is not connected, connecting it to a Space, or running, following, or chatting with an investigationConnecting is first-time setup: route to the setting-up-cloudwatch-observability skill. In the Omni web app an investigation starts from a thread, /investigation new <description>, or a firing alert's Investigate action. Driving the DevOps Agent itself (starting, following, or chatting with investigations from a coding agent) belongs to its dedicated aws-agents-for-devsecops plugin; point there and do not reconstruct that workflow or invent aidevops calls
Who has access to a Space, granting or revoking access, access profiles, creating a Space or Domain, ingestion, forwarding, Slack, Azure, instrumenting an app or AI agent — and, on the CloudWatch side, onboarding a service to Application SignalsRoute to the setting-up-cloudwatch-observability skill. If it is not installed locally, load it with the AWS MCP retrieve_skill tool (skill_name: setting-up-cloudwatch-observability; pass file for a reference it cites)
Spans multiple areasRead the most specific reference first, then consult others as needed

Files

references/cloudwatch/
FileContent
alarms.mdMetric, composite, anomaly detection alarms — configuration, constraints, recommended defaults
log-insights.mdComplete query syntax, commands, functions, known issues, reusable query library
metrics.mdCustom metrics, EMF spec, metric filters, high-resolution, retention
tracing.mdX-Ray → ADOT migration, sampling rules, annotations vs metadata, collector config
dashboards.mdWidget types, cross-account/region, dynamic labels, sharing
troubleshooting.mdError → cause → fix for all observability services
cloudtrail.mdOperational auditing, event types, S3+Athena queries
synthetics.mdCanary runtime/blueprint constraints, VPC networking, common failures
dynamic-instrumentation.mdDynamic Instrumentation debugging loop — breakpoints/probes on live code, snapshot capture + correlation analysis, create/delete gating, snapshot PII handling. Runs via scripts/cloudwatch/di_instrumentation.py + scripts/cloudwatch/di_snapshots.py; details in dynamic-instrumentation/
alarm-template.tsBest-practice CDK Lambda monitoring (alarms + dashboard)
otel-config.yamlADOT collector config for X-Ray traces + CloudWatch EMF metrics

Not here, by design: Application Signals onboarding (the enablement procedure, the ServiceEvents CI/CD chain, the 16 per-platform guides) is first-time setup and lives in setting-up-cloudwatch-observability. Do not reconstruct it here from memory; route.

references/cloudwatch-omni/
FileContent
concepts.mdWhat Omni is and is not; glossary (Domain, Space, Dataset, grant, profile, view, alert, dashboard, context graph, evaluator); Omni-vs-CloudWatch feature-equivalence matrix; how to tell which product the customer means; the setup sequence and where it lives
context-graph.mdThe service/resource topology Omni builds from traces and metrics; GetContextGraph request/response and CLI; reading upstream vs downstream and blast radius; walking from an insight or anomaly hop-by-hop to a root cause, then pivoting to queries
intelligence.mdOmni Intelligence — the insight and observation operations and data model, ListInsights / ListObservations inputs and filters, GetContextGraph with insights, answering status, troubleshooting, root-cause and past-incident questions, the 404-vs-403-vs-local error rule (owned by context-graph.md), cost and performance guardrails, and the empty-result/stale-insight gotchas
service-health.mdService health verdict and dependency analysis — dependency map with roles (caller/callee/datastore/runs-on), the multi-signal HEALTHY/DEGRADED/DOWN verdict (RED for services, USE for resources, alarm state), and how to build the checks from a node's includeMetadata selectors. Ties together context-graph, promql-metrics, sql-logs-traces, and alerts rather than duplicating them — defers traversal and blast radius to context-graph and the resource-specific signal catalog to promql-metrics §5
programmatic-access.mdThe public SigV4 API (cloudwatch-omni endpoint prefix, cloudwatch signing name), how access grants authorize a programmatic caller, CLI/SDK access (and why an unsupported-service error is a client-version issue), CloudFormation/CDK, AI coding agents, and the wrong answers to avoid
query/sql-logs-traces.mdSQL over logs and traces — table addressing, required time range, system fields, field access and quoting, schema discovery, supported operations, functions, common patterns (including durationNano span duration), constraints, TABLESAMPLE
query/promql-metrics.mdMetrics in Omni are PromQL — what is queryable (OTLP, span RED, OTel-enriched vended metrics) and what is not, label conventions, __name__ matcher, rate() on counters, per-AWS-service metric catalog with derived formulas and dimension traps, and running queries and discovering names through the Prometheus-compatible HTTP API and MCP tools
query/trace-exploration.mdWorking with whole traces on traces.default — span fields and AI-agent span attributes (span role, tool name, input/output, token usage), fetching one trace in the right window, time breakdown and self-time, failure analysis with exception events, comparing runs, tying a bad evaluation score to spans, tool-call and repeated-tool patterns, trace-level latency and "today", field discovery for traces, reporting, agent sessions, walking a session, token cost, signals the spans may not record, before/after comparison by layer
query/views.mdNamed SQL views: CreateView / UpdateView / DeleteView / ListViews, FROM view.<name>, naming and definition rules, composition patterns
dashboards.mdOmni dashboards — composition recipes, grounding panel queries, the panels[] body and panel types, visualizations, the 60-column grid, the API save semantics (unknown root/panel keys are rejected 400; unknown keys inside config save 200 and are ignored at render; validate before save), troubleshooting empty/blank panels, the Create/Get/List/Update/DeleteOmniDashboard APIs, archetype templates
alerts.mdOmni alerts — alert vs alarm, evaluation (FIELD_VALUE / COUNT_OF_RESULTS, contributors), states and no-data treatment, notification rules, step-by-step create / update / delete / tag / fetch, and the alert APIs
agent-evaluation.mdAgent-quality evaluation on OTel traces — instrumentation health audit, evaluator selection, on-demand scoring, online evaluation, custom evaluators, datasets from traces, reading back stored gen_ai.evaluation.* scores (retrieval plan + SQL mechanics), and turning scores into changes (monitoring, regressions, evaluator drills, golden datasets, change and re-run). Uses scripts/cloudwatch-omni/evaluate_traces.py and scripts/cloudwatch-omni/capture_dataset_from_traces.py

Files

references/cloudwatch-omni/query/promql-metrics.md

Querying metrics in CloudWatch Omni with PromQL

How metric querying works in CloudWatch Omni, and which metric answers which symptom for each AWS service. Metrics in Omni are queried with PromQL, not SQL — the SQL dialect in sql-logs-traces.md reaches logs and traces only. This file covers the decision between the two surfaces, what is and is not queryable, how to discover metric names and labels, the PromQL conventions Omni adds (the @-labels and the __name__ matcher), a per-service catalog of the metrics that matter (unit, gauge vs counter, identity label, derived formulas, dimension traps), and how to read an empty result.

Treat every metric, label, and scope name in this file as vocabulary to recognise, not schema to trust. Confirm the exact name against the account's own data (section 3) before putting it in a query, a dashboard panel, or an alert rule.

Contents

  1. Metrics are PromQL, not SQL

  2. What is queryable — and what is not

    • Service-health question — facts you MUST surface

  3. Discovering metric names and labels

  4. Query mechanics

  5. Per-service metric catalog

  6. Reading results

  7. Where PromQL is used


1. Metrics are PromQL, not SQL

Omni has two query surfaces, and the shape of the ask decides which one you use:

The ask is aboutSurfaceLooks likeReference
A number over time — CPU, latency, request/error rate, queue depth, throttles, saturation, "how is X trending"PromQL over metricsAn expression that does not start with SELECT, e.g. histogram_avg({__name__="CPUUtilization", "@instrumentation.@name"="cloudwatch.aws/ec2"})this file
Log lines, log fields, log counts by fieldSQL over logs.defaultSELECT ... FROM logs.default WHERE \@timestamp` BETWEEN ...`sql-logs-traces.md
Spans, traces, span attributes, per-span durationsSQL over traces.defaultSELECT ... FROM traces.default WHERE ...sql-logs-traces.md

Rules that follow from this:

  • There is no metrics.default SQL table. A metric request is answered with a raw PromQL expression, not SELECT ... FROM metrics.default — that table does not exist. The one place PromQL text legally appears inside SQL is the promql('<data set>', '<expression>', <step-seconds>) table function (section 4, "Time range binding"; section 7) — never as a bare expression. Omni's StartTelemetryQuery does not serve metric reads, so a metric read there fails either way: a bare PromQL expression is rejected by the SQL parser with ValidationException: Invalid SQL, while SELECT ... FROM metrics.default or a promql() wrapper parses but is rejected with ValidationException: Metrics queries are not supported.

  • Metrics are queried directly through CloudWatch's Prometheus-compatible HTTP API. An ad hoc or programmatic metric read (a script, the CLI, an AI agent, Grafana) sends the PromQL expression to https://monitoring.<region>.amazonaws.com/api/v1/query or /api/v1/query_range, SigV4-signed with the service name monitoring; the CloudWatch MCP server wraps the same API as tools (section 4, "Running a PromQL query"). Run a metric query this way whenever the user wants the numbers; a dashboard panel or an alert rule is where a query lives, not a prerequisite for running one.

  • Nothing SQL-specific applies to metrics. `@record`, `@message`, TABLESAMPLE, EXPLAIN (ANALYZE_FIELDS), and fine-grained access control (FGAC) are logs/traces features. Metrics do not apply FGAC.

  • No cross-surface JOIN. There is no metric↔metric or metric↔logs/traces JOIN. To correlate a metric with logs or traces, run the PromQL query and the SQL query as separate steps and line them up on a shared time window and a shared identity (service name, function name, instance id).

  • Derive from spans only as a fallback. A per-request latency percentile or error count can also be computed from traces.default (span durationNano, status['code']) in SQL. Prefer the metric when one exists — it is cheaper and pre-aggregated — and fall back to the span derivation only when no metric grounds the signal.

Answer the construction question directly — do not stall or give up. A metric-query how-to ("how do I query / what PromQL shows X for <service>", "which metric tells me if <service> is throttled/slow/erroring") is answered with the PromQL construction pattern, even when a live probe returns no matching metric, or you have no Space in hand. When the metric is not discoverable live, still give the query template — the __name__ matcher, the identity-label / "@instrumentation.@name" (or "@resource.*") scope, and the correct gauge-vs-counter read — and note to confirm the exact names against the account's own data (section 3). Never substitute "no matching metrics found" or a clarifying question for the query; the construction pattern is the answer.

How this differs from CloudWatch metrics

CloudWatch (aws cloudwatch get-metric-data, metric math, Metrics Insights) and Omni PromQL are different query models over different surfaces. Do not translate one into the other mechanically:

CloudWatchOmni PromQL
Metric is addressed by namespace (AWS/Lambda) + name + dimensionsMetric is addressed by name (via __name__) + labels; the namespace becomes an instrumentation-scope label ("@instrumentation.@name"="cloudwatch.aws/lambda")
Dimensions (FunctionName)The same names, as labels (FunctionName="...")
Statistic chosen per query (Sum, Average, p99)Aggregation is an operator/function (sum, avg, rate, histogram_quantile) and depends on whether the metric is a gauge, counter, or histogram
PeriodRange vector ([5m]) plus the step of the evaluation (dashboards: promqlQueryOptions.step)
Metric math expressions (m1/m2*100)Ordinary PromQL arithmetic between series with matching labels
Every vended metric is presentOnly OTLP metrics, span-derived metrics, and OTel-enriched vended metrics are present (section 2)

For publishing custom metrics, EMF, metric filters, and CloudWatch metric math, see ../../cloudwatch/metrics.md. If the account does not have Omni enabled, or the metric is a CloudWatch-only vended metric, the CloudWatch route is the right answer — not a forced PromQL query.


2. What is queryable — and what is not

Omni serves metrics from one OpenTelemetry-native, PromQL-queryable surface. It carries resource and scope attributes/labels rather than a CloudWatch namespace.

FamilyWhat it isIdentity labelsExample
OTLP-ingested metricsCustom application metrics your services emit through an OTel SDK or collector, including Kubernetes workload metrics"@resource.service.name", "@resource.k8s.*"http_server_request_duration, k8s.pod.phase
Span-derived RED metricsPlatform-generated from ingested traces — the rate/error/duration signals the service map and service dashboards use"@resource.service.name", "status.code" (no @ scope, but still double-quoted — it is dotted)traces.span.metrics.calls (delta Sum — read it with sum_over_time, never rate()), traces.span.metrics.duration (native histogram)
OTel-enriched vended AWS metricsMetrics AWS services publish natively (EC2, Lambda, RDS, ...) after OTel enrichment has projected them onto this surface"@instrumentation.@name"="cloudwatch.aws/<service>" + the original dimension as a bare label (InstanceId, FunctionName){__name__="CPUUtilization", "@instrumentation.@name"="cloudwatch.aws/ec2"}
Not queryable: CloudWatch vended metrics that are not enriched

Vended CloudWatch metrics that have not been OTel-enriched, and usage/billing-style metrics such as AWS/Logs IncomingBytes / IncomingLogEvents, are not yet supported in CloudWatch Omni. There is no CloudWatch execution path inside Omni (no GetMetricData or Metrics Insights route behind the PromQL surface).

When the ask resolves to a CloudWatch-only metric:

  • Say so plainly: "CloudWatch metric queries are not yet supported in CloudWatch Omni (some vended metrics are not available)." Do not force a PromQL query that can only return empty.

  • Do not substitute a log-content scan for a metric. "How much are my log groups ingesting?" is a metric question (IncomingBytes), not a logs.default query.

  • If the user only needs the number, offer the CloudWatch route as a separate, non-Omni step (aws cloudwatch get-metric-data; see ../../cloudwatch/metrics.md). If they want it in Omni, the fix is enrichment (below) — for resource types enrichment supports.

OTel enrichment for vended AWS metrics

Vended AWS metrics are CloudWatch metrics by default. OTel enrichment ("vended metrics in PromQL" in the AWS documentation) is the opt-in feature that also projects them onto the PromQL surface:

  • Opt-in per account, per region. Enable it in each region you need. The CloudWatch metric is unchanged and stays available through the CloudWatch APIs.

  • Ordered prerequisite. First enable resource tags on telemetry for the account (aws observabilityadmin start-telemetry-enrichment, IAM action observabilityadmin:StartTelemetryEnrichment, which provisions the Resource Explorer indexing that enrichment relies on; check with get-telemetry-enrichment-status), then enable OTel enrichment (aws cloudwatch start-otel-enrichment, IAM action cloudwatch:StartOTelEnrichment; check with get-otel-enrichment, disable with stop-otel-enrichment). Both are public operations of their own services — CloudWatch Observability Admin and CloudWatch — not of cloudwatch-omni, and the CloudWatch API itself documents this order. An AWS CLI released before start-otel-enrichment will not have that subcommand; upgrade the CLI rather than look for it under another service.

  • Mapping is by metric name + dimensions → resource ARN, not by tags. A supported resource does not need to be tagged to become PromQL-queryable; tags that are present are simply surfaced as @aws.tag.* labels.

  • Gated on a supported-resource list. Only supported AWS resource types are enriched.

  • Names are preserved. The metric keeps its CloudWatch name (CPUUtilization, 4xxErrors), its dimensions become PromQL labels, and it gains an instrumentation scope label "@instrumentation.@name"="cloudwatch.aws/<service>" (service segment lowercase). The segment is not always the CloudWatch namespace: ALB is cloudwatch.aws/elasticloadbalancing (not applicationelb) and Bedrock is cloudwatch.aws/bedrock-mantle. Take the literal from the section 5 catalog or from discovery — a wrong scope returns 0 series with no error.

So "why isn't my EC2 / Lambda metric in PromQL?" almost always means one of: enrichment is not enabled for that account/region, resource-tags-on-telemetry was never enabled first, or the resource type is not on the supported list.


Service-health question — facts you MUST surface

A large share of real questions arrive as an operator symptom, not as a query: "is my <service> throttled / slow / erroring / unhealthy," "which of my <service>s are <symptom>," "what's the health of my <service>," "what signals / what should be on a dashboard or view for <service>." These are observability-data questions — answer them from the telemetry surface, not from the resource's control plane. Answering with the methodology — the correct signals, aggregation, scoping, and caveats — is an output contract: you owe the facts below whether or not you also pull live numbers, and even if no matching resource exists in the account. Presenting real metric values, or reporting "none found" and asking for a resource name, without these facts is the single most common wrong answer here. Find the service's entry in the per-service catalog (section 5) and surface every item that applies:

  1. Answer in PromQL, scoped. Select each metric by name with the __name__ matcher and scope it with "@instrumentation.@name"="cloudwatch.aws/<service>" — or, for OTLP workloads (e.g. Kubernetes), the double-quoted "@resource.*" labels, which have no vended @instrumentation scope. Metrics are PromQL only — never SQL over metrics.default. The scoped selector is part of the answer even when the request names no product, the account has no Space in reach, or a cloudwatch-omni probe fails: write the selectors, then name the AWS/<Service> namespace as the CloudWatch-side equivalent for an account without enrichment. Do not replace the selectors with console statistic names.

  2. Gauge vs counter decides the read — universal, independent of the service. Counters → rate() / increase(); gauges → avg / max / min. Never rate() a gauge; never average or read a raw counter value. On the enriched vended surface the spelling follows the instrument type (section 3): statistic-set metrics are delta exponential histograms, so max is histogram_quantile(1, …), min histogram_quantile(0, …), avg histogram_avg(…), and a percentile histogram_quantile(0.99, …); vended counts are delta Sums read with sum_over_time(…[5m]). On a histogram, max() and min() come back empty while avg() folds the buckets into one histogram instead of a number, and rate() / increase() over a delta Sum return a wrong (not an empty) number — confirm the instrument with the __type__ / __temporality__ labels.

  3. Group by the service's identity label (<identity-label>, e.g. the resource-id/name dimension) so each series is attributed to a specific resource.

  4. Derived signals must be derived. A ratio such as an error rate is rate(<numerator>) / rate(<denominator>) over the same window — there is no single vended "rate" metric. Never read a numerator alone without its denominator.

  5. Vended latency/duration metrics carry no pNN label, but a true percentile is still available — they arrive as delta exponential histograms, so read the percentile with histogram_quantile(0.99, {__name__="<metric>", …}) (section 3/4). Deriving percentiles from span histograms via SQL over traces.default remains an alternative.

  6. An empty series is often CORRECT, not a fault — universal. A signal that exists only under a specific configuration (an opt-in feature, an installed agent, a burstable instance type, or a dimensionality the metric is not published at) returns nothing when that configuration is absent. Never claim an empty result proves the symptom is absent ("no datapoints ⇒ not throttled/slow/erroring").

  7. Confirm exact metric, dimension, and label names against the account's own data (section 3) before relying on the query.

  8. Surface the service-specific traps from the catalog entry (section 5) — units, opt-in-only metrics, dimension tiers, instance-type-only metrics, cold-start exclusions, binary 0/1 metrics that must be read with max. These are what make the answer correct; the universal rules above are necessary but not sufficient.

Asking which signals or panels a <service> needs on a dashboard, view, or panel set is this same question. The dashboard, view, or panel wording does not turn it into a widget-mechanics or layout request, and it is not a live-data request: do not probe for a Space first, and a probe that fails or finds no Space does not change the content. Decide the panel set from the catalog entry (section 5) and write the panels as the scoped PromQL selectors above, in the answer itself — the "@instrumentation.@name"="cloudwatch.aws/<service>" scope and the identity label appear in the reply, not in a follow-up. Add the AWS/<Service> namespace form in one line as the equivalent for an account without enrichment, and do not close by asking which product the customer is on: the customer asked which signals belong on the view, which is the same set either way, and the answer gives both spellings. Neither dashboard file carries these facts: open ../dashboards.md (Omni body and grid) or the CloudWatch dashboards reference only afterwards, and only if the customer wants it built. Do not enumerate the fleet through the service's control plane (ec2 describe-instances, ecs describe-clusters, list-metrics) to decide the content; a resource probe is for grounding names once the panel set is agreed.

Never answer a service-health question by refusing / asking for a resource name first, by presenting live numbers without the discipline above, by calling the service control plane (ecs describe-clusters, ec2 describe-instance-status, dynamodb describe-table, …), or by falling back to the CloudWatch-console / Prometheus mental model (no pNN label on vended gauges; OTLP Kubernetes metrics are k8s.* with "@resource.*" labels, not pod_number_of_container_restarts or kube_* selectors). A dependency / blast-radius question ("what does <service> depend on / what's broken downstream") is answered from the context graph, not here — use GetContextGraph (never X-Ray GetServiceGraph) per context-graph.md, whose "Dependency / blast-radius question — facts you MUST surface" section carries that discipline. If the CLI/SDK errors with an unknown cloudwatch-omni service, that is a client-version issue (upgrade botocore/AWS CLI), not a reason to switch products.


3. Discovering metric names and labels

Metric names, label names, and scope spellings are not guaranteed — they depend on what has been ingested and enriched in this account. A misspelled name does not error; it returns an empty series. Discover before you write.

Method 1 (not available for metrics): ListTelemetryFields. The ListTelemetryFields API enumerates the known fields of a data set for LOGS and TRACES only — METRICS is not an accepted telemetryType in the current service model (the value was removed), so it lists no metric names — use Method 2. Its fields entries carry only name and children, so even for the telemetry types it does cover it returns no instrument type (Sum / Gauge / Histogram) and no temporality (delta / cumulative). For metrics, take names from the catalog below or from Method 2, and read the instrument type and temporality off the __type__ / __temporality__ labels a raw selector returns (Method 2) — the catalog names the statistic each metric needs (max, min, avg, a rate); the instrument type decides how that statistic is spelled.

Once you know the instrument type, read the aggregation off it:

Instrument typeRead it with
Gaugeavg() / max() / min() — never rate()
Sum, cumulativerate() / increase()
Sum, delta (enriched vended counts such as NetworkIn, ConsumedReadCapacityUnits arrive this way)sum_over_time(<m>[5m]), divided by the window seconds for a per-second value — rate() / increase() assume a cumulative counter, so on a delta Sum they return a wrong (not an empty) number
ExponentialHistogram, delta (enriched vended statistic-set metrics — CPUUtilization, StatusCheckFailed*, CPUCreditBalance, Lambda Duration — arrive this way, carrying CloudWatch's SampleCount/Sum/Min/Max)max → histogram_quantile(1, <m>), min → histogram_quantile(0, <m>), avg → histogram_avg(<m>) (or histogram_sum(<m>) / histogram_count(<m>)), a true percentile → histogram_quantile(0.99, <m>). A plain max() / min() yields no series; a plain avg() merges the buckets into one histogram, not a number. histogram_quantile(1, …) / (0, …) approximate the Max / Min via the top / bottom bucket bound (close to CloudWatch's Maximum / Minimum); histogram_quantile(0.99, …) is a real bucket-estimated percentile
Histogram (OTLP / span-derived, e.g. traces.span.metrics.duration)histogram_quantile(0.99, rate(<base>[5m])) on the base name — no _bucket suffix, no by (le)

Method 2: the Prometheus discovery endpoints. CloudWatch's Prometheus-compatible HTTP API (section 4, "Running a PromQL query") enumerates metric names and labels directly; the CloudWatch MCP server exposes the same calls as tools. Every call needs cloudwatch:ListMetrics, and start / end (Unix seconds or RFC 3339) bound the window searched:

NeedEndpoint on https://monitoring.<region>.amazonaws.comMCP tool
Every metric name in the account and RegionGET /api/v1/label/__name__/valuesget_promql_label_values (label __name__)
The values of any label (FunctionName, "@resource.service.name")GET /api/v1/label/<label>/valuesget_promql_label_values
Every series matching a selector, with its full label set/api/v1/series with match[]=<selector> (GET or POST)get_promql_series
The label names present (optionally under a match[])/api/v1/labels (GET or POST)get_promql_labels

This is a confirmatory check, not a prerequisite — per sections 1 and 2 you answer most metric questions from the catalog without any live probe. Reach for it to confirm a name or its labels before committing them to a query, dashboard, or alert. For example, /api/v1/series with:

{__name__="Invocations", "@instrumentation.@name"="cloudwatch.aws/lambda"}

The returned series show every label the metric actually carries (FunctionName, @aws.account, @aws.region, any @aws.tag.*), which is what you group and filter by — plus the instrument metadata labels __type__ (Gauge, Sum, ExponentialHistogram), __temporality__ (delta / cumulative), __monotonicity__, and __unit__. Read those first: they decide whether the statistic you want is spelled max(), histogram_quantile(1, …), rate(), or sum_over_time() (table above). A raw selector on a histogram-typed metric returns the series with their labels but no plottable samples, which is the tell. __name__ must be an exact match — a regex matcher on __name__ (=~) is rejected with Selector must have a metric name; write one selector per metric name and combine with or (plus label_replace to tag each side) when one panel needs several.

Do not use the SQL discovery tools for metrics — EXPLAIN (ANALYZE_FIELDS) and SELECT \@record`` are logs/traces-only.

Use the names discovery returns exactly — do not shorten, alias, pluralize, or invent. Nouns in the request ("throttles", "5xx", "timeouts") describe what to aggregate, not field names; map each to a real, grounded metric.


4. Query mechanics

Label conventions

Labels on Omni's metrics surface follow an @-prefixed convention:

Label formMeaningExamples
"@resource.*"Resource attributes — the identity of an OTLP-native workload"@resource.service.name", "@resource.k8s.namespace.name", "@resource.k8s.deployment.name", "@resource.k8s.pod.name", "@resource.cloud.resource_id"
"@instrumentation.@name"Instrumentation scope — how an enriched vended AWS metric is scoped to its service (lowercase service segment). Replaces the CloudWatch AWS/<Service> namespace, but is not derived from it mechanically (ALB is cloudwatch.aws/elasticloadbalancing)"cloudwatch.aws/ec2", "cloudwatch.aws/lambda", "cloudwatch.aws/rds", "cloudwatch.aws/elasticloadbalancing"
"@aws.tag.*"Resource tags surfaced by enrichment"@aws.tag.Environment"
"@aws.*" (without tag)Reserved system labels"@aws.account", "@aws.region"
"status.code"Span-derived RED error label — no @ scope, but dotted, so double-quote it like every other dotted label (see the quoting rule below): unquoted it fails with Failed to parse query: unexpected character after '.': 'c' in by (...) and unexpected character inside braces: '.' in a matcher. Adding a scope instead is also wrong: "@status.code" is rejected with Invalid scoped label '@status.code': unknown or invalid scope prefixsum by ("status.code") (sum_over_time({__name__="traces.span.metrics.calls"}[5m])) → OK, ERROR, UNSET series
bare namesDatapoint attributes — the original CloudWatch dimensions of a vended metricInstanceId, FunctionName, TableName, QueueName

Quoting rule: any label name that is dotted or @-prefixed must be double-quoted inside the braces — {"@instrumentation.@name"="cloudwatch.aws/ec2"}. The same applies inside by (...): sum by ("@resource.service.name") (...). Bare dimension labels (InstanceId) are written unquoted as usual.

For OTLP / Kubernetes workloads and span RED metrics, the identity is always a "@resource.*" label. Never use a generic Prometheus job= or bare service= selector, and never the underscore k8s_* form — those are ungrounded and return nothing.

Selecting a metric: the __name__ matcher

A PromQL metric name is really the reserved __name__ label. A name that starts with a digit or contains %, ., or spaces is not a legal bare identifier — common for vended AWS metrics (4xxErrors, 4XXError, 5XXError, 4xxErrorRate, EBSIOBalance%, Memory % Committed Bytes In Use) and for every dotted OTel name (traces.span.metrics.calls). Select those with a __name__ matcher inside the braces:

{__name__="4xxErrors", FilterId="EntireBucket"}
{__name__="EBSIOBalance%", InstanceId="<instance-id>"}
{__name__="traces.span.metrics.calls", "@resource.service.name"="<service>"}

The value is an ordinary double-quoted string, so any character is legal there. A plain identifier-safe name (CPUUtilization, http_requests_total) may be written bare (CPUUtilization{InstanceId="..."}), but the __name__ form always works — prefer it for vended metrics so the scope and dimensions read uniformly:

{__name__="Errors", "@instrumentation.@name"="cloudwatch.aws/lambda", FunctionName="<fn>"}
{__name__="CPUUtilization", "@instrumentation.@name"="cloudwatch.aws/ec2", InstanceId="<id>"}

Never write the metric name as a bare token inside the braces (a leading "name", before the label matchers). That form does not parse on this surface. This applies everywhere a metric is selected, including inside rate(...), sum(...), and histogram_quantile(...).

Gauge, counter, histogram — the read decides the function
  • Gauge — a level (CPU %, queue depth, free memory, connections, latency of a vended metric). Read with avg / max / min. Never rate() a gauge.

  • Counter (cumulative Sum) — accumulates (requests, bytes, errors, invocations). Read with rate() (per-second) or increase() (total over the window). Never read a raw counter value or average it.

  • Delta Sum — a per-interval count that does not accumulate. Read with sum_over_time(<m>[w]) (divide by the window for a per-second value); rate() / increase() assume a cumulative counter and misread delta temporality, so they return a wrong — not an empty — number. Enriched vended count metrics (NetworkIn, NetworkOut, ConsumedReadCapacityUnits, …) are delta Sums.

  • Enriched vended statistic-set metric — CPUUtilization, StatusCheckFailed*, CPUCreditBalance, Lambda Duration and the other "gauge"-shaped vended metrics reach this surface as delta exponential histograms that carry CloudWatch's statistic set. The catalog's gauge reads still name the right statistic, but spell them as histogram_quantile(1, <m>) for max, histogram_quantile(0, <m>) for min, histogram_avg(<m>) for avg (equivalently histogram_sum(<m>) / histogram_count(<m>)), and histogram_quantile(0.99, <m>) for a percentile — so a true pNN is available here, unlike what a "no percentile label" note might suggest. Reading it with a plain max() or min() yields nothing, and a plain avg() folds the buckets into one histogram rather than a single value. histogram_quantile(1, …) / (0, …) approximate the Max / Min via the top / bottom bucket bound (close to CloudWatch's Maximum / Minimum); intermediate quantiles are bucket-estimated.

  • Histogram — traces.span.metrics.duration is a native exponential histogram. Read percentiles with histogram_quantile(0.99, rate({__name__="traces.span.metrics.duration", ...}[5m])) on the base metric name — no _bucket suffix, no by (le).

  • Already-per-second gauges (RDS ReadIOPS, ReadThroughput) must not be wrapped in rate() — that double-rates into nonsense.

Applying rate() to a gauge, or averaging a per-period counter, is the most common way to produce a confident-but-meaningless number. When unsure, check the instrument type via discovery (section 3).

Running a PromQL query

CloudWatch serves PromQL through a Prometheus-compatible HTTP API, the same API the Omni console's metric views use. It is a different endpoint and signing name from the Omni cloudwatch-omni API: never send PromQL to cloudwatch-omni or to StartTelemetryQuery.

Endpointhttps://monitoring.<region>.amazonaws.com/api/v1/<operation>
Data operations/api/v1/query (instant: query, optional time), /api/v1/query_range (query, start, end, step); GET, or POST with a form-encoded body
Discovery operations/api/v1/series, /api/v1/labels, /api/v1/label/<name>/values (section 3, Method 2)
SigningSigV4, service name monitoring
IAMcloudwatch:GetMetricData + cloudwatch:ListMetrics for query operations; cloudwatch:ListMetrics for discovery
ResponseThe standard Prometheus JSON envelope (status, data.resultType, data.result)
Limits500 series per response (truncation is reported in warnings, still HTTP 200), 7-day maximum range including lookback, 20 s execution timeout
awscurl --service monitoring --region us-east-1 \
  -X POST 'https://monitoring.us-east-1.amazonaws.com/api/v1/query_range' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d "query=histogram_avg({__name__=\"Duration\", \"@instrumentation.@name\"=\"cloudwatch.aws/lambda\"})&start=$(date -d '-1 hour' +%s)&end=$(date +%s)&step=60"

From code, sign the same request with the SDK's SigV4 signer (for example botocore SigV4Auth(credentials, "monitoring", region)); there is no dedicated SDK method or aws cloudwatch CLI subcommand for these paths.

From an AI agent: the CloudWatch MCP server's PromQL tools call this API and handle signing — execute_promql_query (instant), execute_promql_range_query (time series over a window), and the discovery tools get_promql_label_values, get_promql_series, and get_promql_labels. An agent with those tools, or with shell access to a SigV4 client, runs the query itself; it does not need a dashboard panel or an alert rule first.

Time range binding

Where the query runs decides how time is expressed:

ContextTime handling
Ad hoc / programmatic query (Prometheus HTTP API, MCP tools)/api/v1/query evaluates at one instant (time, default now); /api/v1/query_range takes start, end, and step and returns a series per step. Range selectors inside the expression ([5m]) are the per-point lookback, as in Prometheus.
Omni StartTelemetryQuery (SQL)Takes only queryString and a sessionId; the string is always SQL, with the window in its WHERE `@timestamp` BETWEEN ... AND ... predicate. PromQL appears only embedded as promql('<data set>', '<expression>', <step-seconds>), and metric reads are not served on this path (section 1) — send metric queries to the Prometheus HTTP API instead.
Dashboard panel (../dashboards.md)Keep the expression windowless — the panel/dashboard scope drives the range. A range selector such as [5m] inside sum_over_time / rate is per-point lookback, not a window, and stays. Set queryLanguage: "promql" explicitly and match promqlQueryOptions.step (seconds) to the range vector — a [5m] window at the default 60 s step over-samples 5x; set step: 300.
Alert rule (../alerts.md)The alert has no window of its own — the expression must carry its own range selector ([5m]). Put the window where the instrument type takes a range vector: inside sum_over_time for a delta histogram (histogram_avg(sum_over_time(<sel>[5m])), never histogram_avg(<sel>[5m]), which fails to parse); see the alerts reference's "Windowing a metric alert". Do not bake the threshold comparison into the expression; the threshold is a separate field on the condition. Set language: PROMQL.
Common functions
NeedExpression shape
Per-second rate of a counterrate(<selector>[5m])
Total increase over a windowincrease(<selector>[1h])
Aggregate across seriessum(...), avg(...), max(...), min(...), count(...)
Group by an identitysum by (FunctionName) (rate(...)), avg by ("@resource.service.name") (...)
Top Ntopk(10, sum by (...) (rate(...)))
Ratio / derived metricrate(<errors>[5m]) / rate(<total>[5m]) for cumulative counters; for enriched vended delta Sums (e.g. Lambda / S3 error rates) use sum_over_time(<errors>[5m]) / sum_over_time(<total>[5m]) — a same-window rate()/rate() on delta Sums can read an impossible ratio. Labels must match on both sides; add on (...) / ignoring (...) when they do not
Percentile from a cumulative histogram (OTLP)histogram_quantile(0.99, rate(<base>[5m]))
Percentile from an enriched vended (delta) histogramhistogram_quantile(0.99, sum_over_time(<sel>[5m])), or windowless histogram_quantile(0.99, <sel>) on a dashboard panel
Smooth a gaugeavg_over_time(<gauge>[15m]), max_over_time(<gauge>[15m])

Worked examples:

# Lambda error rate per function (enriched vended delta Sums -> sum_over_time totals, then divide)
sum by (FunctionName) (sum_over_time({__name__="Errors", "@instrumentation.@name"="cloudwatch.aws/lambda"}[5m]))
  /
sum by (FunctionName) (sum_over_time({__name__="Invocations", "@instrumentation.@name"="cloudwatch.aws/lambda"}[5m]))

# Request rate per service from span RED metrics.
# calls is a delta Sum, so sum_over_time the window and divide by its seconds —
# rate() silently under-reads it (see the instrument table above).
sum by ("@resource.service.name") (sum_over_time({__name__="traces.span.metrics.calls"}[5m])) / 300

# p99 latency per service from the native histogram
histogram_quantile(0.99, sum by ("@resource.service.name") (rate({__name__="traces.span.metrics.duration"}[5m])))

# Pods per deployment in a namespace (gauge -> count as-is)
count by ("@resource.k8s.deployment.name") ({__name__="k8s.pod.phase", "@resource.k8s.namespace.name"="<ns>"})

# S3 request error ratio (digit-leading names -> __name__)
sum_over_time({__name__="4xxErrors", FilterId="EntireBucket"}[5m]) / sum_over_time({__name__="AllRequests", FilterId="EntireBucket"}[5m])

5. Per-service metric catalog

Each section names the CloudWatch namespace (for recognition only — the CloudWatch surface is not queryable here) and the identity label to filter and group by. On the PromQL surface an enriched vended metric is reached via "@instrumentation.@name"="cloudwatch.aws/<service>" (lowercase) plus the identity label. Each section below states the literal scope where it differs from the lowercased namespace (ALB: cloudwatch.aws/elasticloadbalancing); confirm the exact scope and label spelling against discovery.

Two universal rules:

  • Gauge vs counter decides the read (section 4).

  • On the enriched vended surface, spell the read by the OTLP instrument type, not the CloudWatch shape. No enriched vended metric is a raw gauge or a cumulative counter: every one arrives with delta temporality as either a Sum (counts — NetworkIn, Invocations, request/error counts) or an ExponentialHistogram (levels, latency, utilization — CPUUtilization, Duration, TargetResponseTime, *Latency). The Type and Read as columns below name the CloudWatch-side shape and the statistic you want; spell that statistic per section 3/4 — histogram: max histogram_quantile(1, …), min histogram_quantile(0, …), avg histogram_avg(…), percentile histogram_quantile(0.99, …); delta Sum: a windowed total with sum_over_time(…[5m]), used on both sides of a ratio. So a "gauge … avg/max" row for a vended latency metric is read with histogram_avg / histogram_quantile, and a true percentile is available even where a row says "no percentile label". A plain max() / min() empties on a histogram; rate() / increase() misread a delta Sum and can return an impossible value on a short window, so derive a rate as a sum_over_time ratio. This is spelling only where the surface is enriched — the same metric read through the CloudWatch APIs (non-enriched) keeps its classic gauge/counter statistics.

  • An empty series is often correct, not a bug. A signal that exists only under a specific configuration (an agent installed, a burstable instance type, a request-metrics filter, an export enabled) returns nothing when that condition is absent. Read "no data" as "not configured / not applicable here" before treating it as an outage.

Signal taxonomies:

  • Request-serving services (ALB, API Gateway, CloudFront, S3 request metrics, Application Signals) → RED: Rate, Errors, Duration.

  • Resources / infrastructure (EC2, EBS, RDS, ECS, Lambda concurrency) → USE / saturation: Utilization, Saturation, Errors, plus headroom signals (credit balances, free storage, free memory, queue depth) that trend toward a limit.

  • Flow / queue services (SQS, SNS) → throughput (counters) vs backlog and age (gauges).

EC2 — AWS/EC2 (+ CWAgent), identity InstanceId
MetricTypeUnitRead as
CPUUtilizationgauge (delta exp. histogram when enriched)Percentavg / max — spelled histogram_avg(…) / histogram_quantile(1, …); never rate()
CPUCreditBalancegauge (delta exp. histogram)Creditsmin — histogram_quantile(0, …); burstable (T-family) only
NetworkIn / NetworkOut / NetworkPacketsIn / NetworkPacketsOutcounter (delta Sum when enriched)Bytes / Countsum_over_time(…[5m]) (÷ 300 for per-second); rate() / increase() misread the delta Sum and return a wrong number
DiskReadBytes / DiskWriteBytes / DiskReadOps / DiskWriteOpscounter (delta Sum)Bytes / Countsum_over_time(…[5m]) — instance-store only, not EBS
StatusCheckFailed / _Instance / _System / _AttachedEBSbinary per-period (delta exp. histogram)0/1max — histogram_quantile(1, …); a 1 means it failed in the period; averaging hides it
InstanceEBSIOPSExceededCheck / InstanceEBSThroughputExceededCheckbinary0/1max (Nitro)
EBSIOBalance% / EBSByteBalance%gaugePercentmin — __name__ form
mem_used_percent, disk_used_percent (CWAgent)gaugePercentavg — only if the CloudWatch agent is installed; if enriched as a histogram, histogram_sum/histogram_count
  • CPUCreditBalance is read with min and exists only on burstable (T-family) instances — an empty series on other types is correct, not a fault. On a burstable instance a balance near 0 while CPU sits at the baseline is throttling: the hypervisor is capping the instance, so a flat "healthy-looking" CPU chart is the symptom, not reassurance. Say so whenever a credit panel is on a health view.

  • Memory and disk-space metrics exist only in CWAgent; an empty guest-OS chart usually means "no agent," not "no data" — the hypervisor cannot see guest RAM. Memory utilization is not available by default from AWS/EC2.

  • StatusCheckFailed* is a per-period 0/1 flag, not a level: read it with max — spelled histogram_quantile(1, …) on the enriched surface — because an average over a window that contained one failed check rounds the failure away.

  • The family is four metrics, and each points at a different owner — say which when you chart them: StatusCheckFailed (either check failed), StatusCheckFailed_System (the AWS host hardware or network underneath the instance — nothing to fix in the guest; stop/start moves it to a healthy host), StatusCheckFailed_Instance (the customer's own OS, boot, or network configuration inside the instance), and StatusCheckFailed_AttachedEBS (an attached volume is unreachable). A panel that shows the parent alone cannot tell the operator which side to look at.

  • DiskRead* / DiskWrite* count instance-store I/O only. The EBS side of an instance's I/O is the AWS/EBS family keyed by VolumeId (next entry), not an AWS/EC2 metric.

  • A health dashboard for a group of EC2 instances is built from this table, and the answer is the selectors themselves — written into the reply, not offered after the customer picks a product, and not replaced by CloudWatch console statistic names. Every selector carries the "@instrumentation.@name"="cloudwatch.aws/ec2" scope and groups by InstanceId; state that scope in the answer and write the selectors out:

    # S = "@instrumentation.@name"="cloudwatch.aws/ec2"; every result already carries InstanceId
    histogram_quantile(1, {__name__="CPUUtilization", "@instrumentation.@name"="cloudwatch.aws/ec2"})              # max CPU per instance
    histogram_avg({__name__="CPUUtilization", "@instrumentation.@name"="cloudwatch.aws/ec2"})                     # avg CPU per instance
    histogram_quantile(1, {__name__="StatusCheckFailed_System", "@instrumentation.@name"="cloudwatch.aws/ec2"})   # 0/1 per instance
    histogram_quantile(1, {__name__="StatusCheckFailed_Instance", "@instrumentation.@name"="cloudwatch.aws/ec2"})
    histogram_quantile(1, {__name__="StatusCheckFailed_AttachedEBS", "@instrumentation.@name"="cloudwatch.aws/ec2"})
    histogram_quantile(0, {__name__="CPUCreditBalance", "@instrumentation.@name"="cloudwatch.aws/ec2"})           # min credits; burstable only
    sum by (InstanceId) (sum_over_time({__name__="NetworkIn", "@instrumentation.@name"="cloudwatch.aws/ec2"}[5m])) # bytes per 5m; / 300 for B/s
    avg by (InstanceId) ({__name__="mem_used_percent", "@instrumentation.@name"="cloudwatch.aws/ec2"})   # CloudWatch agent only; a gauge — check __type__ first

    These are the spellings that return usable series on an enriched Space: max by (InstanceId) (...) returns nothing on the histogram metrics, and rate(...NetworkIn...) misreads the delta Sum and returns a wrong number rather than the per-5m total (section 3). __name__ takes no regex — one selector per check type, combined with or when one panel needs all three. The same names exist under the AWS/EC2 / CWAgent namespaces on the CloudWatch side for an account without enrichment; mention that as the fallback, not as the answer. The Omni dashboard body for this set is the EC2 fleet archetype in ../dashboards.md.

  • Correlate with: EBS (volume side of the same I/O), ALB (instances as targets), RDS (app tier driving the DB).

EBS — AWS/EBS, identity VolumeId
MetricTypeUnitRead as
VolumeReadOps / VolumeWriteOps / VolumeReadBytes / VolumeWriteBytescounterCount / Bytesrate()
VolumeTotalReadTime / VolumeTotalWriteTimecounterSecondsderived latency only (Xen-era)
VolumeAvgReadLatency / VolumeAvgWriteLatencygaugemsavg / max (Nitro — prefer these)
VolumeAvgIOPS / VolumeAvgThroughputgaugeops/s / KiB/savg (Nitro)
VolumeIOPSExceededCheck / VolumeThroughputExceededCheck / VolumeStalledIOCheckbinary0/1max
VolumeQueueLengthgaugeCountavg / max — a count, not a duration
BurstBalancegaugePercentmin — gp2/st1/sc1 only
  • Xen-era latency is derived: per-op read latency ≈ rate(VolumeTotalReadTime[5m]) / rate(VolumeReadOps[5m]) (x1000 for ms) — an average only, never a percentile. On Nitro, VolumeTotalReadTime is documented as not relevant; use the VolumeAvg* gauges.

  • BurstBalance is empty on gp3/io1/io2 (correct).

  • Correlate with: EC2 (instance side), RDS (its storage behaves like an EBS volume — a write-latency spike with healthy CPU points at storage).

Lambda — AWS/Lambda, identity FunctionName
MetricTypeUnitRead as
Invocations / Errors / Throttlescounter (delta Sum when enriched)Countsum_over_time(…[5m]) for a windowed total; error rate is a sum_over_time ratio (rate()/increase() misread the delta Sum)
Durationgauge (delta exp. histogram when enriched)mshistogram_avg(…) for avg, histogram_quantile(1, …) for max, histogram_quantile(0.99, …) for a true p99
ConcurrentExecutionsgaugeCountmax
ProvisionedConcurrencyUtilization / ClaimedAccountConcurrencygaugeratio 0–1 (e.g. 0.7, not a percent) / Countmax (saturation)
UnreservedConcurrentExecutionsgaugeCountaccount/region-level — no FunctionName
AsyncEventsReceived / AsyncEventsDroppedcounterCountrate()
AsyncEventAgegaugemsmax (async backlog)
IteratorAgegaugemsmax — Kinesis / DynamoDB Streams consumer lag
OffsetLaggaugeCountmax — Kafka / MSK consumer lag
  • Error rate is derived: sum_over_time(Errors[5m]) / sum_over_time(Invocations[5m]) (section 4 example) — Errors and Invocations are enriched delta Sums, so a rate()/rate() ratio can read a wrong (even impossible) value on a short window; the windowed-total ratio is stable. Do not read Errors alone without the denominator.

  • Grouping UnreservedConcurrentExecutions by function returns nothing (correct).

  • Cold starts are not a metric (Duration excludes cold-start time) — cold-start and error detail live in the function log group /aws/lambda/<function>, a SQL log query.

  • A true pNN for Duration is available on the enriched surface — it arrives as a delta exponential histogram (section 3/4), so histogram_quantile(0.99, {__name__="Duration", "@instrumentation.@name"="cloudwatch.aws/lambda", FunctionName="<fn>"}) returns a real p99. Deriving the percentile from spans over traces.default in SQL stays a valid alternative when the function's metric is not enriched.

  • Correlate with: API Gateway / ALB (invokers), SQS / DynamoDB (event sources).

DynamoDB — AWS/DynamoDB, identity TableName
MetricTypeUnitDimensionsRead as
SuccessfulRequestLatencygaugemsTableName, Operationavg / max — group by both or GetItem/Query/Scan blend
ThrottledRequestscounterCountTableName, Operationrate()
ConsumedReadCapacityUnits / ConsumedWriteCapacityUnitscounter (delta Sum when enriched)UnitsTableName, GlobalSecondaryIndexName (writes add Source)sum_over_time(…[5m]) (÷ window for per-second) — no Operation
ReadThrottleEvents / WriteThrottleEventscounterCountTableName, GlobalSecondaryIndexNamerate()
SystemErrorscounterCountTableName, Operationrate() — not published on TableName alone
UserErrorscounterCountaccount/region-levelrate()
ConditionalCheckFailedRequests / TransactionConflictcounterCountTableNamerate() (secondary)
  • A SystemErrors alert scoped to TableName only never has data — include Operation.

  • Compare consumed against provisioned capacity for saturation.

  • Correlate with: Lambda (pair throttles with function errors), Application Signals.

ECS — AWS/ECS and ECS/ContainerInsights, identity ClusterName (+ ServiceName)
MetricTypeUnitTierRead as
CPUUtilization / MemoryUtilizationgaugePercentcluster or serviceavg / max
EBSFilesystemUtilizationgaugePercentservice (with an EBS volume)max
CPUReservation / MemoryReservation / GPUReservationgaugePercentcluster, AWS/ECS, EC2 launch type onlyavg
RunningTaskCount / PendingTaskCount / DesiredTaskCountgaugeCountContainer Insights (opt-in)max / min
  • Split the tiers by dimension: service-tier series carry ClusterName and a non-empty ServiceName; cluster-tier series carry ClusterName only. Filtering the wrong one blends or empties the series.

  • Fargate emits no reservation metrics; GPUReservation also needs GPU instances (both correct-empty). Reservations are not under Container Insights.

  • Correlate with: ALB (in front), EC2 (container instances for EC2 launch type).

RDS — AWS/RDS, identity DBInstanceIdentifier
MetricTypeUnitRead as
CPUUtilizationgaugePercentavg / max
DatabaseConnectionsgaugeCountavg / max — never rate()
ReadIOPS / WriteIOPSgauge (already per-second)Count/savg — do not rate()
ReadThroughput / WriteThroughputgauge (already per-second)Bytes/savg — do not rate()
ReadLatency / WriteLatencygaugeSecondsavg / max (x1000 for ms) — raw, not derived
DiskQueueDepthgaugeCountmax (storage saturation)
FreeableMemory / FreeStorageSpacegaugeBytesmin — free, so alert below a threshold
ReplicaLaggaugeSecondsmax
BurstBalance, EBSIOBalance%, EBSByteBalance%gaugePercentmin — burstable storage only; __name__ form for %
ServerlessDatabaseCapacitygaugeACUsavg — Aurora Serverless only
  • Best chart: CPUUtilization overlaid with DatabaseConnections. CPU tracking connections is an app problem; CPU high at flat connections is a query/plan problem.

  • RDS has no *TotalTime metrics — latency is raw, unlike EBS.

  • Logs require export: /aws/rds/instance/<id>/<logType> (error / slowquery / audit / general / postgresql); the error log is free-form.

  • Correlate with: EBS (storage layer), EC2 (app tier).

Application Load Balancer — AWS/ApplicationELB, scope cloudwatch.aws/elasticloadbalancing, identity LoadBalancer
MetricTypeUnitRead as
RequestCount / ActiveConnectionCountcounterCountrate()
HTTPCode_Target_2XX_Count / _3XX_ / _4XX_ / _5XX_CountcounterCountrate() — target-generated
HTTPCode_ELB_3XX_Count / _4XX_ / _5XX_Count (+ _500_ / _502_ / _503_ / _504_Count)counterCountrate() — LB-generated, a different family
TargetResponseTimegauge (delta exp. histogram when enriched)Secondsavg / max (x1000 for ms) — histogram_avg / histogram_quantile on the enriched surface; p99 via histogram_quantile(0.99, …)
HealthyHostCount / UnHealthyHostCountgaugeCountmin / max — per TargetGroup only
RejectedConnectionCount / TargetConnectionErrorCount / TargetTLSNegotiationErrorCountcounterCountrate() (saturation / connection errors)
LambdaUserError / LambdaInternalErrorcounterCountrate() (Lambda targets)
  • Scope is "@instrumentation.@name"="cloudwatch.aws/elasticloadbalancing" — not cloudwatch.aws/applicationelb, which the namespace rule would suggest and which returns 0 series with HTTP 200 (no error, just an empty chart).

  • The names carry a _Count suffix — HTTPCode_Target_5XX alone returns nothing.

  • An ELB-5xx spike with flat target-5xx points at the LB / health-check path (e.g. no healthy target), not the app.

  • The LoadBalancer label value is the app/<name>/<hash> segment, not the full ARN. ALB (AWS/ApplicationELB) ≠ NLB (AWS/NetworkELB) ≠ CloudWatch (AWS/ELB).

  • Fractional host counts are normal from flapping.

  • Correlate with: EC2 (instance targets), Lambda (Lambda targets).

SQS — AWS/SQS, identity QueueName
MetricTypeUnitRead as
ApproximateAgeOfOldestMessagegaugeSecondsmax — the best signal
ApproximateNumberOfMessagesVisiblegaugeCountmax (backlog)
ApproximateNumberOfMessagesNotVisiblegaugeCountmax (in flight — not backlog)
ApproximateNumberOfMessagesDelayedgaugeCountmax (delay timer)
NumberOfMessagesSent / Received / DeletedcounterCountsum(rate(...)) — not the average
  • A low-depth queue with high oldest-age is a poison pill or stalled consumer.

  • Total enqueued ≈ Visible + NotVisible + Delayed.

  • FIFO queues share the same metrics. A dead-letter queue is just another QueueName — rising Visible on a DLQ means the source queue is failing processing.

  • Correlate with: SNS (fan-out source), Lambda (event-source-mapping consumer — pair age/backlog with function Errors / Throttles / Duration).

SNS — AWS/SNS, identity TopicName
MetricTypeUnitRead as
NumberOfMessagesPublished / NumberOfNotificationsDelivered / NumberOfNotificationsFailedcounterCountrate()
NumberOfNotificationsFilteredOut (+ -MessageAttributes / -MessageBody / -InvalidAttributes)counterCountrate() — "why didn't my subscriber get it"
NumberOfNotificationsRedrivenToDlq / NumberOfNotificationsFailedToRedriveToDlqcounterCountrate()
PublishSizegaugeBytesavg
SMSSuccessRategaugeFractionavg — a sum is meaningless
SMSMonthToDateSpentUSDcumulative, resets monthlyUSDmax within the month — never rate()
  • NumberOfNotificationsFailed is a delivery failure — a failed Publish call surfaces in API 4xx/5xx, not here.

  • Do not compute delivered / published as a success rate: one publish fans out to N subscribers, so delivered normally exceeds published.

  • Correlate with: SQS (subscriber queue depth / DLQ), Lambda (subscribed functions).

S3 — AWS/S3, identity BucketName (two families)
FamilyMetricTypeUnitRequired labelRead as
Storage (daily, free)BucketSizeBytesgaugeBytesStorageType (e.g. StandardStorage)max over a multi-day window
Storage (daily, free)NumberOfObjectsgaugeCountStorageType="AllStorageTypes"max over a multi-day window
Request (1-minute, opt-in, billed)AllRequests, GetRequests, PutRequests, ...counter (delta Sum when enriched)CountFilterId (commonly EntireBucket)sum_over_time(…[5m]) for a windowed total
Request4xxErrors / 5xxErrorscounter (delta Sum when enriched)CountFilterIderror rate is a sum_over_time ratio (see section 4); __name__ form
RequestFirstByteLatency / TotalRequestLatencygaugemsFilterIdavg / max
  • Storage metrics look flat or broken at short ranges because emission is daily. Omitting StorageType returns multiple or no series.

  • Request metrics exist only where a request-metrics filter is configured; omitting FilterId double-counts. An empty request overview is a config gap, not a bug.

  • Error ratio: see the S3 example in section 4.

  • Correlate with: CloudFront (most public reads hit the CDN and never reach S3 request metrics), ALB (same RED grammar).

API Gateway — AWS/ApiGateway, identity varies by flavour
FlavourIdentityRED metricsError metrics
REST (v1)ApiName (+ Stage, Resource, Method)Count (counter), Latency, IntegrationLatency (gauges, ms)4XXError / 5XXError (counters, __name__ form)
HTTP (v2)ApiId (+ Stage, Route)Count, Latency, IntegrationLatency, DataProcessed4xx / 5xx (counters, __name__ form)
WebSocket (v2)ApiId (+ Stage, Route)ConnectCount, MessageCount, IntegrationLatencyIntegrationError, ClientError, ExecutionError
  • Using the wrong identity label (ApiName vs ApiId) returns an empty series. WebSocket APIs do not emit 4xx / 5xx / Count / Latency.

  • Derived: gateway overhead = Latency - IntegrationLatency. High Latency with flat IntegrationLatency implicates the gateway/config; the reverse implicates the backend.

  • CacheHitCount / CacheMissCount are empty unless caching is enabled (correct). Method/route-level metrics need "detailed metrics" enabled. Access/execution logs are opt-in with no fixed group name.

  • Correlate with: Lambda (pair IntegrationLatency with Duration), Application Signals.

CloudFront — AWS/CloudFront, identity DistributionId (+ fixed Region="Global")
MetricTypeUnitRead as
RequestscounterCountrate() for req/s, increase() for a daily total
BytesDownloaded / BytesUploadedcounterBytesrate()
TotalErrorRate / 4xxErrorRate / 5xxErrorRategaugePercentavg — never rate(), never divide by Requests (already a percentage); __name__ form
  • 4xx (client / cache-miss 403/404) vs 5xx (origin-fetch) split the headline error rate.

  • Metrics live only in us-east-1 under Region="Global" regardless of edge location — query us-east-1; never emit another region matcher for CloudFront.

  • Cache-hit-rate and per-edge metrics are opt-in (paid). Access logs go to S3 or a stream, not CloudWatch Logs.

  • Correlate with: S3 (pair 5xxErrorRate with S3 5xxErrors), ALB origin (target 5xx).

Application Signals — AWS/ApplicationSignals, identity Service (SLOs: SloName)
MetricTypeUnitRead as
Latencygaugemsavg / max
FaultcounterCountrate() — HTTP 5xx + OTel span-status errors
ErrorcounterCountrate() — HTTP 4xx client errors (excluding intentional, e.g. 429)
TotalRequestCountPerMinute / BadRequestCountPerMinutecounterCountrate() (request-based SLOs; observability only)
AttainmentRate / BreachedCountgauge / counterPercent / Countmin / rate() — dimensioned by SloName
  • The names are Fault / Error, not FaultCount / ErrorCount. Service dimensions are Service, Operation, Environment; dependency metrics add RemoteService / RemoteOperation.

  • Availability = (1 - Fault / Total) x 100, where Total is the request count (classically SampleCount(Latency)).

  • This is the entry point for "how is <service> doing?" when the entity is an Application Signals service or SLO — it aggregates across the infrastructure services above.

  • Correlate with: the infrastructure behind the span (EC2/ECS/Lambda), and the traces themselves for the offending span path.

Kubernetes / EKS (OTLP) — identity "@resource.service.name" (RED), "@resource.k8s.*" (USE)

OTLP-ingested Kubernetes workloads have no CloudWatch namespace and no vended-AWS instrumentation scope. Group and filter by the double-quoted "@resource.*" labels.

SignalMetricTypeRead as
Request / error ratetraces.span.metrics.callsdelta Sumsum by ("@resource.service.name") (sum_over_time(...[5m])) / 300 — not rate(), which under-reads a delta Sum; split errors by "status.code" (no @ scope, but double-quoted — values OK / ERROR / UNSET)
Latency p50/p90/p99traces.span.metrics.durationnative histogramhistogram_quantile(0.99, rate(...[5m])) on the base name
Container / pod CPU and memory vs requests/limitsOTLP k8s.* / container.* resource metricsgaugeavg / max, grouped by "@resource.k8s.namespace.name" → "@resource.k8s.deployment.name" → "@resource.k8s.pod.name" (broadest to narrowest)
Restarts, pod phasek8s.container.restarts, k8s.pod.phasecounter / gaugeincrease() / count by (...)
  • This is the same span-derived layer the service map uses.

  • An empty series usually means the workload is not exporting that OTLP metric (no collector/exporter for it), not that usage is zero.

  • Correlate with: Application Signals (application-scoped RED of the same spans), node / EC2 metrics for host-level saturation.


6. Reading results

Empty series: expected vs real

Before treating "no data" as an outage, check whether the metric can exist here:

Empty series onUsually means
Any vended AWS metric (CPUUtilization, Invocations, ...)OTel enrichment is not enabled for this account/region, resource-tags-on-telemetry was not enabled first, or the resource type is not supported (section 2)
A metric selected with a bare dotted/digit-leading nameParse problem — use the __name__ form (section 4)
A vended metric with the wrong identity label (ApiName vs ApiId, HTTPCode_Target_5XX without _Count)Wrong label or name — re-check discovery
CPUCreditBalance, BurstBalance, EBSIOBalance%Non-burstable instance / volume type — correct
CWAgent memory / disk-space metricsCloudWatch agent not installed — correct
S3 request metricsNo request-metrics filter configured — correct
S3 storage metrics at a short rangeDaily emission — widen to multiple days
ECS reservationsFargate, or a service-tier filter on a cluster-tier metric
UnreservedConcurrentExecutions grouped by function; SystemErrors on TableName aloneMetric is not published at that dimensionality
Any enriched vended metric read with a bare max() / min() while the bare selector shows seriesIt is a delta ExponentialHistogram — spell it histogram_quantile(1, …) / (0, …) / histogram_avg(…) (section 3). A bare avg() does not empty (it hands back one merged-bucket series); rate() over a delta Sum returns a wrong, not empty, number
CloudFront outside us-east-1 / with a region matcherMetrics exist only under Region="Global" in us-east-1
API Gateway 4xx / Count / Latency on a WebSocket APIWebSocket emits a different set
Any OTLP / Kubernetes metricNot exported by the workload's collector
Any "@resource.*" groupingUsed job= / service= / k8s_* instead of the "@resource.*" form

An empty result is not a reason to fabricate a number or switch to a log scan; it is a reason to re-ground the name (section 3) or explain the missing configuration.

Common errors and their causes
SymptomCauseFix
ValidationException: Metrics queries are not supported.A SQL query aimed at metrics — there is no metrics.default tableWrite PromQL; set the language to PromQL
"CloudWatch metric queries are not yet supported in CloudWatch Omni (some vended metrics are not available)."The metric is a non-enriched vended or usage metricEnable enrichment (if supported), or use CloudWatch outside Omni
Parse error on a selectorMetric name written as a bare token inside the braces, or a dotted / @ label left unquoted{__name__="<name>", "@label"="..."}
Selector must have a metric name. Found matchers: [__name__, ...] (400)__name__ written as a regex matcher (=~)One selector per exact metric name; combine with or + label_replace
max() / min() over an enriched vended metric returns no series (a bare avg() instead hands back one merged-bucket series, not a number), but the raw selector lists the instancesThe metric is a delta ExponentialHistogram (__type__)histogram_quantile(1, …) / histogram_quantile(0, …) / histogram_avg(…)
rate() / increase() over an enriched vended count returns an impossibly small or wrong numberThe metric is a delta Sum (__temporality__="delta"); rate() / increase() assume a cumulative countersum_over_time(…[5m]) (÷ window for per-second)
A rate that is impossibly small or noisyrate() applied to a gauge, or to an already-per-second gauge (RDS IOPS/throughput)Read gauges with avg / max
A "count" that never grows / averages to nonsenseavg() on a counterrate() / increase() for a cumulative counter; sum_over_time(…[5m]) for an enriched vended delta Sum
A percentile that disagrees with the consoleRead a vended latency histogram (Duration, TargetResponseTime, SuccessfulRequestLatency) with a plain quantile() or a _bucket / by (le) formUse histogram_quantile(0.99, {__name__="<metric>", …}) on the enriched delta histogram, or derive from spans in SQL
Latency off by 1000xUnit mismatch — ALB TargetResponseTime and RDS *Latency are seconds; Lambda Duration, DynamoDB, EBS VolumeAvg* are msConvert explicitly
Alert fires twice as often as expected / cannot be retunedThreshold comparison baked into the PromQL expressionRemove it; the threshold is a field on the condition (../alerts.md)
Dashboard panel frozen or ignoring the time pickerAbsolute time pinned in the expressionKeep panel queries windowless; let scope drive the range (../dashboards.md)
Panel shows nothing, no errorqueryLanguage not set to promqlSet it explicitly — it is not inferred

7. Where PromQL is used

PromQL is the metric language everywhere in Omni, not only for ad hoc queries:

  • Dashboards — ../dashboards.md. Each explore panel takes a queryString, an explicit queryLanguage ("promql" for metrics, "sql" for log/trace content), autoRun: true, and promqlQueryOptions.step in seconds. Panel expressions are windowless; the @-labels are double-quoted PromQL and therefore JSON-escaped once more inside the queryString. The dashboard reference includes per-archetype panel templates (Lambda, EC2, ALB, RDS, Kubernetes) whose PromQL follows the conventions in this file.

  • Alerts — ../alerts.md. A condition on a metric must use language: PROMQL; the expression carries its own range selector ([5m]) and no threshold comparison. Metric alerts on a gauge, a counter rate, or a histogram_quantile percentile all go through this path; SQL alerts reach logs and traces only.

  • Ad hoc and programmatic queries — CloudWatch's Prometheus-compatible HTTP API (monitoring.<region>.amazonaws.com/api/v1/query and /query_range, SigV4 service monitoring) or the CloudWatch MCP server's PromQL tools (section 4, "Running a PromQL query"). Omni's own StartTelemetryQuery is SQL-only and does not serve metric reads (section 1).

  • Views — views.md are SQL over logs and traces; they do not wrap PromQL.

For the concepts behind Spaces, data sets, and access — which govern what an account's PromQL can see — see ../concepts.md.

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server, Needs a command-line tool, Needs Python, Needs Node.js

Security

What our scanner noticed· Low risk

  • Phrasing that asks for actions without confirmation
  • Instructions that use the network
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
AWS
Publisher
AWS (Official)
Source
aws/agent-toolkit-for-aws · skills/core-skills/aws-observability · commit 0a218f8
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • NOTICE (added by AutoGineer from NOTICE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced
Also distributed in
aws/agent-toolkit-for-aws

AutoGineer is not affiliated with or endorsed by AWS.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download aws-observability.zip