AWS Observability
AWSOfficialPublished by AWS
- Skill
- Tool integration
- Cloud and DevOps
- Apache-2.0
- Low risk
- Has scripts
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
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/aws/aws-observability/downloadUnzip 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:
| CloudWatch | CloudWatch Omni | |
|---|---|---|
| What it is | Log groups, metric namespaces, alarms, Log Insights, X-Ray, Application Signals | Application 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 plane | aws cloudwatch, aws logs, aws xray, aws application-signals | aws cloudwatchomni (endpoint prefix cloudwatch-omni, signing name cloudwatch) |
| Query | Log Insights query language; GetMetricData | SQL over logs.default / traces.default; PromQL over metrics; named views |
| Notify | Alarms (metric, composite, anomaly) | Alerts (SQL/PromQL rule, contributors, OK/WARNING/CRITICAL/NODATA) |
| Topology | Application Signals service map | Context graph (GetContextGraph) |
| Access | IAM only | Domain → Space → access grants and access profiles (set up in setting-up-cloudwatch-observability) |
| Only here | Dynamic Instrumentation, Synthetics canaries, CloudTrail auditing, EMF | Agent-quality evaluation, views, context graph |
| First-time setup | Application Signals onboarding — ADOT auto-instrumentation, the amazon-cloudwatch-observability add-on, ServiceEvents, CI/CD metadata | Domain → Space → grants → telemetry in → plain-ADOT instrumentation |
| References | references/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.
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".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.
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 Regionlist-spacesis account-global (the--regionflag 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. Seereferences/cloudwatch-omni/concepts.mdfor 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 orbrew upgrade awscli;pip install -U boto3 botocore) and have them re-runaws 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.
First-time setup, on EITHER product → STOP and route to the
setting-up-cloudwatch-observabilityskill. 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, theamazon-cloudwatch-observabilityadd-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 probinglist-spacesyourself. 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 doCALLS,ACCESSES,RUNS_ONmean → 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-investigationskill and read itsreferences/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.
| Task | Open | Section to surface |
|---|---|---|
| Authoring or advising on an Omni alert | alerts.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 changes | agent-evaluation.md | every "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 windows | query/trace-exploration.md | every "state … in the answer" callout |
| Authoring, saving, reading back, or debugging an Omni dashboard | dashboards.md | "Facts you MUST surface when building or saving an Omni dashboard" |
Routing — CloudWatch (references/cloudwatch/)
| User need | Action |
|---|---|
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 steps | STOP — 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 EMF | Read 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 traps | Read 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 issues | Read 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 cause | Load the aws-investigation skill — completeness and scope first (references/completeness-and-root-cause.md), then root-cause analysis and the closing checklist |
| Debugging canary failures | Read synthetics.md — see Common failures table |
| CloudTrail operational auditing | Read cloudtrail.md |
| Setting up Lambda monitoring with CDK | Use alarm-template.ts as a starting point |
| Creating synthetic canaries | Read synthetics.md |
| Configuring ADOT collector | Use 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 need | Action |
|---|---|
| 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 starts | Read concepts.md |
Query logs or traces — SQL (SELECT … FROM logs.default / traces.default / default), field access, schema discovery, slowest / failed spans (durationNano, status.code), TABLESAMPLE | Read 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 object | Read 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 counter | Read 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 APIs | Read 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; notifications | Read 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, ListObservations | Read 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, GetContextGraph | Read 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 it | Read 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 baseline | Read 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 agent | Read 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 investigation | Connecting 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 Signals | Route 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 areas | Read the most specific reference first, then consult others as needed |
Files
references/cloudwatch/
| File | Content |
|---|---|
| alarms.md | Metric, composite, anomaly detection alarms — configuration, constraints, recommended defaults |
| log-insights.md | Complete query syntax, commands, functions, known issues, reusable query library |
| metrics.md | Custom metrics, EMF spec, metric filters, high-resolution, retention |
| tracing.md | X-Ray → ADOT migration, sampling rules, annotations vs metadata, collector config |
| dashboards.md | Widget types, cross-account/region, dynamic labels, sharing |
| troubleshooting.md | Error → cause → fix for all observability services |
| cloudtrail.md | Operational auditing, event types, S3+Athena queries |
| synthetics.md | Canary runtime/blueprint constraints, VPC networking, common failures |
| dynamic-instrumentation.md | Dynamic 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.ts | Best-practice CDK Lambda monitoring (alarms + dashboard) |
| otel-config.yaml | ADOT 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/
| File | Content |
|---|---|
| concepts.md | What 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.md | The 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.md | Omni 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.md | Service 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.md | The 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.md | SQL 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.md | Metrics 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.md | Working 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.md | Named SQL views: CreateView / UpdateView / DeleteView / ListViews, FROM view.<name>, naming and definition rules, composition patterns |
| dashboards.md | Omni 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.md | Omni 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.md | Agent-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
- ATTRIBUTION.mdAdded by AutoGineer899 B
- LICENSELicence file added by AutoGineer9.9 KiB
- NOTICELicence file added by AutoGineer67 B
- SKILL.md37.0 KiB
- assets/cloudwatch/alarm-template.tsScript3.9 KiB
- assets/cloudwatch/otel-config.yaml1.9 KiB
- references/cloudwatch-omni/agent-evaluation.md114.9 KiB
- references/cloudwatch-omni/alerts.md108.5 KiB
- references/cloudwatch-omni/concepts.md29.3 KiB
- references/cloudwatch-omni/context-graph.md32.6 KiB
- references/cloudwatch-omni/dashboards.md79.4 KiB
- references/cloudwatch-omni/intelligence.md16.9 KiB
- references/cloudwatch-omni/programmatic-access.md15.5 KiB
- references/cloudwatch-omni/query/promql-metrics.md66.8 KiB
- references/cloudwatch-omni/query/sql-logs-traces.md45.1 KiB
- references/cloudwatch-omni/query/trace-exploration.md26.1 KiB
- references/cloudwatch-omni/query/views.md12.8 KiB
- references/cloudwatch-omni/service-health.md17.5 KiB
- references/cloudwatch/alarms.md11.6 KiB
- references/cloudwatch/cloudtrail.md5.4 KiB
- references/cloudwatch/dashboards.md5.8 KiB
- references/cloudwatch/dynamic-instrumentation.md39.3 KiB
- references/cloudwatch/dynamic-instrumentation/breakpoint-creation.md16.3 KiB
- references/cloudwatch/dynamic-instrumentation/call-tree-and-directions.md5.7 KiB
- references/cloudwatch/dynamic-instrumentation/snapshot-parsing.md4.2 KiB
- references/cloudwatch/log-insights.md8.3 KiB
- references/cloudwatch/metrics.md7.5 KiB
- references/cloudwatch/synthetics.md6.5 KiB
- references/cloudwatch/tracing.md9.4 KiB
- references/cloudwatch/troubleshooting.md7.5 KiB
- scripts/cloudwatch-omni/capture_dataset_from_traces.pyScript25.2 KiB
- scripts/cloudwatch-omni/evaluate_traces.pyScript42.7 KiB
- scripts/cloudwatch-omni/test_evaluate_traces.pyScript12.6 KiB
- scripts/cloudwatch/di_app_signals_client.pyScript3.3 KiB
- scripts/cloudwatch/di_capture.pyScript8.5 KiB
- scripts/cloudwatch/di_constants.pyScript560 B
- scripts/cloudwatch/di_crud_rendering.pyScript13.1 KiB
- scripts/cloudwatch/di_crud_tools.pyScript28.8 KiB
- scripts/cloudwatch/di_error_translation.pyScript5.6 KiB
- scripts/cloudwatch/di_formatting.pyScript992 B
- scripts/cloudwatch/di_gateway.pyScript7.0 KiB
- scripts/cloudwatch/di_instrumentation.pyScript13.0 KiB
- scripts/cloudwatch/di_location.pyScript15.3 KiB
- scripts/cloudwatch/di_logs_client.pyScript4.2 KiB
- scripts/cloudwatch/di_region.pyScript2.0 KiB
- scripts/cloudwatch/di_result.pyScript2.0 KiB
- scripts/cloudwatch/di_session.pyScript2.0 KiB
- scripts/cloudwatch/di_snapshot_parsing.pyScript11.4 KiB
- scripts/cloudwatch/di_snapshot_queries.pyScript2.4 KiB
- scripts/cloudwatch/di_snapshot_rendering.pyScript12.5 KiB
- scripts/cloudwatch/di_snapshot_tools.pyScript12.0 KiB
- scripts/cloudwatch/di_snapshots.pyScript12.8 KiB
- scripts/cloudwatch/di_status_assessment.pyScript4.7 KiB
- scripts/cloudwatch/di_status_rendering.pyScript11.8 KiB
- scripts/cloudwatch/di_status_tools.pyScript13.0 KiB
- scripts/cloudwatch/di_validation.pyScript11.2 KiB
references/cloudwatch-omni/query/sql-logs-traces.md
Omni Query Syntax Reference
The SQL dialect for querying logs and traces in CloudWatch Omni. Both Omni experiences — Application Observability (APM, distributed tracing, log analysis) and Agent Observability (LLM/AI agent monitoring) — use this same SQL query surface. All queries are read-only SELECT statements.
The SQL syntax is identical whether you query logs or traces. They share the exact same grammar, functions, quoting rules, and constraints. The only difference between tables is the data they contain — that is, which fields are present. This reference documents the dialect once, covering logs and traces in Omni.
The schema is dynamic and varies across the data. All telemetry for an account (logs, traces, metrics) is stored in Omni, and different applications, services, or telemetry types will have completely different field structures. One application's logs may have fields that another's does not. The only fields guaranteed to exist on every record are the
@-prefixed system fields documented below. Before writing a query that references any other field, discover the actual schema by narrowing to the relevant slice of data withEXPLAIN (ANALYZE_FIELDS)(see Schema Discovery).
0. Running a Query
A statement is executed with these three operations, in order — there is no single "execute query" call:
StartTelemetryQuerySession→ returns asessionId. One session carries many queries; reuse it rather than opening one per statement.StartTelemetryQuerywithsessionId+queryString(the SQL below) → returns aqueryId. A statement that cannot be planned fails here, with the schema or parse error in the response.GetTelemetryQueryResultswithqueryId— and onlyqueryId; passingsessionIdtoo is rejected as an unknown parameter. Poll it untilstatusisComplete(results inrows) orFailed. A fresh query needs a moment: expect an in-progress status before the firstComplete.
A session is closed with StopTelemetryQuerySession, and a long-running query with
StopTelemetryQuery. Sessions expire on their own; a reused sessionId that has gone
stale fails with Session has been terminated. Please call StartSession to create a new session., which means open a new session, not that the SQL is wrong.
In the AWS CLI these are aws cloudwatchomni start-telemetry-query-session,
start-telemetry-query and get-telemetry-query-results. The same three operations run
the queries in views.md; see
programmatic-access.md for client setup and versions.
1. Tables & Addressing
| Table Reference | Contents |
|---|---|
default | Logs + traces combined |
logs.default | Logs only — syntactic sugar for WHERE \@telemetry_type` = 'logs'` |
traces.default | Traces/spans only — syntactic sugar for WHERE \@telemetry_type` = 'traces'` |
Each <type>.default form is simply a convenience filter on `@telemetry_type`. Querying default and adding WHERE \@telemetry_type` = 'logs'is equivalent to queryinglogs.default`.
Agent-evaluation scores (gen_ai.evaluation.*) are log records in logs.default, never
span columns on traces.default; filtering on them against traces.default returns zero rows
without error, indistinguishable from "no evaluations ran" — see agent-evaluation.md's "Query
surface" section.
Metrics are not in this table. Metrics are not SQL — they are queried with PromQL, and there is no metrics.default FROM target. See promql-metrics.md for the PromQL surface, label conventions, and per-service metric catalog.
FROM clause quoting is flexible — all of these are equivalent:
SELECT * FROM logs.default WHERE ...
SELECT * FROM "logs.default" WHERE ...
SELECT * FROM "logs"."default" WHERE ...The dot-separated form (logs.default) is the most common. Use whichever you prefer.
2. Required Time Range
Every query must include a filter on `@timestamp`. Queries without a time-range filter are rejected.
`@timestamp`values are timestampsOn a span,
`@timestamp`is the span END time (endTimeUnixNano), not the start. A window that only brackets when a request started therefore excludes every span of that request that finished after the window closed — typically the slow root or client span that carries the error — and the query still completes normally, returning the fast child spans as if they were the whole trace. When fetching a whole trace or correlating bytraceId, push the upper bound out by the longest span duration you expect (a span that ran for ten minutes is stamped ten minutes after it started).When self-joining, the
`@timestamp`filter is required on both sides of the join
Relative time range:
SELECT `@timestamp`, `@record`
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()Absolute time range:
SELECT `@timestamp`, `@record`
FROM default
WHERE `@timestamp` BETWEEN to_timestamp_nanos('2026-08-20T15:25:48.000Z')
AND to_timestamp_nanos('2026-08-20T16:25:48.000Z')INTERVAL syntax: a quoted number followed by the time unit — MINUTE/MINUTES, HOUR/HOURS, or DAY/DAYS. Example: INTERVAL '30 MINUTE', INTERVAL '2 HOURS', INTERVAL '7 DAYS'.
Debugging a rejected or empty query (the three things that surprise people, together):
The time filter is mandatory. A query with no
`@timestamp`bound is rejected outright. This is the most common cause of a "why was this rejected?".SELECT *is not the wide row you expect. It collapses to`@timestamp`, `@record`(see System Fields), not every discovered column.A bare top-level field name is often wrong. A name like
serviceorhostis usually a nested resource attribute reached with bracket notation (e.g.resource['attributes']['service.name']). Under the permissive schema, a field that does not exist silently returns NULL instead of erroring (see Field Access), so a mistyped or wrong-level name gives empty results rather than a rejection.
3. System Fields
These @-prefixed fields are the only fields guaranteed to exist on every record, regardless of telemetry type or what was ingested. Everything else is dynamic.
| Field | Description |
|---|---|
`@timestamp` | Mandatory filter field. Every query must constrain this. On a span it is the span END time (endTimeUnixNano), not the start — see the note in Required Time Range. |
`@record` | All user-visible data as JSON. Contains the full record structure. |
`@message` | Original ingestion payload. Permission-gated — may not be available to all users. |
`@ingest_time` | When the record was ingested. |
`@telemetry_type` | The record's telemetry type, e.g. 'logs' or 'traces'. |
The following system fields are present when the data source provides them:
| Field | Description |
|---|---|
`@aws.account` | The AWS account where the data was originally ingested. |
`@aws.region` | The AWS region where the data was originally ingested. |
`@data_source_name` | Identifies the source of the data (e.g., "amazon_vpc"). See AWS data sources and third-party data sources. |
`@data_source_type` | The type of data source (e.g., "flow"). See AWS data sources and third-party data sources. |
`@data_format` | The format of the ingested data (e.g., "VPC_FLOW_LOGS"). |
`@logGroupName` | The CloudWatch log group name (e.g., "/aws/vpcflowlogs/vpc-xxx"). |
`@logStream` | The CloudWatch log stream name. |
Within the same @data_source_name and @data_source_type (or @data_format), the schema is mostly stable and publicly documented. It is worth browsing the relevant AWS service documentation externally for detailed field definitions.
Important: SELECT * collapses to `@timestamp`, `@record`.
4. Field Access & Quoting
Top-level fields
Plain identifiers work for simple field names:
SELECT myField FROM ...Fields starting with
@or containing special characters (.,-) MUST be backtick-quoted:`@timestamp`,`field-name`Unquoted
@-prefixed fields are rejected with an error about variable references
-- Correct
SELECT `@message`, `@timestamp` FROM default WHERE ...
-- WRONG — will fail
SELECT @message, @timestamp FROM default WHERE ...Nested field access (bracket notation)
When JSON with nested objects is ingested, navigate into nested values using the root-level field name followed by bracket notation with string literals:
-- Given ingested JSON: { "request": { "method": "GET", "url": "/api" } }
SELECT request['method'], request['url']
FROM logs.default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()Key rules:
The root-level field is referenced as-is (or backtick-quoted if it has special characters)
Nested keys use
['key']with single-quoted string literalsSpecial characters inside the bracket string are fine — no backticks needed there:
-- Given ingested JSON: { "attrs": { "http.method": "GET", "service-name": "api" } }
SELECT attrs['http.method'], attrs['service-name']
FROM logs.default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()Multiple nesting levels chain brackets:
root['level1']['level2']
The field names shown here are examples of the syntax. The actual fields present depend entirely on the ingested data — use Schema Discovery to find what exists.
Permissive schema: Referencing a field that doesn't exist in the data will silently return NULL rather than producing an error. This means typos in field names won't fail your query — they'll just give empty results. Always verify field names via schema discovery if results look unexpectedly empty.
An alias-qualified bracket path resolves to NULL. With a table alias in scope,
t.attributes['k'] and t.resource['attributes']['k'] return NULL everywhere — in
SELECT, in WHERE and in a JOIN ... ON — while the same path written without the
alias returns the value. Measured on one query over traces.default:
COUNT(t.attributes['session.id']) = 0 against COUNT(attributes['session.id']) =
122,670. Nothing errors, so a join written the qualified way silently matches zero rows.
Write bracket paths unqualified, or project the nested field to a flat alias in a CTE or
derived table first and refer to that alias. Alias-qualifying a top-level column
(t.`@timestamp`, t.name) is unaffected.
An all-NULL column from a query that ran is a signal to verify the field name before concluding the data is absent — not proof the name is wrong. A correctly-named field can legitimately be NULL across every matching record, so rule out the three ways a field name can be wrong first:
Misspelled or non-existent field — permissive schema returns NULL, not an error. Re-discover the real names with Schema Discovery.
A root-level field quoted wrong — a field starting with
@or containing a./-MUST be backtick-quoted, and an unquoted@-prefixed field does not run empty — it is rejected with a variable-reference error. So a query that ran at all had its@-fields quoted correctly; the emptiness is in some non-@field.A nested key over-quoted — inside bracket notation the key is a plain single-quoted string; special characters inside the bracket string need no backticks (
attrs['http.method']). Adding backticks there changes the key and silently returns NULL.
5. Schema Discovery
The schema is dynamic — it reflects what has been ingested, not a fixed definition. Since all telemetry for an account is stored together, different applications and services will have completely different fields. A query against all data will show the union of every application's schema, which is rarely useful. Always narrow discovery to a specific telemetry type or application to get a meaningful result. Aside from the @-prefixed system fields, you cannot assume any field exists.
Method 1: EXPLAIN (ANALYZE_FIELDS) — the primary schema discovery mechanism
EXPLAIN (ANALYZE_FIELDS)
SELECT `@record`
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()Returns per-field type descriptors with sample values. It can be narrowed with WHERE filters to scope discovery to a specific time range or subset of data:
EXPLAIN (ANALYZE_FIELDS)
SELECT `@record`
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
AND attributes['your.filter.key'] = 'some-value'Whenever you are unsure about a field name, run ANALYZE_FIELDS first. The available fields depend entirely on what was instrumented and ingested.
Method 2: SELECT @record
SELECT `@record`
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '15 MINUTES' AND NOW()
LIMIT 10Returns the full JSON structure of each record, showing all available fields for that record.
Method 3: ListTelemetryFields API
Call the ListTelemetryFields API to enumerate known fields for a data set and telemetry type (LOGS or TRACES — metrics are not listed; see promql-metrics.md). It takes dataSetName, telemetryType, and optional startTime/endTime; each entry is {name, children}. A nextToken member is reserved in the shape but the service does not page today — it is always null, so read the whole field list from the single response and do not loop on it.
6. Supported SQL Operations
Only SELECT statements are allowed. All DDL (CREATE, ALTER, DROP) and DML (INSERT, UPDATE, DELETE) are blocked.
Clauses:
SELECT(with column expressions, aliases)FROM(single table or JOINs)WHERE(must include`@timestamp`)GROUP BYHAVING(post-aggregation filter)ORDER BY(ASC/DESC, multiple columns)LIMIT— default is 10,000 if omittedDISTINCT
Joins — on base tables the outer joins need the OUTER keyword spelled out:
INNER JOIN(and bareJOIN)LEFT OUTER JOIN— notLEFT JOINRIGHT OUTER JOIN— notRIGHT JOINFULL OUTER JOIN
The short LEFT JOIN / RIGHT JOIN spellings are rejected against base tables, and the
error blames a column rather than the keyword: Schema error: No field named a."spanId". Valid fields are a."@timestamp", b."@timestamp". — every non-time column vanishes from
the reported schema, so it reads as a missing field. Add OUTER and the identical
ON/WHERE runs unchanged.
Subqueries & Composition:
Common Table Expressions (WITH clause):
WITH name AS (SELECT ...) SELECT ... FROM nameSubqueries:
IN (SELECT ...),EXISTS (SELECT ...), derived tablesFROM (SELECT ...) AS tSet operations:
UNION,UNION ALL,INTERSECT,EXCEPT
Window functions:
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)RANK() OVER (...)Aggregate functions with
OVER (PARTITION BY ... ORDER BY ...)A window's
PARTITION BY/ORDER BYtakes a column or an alias, never a bracket path.PARTITION BY attributes['http.method']is rejected withSchema error: No field named attributes. Valid fields are "@timestamp", attributes— an error that names nothing wrong in the query. Project the nested field in a CTE or derived table first and partition by that alias (see the running-count pattern in §8). Top-level columns (kind,name) work directly.
Expressions:
CASE WHEN ... THEN ... ELSE ... ENDLIKE/NOT LIKEIN (value1, value2, ...)BETWEEN ... AND ...IS NULL/IS NOT NULLCAST(expr AS type)TRY_CAST(expr AS type)— returns NULL instead of failing on invalid conversionArithmetic:
+,-,*,/
Table Sampling (TABLESAMPLE)
TABLESAMPLE reduces the amount of data scanned by sampling a table reference. It is placed immediately after a table reference in the FROM clause.
Syntax: TABLESAMPLE (<p> PERCENT), where p must satisfy 0 < p <= 100. An extended form adds MAX_VOLUME and BIAS (below).
SELECT * FROM "logs.default" TABLESAMPLE (10 PERCENT)
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()Per-occurrence / per-table — each table reference carries its own independent sampling rate. In multi-datastore joins and self-joins, every occurrence is sampled separately:
SELECT a.`@timestamp` AS log_time, b.name AS span_name
FROM "logs.default" a TABLESAMPLE (10 PERCENT)
JOIN "traces.default" b TABLESAMPLE (50 PERCENT) ON a.traceId = b.traceId
WHERE a.`@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
AND b.`@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
LIMIT 3Project columns explicitly and qualify them, as above: SELECT * across two tables that
both expose @timestamp is rejected with Schema error: Ambiguous reference to unqualified field "@timestamp" — the join itself is fine, the star is not. Join on a
field the two tables actually share (traceId), not a placeholder id.
Behavior — TABLESAMPLE returns an approximate sample, not an exact p% of rows: the returned row count is an approximation of the requested percentage. Sampling is repeatable — re-running the same query over the same time range returns the same sample.
Supported tables — logs. / traces. / bare default. It is NOT supported on metrics. tables.
Rejection rules — each of the following raises an explicit error:
Combining with
EXPLAIN (ANALYZE_FIELDS).A percentage outside
(0, 100].ROWS, or a quantity with no unit at all —PERCENTandMAX_VOLUMEare the only units.A sampling method name such as
BERNOULLI/SYSTEM.REPEATABLE/SEED— sampling is deterministic and cannot be seeded.BUCKET.A non-numeric percentage.
TABLESAMPLEin an unsupported position — aSELECT-list scalar subquery, aHAVINGsubquery, or aJOIN ... ONsubquery. It is allowed onFROMrelations,WHEREsubqueries, derived tables, andUNIONbranches.Conflicting percentages on the same table occurrence.
MAX_VOLUME and BIAS — bounding a scan by bytes
Arguments are order-independent and each is optional, so a budget can stand alone — an omitted
percentage means 100, an omitted bias means SPREAD:
SELECT `@timestamp`, `@record`
FROM "logs.default" TABLESAMPLE (100 PERCENT, 10G MAX_VOLUME, BIAS SPREAD)
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()| Argument | Meaning |
|---|---|
<n> MAX_VOLUME | Ceiling on declared uncompressed bytes selected. Units K / M / G / T, optional B, base 1024; a bare number is bytes |
BIAS EARLIEST / BIAS LATEST | Keep the earliest / latest part of the range at full time resolution |
BIAS SPREAD | Keep the whole range and widen the stride instead (default) |
The budget is not the reported scan size. It caps the declared uncompressed bytes of the selected input, counted before pruning; reported bytes scanned counts resolved files after pruning. The two are different quantities, so reported scan size can land either side of the budget. Never tell the user a budget means they will scan exactly, or at most, that much.
Recent data is not sampled. Data still served from the ingest tier is scanned in full regardless of the sampling clause, and it counts toward reported bytes scanned.
Takes the tighter of the two bounds. Given a percentage and a budget together, each is evaluated against the full candidate set and whichever selects fewer bytes is applied.
Manifest-granular. Whole manifests are selected or skipped, so a budget is never met exactly. On skewed data it binds coarsely, and a budget below the smallest manifest can select nothing at all and reports
BELOW_MINIMUM_GRANULARITY. Widen the budget or narrow the time range.Binds per table occurrence, exactly like the percentage form.
7. Functions Reference
Conditional
COALESCE(a, b, ...)— returns the first non-null argumentNULLIF(a, b)— returns NULL if a = b, otherwise aGREATEST(a, b, ...)— returns the largest valueLEAST(a, b, ...)— returns the smallest valueNVL(a, b)— returns b if a is NULL (alias: IFNULL)
Math
ABS(x)— absolute valueCEIL(x)— round up to integerFLOOR(x)— round down to integerROUND(x, n)— round to n decimal placesPOWER(base, exp)— exponentiationSQRT(x)— square rootLN(x)— natural logLOG(base, x)— logarithmLOG10(x)/LOG2(x)— base-10 / base-2 logTRUNC(x, n)— truncate to n decimal placesPI()— pi constantRANDOM()— random float 0-1safe_div(a, b)— division returning NULL on divide-by-zerowidth_bucket(value, min, max, n)— histogram bucketingclamp(value, min, max)— clamp to rangesigmoid(x)— sigmoid function
String
length(s)— string lengthlower(s)/upper(s)— case conversiontrim(s)/ltrim(s)/rtrim(s)— whitespace removalleft(s, n)/right(s, n)— first/last n characterssubstr(s, start, len)— substring extractionconcat(a, b, ...)— concatenate stringsconcat_ws(sep, a, b, ...)— concatenate with separatorreplace(s, from, to)— replace occurrencesreverse(s)— reverse a stringrepeat(s, n)— repeat string n timessplit_part(s, delimiter, n)— split and return part n (1-based)strpos(s, substr)— position of substring (1-based, 0 if not found)position(substr IN s)— same as strposstarts_with(s, prefix)/ends_with(s, suffix)— prefix/suffix checkcontains(s, substr)— substring existence checklpad(s, len, pad)/rpad(s, len, pad)— pad stringinitcap(s)— capitalize first letter of each wordparse_url(url, component)— extract URL component (e.g., 'HOST', 'PATH')url_decode(s)/url_encode(s)— URL encodingmask(s, ...)— mask sensitive data
Regex
regexp_matches(s, pattern)— true if string matches patternregexp_like(s, pattern)— alias for regexp_matchesregexp_replace(s, pattern, replacement)— regex replaceregexp_extract_all(s, pattern)— all matches as arrayregexp_substr(s, pattern)— first matchregexp_count(s, pattern)— count of matches
Time & Date
`@timestamp` is Timestamp(Nanosecond). dateceil and convert_timezone accept only
Timestamp(Microsecond), so call them on arrow_cast(\@timestamp`, 'Timestamp(Microsecond, None)'); passed the raw column they fail at planning withFailed to coerce arguments ... Timestamp(ns) to
the signature Exact(..., Timestamp(µs)).dateceiltakes anINTERVAL, not a'5 minutes'string.date_difftakes dates:date_diff(CAST(a AS DATE), CAST(b AS DATE))`.
NOW()— current timestampto_timestamp_nanos(string)— parse ISO-8601 string to timestamp. E.g.,to_timestamp_nanos('2026-08-20T15:00:00.000Z')to_timestamp(string)— parse string to timestampto_unixtime(timestamp)— timestamp to epoch secondsfrom_unixtime(integer)— epoch seconds to timestampfromMillis(integer)— epoch milliseconds to timestamptoMillis(timestamp)— timestamp to epoch millisecondsdate_trunc(unit, timestamp)— truncate to unit. E.g.,date_trunc('minute', \@timestamp`)`dateceil(timestamp_us, interval)— ceiling to interval; needs the microsecond cast. E.g.,dateceil(arrow_cast(\@timestamp`, 'Timestamp(Microsecond, None)'), INTERVAL '5 minutes')`date_part(field, timestamp)— extract part (year, month, day, hour, minute, second)extract(field FROM timestamp)— same as date_partdate_bin(interval, timestamp, origin)— bin timestamps into fixed intervalsdate_diff(date, date)— difference between dates; cast timestamps first:date_diff(CAST(\@timestamp` AS DATE), CAST(NOW() AS DATE))`convert_timezone(from, to, timestamp_us)— timezone conversion; needs the microsecond cast. E.g.,convert_timezone('UTC', 'Europe/London', arrow_cast(\@timestamp`, 'Timestamp(Microsecond, None)'))`add_months(date, n)— add monthsmake_timestamp(y, m, d, h, min, sec)— create timestamp from parts
JSON
Prefer bracket notation for ingested JSON data (already extracted). Use these only for fields containing raw JSON strings.
json_get(s, path...)— extract value at pathjson_get_str(s, path...)/json_get_int(...)/json_get_float(...)/json_get_bool(...)— typed extractionjson_length(s, path...)— array/object length at pathjson_contains(s, path...)— check if path existsjson_keys(s, path...)— list keys at pathjsonParse(s)— parse and validate JSON stringjsonStringify(value)— serialize to JSON string
IP
isIpInSubnet(ip, cidr)— check if IP in subnet. E.g.,isIpInSubnet(ip, '10.0.0.0/8')isValidIp(s)— validate IP addressisValidIpV4(s)/isValidIpV6(s)— validate specific versionisIpv4InSubnet(ip, cidr)/isIpv6InSubnet(ip, cidr)— version-specific subnet checkipv4_string_to_num(s)/ipv4_num_to_string(n)— IPv4 conversion
Array
array_agg(expr [ORDER BY ...]) [FILTER (WHERE ...)]— collect values into arrayarray_length(arr)— array lengtharray_first(arr)/array_last(arr)— first/last elementarray_sum(arr)/array_avg(arr)/array_count(arr)— array matharray_filter(arr, condition)— filter elementsarray_contains(arr, value)— check membershiparray_distinct(arr)— deduplicatearray_sort(arr)— sort elementsarray_concat(arr1, arr2)— concatenate arraysflatten(arr)— flatten nested arrays
Aggregate
COUNT(expr)/COUNT(*)/COUNT(DISTINCT expr)— countSUM(expr)— sumAVG(expr)— averageMIN(expr)/MAX(expr)— minimum / maximumMEDIAN(expr)— median valueSTDDEV(expr)/STDDEV_POP(expr)— standard deviation (sample / population)FIRST_VALUE(expr [ORDER BY ...])— first value by orderingLAST_VALUE(expr [ORDER BY ...])— last value by orderingstring_agg(expr, delimiter)— concatenate strings with delimiterapprox_percentile_cont(expr, quantile)— approximate percentile. E.g.,approx_percentile_cont(value, 0.99)bool_and(expr)/bool_or(expr)— boolean aggregationpattern(string)— log clustering (groups similar text into patterns)
All aggregate functions support the FILTER (WHERE condition) clause:
SUM(tokens) FILTER (WHERE kind = 'CLIENT') as client_tokensWindow
All aggregate functions can be used as window functions with OVER (...):
ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)RANK()/DENSE_RANK()— ranking with/without gapsLAG(expr, offset)/LEAD(expr, offset)— access previous/next rowsFIRST_VALUE(expr) OVER (...)/LAST_VALUE(expr) OVER (...)NTILE(n)— divide into n bucketsCUME_DIST()/PERCENT_RANK()— cumulative distribution
Hashing
md5(s)— MD5 hashsha256(s)/sha512(s)— SHA hashesdigest(s, algorithm)— generic hash
8. Common Query Patterns
The patterns below work regardless of telemetry type. They reference only the guaranteed @-prefixed system fields plus placeholder field names shown purely to demonstrate syntax. Replace any non-@ field name with a real field discovered via Schema Discovery.
Time Filtering + Ordering
SELECT `@timestamp`, `@record`
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
ORDER BY `@timestamp` DESC
LIMIT 100Table Sampling
Scan a fraction of the data with TABLESAMPLE (<p> PERCENT) placed after the table reference. The `@timestamp` bound is still mandatory. Sampling is approximate and repeatable, so this is best for exploratory scans over large windows rather than exact counts:
SELECT `@timestamp`, `@record`
FROM "logs.default" TABLESAMPLE (10 PERCENT)
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
ORDER BY `@timestamp` DESC
LIMIT 100p must satisfy 0 < p <= 100, and TABLESAMPLE is unavailable on metrics. tables. An extended form can also bound a scan by bytes with MAX_VOLUME / BIAS.
Filter on a Discovered Field
-- 'your.field.key' is a placeholder — confirm real keys with ANALYZE_FIELDS first
SELECT `@timestamp`, `@record`
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
AND attributes['your.field.key'] = 'some-value'
ORDER BY `@timestamp` DESCAggregation Over Time Buckets
-- 'your.group.key' is a placeholder for a real field in your data
SELECT date_trunc('minute', `@timestamp`) AS time_bucket,
attributes['your.group.key'] AS group_key,
count(*) AS record_count
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '6 HOURS' AND NOW()
GROUP BY time_bucket, group_key
ORDER BY time_bucket DESCJSON Extraction
Most ingested JSON data is already extracted — use bracket notation (field['key']) to access nested values directly. Use json_* functions only when a field contains a raw JSON string that was not automatically extracted at ingestion (e.g., a serialized payload in a log body):
SELECT `@timestamp`,
json_get_str(body, 'error', 'message') AS error_message,
json_get_int(body, 'error', 'code') AS error_code
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '30 MINUTE' AND NOW()
AND json_contains(body, 'error')
ORDER BY `@timestamp` DESCPattern Matching (Text Clustering)
The pattern() function groups similar text values into clusters. Use UNNEST to expand the results:
SELECT UNNEST(pattern(`@message`))
FROM "default"
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
LIMIT 1000Span Duration (traces.default)
There is no duration_ms column on traces.default; a query that CASTs or aggregates
duration_ms is rejected at planning time. The span duration field is durationNano in
NANOseconds (string-typed; not milliseconds — a naive read is off by 1e6). Always
CAST(durationNano AS DOUBLE) before comparing, ranking, or aggregating — without the cast
the engine cannot coerce the string for numeric ordering, so ORDER BY durationNano DESC
on the raw field is wrong. Exclude NULL durations — under ORDER BY … DESC, NULLs sort
first, so without durationNano IS NOT NULL row 1 is not the slowest span. Service
identity is a resource attribute reached with bracket notation,
resource['attributes']['service.name'], not a bare top-level column.
SELECT `@timestamp`, name, resource['attributes']['service.name'] AS service,
CAST(durationNano AS DOUBLE) / 1e6 AS duration_ms_derived
FROM traces.default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
AND resource['attributes']['service.name'] = '<service-name>'
AND durationNano IS NOT NULL
ORDER BY CAST(durationNano AS DOUBLE) DESC
LIMIT 20Slowest / longest / duration-ranked spans — state ALL of this in the answer, not just the query: the duration field is
durationNano(there is noduration_mscolumn — it fails at planning); it is NANOseconds, so a millisecond read is off by 1e6; it is string-typed, so it needsCAST(durationNano AS DOUBLE); NULL durations sort first underDESC, sodurationNano IS NOT NULLis required or the top row is not the slowest span; the`@timestamp`filter with a relative window is mandatory; and the service isresource['attributes']['service.name'](bracket notation), not a top-level field.
The duration_ms_derived alias above is computed from durationNano; it is not a stored
column and cannot be used in the same query's WHERE. attributes['duration_ms'] is a
valid but almost always empty per-customer attribute lookup, not the span duration.
Finding failed spans
A span's failure is read from its status code, not an invented is_error/success
boolean — no such top-level column exists. The OTel span status lives at status['code'], and an error is the ERROR status (OTel StatusCode
enum value 2). The value may be ingested as the proto numeric enum (integer 2 or string '2') or
as the string 'ERROR' or 'STATUS_CODE_ERROR' in any letter case, so normalise before matching:
upper(TRY_CAST(status['code'] AS VARCHAR)) IN ('2', 'ERROR', 'STATUS_CODE_ERROR') casts whatever type is present to
text and folds case, catching every ingestion variant with a single-typed IN list (no
engine-dependent mixed-type list). An HTTP status attribute such as
http.response.status_code is corroborating evidence, reached with bracket notation
(attributes['http.response.status_code']), never as a bare top-level field.
SELECT `@timestamp`, `@record`
FROM traces.default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
AND upper(TRY_CAST(status['code'] AS VARCHAR)) IN ('2', 'ERROR', 'STATUS_CODE_ERROR')
-- narrow to the service via a discovered resource attribute, e.g.
-- AND resource['attributes']['service.name'] = '<your-service-name>'
LIMIT 100Under the permissive schema a misspelled status field silently returns NULL rather than
erroring, so an empty result should prompt schema discovery (ListTelemetryFields /
EXPLAIN (ANALYZE_FIELDS)), not a conclusion that nothing failed.
Failed / erroring spans — state ALL of this in the answer, not just the query. Open the answer with the schema caveat, before any filter query: field names depend on what was ingested, so first run
EXPLAIN (ANALYZE_FIELDS)(orListTelemetryFields) to confirm which field carries the span status —status['code']is the OTel convention and the usual answer, not a guarantee. Show that discovery query first, then the filter query. Then: failure is read from that status field (ERROR /2/STATUS_CODE_ERROR, normalised withupper(TRY_CAST(... AS VARCHAR))), never from an inventedis_errorcolumn; an HTTP or gRPC status attribute is corroborating evidence reached with bracket notation; the`@timestamp`window is mandatory; the service isresource['attributes']['service.name']; and an empty result under the permissive schema means "check the field names", not "nothing failed".
For whole traces — fetching one by id, its span tree and time breakdown, failure analysis, AI-agent span attributes, and finding traces by their spans — see trace-exploration.md.
Reading a span or trace you already have (pasted, or passed as UI context)
When a span object is handed to you directly (the user pastes it, or the UI passes the currently open span as context), read it in place — the data is already supplied, so do not probe the account, run a query, or ask the user to fetch it again. Its own fields are the answer. State all of these that apply, in the answer text:
Error state is
status.code.STATUS_CODE_ERROR(numeric2in some exports) means the span failed, andstatus.messagecarries the reason.STATUS_CODE_OKis an explicit success.STATUS_CODE_UNSETmeans the producer set no explicit status — it is not an error signal, but it is not proof of success either: corroborating fields (an HTTP 5xxhttp.response.status_code, a non-OKrpc.grpc.status_code) can still indicate a failure on an UNSET span, so check them before calling it healthy.Corroborate with the protocol status.
attributes['http.response.status_code'](a 5xx) orattributes['rpc.grpc.status_code']is supporting evidence for the same error, not a second independent one — report it as corroboration.Service identity lives under
resource—resource['attributes']['service.name'], not a top-level field. Attribute the span to that service.durationNanois NANOseconds, string-typed. Convert it and show the arithmetic:durationNano / 1e9= seconds,/ 1e6= milliseconds. Reading the raw value as milliseconds is off by 1e6.Point downstream when the evidence does.
attributes['rpc.service'],attributes['peer.service'], orattributes['server.address']names the callee; a 5xx gateway status (502/503/504) plus a timeout-shapedstatus.messageindicates the dependency did not respond in time rather than this service erroring internally.Do not invent errors, attributes, or child spans the object does not contain, and do not claim a root cause from a single span — say that confirming it needs the downstream spans in the same trace (join on
traceId).
Numeric Comparison on String-Typed Fields
Ingested values retain their original type. When values are stored as strings, use TRY_CAST to convert to numeric types. The field name below is a placeholder:
SELECT `@timestamp`, `@record`
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
AND TRY_CAST(attributes['your.numeric.key'] AS BIGINT) > 1000
ORDER BY TRY_CAST(attributes['your.numeric.key'] AS BIGINT) DESC
LIMIT 20Window Function — Running Count per Group
The nested field has to be aliased in a CTE first — a window cannot partition by a bracket path (§6):
-- 'your.partition.key' is a placeholder for a real field
WITH rows AS (
SELECT `@timestamp`,
attributes['your.partition.key'] AS group_key
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
)
SELECT `@timestamp`,
group_key,
count(*) OVER (
PARTITION BY group_key
ORDER BY `@timestamp`
) AS running_count
FROM rows
ORDER BY `@timestamp` DESC
LIMIT 200Partitioning by the bracket path directly — PARTITION BY attributes['your.partition.key']
— fails with Schema error: No field named attributes, with or without ORDER BY, for
count(*) and ROW_NUMBER() alike.
Approximate Percentiles
-- 'your.value.key' is a placeholder for a real numeric field
SELECT attributes['your.group.key'] AS group_key,
approx_percentile_cont(TRY_CAST(attributes['your.value.key'] AS BIGINT), 0.50) AS p50,
approx_percentile_cont(TRY_CAST(attributes['your.value.key'] AS BIGINT), 0.95) AS p95,
approx_percentile_cont(TRY_CAST(attributes['your.value.key'] AS BIGINT), 0.99) AS p99
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
GROUP BY group_key
ORDER BY p99 DESCCommon Table Expression + Self-Join
Note the `@timestamp` filter is required on both sides of a self-join. Field names below are placeholders:
WITH candidates AS (
SELECT DISTINCT attributes['your.correlation.key'] AS corr_key
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
AND TRY_CAST(attributes['your.value.key'] AS BIGINT) > 5000
)
SELECT t.`@timestamp`, t.`@record`
FROM default AS t
INNER JOIN candidates AS c
ON attributes['your.correlation.key'] = c.corr_key
WHERE t.`@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
ORDER BY t.`@timestamp` ASCThe ON clause deliberately writes the bracket path unqualified — attributes[...],
not t.attributes[...]. An alias-qualified bracket path resolves to NULL (see
Field Access & Quoting), so the qualified spelling makes this
join match nothing and return zero rows with no error. Alias-qualifying a top-level
field, as in t.`@timestamp` above, is fine.
Correlating logs with traces
Logs and spans correlate through a field they share — commonly the trace identifier the
logging library injected (traceId on OTel-shaped records, or a nested variant such as
attributes['trace_id']), but the name is not guaranteed. Discover it on both tables
first with EXPLAIN (ANALYZE_FIELDS), then use either approach — show both in the
answer, each with the mandatory `@timestamp` bound:
One unified query over
default, which holds both telemetry types, filtered by the shared field and tagged with`@telemetry_type`:SELECT `@telemetry_type`, `@timestamp`, `@record` FROM default WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW() AND <shared_field> = '<trace-id>' ORDER BY `@timestamp` ASCA
JOINoflogs.defaultwithtraces.defaulton the shared field, with the`@timestamp`filter on both sides:SELECT l.`@timestamp` AS log_time, l.`@message`, t.name AS span_name FROM logs.default AS l INNER JOIN traces.default AS t ON l.<shared_field> = t.<shared_field> WHERE l.`@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW() AND t.`@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW() AND t.<shared_field> = '<trace-id>' ORDER BY l.`@timestamp` ASC
Give the span side a wide upper bound: a span's `@timestamp` is its end time, so a
window that stops shortly after the request began drops its longest spans (section 2).
If the log records carry no trace context at all, no query can associate them after ingestion — the fix is instrumentation (inject the active trace id into each log record), not a different SQL shape.
Combining Telemetry Types
Query multiple telemetry types together using the combined default table and `@telemetry_type` to distinguish them:
SELECT `@telemetry_type`, `@timestamp`, `@record`
FROM default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
AND `@telemetry_type` IN ('logs', 'traces')
ORDER BY `@timestamp` ASCOr use UNION ALL to combine results from separate tables:
SELECT 'logs' AS source, `@message` AS detail, `@timestamp`
FROM logs.default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
UNION ALL
SELECT 'traces' AS source, `@message` AS detail, `@timestamp`
FROM traces.default
WHERE `@timestamp` BETWEEN NOW() - INTERVAL '1 HOUR' AND NOW()
ORDER BY `@timestamp` ASC9. Constraints & Gotchas
Single statement only — no semicolons separating multiple queries. One
SELECTper request.`@timestamp`filter is mandatory — everyWHEREmust constrain it, and both sides of any self-join must constrain it.Default LIMIT 10,000 — if you omit
LIMIT, the query returns at most 10,000 rows.Permissive schema hides mistakes — a misspelled field, or a field that doesn't exist in your data, silently returns NULL instead of raising an error. If results look unexpectedly empty, verify field names with schema discovery.
Use
to_timestamp_nanos()for ISO strings — when specifying absolute timestamps, pass ISO-8601 strings throughto_timestamp_nanos('2026-08-20T15:00:00.000Z').Unquoted
@-fields fail — always backtick-quote:`@timestamp`,`@message`, etc.Types match ingested values — fields retain the type they were ingested with. When values are ingested as strings, use
TRY_CASTto convert them to numeric types (BIGINT,DOUBLE,INT,DECIMAL) before arithmetic or comparison.TRY_CASTreturns NULL if the conversion is not possible.Field names are never guaranteed — the schema depends entirely on what was ingested. Only the
@-prefixed system fields are guaranteed. Always confirm any other field withEXPLAIN (ANALYZE_FIELDS)before relying on it.No DDL/DML —
INSERT,UPDATE,DELETE,CREATE,ALTER,DROPare all blocked. OnlySELECTis supported.TABLESAMPLEis approximate — it returns an approximate sample rather than an exactp%of rows, is repeatable for the same query and time range, supportsPERCENT(0 < p <= 100) plus theMAX_VOLUME/BIASarguments, and is unavailable onmetrics.tables.
Performance Tips
Narrow the time range — this is the main driver of query latency. A shorter
@timestampwindow scans less data and returns results faster. If sorting by timestamp, try a narrower period first.Avoid ORDER BY when order doesn't matter — sorting adds overhead. Omit it for aggregation or exploratory queries where row order is irrelevant.
Prefer equality predicates —
=andIN (...)filters are the most efficient for narrowing down results.IS NULL / IS NOT NULL are efficient — these help narrow down the search in most cases.
Avoid LIKE when possible — pattern matching is expensive. If you must use LIKE, combine it with other more selective filters (equality, time range) to reduce the data scanned first.
Set a LIMIT — especially useful for "find the slowest" or TOP-K type queries. Adding
ORDER BY CAST(durationNano AS DOUBLE) DESC LIMIT 10(withdurationNano IS NOT NULL) is much cheaper than sorting the entire dataset.Use EXPLAIN (ANALYZE_FIELDS) once to discover the schema — then write precise queries against known fields rather than scanning with
@record.
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