Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(apigateway): relax access log format check to allow either requestId or extendedRequestId #22591

Merged
merged 5 commits into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1128,8 +1128,8 @@ Access logging creates logs every time an API method is accessed. Access logs ca
who has accessed the API, how the caller accessed the API and what responses were generated.
Access logs are configured on a Stage of the RestApi.
Access logs can be expressed in a format of your choosing, and can contain any access details, with a
minimum that it must include the 'requestId'. The list of variables that can be expressed in the access
log can be found
minimum that it must include either 'requestId' or 'extendedRequestId'. The list of variables that
can be expressed in the access log can be found
[here](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference).
Read more at [Setting Up CloudWatch API Logging in API
Gateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-logging.html)
Expand All @@ -1140,9 +1140,9 @@ const prdLogGroup = new logs.LogGroup(this, "PrdLogs");
const api = new apigateway.RestApi(this, 'books', {
deployOptions: {
accessLogDestination: new apigateway.LogGroupLogDestination(prdLogGroup),
accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields()
}
})
accessLogFormat: apigateway.AccessLogFormat.jsonWithStandardFields(),
},
});
const deployment = new apigateway.Deployment(this, 'Deployment', {api});

// development stage
Expand All @@ -1159,8 +1159,8 @@ new apigateway.Stage(this, 'dev', {
resourcePath: true,
responseLength: true,
status: true,
user: true
})
user: true,
}),
});
```

Expand Down
8 changes: 5 additions & 3 deletions packages/@aws-cdk/aws-apigateway/lib/stage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ export interface StageOptions extends MethodDeploymentOptions {

/**
* A single line format of access logs of data, as specified by selected $content variables.
* The format must include at least `AccessLogFormat.contextRequestId()`.
* The format must include either `AccessLogFormat.contextRequestId()`
* or `AccessLogFormat.contextExtendedRequestId()`.
*
* @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference
*
* @default - Common Log Format
Expand Down Expand Up @@ -387,9 +389,9 @@ export class Stage extends StageBase {
} else {
if (accessLogFormat !== undefined &&
!Token.isUnresolved(accessLogFormat.toString()) &&
!/.*\$context.requestId.*/.test(accessLogFormat.toString())) {
!/.*\$context.(requestId|extendedRequestId)\b.*/.test(accessLogFormat.toString())) {

throw new Error('Access log must include at least `AccessLogFormat.contextRequestId()`');
throw new Error('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`');
}
if (accessLogFormat !== undefined && accessLogDestination === undefined) {
throw new Error('Access log format is specified without a destination');
Expand Down
173 changes: 130 additions & 43 deletions packages/@aws-cdk/aws-apigateway/test/stage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,59 +343,146 @@ describe('stage', () => {
});
});

test('fails when access log format does not contain `AccessLogFormat.contextRequestId()`', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');
describe('access log check', () => {
test('fails when access log format does not contain `contextRequestId()` or `contextExtendedRequestId()', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');

// WHEN
const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
const testFormat = apigateway.AccessLogFormat.custom('');
// WHEN
const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
const testFormat = apigateway.AccessLogFormat.custom('');

// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogDestination: new apigateway.LogGroupLogDestination(testLogGroup),
accessLogFormat: testFormat,
})).toThrow(/Access log must include at least `AccessLogFormat.contextRequestId\(\)`/);
});
// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogDestination: new apigateway.LogGroupLogDestination(testLogGroup),
accessLogFormat: testFormat,
})).toThrow('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`');
});

test('succeeds when access log format contains `contextRequestId()`', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');

// WHEN
const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
const testFormat = apigateway.AccessLogFormat.custom(JSON.stringify({
requestId: apigateway.AccessLogField.contextRequestId(),
}));

// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogDestination: new apigateway.LogGroupLogDestination(testLogGroup),
accessLogFormat: testFormat,
})).not.toThrow();
});

test('succeeds when access log format contains `contextExtendedRequestId()`', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');

// WHEN
const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
const testFormat = apigateway.AccessLogFormat.custom(JSON.stringify({
extendedRequestId: apigateway.AccessLogField.contextExtendedRequestId(),
}));

// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogDestination: new apigateway.LogGroupLogDestination(testLogGroup),
accessLogFormat: testFormat,
})).not.toThrow();
});

test('succeeds when access log format contains both `contextRequestId()` and `contextExtendedRequestId`', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');

// 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 apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogDestination: new apigateway.LogGroupLogDestination(testLogGroup),
accessLogFormat: testFormat,
})).not.toThrow();
});

test('fails when access log format contains `contextRequestIdXxx`', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');

// WHEN
const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
const testFormat = apigateway.AccessLogFormat.custom(JSON.stringify({
requestIdXxx: '$context.requestIdXxx',
}));

// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogDestination: new apigateway.LogGroupLogDestination(testLogGroup),
accessLogFormat: testFormat,
})).toThrow('Access log must include either `AccessLogFormat.contextRequestId()` or `AccessLogFormat.contextExtendedRequestId()`');
});

test('does not fail when access log format is a token', () => {
test('does not fail when access log format is a token', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');

// WHEN
const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
const testFormat = apigateway.AccessLogFormat.custom(cdk.Lazy.string({ produce: () => 'test' }));
// WHEN
const testLogGroup = new logs.LogGroup(stack, 'LogGroup');
const testFormat = apigateway.AccessLogFormat.custom(cdk.Lazy.string({ produce: () => 'test' }));

// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogDestination: new apigateway.LogGroupLogDestination(testLogGroup),
accessLogFormat: testFormat,
})).not.toThrow();
});
// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogDestination: new apigateway.LogGroupLogDestination(testLogGroup),
accessLogFormat: testFormat,
})).not.toThrow();
});

test('fails when access log destination is empty', () => {
test('fails when access log destination is empty', () => {
// GIVEN
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');
const stack = new cdk.Stack();
const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: false });
const deployment = new apigateway.Deployment(stack, 'my-deployment', { api });
api.root.addMethod('GET');

// WHEN
const testFormat = apigateway.AccessLogFormat.jsonWithStandardFields();
// WHEN
const testFormat = apigateway.AccessLogFormat.jsonWithStandardFields();

// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogFormat: testFormat,
})).toThrow(/Access log format is specified without a destination/);
// THEN
expect(() => new apigateway.Stage(stack, 'my-stage', {
deployment,
accessLogFormat: testFormat,
})).toThrow(/Access log format is specified without a destination/);
});
});

test('default throttling settings', () => {
Expand Down