Repository: aws/aws-cdk
Pull request #33977: feat(apigatewayv2): `HttpStage` access logging
Author: login: phuhung273
Labels: name: p1
name: feature-request
description: A feature should be added or improved.
name: effort/medium
description: Medium work item – several days of effort
name: admired-contributor
description: [Pilot] contributed between 13-24 PRs to the CDK
name: pr/needs-maintainer-review
description: This PR needs a review from a Core Team Member
Created: 2025-03-30T10:54:19Z | Merged: 2025-04-10T19:24:34Z

### Issue # (if applicable)

Closes #11100

### Description of changes
- apigwv2 HttpStage support access logging to CloudWatch Logs https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging.html
- Same logic as V1 without Firehose as not supported.

### Description of how you validated changes
Unit + Integ

### Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*


Linked issue #11100: [apigatewayv2] Allow configuring access logging
<!-- short description of the feature you are proposing: -->
Currently there's no way on the HttpApi or HttpStage construct to enable access logging as specified via https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-stage-accesslogsettings.html.

This is pretty easy to setup [via sam](https://github.com/aws-samples/sessions-with-aws-sam/blob/master/http-api-logging/template.yaml#L13) but there's no equivalent atm for cdk.


### Use Case

Enable access logs for HttpApi





### Proposed Solution

<!-- Please include prototype/workaround/sketch/reference implementation: -->

- Implement AccessLogSettings on HttpStage
- Add `logging` prop to HttpApi which enables logging for all routes

I also think when the above is added to HttpApi that it should create its own log group and the permissions necessary for the HttpApi to write to it.

### Other

<!-- 
e.g. detailed explanation, stacktraces, related issues, suggestions on how to fix, 
links for us to have context, eg. associated pull-request, stackoverflow, slack, etc
-->





* [x] :wave: I may be able to implement this feature request

---

This is a :rocket: Feature Request

Linked issue comments: user: login: franciscoandrade82
created_at: 2021-01-06T14:11:13Z
body: Hello! Do you have news on this feature? Any plans to include it in a future version?
author_association: NONE
user: login: PeterAronZentai
created_at: 2021-02-17T16:49:21Z
body: Hello would someone help formulating logging setting for HTTP API - with maybe the help of  Cf* objects and the build hook I've read about? 
Or is there a way to run some aws sdk commands as part of CDK provisioning code? 
author_association: NONE
user: login: iRoachie
created_at: 2021-02-18T00:01:04Z
body: @franciscoandrade-unit I started work on it in #11126 but I haven't finished it as yet
author_association: CONTRIBUTOR
user: login: ltearno
created_at: 2021-02-19T16:15:02Z
body: @PeterAronZentai this page should help to formulate the cdk code to add the accesslog parameter on the underlying CfnStage resource of a Stage construct : https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html#cfn_layer_resource

Then the CfnStage has its options available (cdk: https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-apigatewayv2.CfnStage.html, cloudformation : https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-stage.html).

Something like this should work :

```typescript
// "apigateway.defaultStage.node.defaultChild" accesses the L1 Resource in the L2 Stage
const defaultStage = apiGateway.defaultStage.node.defaultChild as apigw.CfnStage

// the L1 settings are always available because they are CloudFormation constructs and
// AWS seems to always guarantee the 1 to 1 mapping
defaultStage.accessLogSettings = {
    destinationArn: `...`,
    format: JSON.stringify({
        "requestId": "$context.requestId",
        "ip": "$context.identity.sourceIp",
        "caller": "$context.identity.caller",
        "user": "$context.identity.user",
        "requestTime": "$context.requestTime",
        "httpMethod": "$context.httpMethod",
        "resourcePath": "$context.resourcePath",
        "status": "$context.status",
        "protocol": "$context.protocol",
        "responseLength": "$context.responseLength"
    })
}
```

If you try it, let me know because I will need it soon ! :)
author_association: NONE
user: login: PeterAronZentai
created_at: 2021-02-19T17:15:19Z
body: @ltearno  it does indeed work!

```TypeScript
    const log = new awslog.LogGroup(this, 'log')
    const stage = backendB.defaultStage?.node.defaultChild as  apigwv2.CfnStage;
    stage.accessLogSettings = {
        destinationArn: log.logGroupArn,
        format: `$context.identity.sourceIp - - [$context.requestTime] "$context.httpMethod $context.routeKey $context.protocol" $context.status $context.responseLength $context.requestId`,
    }
```

Much thanks!

Also you unlocked L1 hacking for me  - megathanks for that.
author_association: NONE
user: login: ltearno
created_at: 2021-02-25T07:20:31Z
body: You're welcome, thank you !
author_association: NONE
user: login: bonoethanPFG
created_at: 2021-06-15T20:22:28Z
body: Hi! Just curious if there's been any more progress made on this feature. Would love to have this available some time in the future!
author_association: NONE
user: login: lifeofzero
created_at: 2021-07-07T18:17:12Z
body: > @ltearno it does indeed work!
> 
> ```ts
>     const log = new awslog.LogGroup(this, 'log')
>     const stage = backendB.defaultStage?.node.defaultChild as  apigwv2.CfnStage;
>     stage.accessLogSettings = {
>         destinationArn: log.logGroupArn,
>         format: `$context.identity.sourceIp - - [$context.requestTime] "$context.httpMethod $context.routeKey $context.protocol" $context.status $context.responseLength $context.requestId`,
>     }
> ```
> 
> Much thanks!
> 
> Also you unlocked L1 hacking for me - megathanks for that.

@PeterAronZentai or @ltearno How were you able to apply the needed permissions for the apigwv2 to have permissions to write to the log group?  I keep getting the following:

`Insufficient permissions to enable logging (Service: AmazonApiGatewayV2; Status Code: 400`
author_association: NONE
user: login: jumpinjan
created_at: 2021-08-24T13:02:27Z
body: I could really use this feature too.   Thanks!
author_association: NONE
user: login: lifeofzero
created_at: 2021-08-24T13:09:21Z
body: @jumpinjan this is how I got it to work, if that's helpful.
```ts 
    // Setup the access log for APIGWv2
    const accessLogs = new logs.LogGroup(this, 'APIGW-AccessLogs')
    const stage = httpApi.defaultStage?.node.defaultChild as apigateway.CfnStage
    stage.accessLogSettings = {
      destinationArn: accessLogs.logGroupArn,
      format: JSON.stringify({
        requestId: '$context.requestId',
        userAgent: '$context.identity.userAgent',
        sourceIp: '$context.identity.sourceIp',
        requestTime: '$context.requestTime',
        requestTimeEpoch: '$context.requestTimeEpoch',
        httpMethod: '$context.httpMethod',
        path: '$context.path',
        status: '$context.status',
        protocol: '$context.protocol',
        responseLength: '$context.responseLength',
        domainName: '$context.domainName'
      })
    }

    const role = new iam.Role(this, 'ApiGWLogWriterRole', {
      assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com')
    })

    const policy = new iam.PolicyStatement({
      actions: [
        'logs:CreateLogGroup',
        'logs:CreateLogStream',
        'logs:DescribeLogGroups',
        'logs:DescribeLogStreams',
        'logs:PutLogEvents',
        'logs:GetLogEvents',
        'logs:FilterLogEvents'
      ],
      resources: ['*']
    })

    role.addToPolicy(policy)
    accessLogs.grantWrite(role) 
  ```
author_association: NONE
user: login: rpivo
created_at: 2021-12-27T23:49:31Z
body: Going to try some solutions from this thread, but would love if this feature were built-in!
author_association: NONE
user: login: rpivo
created_at: 2021-12-28T01:00:43Z
body: @lifeofzero 's example works well -- thanks!
author_association: NONE
user: login: atkinsonm
created_at: 2022-01-14T17:13:34Z
body: @lifeofzero's example worked for me without the ~~policy; the `grantWrite` provided `logs:CreateLogStream` and `logs:PutLogEvents` which was sufficient in my case for writing logs~~

(edit) it actually worked without any IAM resources at all, just the log group and the `accessLogSettings`. That's consistent with the [documentation](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging.html#http-api-enable-logging.console)
author_association: NONE
user: login: tjgurwara99
created_at: 2022-02-11T15:49:06Z
body: In case anyone from Python-CDK stumbles upon this thread and is unaware of how to use the answer by @ltearno in Python, all you need to do is this

```Python
from aws_cdk import aws_logs as _logs
from aws_cdk import aws_apigatewayv2 as _apigw

class YourStack(cdk.Stack):
    def __init__(self, scope, construct_id):
        super().__init__(scope, construct_id)
        log_group = _logs.LogGroup(self, "YourAccessLogGroup", retention=_logs.RetentionDays.ONE_MONTH)
        http_api = _apigw.HttpApi(self, "SomeIdentForApi", cors_preflight=...)
        # this is where the magic happens by references
        _stage: _apigw.CfnStage = http_api.default_stage.node.default_child
        _stage.access_log_settings = _apigw.CfnStage.AccessLogSettingsProperty(
            destination_arn=log_group.log_group_arn,
            format="$context.requestId", # choose whatever format you want $context.requestId is the necessary one.
        )
        # after this just add your routes and you're good to go
```
author_association: NONE
user: login: PatrykMilewski
created_at: 2022-05-24T09:43:43Z
body: It would be nice to include this as a core feature!
author_association: NONE
user: login: christophercarney
created_at: 2022-06-30T15:36:40Z
body: To anyone having trouble getting this to work like I did when upgrading  resources -- make sure you are importing the correct `CfnStage`

```
import { CfnStage } from "aws-cdk-lib/aws-apigatewayv2";
import { CfnStage } from "aws-cdk-lib/aws-apigateway";
```

Are different and the latter will turn not work for access logs on your API GW v2
author_association: NONE
user: login: tvb
created_at: 2022-09-07T14:25:42Z
body: > ```python
>         _stage: _apigw.CfnStage = http_api.default_stage.node.default_child
>         _stage.access_log_settings = _apigw.CfnStage.AccessLogSettingsProperty(
>             destination_arn=log_group.log_group_arn,
>             format="$context.requestId", # choose whatever format you want $context.requestId is the necessary one.
>         )
> ```

Hi @tjgurwara99 how to implement this when the default stage is disabled?

```
apigwv2.HttpApi(
            self,
...
            create_default_stage=False,
)
```
author_association: NONE
user: login: tjgurwara99
created_at: 2022-09-07T21:16:43Z
body: @tvb If I remember correctly, it all depends on how you handle creating a stage or whether you even create a stage. For example, you might be using `add_stage` method to create a custom stage or whether you have no stage at all (in which case I don't know how that works - I'm not using CDK anymore) but if you use `add_stage` you should be able to create the stage with `CfnStage` and set the `AccessLogSettingsProperty`.
author_association: NONE
user: login: github-actions[bot]
created_at: 2024-06-16T00:29:26Z
body: <!--p2 to p1-->
This issue has received a significant amount of attention so we are automatically upgrading its priority. A member of the community will see the re-prioritization and provide an update on the issue.
author_association: CONTRIBUTOR
user: login: azaylamba
created_at: 2024-07-12T11:11:24Z
body: It is an important feature missing for WebSocket APIs.
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-10T19:24:46Z
body: Comments on closed issues and PRs are hard for our team to see. 
If you need help, please open a new issue that references this one.
author_association: CONTRIBUTOR

Diff:
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/aws-cdk-aws-apigatewayv2-http-stage.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/aws-cdk-aws-apigatewayv2-http-stage.assets.json
index e8e4030e9bda3..a10c471e73cc2 100644
--- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/aws-cdk-aws-apigatewayv2-http-stage.assets.json
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/aws-cdk-aws-apigatewayv2-http-stage.assets.json
@@ -1,7 +1,8 @@
 {
-  "version": "39.0.0",
+  "version": "41.0.0",
   "files": {
-    "8a139312fcbabd6668985694bc84920a0266fbf9ed13095c117cdf1f87b97373": {
+    "e061f5a6d916014651daeef51dd06d34c84e734e619b1cae27e78b998029a70a": {
+      "displayName": "aws-cdk-aws-apigatewayv2-http-stage Template",
       "source": {
         "path": "aws-cdk-aws-apigatewayv2-http-stage.template.json",
         "packaging": "file"
@@ -9,7 +10,7 @@
       "destinations": {
         "current_account-current_region": {
           "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
-          "objectKey": "8a139312fcbabd6668985694bc84920a0266fbf9ed13095c117cdf1f87b97373.json",
+          "objectKey": "e061f5a6d916014651daeef51dd06d34c84e734e619b1cae27e78b998029a70a.json",
           "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
         }
       }
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/aws-cdk-aws-apigatewayv2-http-stage.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/aws-cdk-aws-apigatewayv2-http-stage.template.json
index 823f4be372aa4..fd27364145733 100644
--- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/aws-cdk-aws-apigatewayv2-http-stage.template.json
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/aws-cdk-aws-apigatewayv2-http-stage.template.json
@@ -1,5 +1,13 @@
 {
  "Resources": {
+  "MyLogGroup5C0DAD85": {
+   "Type": "AWS::Logs::LogGroup",
+   "Properties": {
+    "RetentionInDays": 731
+   },
+   "UpdateReplacePolicy": "Retain",
+   "DeletionPolicy": "Retain"
+  },
   "HttpApiF5A9A8A7": {
    "Type": "AWS::ApiGatewayV2::Api",
    "Properties": {
@@ -10,6 +18,15 @@
   "HttpStageWithPropertiesC0AABA83": {
    "Type": "AWS::ApiGatewayV2::Stage",
    "Properties": {
+    "AccessLogSettings": {
+     "DestinationArn": {
+      "Fn::GetAtt": [
+       "MyLogGroup5C0DAD85",
+       "Arn"
+      ]
+     },
+     "Format": "{\"extendedRequestId\":\"$context.extendedRequestId\",\"requestTime\":\"$context.requestTime\"}"
+    },
     "ApiId": {
      "Ref": "HttpApiF5A9A8A7"
     },
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.assets.json
new file mode 100644
index 0000000000000..eda752b72019c
--- /dev/null
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.assets.json
@@ -0,0 +1,20 @@
+{
+  "version": "41.0.0",
+  "files": {
+    "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": {
+      "displayName": "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52 Template",
+      "source": {
+        "path": "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.template.json",
+        "packaging": "file"
+      },
+      "destinations": {
+        "current_account-current_region": {
+          "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}",
+          "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
+          "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}"
+        }
+      }
+    }
+  },
+  "dockerImages": {}
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.template.json
new file mode 100644
index 0000000000000..ad9d0fb73d1dd
--- /dev/null
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.template.json
@@ -0,0 +1,36 @@
+{
+ "Parameters": {
+  "BootstrapVersion": {
+   "Type": "AWS::SSM::Parameter::Value<String>",
+   "Default": "/cdk-bootstrap/hnb659fds/version",
+   "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]"
+  }
+ },
+ "Rules": {
+  "CheckBootstrapVersion": {
+   "Assertions": [
+    {
+     "Assert": {
+      "Fn::Not": [
+       {
+        "Fn::Contains": [
+         [
+          "1",
+          "2",
+          "3",
+          "4",
+          "5"
+         ],
+         {
+          "Ref": "BootstrapVersion"
+         }
+        ]
+       }
+      ]
+     },
+     "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI."
+    }
+   ]
+  }
+ }
+}
\ No newline at end of file
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/cdk.out
index 91e1a8b9901d5..188478b55560e 100644
--- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/cdk.out
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/cdk.out
@@ -1 +1 @@
-{"version":"39.0.0"}
\ No newline at end of file
+{"version":"41.0.0"}
\ No newline at end of file
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/integ.json
index 108cff4a7691e..f64fce347b121 100644
--- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/integ.json
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/integ.json
@@ -1,14 +1,12 @@
 {
-  "version": "39.0.0",
+  "version": "41.0.0",
   "testCases": {
-    "integ.stage": {
+    "aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest": {
       "stacks": [
         "aws-cdk-aws-apigatewayv2-http-stage"
       ],
-      "diffAssets": false,
-      "stackUpdateWorkflow": true
+      "assertionStack": "aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest/DeployAssert",
+      "assertionStackName": "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52"
     }
-  },
-  "synthContext": {},
-  "enableLookups": false
+  }
 }
\ No newline at end of file
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/manifest.json
index 927b2f8f0c83e..d900d1e1671b7 100644
--- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/manifest.json
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/manifest.json
@@ -1,5 +1,5 @@
 {
-  "version": "39.0.0",
+  "version": "42.0.0",
   "artifacts": {
     "aws-cdk-aws-apigatewayv2-http-stage.assets": {
       "type": "cdk:asset-manifest",
@@ -18,7 +18,7 @@
         "validateOnSynth": false,
         "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
         "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
-        "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/8a139312fcbabd6668985694bc84920a0266fbf9ed13095c117cdf1f87b97373.json",
+        "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/e061f5a6d916014651daeef51dd06d34c84e734e619b1cae27e78b998029a70a.json",
         "requiresBootstrapStackVersion": 6,
         "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
         "additionalDependencies": [
@@ -34,6 +34,18 @@
         "aws-cdk-aws-apigatewayv2-http-stage.assets"
       ],
       "metadata": {
+        "/aws-cdk-aws-apigatewayv2-http-stage/MyLogGroup": [
+          {
+            "type": "aws:cdk:analytics:construct",
+            "data": "*"
+          }
+        ],
+        "/aws-cdk-aws-apigatewayv2-http-stage/MyLogGroup/Resource": [
+          {
+            "type": "aws:cdk:logicalId",
+            "data": "MyLogGroup5C0DAD85"
+          }
+        ],
         "/aws-cdk-aws-apigatewayv2-http-stage/HttpApi": [
           {
             "type": "aws:cdk:analytics:construct",
@@ -73,11 +85,60 @@
       },
       "displayName": "aws-cdk-aws-apigatewayv2-http-stage"
     },
+    "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.assets": {
+      "type": "cdk:asset-manifest",
+      "properties": {
+        "file": "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.assets.json",
+        "requiresBootstrapStackVersion": 6,
+        "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
+      }
+    },
+    "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52": {
+      "type": "aws:cloudformation:stack",
+      "environment": "aws://unknown-account/unknown-region",
+      "properties": {
+        "templateFile": "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.template.json",
+        "terminationProtection": false,
+        "validateOnSynth": false,
+        "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}",
+        "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}",
+        "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json",
+        "requiresBootstrapStackVersion": 6,
+        "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version",
+        "additionalDependencies": [
+          "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.assets"
+        ],
+        "lookupRole": {
+          "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}",
+          "requiresBootstrapStackVersion": 8,
+          "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version"
+        }
+      },
+      "dependencies": [
+        "awscdkawsapigatewayv2httpstagetestDefaultTestDeployAssert66182A52.assets"
+      ],
+      "metadata": {
+        "/aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest/DeployAssert/BootstrapVersion": [
+          {
+            "type": "aws:cdk:logicalId",
+            "data": "BootstrapVersion"
+          }
+        ],
+        "/aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest/DeployAssert/CheckBootstrapVersion": [
+          {
+            "type": "aws:cdk:logicalId",
+            "data": "CheckBootstrapVersion"
+          }
+        ]
+      },
+      "displayName": "aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest/DeployAssert"
+    },
     "Tree": {
       "type": "cdk:tree",
       "properties": {
         "file": "tree.json"
       }
     }
-  }
+  },
+  "minimumCliVersion": "2.1006.0"
 }
\ No newline at end of file
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/tree.json
index df35f6d3e5382..7be69e4fc071f 100644
--- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/tree.json
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.js.snapshot/tree.json
@@ -1,111 +1 @@
-{
-  "version": "tree-0.1",
-  "tree": {
-    "id": "App",
-    "path": "",
-    "children": {
-      "aws-cdk-aws-apigatewayv2-http-stage": {
-        "id": "aws-cdk-aws-apigatewayv2-http-stage",
-        "path": "aws-cdk-aws-apigatewayv2-http-stage",
-        "children": {
-          "HttpApi": {
-            "id": "HttpApi",
-            "path": "aws-cdk-aws-apigatewayv2-http-stage/HttpApi",
-            "children": {
-              "Resource": {
-                "id": "Resource",
-                "path": "aws-cdk-aws-apigatewayv2-http-stage/HttpApi/Resource",
-                "attributes": {
-                  "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Api",
-                  "aws:cdk:cloudformation:props": {
-                    "name": "HttpApi",
-                    "protocolType": "HTTP"
-                  }
-                },
-                "constructInfo": {
-                  "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnApi",
-                  "version": "0.0.0"
-                }
-              }
-            },
-            "constructInfo": {
-              "fqn": "aws-cdk-lib.aws_apigatewayv2.HttpApi",
-              "version": "0.0.0",
-              "metadata": [
-                "*"
-              ]
-            }
-          },
-          "HttpStageWithProperties": {
-            "id": "HttpStageWithProperties",
-            "path": "aws-cdk-aws-apigatewayv2-http-stage/HttpStageWithProperties",
-            "children": {
-              "Resource": {
-                "id": "Resource",
-                "path": "aws-cdk-aws-apigatewayv2-http-stage/HttpStageWithProperties/Resource",
-                "attributes": {
-                  "aws:cdk:cloudformation:type": "AWS::ApiGatewayV2::Stage",
-                  "aws:cdk:cloudformation:props": {
-                    "apiId": {
-                      "Ref": "HttpApiF5A9A8A7"
-                    },
-                    "defaultRouteSettings": {
-                      "throttlingBurstLimit": 1000,
-                      "throttlingRateLimit": 1000,
-                      "detailedMetricsEnabled": true
-                    },
-                    "description": "My Stage",
-                    "stageName": "$default"
-                  }
-                },
-                "constructInfo": {
-                  "fqn": "aws-cdk-lib.aws_apigatewayv2.CfnStage",
-                  "version": "0.0.0"
-                }
-              }
-            },
-            "constructInfo": {
-              "fqn": "aws-cdk-lib.aws_apigatewayv2.HttpStage",
-              "version": "0.0.0",
-              "metadata": [
-                "*"
-              ]
-            }
-          },
-          "BootstrapVersion": {
-            "id": "BootstrapVersion",
-            "path": "aws-cdk-aws-apigatewayv2-http-stage/BootstrapVersion",
-            "constructInfo": {
-              "fqn": "aws-cdk-lib.CfnParameter",
-              "version": "0.0.0"
-            }
-          },
-          "CheckBootstrapVersion": {
-            "id": "CheckBootstrapVersion",
-            "path": "aws-cdk-aws-apigatewayv2-http-stage/CheckBootstrapVersion",
-            "constructInfo": {
-              "fqn": "aws-cdk-lib.CfnRule",
-              "version": "0.0.0"
-            }
-          }
-        },
-        "constructInfo": {
-          "fqn": "aws-cdk-lib.Stack",
-          "version": "0.0.0"
-        }
-      },
-      "Tree": {
-        "id": "Tree",
-        "path": "Tree",
-        "constructInfo": {
-          "fqn": "constructs.Construct",
-          "version": "10.4.2"
-        }
-      }
-    },
-    "constructInfo": {
-      "fqn": "aws-cdk-lib.App",
-      "version": "0.0.0"
-    }
-  }
-}
\ No newline at end of file
+{"version":"tree-0.1","tree":{"id":"App","path":"","children":{"aws-cdk-aws-apigatewayv2-http-stage":{"id":"aws-cdk-aws-apigatewayv2-http-stage","path":"aws-cdk-aws-apigatewayv2-http-stage","children":{"MyLogGroup":{"id":"MyLogGroup","path":"aws-cdk-aws-apigatewayv2-http-stage/MyLogGroup","children":{"Resource":{"id":"Resource","path":"aws-cdk-aws-apigatewayv2-http-stage/MyLogGroup/Resource","attributes":{"aws:cdk:cloudformation:type":"AWS::Logs::LogGroup","aws:cdk:cloudformation:props":{"retentionInDays":731}},"constructInfo":{"fqn":"aws-cdk-lib.aws_logs.CfnLogGroup","version":"0.0.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.aws_logs.LogGroup","version":"0.0.0","metadata":["*"]}},"HttpApi":{"id":"HttpApi","path":"aws-cdk-aws-apigatewayv2-http-stage/HttpApi","children":{"Resource":{"id":"Resource","path":"aws-cdk-aws-apigatewayv2-http-stage/HttpApi/Resource","attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Api","aws:cdk:cloudformation:props":{"name":"HttpApi","protocolType":"HTTP"}},"constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnApi","version":"0.0.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpApi","version":"0.0.0","metadata":["*"]}},"HttpStageWithProperties":{"id":"HttpStageWithProperties","path":"aws-cdk-aws-apigatewayv2-http-stage/HttpStageWithProperties","children":{"Resource":{"id":"Resource","path":"aws-cdk-aws-apigatewayv2-http-stage/HttpStageWithProperties/Resource","attributes":{"aws:cdk:cloudformation:type":"AWS::ApiGatewayV2::Stage","aws:cdk:cloudformation:props":{"accessLogSettings":{"destinationArn":{"Fn::GetAtt":["MyLogGroup5C0DAD85","Arn"]},"format":"{\"extendedRequestId\":\"$context.extendedRequestId\",\"requestTime\":\"$context.requestTime\"}"},"apiId":{"Ref":"HttpApiF5A9A8A7"},"defaultRouteSettings":{"throttlingBurstLimit":1000,"throttlingRateLimit":1000,"detailedMetricsEnabled":true},"description":"My Stage","stageName":"$default"}},"constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.CfnStage","version":"0.0.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.aws_apigatewayv2.HttpStage","version":"0.0.0","metadata":["*"]}},"BootstrapVersion":{"id":"BootstrapVersion","path":"aws-cdk-aws-apigatewayv2-http-stage/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"aws-cdk-aws-apigatewayv2-http-stage/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"}},"aws-cdk-aws-apigatewayv2-http-stage-test":{"id":"aws-cdk-aws-apigatewayv2-http-stage-test","path":"aws-cdk-aws-apigatewayv2-http-stage-test","children":{"DefaultTest":{"id":"DefaultTest","path":"aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest","children":{"Default":{"id":"Default","path":"aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest/Default","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"DeployAssert":{"id":"DeployAssert","path":"aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest/DeployAssert","children":{"BootstrapVersion":{"id":"BootstrapVersion","path":"aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest/DeployAssert/BootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnParameter","version":"0.0.0"}},"CheckBootstrapVersion":{"id":"CheckBootstrapVersion","path":"aws-cdk-aws-apigatewayv2-http-stage-test/DefaultTest/DeployAssert/CheckBootstrapVersion","constructInfo":{"fqn":"aws-cdk-lib.CfnRule","version":"0.0.0"}}},"constructInfo":{"fqn":"aws-cdk-lib.Stack","version":"0.0.0"}}},"constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}},"constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}},"Tree":{"id":"Tree","path":"Tree","constructInfo":{"fqn":"constructs.Construct","version":"10.4.2"}}},"constructInfo":{"fqn":"aws-cdk-lib.App","version":"0.0.0"}}}
\ No newline at end of file
diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.ts
index 7c745d8dfa603..768bc8b21cce7 100644
--- a/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.ts
+++ b/packages/@aws-cdk-testing/framework-integ/test/aws-apigatewayv2/test/http/integ.stage.ts
@@ -1,12 +1,17 @@
 #!/usr/bin/env node
+import { IntegTest } from '@aws-cdk/integ-tests-alpha';
 import * as cdk from 'aws-cdk-lib';
-import * as apigw from 'aws-cdk-lib/aws-apigatewayv2';
+import * as apigwv2 from 'aws-cdk-lib/aws-apigatewayv2';
+import * as apigw from 'aws-cdk-lib/aws-apigateway';
+import * as logs from 'aws-cdk-lib/aws-logs';
 
 const app = new cdk.App();
 const stack = new cdk.Stack(app, 'aws-cdk-aws-apigatewayv2-http-stage');
 
-const httpApi = new apigw.HttpApi(stack, 'HttpApi', { createDefaultStage: false });
-new apigw.HttpStage(stack, 'HttpStageWithProperties', {
+const testLogGroup = new logs.LogGroup(stack, 'MyLogGroup');
+
+const httpApi = new apigwv2.HttpApi(stack, 'HttpApi', { createDefaultStage: false });
+new apigwv2.HttpStage(stack, 'HttpStageWithProperties', {
   httpApi,
   throttle: {
     rateLimit: 1000,
@@ -14,6 +19,15 @@ new apigw.HttpStage(stack, 'HttpStageWithProperties', {
   },
   detailedMetricsEnabled: true,
   description: 'My Stage',
+  accessLogSettings: {
+    destination: new apigwv2.LogGroupLogDestination(testLogGroup),
+    format: apigw.AccessLogFormat.custom(JSON.stringify({
+      extendedRequestId: apigw.AccessLogField.contextExtendedRequestId(),
+      requestTime: apigw.AccessLogField.contextRequestTime(),
+    })),
+  },
 });
 
-app.synth();
+new IntegTest(app, 'aws-cdk-aws-apigatewayv2-http-stage-test', {
+  testCases: [stack],
+});
diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/README.md b/packages/aws-cdk-lib/aws-apigatewayv2/README.md
index 6d62255d67d58..1e70baada1c4e 100644
--- a/packages/aws-cdk-lib/aws-apigatewayv2/README.md
+++ b/packages/aws-cdk-lib/aws-apigatewayv2/README.md
@@ -14,6 +14,7 @@
   - [VPC Link](#vpc-link)
   - [Private Integration](#private-integration)
   - [Generating ARN for Execute API](#generating-arn-for-execute-api)
+  - [Access Logging](#access-logging)
 - [WebSocket API](#websocket-api)
   - [Manage Connections Permission](#manage-connections-permission)
   - [Managing access to WebSocket APIs](#managing-access-to-websocket-apis)
@@ -357,6 +358,65 @@ const arn = api.arnForExecuteApi('GET', '/myApiPath', 'dev');
 - The 'ANY' method can be used for matching any HTTP methods not explicitly defined.
 - The function gracefully handles undefined parameters by using wildcards, making it flexible for various API configurations.
 
+## Access Logging
+
+You can turn on logging to write logs to CloudWatch Logs.
+Read more at [Configure logging for HTTP APIs in API Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging.html)
+
+```ts
+import * as logs from 'aws-cdk-lib/aws-logs';
+
+declare const api: apigwv2.HttpApi;
+declare const logGroup: logs.LogGroup;
+
+const stage = new apigwv2.HttpStage(this, 'Stage', {
+  httpApi: api,
+  accessLogSettings: {
+    destination: new apigwv2.LogGroupLogDestination(logGroup),
+  },
+});
+```
+
+The following code will generate the access log in the [CLF format](https://en.wikipedia.org/wiki/Common_Log_Format).
+
+```ts
+import * as apigw from 'aws-cdk-lib/aws-apigateway';
+import * as logs from 'aws-cdk-lib/aws-logs';
+
+declare const api: apigwv2.HttpApi;
+declare const logGroup: logs.LogGroup;
+
+const stage = new apigwv2.HttpStage(this, 'Stage', {
+  httpApi: api,
+  accessLogSettings: {
+    destination: new apigwv2.LogGroupLogDestination(logGroup),
+    format: apigw.AccessLogFormat.clf(),
+  },
+});
+```
+
+You can also configure your own access log format by using the `AccessLogFormat.custom()` API.
+`AccessLogField` provides commonly used fields. The following code configures access log to contain.
+
+```ts
+import * as apigw from 'aws-cdk-lib/aws-apigateway';
+import * as logs from 'aws-cdk-lib/aws-logs';
+
+declare const api: apigwv2.HttpApi;
+declare const logGroup: logs.LogGroup;
+
+const stage = new apigwv2.HttpStage(this, 'Stage', {
+  httpApi: api,
+  accessLogSettings: {
+    destination: new apigwv2.LogGroupLogDestination(logGroup),
+    format: apigw.AccessLogFormat.custom(
+      `${apigw.AccessLogField.contextRequestId()} ${apigw.AccessLogField.contextErrorMessage()} ${apigw.AccessLogField.contextErrorMessageString()}
+      ${apigw.AccessLogField.contextAuthorizerError()} ${apigw.AccessLogField.contextAuthorizerIntegrationStatus()}`
+    ),
+  },
+});
+```
+
 ## WebSocket API
 
 A WebSocket API in API Gateway is a collection of WebSocket routes that are integrated with backend HTTP endpoints,
diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/access-log.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/access-log.ts
new file mode 100644
index 0000000000000..609e6d0b35e36
--- /dev/null
+++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/access-log.ts
@@ -0,0 +1,39 @@
+import { IStage } from './stage';
+import { ILogGroup } from '../../../aws-logs';
+
+/**
+ * Access log destination for a HttpApi Stage.
+ */
+export interface IAccessLogDestination {
+  /**
+   * Binds this destination to the HttpApi Stage.
+   */
+  bind(stage: IStage): AccessLogDestinationConfig;
+}
+
+/**
+ * Options when binding a log destination to a HttpApi Stage.
+ */
+export interface AccessLogDestinationConfig {
+  /**
+   * The Amazon Resource Name (ARN) of the destination resource
+   */
+  readonly destinationArn: string;
+}
+
+/**
+ * Use CloudWatch Logs as a custom access log destination for API Gateway.
+ */
+export class LogGroupLogDestination implements IAccessLogDestination {
+  constructor(private readonly logGroup: ILogGroup) {
+  }
+
+  /**
+   * Binds this destination to the CloudWatch Logs.
+   */
+  public bind(_stage: IStage): AccessLogDestinationConfig {
+    return {
+      destinationArn: this.logGroup.logGroupArn,
+    };
+  }
+}
diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts
index 975eb1754c287..43d7862592305 100644
--- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts
+++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts
@@ -1,9 +1,11 @@
 import { IApi } from './api';
 import { ApiMapping } from './api-mapping';
-import { DomainMappingOptions, IStage } from './stage';
+import { DomainMappingOptions, IAccessLogSettings, IStage } from './stage';
+import { AccessLogFormat } from '../../../aws-apigateway/lib';
 import * as cloudwatch from '../../../aws-cloudwatch';
-import { Resource } from '../../../core';
-import { UnscopedValidationError } from '../../../core/lib/errors';
+import { Resource, Token } from '../../../core';
+import { UnscopedValidationError, ValidationError } from '../../../core/lib/errors';
+import { CfnStage } from '../apigatewayv2.generated';
 
 /**
  * Base class representing an API
@@ -59,6 +61,27 @@ export abstract class StageBase extends Resource implements IStage {
     this.node.addDependency(domainMapping.domainName);
   }
 
+  /**
+   * @internal
+   */
+  protected _validateAccessLogSettings(props?: IAccessLogSettings): CfnStage.AccessLogSettingsProperty | undefined {
+    if (!props) return;
+
+    const format = props.format;
+    if (
+      format &&
+      !Token.isUnresolved(format.toString()) &&
+      !/\$context\.(?:requestId|extendedRequestId)\b/.test(format.toString())
+    ) {
+      throw new ValidationError('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`', this);
+    }
+
+    return {
+      destinationArn: props.destination.bind(this).destinationArn,
+      format: format ? format.toString() : AccessLogFormat.clf().toString(),
+    };
+  }
+
   public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric {
     return this.baseApi.metric(metricName, props).with({
       dimensionsMap: { ApiId: this.baseApi.apiId, Stage: this.stageName },
diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/index.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/index.ts
index b0a0f1c0265eb..6310d093317f2 100644
--- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/index.ts
+++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/index.ts
@@ -1,3 +1,4 @@
+export * from './access-log';
 export * from './api';
 export * from './integration';
 export * from './route';
diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/stage.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/stage.ts
index be436d2332d33..41ff0d80623c6 100644
--- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/stage.ts
+++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/common/stage.ts
@@ -1,4 +1,6 @@
+import { IAccessLogDestination } from './access-log';
 import { IDomainName } from './domain-name';
+import { AccessLogFormat } from '../../../aws-apigateway/lib';
 import { Metric, MetricOptions } from '../../../aws-cloudwatch';
 import { IResource } from '../../../core';
 
@@ -80,6 +82,13 @@ export interface StageOptions {
    * @default false
    */
   readonly detailedMetricsEnabled?: boolean;
+
+  /**
+   * Settings for access logging.
+   *
+   * @default - No access logging
+   */
+  readonly accessLogSettings?: IAccessLogSettings;
 }
 
 /**
@@ -108,3 +117,26 @@ export interface ThrottleSettings {
    */
   readonly burstLimit?: number;
 }
+
+/**
+ * Settings for access logging.
+ */
+export interface IAccessLogSettings {
+  /**
+   * The destination where to write access logs.
+   *
+   * @default - No destination
+   */
+  readonly destination: IAccessLogDestination;
+
+  /**
+   * A single line format of access logs of data, as specified by selected $context variables.
+   * The format must include either `AccessLogFormat.contextRequestId()`
+   * or `AccessLogFormat.contextExtendedRequestId()`.
+   *
+   * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html
+   *
+   * @default - Common Log Format
+   */
+  readonly format?: AccessLogFormat;
+}
diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts b/packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
index 85fbcd595a16c..b68065d88fc84 100644
--- a/packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
+++ b/packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
@@ -170,6 +170,7 @@ export class HttpStage extends HttpStageBase {
     new CfnStage(this, 'Resource', {
       apiId: props.httpApi.apiId,
       stageName: this.physicalName,
+      accessLogSettings: this._validateAccessLogSettings(props.accessLogSettings),
       autoDeploy: props.autoDeploy,
       defaultRouteSettings: props.throttle || props.detailedMetricsEnabled ? {
         throttlingBurstLimit: props.throttle?.burstLimit,
diff --git a/packages/aws-cdk-lib/aws-apigatewayv2/test/http/stage.test.ts b/packages/aws-cdk-lib/aws-apigatewayv2/test/http/stage.test.ts
index e18c374bc7c32..3c32098058871 100644
--- a/packages/aws-cdk-lib/aws-apigatewayv2/test/http/stage.test.ts
+++ b/packages/aws-cdk-lib/aws-apigatewayv2/test/http/stage.test.ts
@@ -1,8 +1,10 @@
 import { Template } from '../../../assertions';
+import * as apigateway from '../../../aws-apigateway';
 import { Certificate } from '../../../aws-certificatemanager';
 import { Metric } from '../../../aws-cloudwatch';
-import { Stack } from '../../../core';
-import { DomainName, HttpApi, HttpStage } from '../../lib';
+import * as logs from '../../../aws-logs';
+import { Lazy, Stack } from '../../../core';
+import { DomainName, HttpApi, HttpStage, LogGroupLogDestination } from '../../lib';
 
 let stack: Stack;
 let api: HttpApi;
@@ -103,6 +105,157 @@ describe('HttpStage', () => {
     const metricNames = metrics.map(m => m.metricName);
     expect(metricNames).toEqual(['4xx', '5xx', 'DataProcessed', 'Latency', 'IntegrationLatency', 'Count']);
   });
+
+  test('if only the custom log destination log group is set', () => {
+    // WHEN
+    const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
+    new HttpStage(stack, 'my-stage', {
+      httpApi: api,
+      accessLogSettings: {
+        destination: new LogGroupLogDestination(testLogGroup),
+      },
+    });
+
+    // THEN
+    Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Stage', {
+      AccessLogSettings: {
+        DestinationArn: {
+          'Fn::GetAtt': [
+            'LogGroupF5B46931',
+            'Arn',
+          ],
+        },
+        Format: '$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] "$context.httpMethod $context.resourcePath $context.protocol" $context.status $context.responseLength $context.requestId',
+      },
+    });
+  });
+
+  test('if the custom log destination log group and format is set', () => {
+    // WHEN
+    const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
+    const testFormat = apigateway.AccessLogFormat.jsonWithStandardFields();
+    new HttpStage(stack, 'my-stage', {
+      httpApi: api,
+      accessLogSettings: {
+        destination: new LogGroupLogDestination(testLogGroup),
+        format: testFormat,
+      },
+    });
+
+    // THEN
+    Template.fromStack(stack).hasResourceProperties('AWS::ApiGatewayV2::Stage', {
+      AccessLogSettings: {
+        DestinationArn: {
+          'Fn::GetAtt': [
+            'LogGroupF5B46931',
+            'Arn',
+          ],
+        },
+        Format: '{"requestId":"$context.requestId","ip":"$context.identity.sourceIp","user":"$context.identity.user","caller":"$context.identity.caller","requestTime":"$context.requestTime","httpMethod":"$context.httpMethod","resourcePath":"$context.resourcePath","status":"$context.status","protocol":"$context.protocol","responseLength":"$context.responseLength"}',
+      },
+    });
+  });
+
+  describe('access log check', () => {
+    test('fails when access log format does not contain `contextRequestId()` or `contextExtendedRequestId()', () => {
+      // WHEN
+      const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
+      const testFormat = apigateway.AccessLogFormat.custom('');
+
+      // THEN
+      expect(() => new HttpStage(stack, 'my-stage', {
+        httpApi: api,
+        accessLogSettings: {
+          destination: new LogGroupLogDestination(testLogGroup),
+          format: testFormat,
+        },
+      })).toThrow('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`');
+    });
+
+    test('succeeds when access log format contains `contextRequestId()`', () => {
+      // WHEN
+      const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
+      const testFormat = apigateway.AccessLogFormat.custom(JSON.stringify({
+        requestId: apigateway.AccessLogField.contextRequestId(),
+      }));
+
+      // THEN
+      expect(() => new HttpStage(stack, 'my-stage', {
+        httpApi: api,
+        accessLogSettings: {
+          destination: new LogGroupLogDestination(testLogGroup),
+          format: testFormat,
+        },
+      })).not.toThrow();
+    });
+
+    test('succeeds when access log format contains `contextExtendedRequestId()`', () => {
+      // WHEN
+      const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
+      const testFormat = apigateway.AccessLogFormat.custom(JSON.stringify({
+        extendedRequestId: apigateway.AccessLogField.contextExtendedRequestId(),
+      }));
+
+      // THEN
+      expect(() => new HttpStage(stack, 'my-stage', {
+        httpApi: api,
+        accessLogSettings: {
+          destination: new LogGroupLogDestination(testLogGroup),
+          format: testFormat,
+        },
+      })).not.toThrow();
+    });
+
+    test('succeeds when access log format contains both `contextRequestId()` and `contextExtendedRequestId`', () => {
+      // WHEN
+      const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
+      const testFormat = apigateway.AccessLogFormat.custom(JSON.stringify({
+        requestId: apigateway.AccessLogField.contextRequestId(),
+        extendedRequestId: apigateway.AccessLogField.contextExtendedRequestId(),
+      }));
+
+      // THEN
+      expect(() => new HttpStage(stack, 'my-stage', {
+        httpApi: api,
+        accessLogSettings: {
+          destination: new LogGroupLogDestination(testLogGroup),
+          format: testFormat,
+        },
+      })).not.toThrow();
+    });
+
+    test('fails when access log format contains `contextRequestIdXxx`', () => {
+      // WHEN
+      const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
+      const testFormat = apigateway.AccessLogFormat.custom(JSON.stringify({
+        requestIdXxx: '$context.requestIdXxx',
+      }));
+
+      // THEN
+      expect(() => new HttpStage(stack, 'my-stage', {
+        httpApi: api,
+        accessLogSettings: {
+          destination: new LogGroupLogDestination(testLogGroup),
+          format: testFormat,
+        },
+      })).toThrow('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`');
+    });
+
+    test('does not fail when access log format is a token', () => {
+      // WHEN
+      const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
+      const testFormat = apigateway.AccessLogFormat.custom(Lazy.string({ produce: () => 'test' }));
+
+      // THEN
+      expect(() => new HttpStage(stack, 'my-stage', {
+        httpApi: api,
+        accessLogSettings: {
+          destination: new LogGroupLogDestination(testLogGroup),
+          format: testFormat,
+        },
+      })).not.toThrow();
+    });
+  });
 });
 
 describe('HttpStage with domain mapping', () => {


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-30T11:00:06Z
user: login: paulhcsun
body: Hi @phuhung273, thank you for this contribution for this very highly community-requested feature! I've just left a few comments for some changes. 
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-04-04T21:36:24Z
user: login: phuhung273
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T03:37:35Z
user: login: phuhung273
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T04:29:49Z
user: login: phuhung273
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T04:45:03Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-05T06:31:40Z
user: login: paulhcsun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:09:35Z
user: login: paulhcsun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:15:03Z
user: login: paulhcsun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:17:24Z
user: login: paulhcsun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T22:37:06Z
user: login: phuhung273
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T01:09:18Z
user: login: phuhung273
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T01:14:07Z
user: login: phuhung273
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T01:16:04Z
user: login: paulhcsun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T18:00:26Z
user: login: paulhcsun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:58:20Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-10T00:43:37Z
user: login: phuhung273
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T01:04:07Z
user: login: paulhcsun
body: Great work! Thanks for your contribution @phuhung273!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T18:52:59Z
user: login: paulhcsun
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T18:53:14Z

Review comments:
diff_hunk: @@ -167,9 +187,32 @@
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: github-advanced-security[bot]
body: ## Polynomial regular expression used on uncontrolled data

This [regular expression](1) that depends on [library input](2) may run slow on strings with many repetitions of 'a'.
This [regular expression](1) that depends on [library input](3) may run slow on strings with many repetitions of 'a'.

[Show more details](https://github.com/aws/aws-cdk/security/code-scanning/684)
created_at: 2025-03-30T11:00:06Z
author_association: NONE
diff_hunk: @@ -167,9 +187,32 @@
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: Could we modify this check to check that the `accessLofFormat.toString()` includes either contextRequestId or contextExtendedRequestId instead of using a regex? Something like this:
```
if (
  accessLogFormat !== undefined &&
  !Token.isUnresolved(accessLogFormat.toString()) &&
  !accessLogFormat.toString().includes('$context.requestId') &&
  !accessLogFormat.toString().includes('$context.extendedRequestId')
) {
```
created_at: 2025-04-04T18:56:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,9 +187,32 @@ export class HttpStage extends HttpStageBase {
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
+        throw new ValidationError('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`', this);
+      }
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: Could you provide a link for where you found this requirement? From the CFN docs I only see that the format must include at least `$context.requestId`: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigatewayv2-stage-accesslogsettings.html#aws-properties-apigatewayv2-stage-accesslogsettings-properties

Edit: nevermind I see that it's specified that they're equivalent [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html). Are you able to verify that it's possible to pass in a format with `$context.extendedRequestId` and not `$context.requestId`?
created_at: 2025-04-04T19:06:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,6 +82,24 @@ export interface HttpStageOptions extends StageOptions {
    * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint.
    */
   readonly stageName?: string;
+
+  /**
+   * The CloudWatch Logs log group or Firehose delivery stream where to write access logs.
+   *
+   * @default - No destination
+   */
+  readonly accessLogDestination?: IAccessLogDestination;
+
+  /**
+   * A single line format of access logs of data, as specified by selected $context variables.
+   * The format must include either `AccessLogFormat.contextRequestId()`
+   * or `AccessLogFormat.contextExtendedRequestId()`.
+   *
+   * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html
+   *
+   * @default - Common Log Format
+   */
+  readonly accessLogFormat?: AccessLogFormat;
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: Since the `accessLogDestination` property must be conditionally set if `accessLogFormat` is set, let's combine these two properties into an interface so that they must be specified together. Something like 
```ts
interface IAccessLogOptions { // or IAccessLogSettings
  readonly accessLogDestnation: IAccessLogDestination;
  readonly accessLogFormat: AccessLogFormat;
}
```

This way we enforce that they must be set together and then we won't need the validation in the `HttpStage` constructor.

Note: I see that the old `aws-apigateway`'s implementation for `accessLogSettings` has these two properties as separate properties, but I think we should still combine the properties even though it would differ from the existing convention. Wherever possible, we would like to enforce conditional properties with the API design rather than runtime validation. 
created_at: 2025-04-04T20:40:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,6 +82,24 @@ export interface HttpStageOptions extends StageOptions {
    * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint.
    */
   readonly stageName?: string;
+
+  /**
+   * The CloudWatch Logs log group or Firehose delivery stream where to write access logs.
+   *
+   * @default - No destination
+   */
+  readonly accessLogDestination?: IAccessLogDestination;
+
+  /**
+   * A single line format of access logs of data, as specified by selected $context variables.
+   * The format must include either `AccessLogFormat.contextRequestId()`
+   * or `AccessLogFormat.contextExtendedRequestId()`.
+   *
+   * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html
+   *
+   * @default - Common Log Format
+   */
+  readonly accessLogFormat?: AccessLogFormat;
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: phuhung273
body: Thanks for your advice. Agree that we should combine.

Another point, what do you think about making `accessLogFormat` optional with default value `AccessLogFormat.clf()`. IMO, most user just want to put the destination.
created_at: 2025-04-05T03:37:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,9 +187,32 @@ export class HttpStage extends HttpStageBase {
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
+        throw new ValidationError('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`', this);
+      }
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: phuhung273
body: Yes, tried on console and confirm that only `$context.extendedRequestId` is sufficient
<img width="522" alt="Screenshot 2025-04-05 111948" src="https://github.com/user-attachments/assets/5c61d3eb-53a7-487d-b96c-c7a39e54119d" />

Throw when neither `$context.extendedRequestId` nor `$context.requestId` given
<img width="732" alt="Screenshot 2025-04-05 111654" src="https://github.com/user-attachments/assets/9fa8d46d-32a5-48b6-9134-142fc1b574d7" />

Also cover this case in integ. Thank you for adressing this.
created_at: 2025-04-05T04:29:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,9 +187,32 @@
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: phuhung273
body: Intention is to prevent user from mistakenly using something like `$context.requestIdxxx`. In such case, `.includes('$context.requestId')` passes, but later fail on runtime.

I personally think we are fine with this. Let me know your point.
created_at: 2025-04-05T04:45:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,6 +61,35 @@
     this.node.addDependency(domainMapping.domainName);
   }
 
+  /**
+   * @internal
+   */
+  protected _validateAccessLogSettings(props?: IAccessLogSettings): CfnStage.AccessLogSettingsProperty | undefined {
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props?.destination;
+    const accessLogFormat = props?.format;
+
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts
user: login: github-advanced-security[bot]
body: ## Polynomial regular expression used on uncontrolled data

This [regular expression](1) that depends on [library input](2) may run slow on strings with many repetitions of 'a'.
This [regular expression](1) that depends on [library input](3) may run slow on strings with many repetitions of 'a'.

[Show more details](https://github.com/aws/aws-cdk/security/code-scanning/685)
created_at: 2025-04-05T06:31:40Z
author_association: NONE
diff_hunk: @@ -167,9 +187,32 @@ export class HttpStage extends HttpStageBase {
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
+        throw new ValidationError('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`', this);
+      }
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: Awesome, thanks for confirming!
created_at: 2025-04-08T22:09:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,6 +61,35 @@ export abstract class StageBase extends Resource implements IStage {
     this.node.addDependency(domainMapping.domainName);
   }
 
+  /**
+   * @internal
+   */
+  protected _validateAccessLogSettings(props?: IAccessLogSettings): CfnStage.AccessLogSettingsProperty | undefined {
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props?.destination;
+    const accessLogFormat = props?.format;
+
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
+        throw new ValidationError('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`', this);
+      }
+      if (accessLogFormat !== undefined && accessLogDestination === undefined) {
+        throw new ValidationError('Access log format is specified without a destination', this);
+      }
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts
user: login: paulhcsun
body: This check can be removed now with the updated `accessLogSettings` interface.
created_at: 2025-04-08T22:14:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,6 +82,24 @@ export interface HttpStageOptions extends StageOptions {
    * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint.
    */
   readonly stageName?: string;
+
+  /**
+   * The CloudWatch Logs log group or Firehose delivery stream where to write access logs.
+   *
+   * @default - No destination
+   */
+  readonly accessLogDestination?: IAccessLogDestination;
+
+  /**
+   * A single line format of access logs of data, as specified by selected $context variables.
+   * The format must include either `AccessLogFormat.contextRequestId()`
+   * or `AccessLogFormat.contextExtendedRequestId()`.
+   *
+   * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html
+   *
+   * @default - Common Log Format
+   */
+  readonly accessLogFormat?: AccessLogFormat;
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: Ya I think that's a good idea. Is it a valid use case to specify the `destination` without a `format`? If so then I think this change is fine as it is now. 
created_at: 2025-04-08T22:17:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,9 +187,32 @@
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: Oh I see, thanks for clarifying. Then maybe we can do a stricter exact match like so:
```ts
const allowedValues = new Set([
    '$context.requestId',
    '$context.extendedRequestId'
  ]);

if (
  accessLogFormat !== undefined &&
  !Token.isUnresolved(accessLogFormat.toString()) &&
  !allowedValues.has(accessLogFormat.toString())
) {
  throw new ValidationError(...);
}
```

I agree the current change would probably be fine but better to satisfy the CodeQL checks where possible. What do you think?
created_at: 2025-04-08T22:37:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,6 +61,35 @@ export abstract class StageBase extends Resource implements IStage {
     this.node.addDependency(domainMapping.domainName);
   }
 
+  /**
+   * @internal
+   */
+  protected _validateAccessLogSettings(props?: IAccessLogSettings): CfnStage.AccessLogSettingsProperty | undefined {
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props?.destination;
+    const accessLogFormat = props?.format;
+
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
+        throw new ValidationError('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`', this);
+      }
+      if (accessLogFormat !== undefined && accessLogDestination === undefined) {
+        throw new ValidationError('Access log format is specified without a destination', this);
+      }
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts
user: login: phuhung273
body: Thank you, this check is removed.
created_at: 2025-04-09T01:09:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,6 +82,24 @@ export interface HttpStageOptions extends StageOptions {
    * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint.
    */
   readonly stageName?: string;
+
+  /**
+   * The CloudWatch Logs log group or Firehose delivery stream where to write access logs.
+   *
+   * @default - No destination
+   */
+  readonly accessLogDestination?: IAccessLogDestination;
+
+  /**
+   * A single line format of access logs of data, as specified by selected $context variables.
+   * The format must include either `AccessLogFormat.contextRequestId()`
+   * or `AccessLogFormat.contextExtendedRequestId()`.
+   *
+   * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html
+   *
+   * @default - Common Log Format
+   */
+  readonly accessLogFormat?: AccessLogFormat;
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: phuhung273
body: `format` is always required according to CloudFormation. But I like the logic of apigwv1 to make it optional by providing default value if not specified, more user-friendly I think. So I kept it.
created_at: 2025-04-09T01:14:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,9 +187,32 @@
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: phuhung273
body: Thanks for the recommendation. I updated to use your first solution.
created_at: 2025-04-09T01:16:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,9 +187,32 @@
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: The first solution would accept an input like `$context.requestIdxxx` like you mentioned but that's not what we want. I think we should use the second option which uses stricter exact string matching.
created_at: 2025-04-09T18:00:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -80,6 +82,24 @@ export interface HttpStageOptions extends StageOptions {
    * @default '$default' the default stage of the API. This stage will have the URL at the root of the API endpoint.
    */
   readonly stageName?: string;
+
+  /**
+   * The CloudWatch Logs log group or Firehose delivery stream where to write access logs.
+   *
+   * @default - No destination
+   */
+  readonly accessLogDestination?: IAccessLogDestination;
+
+  /**
+   * A single line format of access logs of data, as specified by selected $context variables.
+   * The format must include either `AccessLogFormat.contextRequestId()`
+   * or `AccessLogFormat.contextExtendedRequestId()`.
+   *
+   * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html
+   *
+   * @default - Common Log Format
+   */
+  readonly accessLogFormat?: AccessLogFormat;
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: I see, I think that is fine then if we have the same behaviour in apigv1. Thanks!
created_at: 2025-04-09T21:58:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -59,6 +61,27 @@
     this.node.addDependency(domainMapping.domainName);
   }
 
+  /**
+   * @internal
+   */
+  protected _validateAccessLogSettings(props?: IAccessLogSettings): CfnStage.AccessLogSettingsProperty | undefined {
+    if (!props) return;
+
+    const format = props.format;
+    if (
+      format &&
+      !Token.isUnresolved(format.toString()) &&
+      !/[^$]*\$context\.(requestId|extendedRequestId)\b/.test(format.toString())
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/common/base.ts
user: login: github-advanced-security[bot]
body: ## Polynomial regular expression used on uncontrolled data

This [regular expression](1) that depends on [library input](2) may run slow on strings with many repetitions of '#'.
This [regular expression](1) that depends on [library input](3) may run slow on strings with many repetitions of '#'.

[Show more details](https://github.com/aws/aws-cdk/security/code-scanning/688)
created_at: 2025-04-10T00:43:37Z
author_association: NONE
diff_hunk: @@ -167,9 +187,32 @@
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: phuhung273
body: `format` contains multiple parameters, eg: `$context.responseLength $context.requestId`. So 2nd solution `.has` doesn't work as it check for the exact string. Anw, i've updated the regex to pass CodeQL. Please let me know what you think.
created_at: 2025-04-10T01:04:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -167,9 +187,32 @@
     // Enhanced CDK Analytics Telemetry
     addConstructMetadata(this, props);
 
+    // custom access logging
+    let accessLogSettings: CfnStage.AccessLogSettingsProperty | undefined;
+    const accessLogDestination = props.accessLogDestination;
+    const accessLogFormat = props.accessLogFormat;
+    if (!accessLogDestination && !accessLogFormat) {
+      accessLogSettings = undefined;
+    } else {
+      if (accessLogFormat !== undefined &&
+        !Token.isUnresolved(accessLogFormat.toString()) &&
+        !/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {
path: packages/aws-cdk-lib/aws-apigatewayv2/lib/http/stage.ts
user: login: paulhcsun
body: Good point, the current solution lgtm!
created_at: 2025-04-10T18:53:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: codecov[bot]
created_at: 2025-03-30T11:03:03Z
body: ## [Codecov](https://app.codecov.io/gh/aws/aws-cdk/pull/33977?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.98%. Comparing base [(`a67c3f5`)](https://app.codecov.io/gh/aws/aws-cdk/commit/a67c3f5a24b2ec7b2dbd0e5244841642f9d41b5f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) to head [(`d67296c`)](https://app.codecov.io/gh/aws/aws-cdk/commit/d67296c452ccfa21e2c5af9671a1c0de470f15f0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws).
> Report is 69 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main   #33977      +/-   ##
==========================================
+ Coverage   82.39%   83.98%   +1.58%     
==========================================
  Files         120      120              
  Lines        6960     6976      +16     
  Branches     1175     1178       +3     
==========================================
+ Hits         5735     5859     +124     
+ Misses       1120     1005     -115     
- Partials      105      112       +7     
```

| [Flag](https://app.codecov.io/gh/aws/aws-cdk/pull/33977/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) | Coverage Δ | |
|---|---|---|
| [suite.unit](https://app.codecov.io/gh/aws/aws-cdk/pull/33977/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) | `83.98% <ø> (+1.58%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Components](https://app.codecov.io/gh/aws/aws-cdk/pull/33977/components?src=pr&el=components&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) | Coverage Δ | |
|---|---|---|
| [packages/aws-cdk](https://app.codecov.io/gh/aws/aws-cdk/pull/33977/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) | `∅ <ø> (∅)` | |
| [packages/aws-cdk-lib/core](https://app.codecov.io/gh/aws/aws-cdk/pull/33977/components?src=pr&el=component&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aws) | `83.98% <ø> (+1.58%)` | :arrow_up: |

</details>
<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: phuhung273
created_at: 2025-04-05T06:43:55Z
body: Seeing community demand for the same setting in `WebSocketApi`, eg: #21935 and comments from original issue. I moved it to `StageBase` to facilitate adding it to `WebSocket` later.

I did try to include `WebSocket` also, but ran into this error `CloudWatch Logs role ARN must be set in account settings to enable logging`. Found out that we need something like this `cloudWatchRole` in V1 for convenience https://github.com/aws/aws-cdk/blob/74cbe275d56d9a58fe8607e415719555c2cd4d75/packages/aws-cdk-lib/aws-apigateway/lib/restapi.ts#L162-L167

Therefore, decided to only change `StageBase` and support `HttpApi` for the scope of this PR. Will create another for `WebSocket`.
author_association: CONTRIBUTOR
user: login: paulhcsun
created_at: 2025-04-09T00:13:53Z
body: Hi @phuhung273, thanks for your quick response to the requested changes. I've just left two more comments:
1. to remove the check that is no longer needed
2. to update the condition for the accessLogFormat

Happy to approve after those changes are made!
author_association: CONTRIBUTOR
user: login: paulhcsun
created_at: 2025-04-10T18:53:30Z
body: @mergifyio update
author_association: CONTRIBUTOR
user: login: mergify[bot]
created_at: 2025-04-10T18:53:37Z
body: Thank you for contributing! Your pull request will be updated from main and then merged automatically (do not update manually, and be sure to [allow changes to be pushed to your fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)).
author_association: CONTRIBUTOR
user: login: mergify[bot]
created_at: 2025-04-10T18:53:45Z
body: > update

#### ☑️ Nothing to do

<details>

- [ ] `queue-position = -1` [📌 update requirement]
- [X] `#commits-behind > 0` [📌 update requirement]
- [X] `-closed` [📌 update requirement]
- [X] `-conflict` [📌 update requirement]

</details>


<!---
DO NOT EDIT
-*- Mergify Payload -*-
{"command": "update", "conclusion": "neutral", "action_is_running": false}
-*- Mergify Payload End -*-
-->
author_association: CONTRIBUTOR
user: login: aws-cdk-automation
created_at: 2025-04-10T19:24:28Z
body: 
### AWS CodeBuild CI Report

* CodeBuild project: AutoBuildv2Project1C6BFA3F-wQm2hXv2jqQv
* Commit ID: 00d25a3e60d2db39562002909560560d1004146c
* Result: SUCCEEDED
* [Build Logs](https://2cl995f30a.execute-api.us-east-1.amazonaws.com/Prod/buildlogs?key=5e2cb0b3-b7b4-4027-862c-ae68d5dc5636%2Fbuild.log) (available for 30 days)

*Powered by [github-codebuild-logs](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:277187709615:applications~github-codebuild-logs), available on the [AWS Serverless Application Repository](https://aws.amazon.com/serverless/serverlessrepo/)*


<!--
CREATED BY GITHUB-CODEBUILD-LOGS
-->


author_association: COLLABORATOR
user: login: mergify[bot]
created_at: 2025-04-10T19:24:36Z
body: Thank you for contributing! Your pull request will be updated from main and then merged automatically (do not update manually, and be sure to [allow changes to be pushed to your fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/allowing-changes-to-a-pull-request-branch-created-from-a-fork)).
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-10T19:24:46Z
body: Comments on closed issues and PRs are hard for our team to see. 
If you need help, please open a new issue that references this one.
author_association: CONTRIBUTOR