Skip to content

feat(core): logRetention + throttling + accessLogging stack defaults (Infrastructure Options) - #385

Open
osama-rizk wants to merge 4 commits into
mainfrom
feat/stack-defaults-logging-throttling-accesslogging
Open

feat(core): logRetention + throttling + accessLogging stack defaults (Infrastructure Options)#385
osama-rizk wants to merge 4 commits into
mainfrom
feat/stack-defaults-logging-throttling-accesslogging

Conversation

@osama-rizk

@osama-rizk osama-rizk commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends the stack-wide BlocksDefaults mechanism (#302) with three additive fields and adopts them across Blocks-managed infrastructure. Each field is read independently via option ?? scope.defaults.field — a per-block option always wins, and no field is derived from another (a #302 reviewer blocker).

New BlocksDefaults fields (and both BlocksPresets):

Field Type sandbox production
logRetention RetentionDays ONE_WEEK ONE_YEAR
throttling { rateLimit; burstLimit } 200 / 400 200 / 400
accessLogging boolean false true

Also newly exported from @aws-blocks/core/cdk: the BlocksThrottling type and ensureApiGatewayAccount(); Scope gains a handlerLogGroup getter.

🔎 For API-BR review

Please sanity-check the three property names/shapes before this is widely adopted:

  • logRetention: RetentionDays (CDK enum — appears only in the /cdk type)
  • throttling: { rateLimit: number; burstLimit: number } (new BlocksThrottling type)
  • accessLogging: boolean

Adoption sites

Log retention — Blocks-managed log groups now follow defaults.logRetention instead of AWS's infinite default:

  • Core shared handler Lambda — setupBlocksInfra now owns a single log group, exposed as scope.handlerLogGroup.

  • bb-logger — reconfigures that shared group's retention (retention ?? defaults.logRetention) instead of creating a colliding /aws/lambda/<fn> group.

  • bb-distributed-table — GSI-manager Lambdas.

  • bb-distributed-data — DSQL migration Lambda.

  • bb-data — Aurora migration Lambda.

  • bb-app-setting — secret-init custom-resource Lambda.

    (The framework custom-resources.Provider Lambdas these BBs wrap still use AWS's default retention — the L2 Provider exposes no log-group override.)

Throttling — applied to the core REST API stage and the bb-realtime WebSocket stage (on a WS stage the throttle unit is messages/sec across the connection; noted in bb-realtime/DESIGN.md).

Access logging — when enabled, structured JSON access logs to a dedicated CloudWatch log group (retention = defaults.logRetention, RemovalPolicy.DESTROY) on the core REST API and bb-realtime WebSocket stages. The account-level API Gateway CloudWatch Logs role is provisioned once per stack via a shared ensureApiGatewayAccount() helper (Symbol.for-keyed so co-located stages reuse one AWS::ApiGateway::Account); the RestApi's auto cloudWatchRole is disabled to avoid a duplicate. Each access-logging stage takes an explicit dependency on the account so a clean-account first deploy applies the setting before the stage is created.

#163 / #326 reconciliation

Both #163 and #326 add throttling to the core REST API directly (near-duplicates). This PR is the single source of truth for API Gateway throttling and supersedes them.

Deferred to a follow-up PR

  • Hosting adoption (SSR REST API throttling/access logging + compute/revalidation log retention, and the account-role wiring for hosting) is intentionally out of scope here and will land in a separate PR.
  • Out of scope per the original plan: secretsStore, pointInTimeRecovery, VPC, compute, sandbox template cleanup.

Testing

  • npm run build && npm run lint && npm run lint:deps && npm test all green (3709 pass / 0 fail).
  • Synth tests (aws-cdk-lib/assertions) cover: preset values; sandbox vs production retention on the handler group; 200/400 throttle on the REST and WS stages; per-block/per-stack overrides winning; access-log group + single AWS::ApiGateway::Account present when enabled and absent when disabled; GSI log-group retention.

Notes for reviewers

  • check:api produces no diff — api-extractor tracks the mock/types entry, not the /cdk entry where these types live.

Sandbox deploy verification (done)

Deployed test-apps/comprehensive to a sandbox (account 444209914584, us-west-2) with accessLogging enabled, exercising the core REST API, bb-realtime WebSocket, bb-data, bb-distributed-data/-table, and bb-app-setting. Verified end-to-end, then torn down (DELETE_COMPLETE):

  • ✅ Deploy succeeded with access logging on — no CreateStage failure. Our BlocksApiGatewayAccount + CloudWatch role were created and apigateway get-account pointed at our stack's role (explicit stage→account dependency ordered it correctly).
  • Access logs land — 3 live POST /aws-blocks/api requests (HTTP 200) produced structured JSON entries in the ApiAccessLogs CloudWatch group (requestId/httpMethod/resourcePath/status…).
  • ✅ REST stage: accessLogSettings → JSON format + ApiAccessLogs group; throttle 200/400 confirmed live.
  • ✅ WebSocket stage: AccessLogSettingsBlocksRtAccessLogs group; throttle 200/400.
  • ✅ All 6 log-retention groups at 7 days (ONE_WEEK, sandbox): handler, GSI-manager, GSI-isComplete, DSQL-migration, bb-data-migration, bb-app-setting-secret-init.

Caveat observed: the test account already had an API Gateway CloudWatch role (from a prior hosting deploy), so this wasn't a truly clean account — our stack provisioned and re-pointed the account role (the documented region-global "last-deploy-wins" behavior), but the clean-account CreateStage failure itself couldn't be reproduced here.

@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ead10cc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@aws-blocks/core Minor
@aws-blocks/blocks Minor
@aws-blocks/bb-logger Patch
@aws-blocks/bb-realtime Patch
@aws-blocks/bb-distributed-table Patch
@aws-blocks/bb-distributed-data Patch
@aws-blocks/bb-data Patch
@aws-blocks/bb-app-setting Patch
@aws-blocks/bb-kv-store Patch
@aws-blocks/auth-common Patch
@aws-blocks/bb-auth-basic Patch
@aws-blocks/bb-auth-cognito Patch
@aws-blocks/bb-auth-oidc Patch
@aws-blocks/bb-async-job Patch
@aws-blocks/bb-dashboard Patch
@aws-blocks/bb-cron-job Patch
@aws-blocks/bb-file-bucket Patch
@aws-blocks/bb-agent Patch
@aws-blocks/bb-knowledge-base Patch
@aws-blocks/bb-email-client Patch
@aws-blocks/bb-tracer Patch
@aws-blocks/bb-metrics Patch
@aws-blocks/bb-lambda-compute Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@osama-rizk
osama-rizk marked this pull request as ready for review August 18, 2026 15:46
@osama-rizk
osama-rizk requested a review from a team as a code owner August 18, 2026 15:46

@sarayev sarayev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed as a senior engineer — focused on correctness, edge cases, security, scaling, and tests (skipping pure style). Overall a clean, well-documented change with good synth coverage. Inline notes below: 3 majors worth resolving before merge (Logger retention clobber, the production throttle default as a silent behavior change, and the per-stack ApiGateway::Account singleton risk), plus a few minors/nits. None are hard blockers on their own — flagging for discussion.

Comment thread packages/bb-logger/src/index.cdk.ts Outdated
// asks for a specific value; otherwise leave the stack-wide default in
// place. Applied via the L1 escape hatch because a per-block option must
// reconfigure the framework-owned group, not spawn a competing one.
const retention = options?.retention ?? this.defaults.logRetention;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Every Logger reconfigures the same shared group, so this unconditional write makes the last-constructed Logger win. A bare new Logger(scope, 'x') computes defaults.logRetention and overwrites a retention that another Logger set earlier — silently and order-dependent. Could we write only when the caller explicitly asked, so a no-op Logger doesn't stomp an explicit override?

if (options?.retention) {
    const cfnLogGroup = this.handlerLogGroup.node.defaultChild as CfnLogGroup | undefined;
    if (cfnLogGroup) cfnLogGroup.retentionInDays = options.retention;
}

The shared group already carries defaults.logRetention from setupBlocksInfra, so writing the default back is redundant anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 784657c. Logger now writes retention only when options.retention is explicitly set; a bare new Logger(...) no longer writes defaults.logRetention back, so it can't clobber an explicit value from another Logger (and the redundant default write is gone). Added a test that pins this: an explicit retention: 14 survives a later bare Logger.

Comment thread packages/bb-logger/src/index.cdk.ts Outdated
// place. Applied via the L1 escape hatch because a per-block option must
// reconfigure the framework-owned group, not spawn a competing one.
const retention = options?.retention ?? this.defaults.logRetention;
const cfnLogGroup = this.handlerLogGroup.node.defaultChild as CfnLogGroup | undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] If defaultChild is ever undefined (e.g. the group resolves to an imported ILogGroup), the retention request is silently dropped and you'd end up with unexpectedly-infinite retention and no signal. It's always a concrete LogGroup today, so this is defensive — but would an else that throws at synth be safer than a silent skip?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 784657c. When retention is explicitly requested but the shared group's defaultChild isn't a concrete LogGroup (e.g. an imported ILogGroup), it now throws an actionable synth error rather than silently dropping the request. (In the no-retention path there's nothing to write, so no throw.)

removalPolicy: RemovalPolicy.RETAIN,
deletionProtection: true,
logRetention: RetentionDays.ONE_YEAR,
throttling: DEFAULT_THROTTLING,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] This applies a 200 req/s (400 burst) cap to production by default. Before this PR the REST stage had no stage-level throttle, so existing deployments ran at the account default (~10k rps). On upgrade, anything serving >200 rps would start getting 429s with no change on their side. Is 200 intentional for production, or should prod get a higher ceiling? Either way, since it can throttle live traffic, could we call this out as a behavior change in the changeset (it currently reads as a routine minor)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 784657c. Production now defaults to 1000/2000 (sandbox stays 200/400) so the new stage cap doesn't 429 real prod traffic, and it's overridable per-stack via throttling. I also called this out explicitly as a behavior change in the changeset (before: no stage throttle → account default ~10k rps) with upgrade guidance.

],
});

const account = new CfnAccount(stack, 'BlocksApiGatewayAccount', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] AWS::ApiGateway::Account is a region singleton, but this emits one per stack. The comment notes "last deploy wins", but the teardown case is sharper: if two Blocks stacks in the same account+region both enable access logging and the one that deployed last is later destroyed, its BlocksApiGatewayCloudWatchRole is deleted and the account setting can end up pointing at a role that no longer exists — silently breaking logging (or blocking CreateStage) for the surviving stack. Should the account be importable/shared (fromExisting), or at least documented as a single-Blocks-stack-per-region assumption?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented in 784657c. ensureApiGatewayAccount() now spells out the region-singleton teardown hazard you describe (later stack's role deleted → account setting points at a dead role → breaks logging / blocks CreateStage for the survivor) and states the single-Blocks-stack-per-region assumption for accessLogging. A shared/importable account for true multi-stack support is deferred; noted that multi-stack users should manage the account role out-of-band. Also flagged in the changeset.

// retention follows the stack-wide default instead of AWS's infinite default,
// and gives bb-logger one group to reconfigure rather than a second, colliding
// one. Torn down with the stack (logs are not durable state).
const handlerLogGroup = new logs.LogGroup(scope, 'HandlerLogGroup', {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] On an existing deployment the handler previously logged to Lambda's auto-created /aws/lambda/<fn> group (infinite retention). Pointing the function at this new CDK group leaves the old one orphaned in CloudWatch — unmanaged and still infinite, i.e. the exact cost leak this PR targets. Worth a migration note in the changeset? (The old bb-logger group also gets DESTROY'd on upgrade, dropping its history.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — added an upgrade/migration note to the changeset in 784657c: on upgrade the handler's old auto-created /aws/lambda/<fn> group (and any prior bb-logger retention group) is left orphaned/unmanaged in CloudWatch and should be deleted manually to reclaim the cost/close the infinite-retention leak.

Comment thread packages/core/src/cdk/blocks-backend.ts Outdated
apiGatewayAccount = ensureApiGatewayAccount(cdk.Stack.of(scope));
accessLogGroup = new logs.LogGroup(scope, 'ApiAccessLogs', {
retention: props.defaults.logRetention,
removalPolicy: cdk.RemovalPolicy.DESTROY,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] In production these access logs are the audit trail, but the group is RemovalPolicy.DESTROY, so they vanish on stack teardown. Consistent with treating logs as disposable — but is that the intent for a production audit log? Might warrant RETAIN for the access-log group specifically.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 784657c. The access-log groups (core REST + realtime WS) now use removalPolicy: defaults.removalPolicy, so production RETAINs the audit trail on teardown while sandbox still DESTROYs. The handler's operational stdout group stays DESTROY (not an audit trail). Reflected in the changeset.

* the stack-wide `defaults.logRetention`; when omitted the log group uses the
* CDK `LogGroup` default retention.
*/
logRetention?: cdk.aws_logs.RetentionDays;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] materialize is exported, and when logRetention is omitted the group falls back to CDK's default (TWO_YEARS), not the Blocks default. Database always passes it so this is fine today — just flagging the escape-hatch path for a direct caller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — Database always passes logRetention from defaults, so the escape-hatch (TWO_YEARS) path is only reachable by a direct materialize caller. The option's JSDoc already notes it falls back to the CDK LogGroup default when omitted, so a direct caller is on notice. Left as-is.

timeout: cdk.Duration.minutes(MIGRATION_LAMBDA_TIMEOUT_MINUTES),
// Own the migration Lambda's log group so its retention follows the
// stack-wide default instead of AWS's infinite retention.
logGroup: new LogGroup(stack, `${this.fullId}DsqlMigrationLogs`, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] Every other adopter got a CDK retention test (bb-data, bb-distributed-table, bb-app-setting, bb-logger, realtime, core) — this DSQL migration group is the one change without one. A one-liner mirroring the bb-data test would close the parity gap:

template.hasResourceProperties('AWS::Logs::LogGroup', { RetentionInDays: 7 });

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 784657c — a parity test in index.cdk.test.ts asserting the DSQL migration Lambda's log group adopts defaults.logRetention (RetentionInDays: 7 under sandbox), matching the other adopters.

@osama-rizk

Copy link
Copy Markdown
Contributor Author

Thanks @sarayev — addressed the review in 784657c8:

Majors

  • Logger clobber → writes retention only when an explicit per-Logger retention is set (bare Logger no longer stomps another's value); added a regression test.
  • Prod throttle → production raised to 1000/2000 (sandbox stays 200/400) so upgrades don't 429 real traffic; overridable, and called out as a behavior change in the changeset.
  • ApiGateway::Account singleton → documented the teardown hazard + single-Blocks-stack-per-region assumption (shared/imported account deferred).

Minors/nits

  • Logger now throws (not silently skips) if the shared group isn't a concrete LogGroup.
  • Access-log groups follow defaults.removalPolicy → production RETAINs the audit trail.
  • Orphaned legacy /aws/lambda/<fn> handler group documented as an upgrade note.
  • Added the missing DSQL migration-Lambda retention test (parity).
  • materialize escape-hatch default acknowledged (Database always passes logRetention).

build + lint + lint:deps + affected unit tests green.

… adopt in core + data BBs + realtime

Extends BlocksDefaults (PR #302) with three additive fields, each read
independently via `option ?? scope.defaults.field` (never derived from another):

- logRetention: RetentionDays — sandbox ONE_WEEK / production ONE_YEAR
- throttling: { rateLimit, burstLimit } — 200/400 in both presets
- accessLogging: boolean — sandbox false / production true

Adoption (hosting is intentionally deferred to a separate PR):

logRetention — Blocks-managed log groups now follow the default instead of
AWS's infinite retention:
- core shared handler Lambda (setupBlocksInfra owns the group, exposed as
  scope.handlerLogGroup)
- bb-logger reconfigures that shared group (option ?? default) instead of
  creating a colliding /aws/lambda/<fn> group
- bb-distributed-table GSI-manager Lambdas
- bb-distributed-data DSQL migration Lambda

throttling — applied to every Blocks API Gateway stage:
- core REST API stage (supersedes #163 and #326)
- bb-realtime WebSocket stage (throttle unit is messages/sec across the
  connection)

accessLogging — when enabled, structured JSON access logs to a dedicated
CloudWatch log group (retention = defaults.logRetention, RemovalPolicy.DESTROY)
on the core REST API and bb-realtime WebSocket stages. Provisions the
account-level API Gateway CloudWatch Logs role once per stack via a shared
ensureApiGatewayAccount() helper (Symbol.for-keyed so co-located stages reuse
one AWS::ApiGateway::Account); RestApi auto-cloudWatchRole disabled to avoid a
duplicate. Synth-tested; needs a clean-sandbox deploy to confirm log delivery
before merge.

Adds synth tests (aws-cdk-lib/assertions) for all sites + preset assertions.
- bb-logger DESIGN/README/types: shared handler log group is reconfigured
  (option ?? default), no second /aws/lambda group.
- bb-realtime DESIGN: WebSocket stage throttling (messages/sec unit) + access
  logging + shared account-level CloudWatch role.
- Changeset covering core (minor), blocks (minor), and the adopting BBs (patch).
  Hosting adoption is deferred to a separate PR.
…/init Lambdas

Two more Blocks-managed Lambdas defaulted to AWS's infinite log retention:
- bb-data: the Aurora migration Lambda (direct analog of bb-distributed-data)
- bb-app-setting: the secret-init custom-resource Lambda

Both now own explicit log groups whose retention follows defaults.logRetention
(RemovalPolicy.DESTROY), with synth tests. The wrapped custom-resources.Provider
framework Lambdas still use AWS defaults — the L2 Provider exposes no override.
…og retain, account-singleton docs, DSQL test

- Production throttle default raised to 1000/2000 (sandbox stays 200/400) so the
  new stage cap doesn't 429 real prod traffic; documented as a behavior change.
- bb-logger only writes retention when an explicit per-Logger `retention` is set
  (a bare Logger no longer clobbers another's value); throws if the shared group
  isn't a concrete LogGroup instead of silently dropping the request.
- Access-log groups (core REST + realtime WS) follow defaults.removalPolicy, so
  production RETAINs the audit trail on teardown.
- ensureApiGatewayAccount: documented the region-singleton teardown hazard and
  the single-Blocks-stack-per-region assumption.
- Added the missing DSQL migration-Lambda log-retention synth test (parity).
- Changeset: throttle values + upgrade/behavior notes (throttle 429 risk,
  orphaned legacy handler log group, access-log RETAIN).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants