-
Notifications
You must be signed in to change notification settings - Fork 39
feat(infra): add Lambda-error alarm to GitHub webhook processor (#284) #674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -40,6 +40,15 @@ const PROCESSOR_DLQ_RETENTION_DAYS = 14; | |||||||||
| /** DLQ-depth alarm metric period (minutes). */ | ||||||||||
| const DLQ_ALARM_PERIOD_MINUTES = 5; | ||||||||||
|
|
||||||||||
| /** Processor Lambda-Errors alarm metric period (minutes). */ | ||||||||||
| const ERROR_ALARM_PERIOD_MINUTES = 5; | ||||||||||
|
|
||||||||||
| /** Processor Lambda-Errors alarm evaluation range (periods). Paired with | ||||||||||
| * `datapointsToAlarm: 1` below, this is 1-of-2, NOT 2-of-2: any single | ||||||||||
| * breaching period alarms. Do not remove `datapointsToAlarm` — CloudWatch | ||||||||||
| * would default it back to this value and a one-off crash would never fire. */ | ||||||||||
| const ERROR_ALARM_EVALUATION_PERIODS = 2; | ||||||||||
|
|
||||||||||
| /** Async screenshot-processor Lambda memory (MB). */ | ||||||||||
| const PROCESSOR_MEMORY_MB = 512; | ||||||||||
|
|
||||||||||
|
|
@@ -128,6 +137,18 @@ export class GitHubScreenshotIntegration extends Construct { | |||||||||
| * "for operator inspection" that no operator is ever told to make. */ | ||||||||||
| public readonly processorDlqDepthAlarm: cloudwatch.Alarm; | ||||||||||
|
|
||||||||||
| /** Fires when the processor Lambda's ``Errors`` metric breaches. The | ||||||||||
| * handler swallows its per-step operational failures (log + return), | ||||||||||
| * so this metric ticks only on faults that ESCAPE the handler — | ||||||||||
| * init-time crashes (missing env at cold start, bundling defect), | ||||||||||
| * unhandled throws in an unguarded path, or the 120s hard timeout. | ||||||||||
| * For that population it fires one evaluation window sooner than the | ||||||||||
| * DLQ-depth alarm (which waits for Lambda's async-retry ladder to land | ||||||||||
| * a message). Follows the ``metricErrors`` + 2-eval-period shape of | ||||||||||
| * ``task-orchestrator.ts`` OrchestratorErrorAlarm (with threshold 1, | ||||||||||
| * since any escaped error here is significant). */ | ||||||||||
| public readonly processorErrorAlarm: cloudwatch.Alarm; | ||||||||||
|
|
||||||||||
| constructor(scope: Construct, id: string, props: GitHubScreenshotIntegrationProps) { | ||||||||||
| super(scope, id); | ||||||||||
|
|
||||||||||
|
|
@@ -219,6 +240,38 @@ export class GitHubScreenshotIntegration extends Construct { | |||||||||
| treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| // Alarm on the processor's Errors metric (#284 AC). Complementary to | ||||||||||
| // the DLQ-depth alarm above, not redundant. The handler is best-effort | ||||||||||
| // by design — it catches its per-step operational failures (bad token, | ||||||||||
| // AgentCore/S3/comment-post errors) and returns success, so those | ||||||||||
| // surface as tagged log events, NOT as Lambda Errors. This alarm | ||||||||||
| // therefore fires on the faults that ESCAPE the handler: an init-time | ||||||||||
| // crash (missing env at cold start, bundling defect), an unhandled | ||||||||||
| // throw in an unguarded path, or the 120s hard timeout. That is the | ||||||||||
| // same failure class the DLQ eventually catches, but before Lambda's async-retry | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. N-c (nit — new, introduced by this fix commit). The reword dropped the main clause and left the subordinate one dangling, so the sentence no longer parses:
"but before … has landed" has nothing to attach to. The pre-fix text carried the verb phrase ("but the Errors metric trips one evaluation window sooner — before Lambda's async-retry ladder has landed …"), and removing the over-claim took the grammar with it. The following sentence already states the timing correctly, so this clause just needs to end cleanly. Also worth reflowing — the line now runs well past the width of its neighbours.
Suggested change
|
||||||||||
| // ladder has landed a message on the DLQ. The Errors metric is stamped | ||||||||||
| // at invocation time, so this alarm fires no later than — and, when the | ||||||||||
| // retry ladder straddles a period boundary, one window before — the | ||||||||||
| // DLQ-depth alarm. threshold 1 / 1-of-2 eval periods matches the AC; | ||||||||||
| // the metricErrors shape follows ``task-orchestrator.ts`` | ||||||||||
| // OrchestratorErrorAlarm (which uses threshold 3 — we use 1 because any | ||||||||||
| // escaped error here is significant). | ||||||||||
| this.processorErrorAlarm = new cloudwatch.Alarm(this, 'WebhookProcessorErrorAlarm', { | ||||||||||
| metric: this.webhookProcessorFn.metricErrors({ | ||||||||||
| period: Duration.minutes(ERROR_ALARM_PERIOD_MINUTES), | ||||||||||
| }), | ||||||||||
| threshold: 1, | ||||||||||
| evaluationPeriods: ERROR_ALARM_EVALUATION_PERIODS, | ||||||||||
|
scottschreckengaust marked this conversation as resolved.
|
||||||||||
| // Fire on the FIRST breaching datapoint. Unset, this defaults to | ||||||||||
| // evaluationPeriods (2 consecutive), which for a sparse invocation | ||||||||||
| // metric means a single-event crash never alarms: its retries land | ||||||||||
| // in one period and NOT_BREACHING fills the neighbour. | ||||||||||
| datapointsToAlarm: 1, | ||||||||||
| alarmDescription: | ||||||||||
| 'Screenshot webhook processor Lambda errors breached threshold — deploy-preview screenshots are failing (check IAM, AgentCore Browser quota, and dependency health)', | ||||||||||
| treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING, | ||||||||||
| }); | ||||||||||
|
|
||||||||||
| this.screenshotBucket.bucket.grantPut(this.webhookProcessorFn); | ||||||||||
| props.githubTokenSecret.grantRead(this.webhookProcessorFn); | ||||||||||
|
|
||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -70,7 +70,12 @@ describe('GitHubScreenshotIntegration construct', () => { | |||||||||||||
| // crashes reach the queue — each one means the screenshot pipeline is | ||||||||||||||
| // silently down. Without the alarm the queue is "for operator | ||||||||||||||
| // inspection" that no operator is ever told to make. | ||||||||||||||
| template.resourceCountIs('AWS::CloudWatch::Alarm', 1); | ||||||||||||||
| // | ||||||||||||||
| // Two alarms total: this DLQ-depth alarm (a record LANDED) and the | ||||||||||||||
| // Lambda-Errors alarm below (invocations are FAILING, before Lambda's | ||||||||||||||
| // async retries have exhausted onto the DLQ). They are complementary, | ||||||||||||||
| // not redundant — see the next test. | ||||||||||||||
| template.resourceCountIs('AWS::CloudWatch::Alarm', 2); | ||||||||||||||
| template.hasResourceProperties('AWS::CloudWatch::Alarm', { | ||||||||||||||
| MetricName: 'ApproximateNumberOfMessagesVisible', | ||||||||||||||
| Namespace: 'AWS/SQS', | ||||||||||||||
|
|
@@ -90,6 +95,44 @@ describe('GitHubScreenshotIntegration construct', () => { | |||||||||||||
| }); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| test('alarms on the processor Lambda Errors metric (>=1 in 5min, 2 eval periods)', () => { | ||||||||||||||
| // #284 AC: an alarm on the processor's Errors metric. This is DISTINCT | ||||||||||||||
| // from the DLQ-depth alarm above — the DLQ-depth alarm only fires once | ||||||||||||||
| // a failure has survived Lambda's async retry ladder and LANDED on the | ||||||||||||||
| // queue, whereas an Errors alarm fires as soon as invocations start | ||||||||||||||
| // failing, catching a systemic break (IAM regression, AgentCore quota | ||||||||||||||
| // exhaustion, dependency outage) at the first faulting invocation. | ||||||||||||||
| // Mirrors `task-orchestrator.ts` OrchestratorErrorAlarm's idiom. | ||||||||||||||
| template.hasResourceProperties('AWS::CloudWatch::Alarm', { | ||||||||||||||
| MetricName: 'Errors', | ||||||||||||||
| Namespace: 'AWS/Lambda', | ||||||||||||||
| // metricErrors() defaults to Sum — the correct statistic for an | ||||||||||||||
| // error COUNT (vs Average/Maximum). Pin it so a future edit that | ||||||||||||||
| // swaps the statistic can't silently change the alarm's semantics. | ||||||||||||||
| Statistic: 'Sum', | ||||||||||||||
| Period: 300, | ||||||||||||||
| Threshold: 1, | ||||||||||||||
| // >= 1, not > 1. Relies on a CDK default otherwise, and | ||||||||||||||
| // GreaterThanThreshold would silently require TWO errors to fire. | ||||||||||||||
| ComparisonOperator: 'GreaterThanOrEqualToThreshold', | ||||||||||||||
| EvaluationPeriods: 2, | ||||||||||||||
|
scottschreckengaust marked this conversation as resolved.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. N5 (nit). This is the same reasoning that motivated pinning
Suggested change
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. Adopted your suggested pin verbatim: the test now asserts |
||||||||||||||
| // 1-of-2, not 2-of-2. Unset, DatapointsToAlarm defaults to | ||||||||||||||
| // EvaluationPeriods, and a single-event crash would never alarm | ||||||||||||||
| // (its Errors datapoints land in one period; NOT_BREACHING fills | ||||||||||||||
| // the neighbour). Pinned so that regression is caught here. | ||||||||||||||
| DatapointsToAlarm: 1, | ||||||||||||||
| TreatMissingData: 'notBreaching', | ||||||||||||||
| Dimensions: Match.arrayWith([ | ||||||||||||||
| Match.objectLike({ | ||||||||||||||
| Name: 'FunctionName', | ||||||||||||||
| Value: Match.objectLike({ | ||||||||||||||
| Ref: Match.stringLikeRegexp('WebhookProcessorFn'), | ||||||||||||||
| }), | ||||||||||||||
| }), | ||||||||||||||
| ]), | ||||||||||||||
| }); | ||||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| test('wires the DLQ as the processor Lambda async-invoke dead-letter target', () => { | ||||||||||||||
| // The queue existing is not enough — it must be bound to the | ||||||||||||||
| // processor function's DeadLetterConfig or failed async invokes | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
N-a (nit — my prior N1-cont, PARTIALLY FIXED). You corrected both of these claims in the constructor comment at
:252-256, but this field JSDoc still carries them verbatim::145— "it fires one evaluation window sooner than the DLQ-depth alarm". This is the unconditional over-claim I flagged.:253-255now correctly says "fires no later than — and, when the retry ladder straddles a period boundary, one window before —". When the fault lands early in a period, the DLQ write at ~T+190s falls in the same period and both alarms fire at the same evaluation: a tie, not a window earlier.:147— "Follows themetricErrors+ 2-eval-period shape oftask-orchestrator.tsOrchestratorErrorAlarm". You deliberately dropped+ 2-eval-periodfrom:256in this same commit, and rightly so:OrchestratorErrorAlarm(task-orchestrator.ts:401-409) setsevaluationPeriods: 2with nodatapointsToAlarm, i.e. genuinely 2-of-2. This alarm is 1-of-2. Saying it follows that shape re-imports the exact mental model that produced the original defect.git show 93a715a:cdk/src/constructs/github-screenshot-integration.tsconfirms:145and:147are byte-identical to the pre-fix text — this block was simply missed. It matters a little more than an internal comment would, because this is the doc on thepublic readonlymember, so it's what a consumer of the construct (and #629, when it callsaddAlarmActionon this handle) reads first.