Add AWS OTel investigation templates (ALB, Lambda, RDS, SQS) - #52
Add AWS OTel investigation templates (ALB, Lambda, RDS, SQS)#52JM-elastic wants to merge 6 commits into
Conversation
Ship four observability RCA templates built for EDOT awscloudwatch metrics so operators can investigate common AWS alert patterns from the Template library. Co-authored-by: Cursor <cursoragent@cursor.com>
| incident_lookback: "20 minutes" | ||
|
|
||
| triggers: | ||
| - type: alert |
There was a problem hiding this comment.
afaiu alert-triggered executions expose the alert payload under event (event.alerts), but everything below reads from inputs.*, which are only defined for the manual trigger. wouldn't the alert path run with empty resource names / timestamp (same for the other 3 templates)? should we derive these from event or remove the alert trigger for now?
There was a problem hiding this comment.
Confirmed and fixed — you were right that the alert path ran with empty inputs. Rather than guess field paths, we captured a real alert payload (probe workflow wired to a live rule): the useful fields are event.alerts[N].kibana.alert.grouping.<Dimension> (the rule's group-by values as structured fields — FunctionName, QueueName, DBInstanceIdentifier, and LoadBalancer arrives intact as the full app/<name>/<id> ref) plus kibana.alert.start for the timestamp.
Each workflow now starts with a resolve step: manual inputs.* take precedence, else the alert's grouping fields, else a sentinel that trips the evidence gate (see the other thread). Inputs are now required: false so alert runs pass validation.
Two wiring facts worth documenting (now in comments in the YAML): the alert trigger only fires when a rule carries the "Run Workflow" action, and that action should use summaryMode: false — with true one event can batch several alerts (in our capture, alerts[0] was a different team's RDS instance) and only the first would be investigated. Also, ES|QL rules can emit alerts for degenerate/null groups whose grouping has no resource field — those runs now end in an explicit insufficient_evidence refusal instead of investigating garbage.
Validated live: alert-triggered runs for all four workflows, fired by the shipped aws_*_otel rule templates against real injected incidents (Lambda error flood, SQS backpressure, RDS connection exhaustion, ALB target-5xx cascade).
| request_count = SUM(`metrics.amazonaws.com/AWS/ApplicationELB/RequestCount`), | ||
| max_target_response = MAX(`metrics.amazonaws.com/AWS/ApplicationELB/TargetResponseTime`) | ||
| format: json | ||
| on-failure: |
There was a problem hiding this comment.
we continue on failure for all the metrics queries, but then always pass their output to ai.classify. if an index is missing or a query fails, wouldn't we still force a potentially "convincing evidence" from empty data? wondering if the required queries should fail the workflow, and only the optional SLO/alert queries should continue.
There was a problem hiding this comment.
Agreed and restructured. Required evidence queries now hard-fail (no on-failure: continue), and — the part static review couldn't see — that alone isn't enough: a successful query over a wrong/missing resource returns one row of nulls, which the classifier happily pattern-matched to its healthy category (a false all-clear, verified live). So every workflow now has a deterministic evidence gate (if on documents_found > 0) in front of all AI steps; the else-branch emits a workflow.output with status: insufficient_evidence and refuses to diagnose.
One refinement to "required should fail": CloudWatch omits zero-valued sparse metrics, so fields like HTTPCode_ELB_5XX_Count are unmapped until the first such error ever lands — and ES|QL fails verification on unknown columns, killing the query even when the alert's own subject fields are present (we hit exactly this on a live target-5xx alert). So required steps reference only dense fields the triggering condition guarantees (RequestCount etc.), and each sparse family is its own optional phase-split step whose absence is treated as "no such errors observed". SLO/related-alert anchors stay optional as you suggested.
| consts: | ||
| lambda_metrics_index: "metrics-aws.lambda.otel-*" | ||
| baseline_lookback: "45 minutes" | ||
| baseline_lookend: "12 minutes" |
There was a problem hiding this comment.
looks like the baseline and incident windows overlap between T-20m and T-12m. is that intentional? wouldn't incident data included in the baseline weaken the comparison?
There was a problem hiding this comment.
Fixed in all four (it was in ALB/RDS/SQS too, same consts): baseline is now T-50m→T-20m against incident T-20m→T+5m — contiguous, no overlap.
| throttles = SUM(`metrics.amazonaws.com/AWS/Lambda/Throttles`), | ||
| max_duration = MAX(`metrics.amazonaws.com/AWS/Lambda/Duration`), | ||
| max_concurrency = MAX(`metrics.amazonaws.com/AWS/Lambda/ConcurrentExecutions`) | ||
| | EVAL error_rate = TO_DOUBLE(errors) / TO_DOUBLE(invocations + 1) |
There was a problem hiding this comment.
not sure on the logic itself, but something my agent flagged: why are we adding 1 to invocations here? e.g. 1 error out of 1 invocation would become a 50% error rate instead of 100%. can we handle zero invocations separately and otherwise divide by the actual count?
There was a problem hiding this comment.
Fixed: CASE(invocations > 0, TO_DOUBLE(errors) / TO_DOUBLE(invocations), null) in both windows, and the classifier is explicitly told a null rate means zero invocations, not 0%. Two related live findings while validating: (1) the queries were also silently mixing the Average/Sum/Maximum stat streams the collector ships (non-integer error counts) — all aggregations now carry per-agg WHERE attributes.stat == … filters, matching what the alert templates themselves do; (2) with correct math, the classifier initially flagged ambient drift (13% vs 11% baseline) as an incident, so the instructions now carry a materiality bar — re-tested healthy on a fleet with ~8% ambient error rate.
|
general comment: there's quite a bit of AWS/domain logic in the queries and classifier prompts here, and i'm not sure we can confidently validate all of it in a regular workflow review. can you run the 4 templates through an AWS/observability-focused agent (or get a domain review) and confirm the query outputs actually support each classification? if you already did that then sorry for the comment 😅 would be great to add the result to the validation section. |
| continue: true | ||
|
|
||
| - name: synthesize_findings | ||
| type: ai.summarize |
There was a problem hiding this comment.
what happens with the output of this step when the workflow ends?
do we want a final workflow.output here? ai.summarize generates the actual investigation result, but afaiu it remains only a step output, so callers/composed workflows can't consume it as the workflow result. maybe we should expose steps.synthesize_findings.output.content?
There was a problem hiding this comment.
Added — each workflow now ends with a workflow.output step exposing status, resource, triggered_by, classification, rationale, and summary (and the insufficient-evidence branch emits its own), so callers and composed workflows get a real result. Validated on a 9.6.0 stack.
|
ran the new workflow validator against a release schema generated from my local 9.6.0 Kibana (i don't think our schemas changed since): schema completeness gate passed (62 registered steps / 28 triggers all present) |
schema validation is green, but it only checks the schema, DAG, and the Liquid syntax not whether references resolve or the steps actually execute. can you add actual run results for each template, including at least one alert-triggered run? |
…tes, window/rate fixes Addresses all review feedback, with every change validated live on a real AWS fleet (EDOT awscloudwatch -> Elastic 9.6.0) via alert-triggered runs fired by the shipped aws_*_otel alerting rule templates: - Alert-triggered runs now work: a `resolve` step maps manual inputs OR the triggering alert's structured fields (event.alerts[0].kibana.alert.grouping.<Dimension>, kibana.alert.start). Inputs are optional; manual values take precedence. Wire rules via the "Run Workflow" action with summaryMode:false so each run carries exactly one alert. Proven on live runs for all four services. - Required queries hard-fail (on-failure:continue removed) and reference only dense metrics; sparse CloudWatch counters (e.g. HTTPCode_ELB_5XX_Count, unmapped until first occurrence — ES|QL fails verification on unknown columns) moved to optional phase-split steps. - Deterministic evidence gate (documents_found > 0) in front of every AI step; zero-evidence paths (wrong resource, degenerate alert group, not-yet-ingested window) emit an explicit insufficient_evidence workflow.output instead of a plausible-but-unsupported diagnosis. - Baseline/incident windows no longer overlap (T-50m..T-20m vs T-20m..T+5m). - Lambda error rate: CASE(invocations > 0, errors/invocations, null) — no +1 distortion, explicit null on zero invocations; classifier told not to read null as 0%. Materiality bar added so ambient drift isn't classified as an incident (verified against a fleet with ~8% ambient error rate). - Per-aggregation stat filters (WHERE attributes.stat == "Sum"/"Maximum"/"Average") so multi-stat streams aren't mixed — matches the shipped alert templates. - ALB index corrected to metrics-aws.elb.otel-* (the aws_elb_metrics_otel package's canonical dataset; its alert templates + dashboard all read it). - Every workflow ends in workflow.output exposing status, resource, classification, rationale, and summary for callers/composed workflows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Pushed f65459a addressing all review feedback — every change was validated live rather than just re-authored: real AWS fleet (EC2/ECS/RDS/SQS/Lambda/ALB) → EDOT Validation matrix (all alert-triggered end-to-end unless noted):
One fix beyond the review threads: the ALB workflow queried Two calibration observations for the integrations packages (not changed here, just noting from the live runs): the Lambda high-error-rate default of 1% fires perpetually on any workload with a nontrivial ambient error rate, and the RDS 🤖 Generated with Claude Code |
…eriality bar Live testing showed the taxonomy lacked a volume-driven category: a 13.5x invocation flood with a flat error RATE was (correctly) refused as elevated_errors, but the classifier then reached for duration_degradation on a +6% duration wiggle. Added: - load_surge: invocations >> baseline (~3x+) with error_rate near baseline and no throttles — names the real driver (absolute error counts / budget burn tripping the alert at a healthy rate) instead of inventing a code failure. - duration_degradation now requires a MATERIAL duration jump (~1.5x+); small wiggles during load are called out as ambient. - Explicit precedence: throttling > elevated_errors > load_surge > duration_degradation. Regression-tested against the recorded incidents: the flood window now classifies load_surge; the healthy window still classifies healthy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Two follow-ups for review honesty: e6a3606 closes a classifier gap the live rounds exposed: the Lambda taxonomy had no volume-driven category, so a 13.5× invocation flood with a flat error rate — correctly refused as ALB validation caveat: our lab account's ApplicationELB metrics are discovery-truncated (shared account, ~6.5k series vs the receiver's 500 limit), so the ALB end-to-end run rides a named-mode collector workaround that reshapes summary datapoints into the per-stat gauge form discovery mode produces natively. The workflow's queries were validated against that reshaped data and match what the 🤖 Generated with Claude Code |
…ity, ALB pressure, app logs Adopts the layered investigation model from obs-infraobs review feedback: Layer 1 WHERE is the 5xx generated (target 5xx vs ELB-generated split by code: 500 internal / 502 bad gateway / 503 no capacity / 504 timeout); Layer 2 WHY can't the ALB reach targets (TargetConnectionErrorCount, TargetTLSNegotiationErrorCount, host health); Layer 3 is the ALB ITSELF under pressure (RejectedConnectionCount); Layer 4 application evidence (bounded error-log query, graceful when logs aren't onboarded). Classifier taxonomy expands 4 → 9 categories (target_application_5xx, target_connection_failure, target_tls_failure, no_healthy_targets, alb_capacity_pressure, alb_internal_5xx, backend_latency_timeout, healthy, unrecognized) with explicit ABSENCE semantics (never-emitted sparse counter = "no such errors observed"), a COARSENING rule (pick the coarser truthful category when the discriminating metric family is absent, and say so), and materiality guidance. insufficient_evidence remains the deterministic gate's else-branch rather than an AI category. Every sparse CloudWatch family is its own on-failure:continue step (a combined per-code query would fail ES|QL verification unless all four codes have historically occurred on the LB). client-side TLS negotiation failures are deliberately excluded: they occur before HTTP exists and produce no 5xx. Validated live on real telemetry: DB-exhaustion cascade window classifies target_application_5xx (6,211 target 5xx vs zero baseline, flat traffic, all ELB code families absent -> ALB-generated causes ruled out via absence); ambient window classifies healthy; junk resource refuses via the gate. target_application_5xx and healthy are incident-validated; the remaining categories are schema-validated pending chaos mechanisms for 502/503/reject. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…host-count datapoints Live validation of the no_healthy_targets category (gateway service scaled to zero behind the ALB): 5,525 ELB 503s with every other 5xx family absent — and HealthyHostCount/UnHealthyHostCount went ABSENT rather than reading 0, because deregistered targets emit no host-count datapoints at all. The classifier correctly inferred this on the live run; this commit encodes the signature explicitly so the inference doesn't depend on the model. no_healthy_targets is now incident-validated alongside target_application_5xx and healthy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Self-review (author-conducted, fresh adversarial pass)I authored this PR, so this isn't independent review — flagging that up front; a human co-sign is still worth having. But here's a genuine critical pass, actively trying to break my own assumptions. Findings, most substantive first: IMPORTANT — required queries hard-fail on transient errors, not just on missing data. The whole design removes IMPORTANT — self-relative baseline degrades for sustained incidents. Windows are baseline T−50m→T−20m vs incident T−20m→T+5m. That's clean for a recent onset, but an incident sustained past ~30 minutes poisons its own baseline (the "baseline" window already shows the degraded state), weakening every comparison. Same limitation the alert-rule side has. Not fixable without a longer-history baseline; worth a one-line caveat in the template so operators know onset-detection is the design point. SUGGESTION — the ALB SUGGESTION — Liquid → ES|QL string interpolation. Resolved resource identifiers go straight into Testing gap. The PR ships four customer-facing templates with no CI-runnable test — validation was live (extensively, see the run history), but there's no automated check that the YAML stays schema-valid or that the Liquid renders. If this repo has a template-validation harness, these should hook into it; if not, that's a broader gap worth a follow-up. What holds up under scrutiny: the deterministic evidence gate ahead of every AI step, the sparse-metric-per-family handling (and the live-discovered no-healthy-targets absence signature), the per-stat aggregation filters, and the honesty of the scope caveats (ALB partially incident-validated, etc.). The live validation evidence is genuinely thorough. Verdict: Comment (not a self-approval). Nothing here is merge-blocking, but the two IMPORTANT items deserve either a fix or an explicit "accepted, documented" before this ships to customers. I'd merge after those are addressed or consciously waived. |
|
@JM-elastic is this ready to be reviewed again? the comments seems AI generated and i'm not sure whether it's ready to pick up again or not yet? |
|
@talboren thanks for checking in - yes I finished my last round of updates yesterday and hadn't gotten around to pinging you yet. Not sure if you'd prefer to keep the AI generated comments in or not (I left them so that others who may be using AI for review can have the extra context). |
talboren
left a comment
There was a problem hiding this comment.
lgtm - 1 note: in alerting rules, summaryMode defaults to true, but these workflows only inspect event.alerts[0]. unless the user enables “Run per alert,” the remaining alerts are silently ignored. can we guard against multiple alerts or handle them instead of relying on a non-default action setting?
talboren
left a comment
There was a problem hiding this comment.
just noticed the 4 templates moved inputs to the workflow root level, they need to be under the manual trigger..
The four AWS investigation templates declared `inputs` at the workflow root; per the schema they belong under the manual trigger. Alert-triggered runs carry no inputs and continue to resolve the target from the alert payload, so this is a placement-only change with no behavior difference.
4 screenshots provided below, showing example execution of each workflowcc @talboren |




Summary
Adds four observability root-cause-analysis templates for the AWS OTel (EDOT awscloudwatch) integrations currently in tech preview / heading to GA:
aws-alb-5xx-investigation-otel— ALB 5xx surge (target vs ELB 5xx vs latency)aws-lambda-errors-investigation-otel— Lambda error spike (errors vs throttles vs duration)aws-rds-connection-exhaustion-investigation-otel— RDS connection exhaustion vs other DB bottlenecks + ALB cascadeaws-sqs-consumer-lag-investigation-otel— SQS backlog (consumer lag vs stalled vs producer spike)Each template:
solutions: [observability],categories: [root-cause-analysis, monitoring],availability: \">=9.5.0\"alert+manual(manual inputs for testing without an alert)elasticsearch.esql.query+ai.classify+ai.summarizeagainst default EDOT index patterns (metrics-aws.*.otel-*)install.form— no connectors; index patterns stay inconstsas EDOT defaultsSource: developed/proven against the Forge AWS FIS plane for the AWS OTel content workstream. Confirmed with @talboren that library PRs are welcome; CI validation is still incomplete so please help validate step types / runtime once reviewed.
Validation
npm run build:catalog(local, with env overrides):Test plan
metrics-aws.*.otel-*data (or confirm gracefulon-failure: continuewhen empty)ai.classify/ai.summarizestep types resolve on the target stackcc @talboren — as discussed, please help with validation since library CI validation isn't landed yet.
Made with Cursor