feat(aws-cdk): surface custom resource Lambda logs on deployment failure#1645
Conversation
…failure
When a custom resource (Custom::* or AWS::CloudFormation::CustomResource) fails
to deploy, fetch the backing Lambda's CloudWatch logs and surface them as
additional diagnostic context, mirroring the ECS service investigation.
- Resolve the backing Lambda by reading the resource's ServiceToken from the
stack's original template (literal ARN, Fn::GetAtt, or Ref via
describeStackResources).
- Derive the log group from the /aws/lambda/<fn> convention; only call
getFunctionConfiguration to read a custom LoggingConfig.LogGroup if the
convention group has no events. (cfn-response usage does not imply the default
group; advanced logging controls can override it.)
- Target the exact failing invocation by extracting the log stream name from the
cfn-response status reason ("See the details in CloudWatch Log Stream: <name>").
- Bound the query to a window around the failure event timestamp so update and
rollback failures resolve the right invocation, not the latest stream. Adds an
optional ResourceError.timestamp for this.
- Add getFunctionConfiguration to the Lambda SDK client.
All exploratory calls are best-effort: failures are logged at debug level and
never break diagnosis. Verified end-to-end against a live cfn-response custom
resource deployment failure.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
…ive function A first-deploy custom resource failure rolls back and deletes the backing Lambda, so getFunctionConfiguration is unavailable exactly when we need the configured (advanced-logging) log group. Read LoggingConfig.LogGroup from the stack template instead — the template survives rollback — handling both a literal value and the common CDK shape where it is a Ref to an AWS::Logs::LogGroup resource. Fall back to the live function configuration only when the template value is an unresolvable intrinsic or the function is defined outside this stack.
…g group names The template-sourced log group only handled a literal LogGroupName. The common CDK case is an AWS::Logs::LogGroup with no explicit name, where CloudFormation generates the physical name — absent from the template. Resolve that name via describeStackResources (which still returns RETAINed/orphaned resources after a rollback), so advanced-logging functions whose backing Lambda was deleted by rollback still surface their logs. Extract a shared resolvePhysicalId helper used by both the ServiceToken and log-group resolution paths. Verified end-to-end against a live advanced-logging custom resource deployment.
Parse Lambda CloudWatch log events into readable lines before rendering, on the custom-resource path only (ECS logs are arbitrary container output and are left as-is). Handles both Lambda log formats: - Text: strip the per-line timestamp/requestId prefix, render aligned 'LEVEL message'. - JSON: render the level + message (and error envelopes' errorMessage + stackTrace) instead of dumping raw JSON objects. Drops only Lambda platform boilerplate (INIT_START/START/END/REPORT and JSON platform.* events). Application output is never dropped by level — failure detail often rides in INFO lines (e.g. the cfn-response 'Response body'). Lines we don't recognize pass through verbatim, and full logs remain available via the console link.
- Confirm an intrinsic ServiceToken resolves to an AWS::Lambda::Function before treating its physical ID as a function name. A Ref/GetAtt to a non-Lambda resource whose physical ID is a bare name would otherwise produce a misleading /aws/lambda/<name> log lookup. (resolveStackResource now returns ResourceType.) - Distinguish a failed log fetch (e.g. missing logs:FilterLogEvents, or the group doesn't exist) from a genuinely empty log group, so the user-facing message reflects the real cause instead of always saying 'No log events found'. Both addressed from PR review feedback; each covered by a mutation-verified test.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…tion resource-investigation.ts had grown to mix orchestration (the AWS-calling investigate/fetch/resolve functions) with a pile of pure utilities. Move the pure helpers to dedicated homes, leaving resource-investigation focused on orchestration (908 -> 681 lines): - CloudWatch log formatting (trimToRecentLines, parseLambdaLogEvents + helpers, cloudWatchLogsConsoleUrl, MAX_LOG_LINES) -> format-utils.ts - Resource identifier parsers (parseEcsServiceIdentifier, serviceTokenReferencedLogicalId, functionNameFromArnOrName, extractLogStreamName, logStreamNameFromPhysicalId, ecsStoppedTasksConsoleUrl) -> new resource-identifiers.ts Behavior-preserving; tests only change import paths.
|
More thorough log examples: cfn-response module - Output of cdk deploy with JSON lambda logs, logging to default lambda log group Logs from testing against the following project: https://github.com/iankhou/cdk-app-with-problems/tree/iankhou-cr-main on commit iankhou/cdk-app-with-problems@d733eb6 TEXT lambda logs also yields basically the same result |
|
Total lines changed 1204 is greater than 1000. Please consider breaking this PR down. |
|
Sorry for the big PR! Includes some refactoring to move util functions out of |
rix0rrr
left a comment
There was a problem hiding this comment.
<3 it!
Couple small nits.
| * | ||
| * This is Lambda-specific; it is not applied to ECS logs, which are arbitrary container output. | ||
| */ | ||
| export function parseLambdaLogEvents(events: Array<{ message?: string }>): Array<{ message: string }> { |
There was a problem hiding this comment.
The documentation says you are normalizing to LEVEL message, so that's 2 fields.
The return type here only has message, so either that's a string containing both, or the comment is wrong.
- If the return type is a combined string, just return
string[] - Otherwise add
level: stringand leave it to the caller to format these.
There was a problem hiding this comment.
Will correct the documentation and adjust the field names the prevent confusion.
| } | ||
|
|
||
| /** | ||
| * If `line` is a JSON-format Lambda log object, render it as `LEVEL<tab>message` |
There was a problem hiding this comment.
Let's not to <tab>s, I don't trust em.
Pad with spaces to the desired length.
There was a problem hiding this comment.
It was actually using spaces, the documentation here was wrong. Will make the correction, thanks for the catch!
| * in the status reason ("See the details in CloudWatch Log Stream: <name>"), so we can | ||
| * target that exact invocation. | ||
| */ | ||
| async function investigateCustomResource( |
There was a problem hiding this comment.
I think these functions should probably go into their own files as well. investigate-custom-resource.ts, investigate-ecs-service.ts ?
Move investigateEcsService and investigateCustomResource into their own files (investigate-ecs-service.ts, investigate-custom-resource.ts), leaving resource-investigation.ts as the dispatcher. Simplify the log line plumbing: parseLambdaLogEvents now returns string[] (the level is already combined into each rendered line) and trimToRecentLines operates on lines rather than raw CloudWatch events.
Summary
When a CloudFormation custom resource fails during deployment, the CLI today shows only the raw status reason — typically a cfn-response pointer like "See the details in CloudWatch Log Stream: …" — leaving the developer to go search for the lambda logs by hand. This PR fetches the backing Lambda's CloudWatch logs and surfaces them inline in the failure diagnosis, mirroring the existing ECS service investigation. The result is the actual handler error, where the failure is reported.
Applies to
Custom::*andAWS::CloudFormation::CustomResource. The whole investigation is best-effort: every AWS call is wrapped so a failure is logged at debug level and never breaks the deployment error path.Future work - explore Cloudtrail events following deployment failure. This will come in another PR! Cloudtrail events aren't available in console until 5-15 minutes after they occur, so will only be available on
cdk diagnoseruns after failure.Before
After
How it works
1. Find the backing Lambda. The CloudFormation event doesn't name the function — only the resource's
ServiceTokendoes, and that lives in the template. We fetch the template once and readServiceToken, resolving:Fn::GetAtt(JSON array form and the YAML string short-form"Fn.Arn"), orRef, viadescribeStackResources.For an intrinsic reference we confirm the target is actually
AWS::Lambda::Functionbefore treating its physical ID as a function name (aRefto, say, an SNS topic is ignored rather than producing a bogus/aws/lambda/<name>lookup).2. Resolve the log group — rollback-proof on stack CREATE. A first-deploy failure rolls back and deletes the backing Lambda, so
getFunctionConfigurationis unavailable exactly when we need it. We therefore derive the log group without the live function wherever possible:/aws/lambda/<function-name>convention (covers the vast majority);LoggingConfig.LogGroup), read it from the template — including the common CDK shape where it's aRefto anAWS::Logs::LogGroupwhose physical name CloudFormation generates (resolved viadescribeStackResources, which still returns RETAINed resources after rollback);getFunctionConfigurationis used only as a last-resort fallback.3. Target the right invocation. cfn-response writes the failing log stream name into the status reason, so we query that exact stream. Because that stream is pinned to the original create invocation, on update/rollback failures we bound the query to a time window around the failure event and fall back to a group-wide scan, so a stale stream can't hide the real logs. This required adding an optional
timestamptoResourceError(populated from stack events).4. Readable output. Lambda log events are normalized before display (Lambda-only — ECS logs are arbitrary container output and left untouched):
LEVEL message;level+message(and error envelopes'errorMessage/stackTrace) instead of dumping raw JSON;INIT_START/START/END/REPORT, JSONplatform.*) is dropped;INFOlines (e.g. the cfn-response "Response body") — and anything unrecognized passes through verbatim.When logs genuinely can't be retrieved, the message distinguishes an empty group ("No log events found…") from a failed fetch ("Could not fetch logs… the credentials may lack
logs:FilterLogEvents"), and the console deep-link is always emitted so the full logs are one click away.Changes
lib/api/diagnosing/resource-investigation.ts— custom-resource investigation, log-group resolution, stream targeting, and log normalization (shares the log-trimming helper with the ECS path).lib/api/stack-events/resource-errors.ts— optionalResourceError.timestamp, populated from stack events.lib/api/aws-auth/sdk.ts—getFunctionConfigurationon the Lambda client (fallback log-group discovery).test/api/diagnosing/resource-investigation.test.ts— unit tests for ServiceToken resolution (ARN / GetAtt array+string / Ref), log-group resolution (convention / template literal / template Ref / generated name), stream targeting + timestamp window, log normalization (text/JSON/platform/malformed), and the empty-vs-error messaging.Testing
Notes
/aws/lambda/<fn>group's data persists regardless.Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license