Skip to content

Commit

Permalink
feat(AWS HTTP API): Support access logs configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
medikoo committed Feb 26, 2020
1 parent 6a13eee commit f2cb89a
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 7 deletions.
36 changes: 36 additions & 0 deletions docs/providers/aws/events/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,39 @@ functions:
- user.id
- user.email
```

## Access logs

Deployed stage can have acess logging enabled, for that just turn on logs for HTTP API in provider settings as follows:

```yaml
provider:
logs:
httpApi: true
```

Default logs format is:

```json
{
"requestId": "$context.requestId",
"ip": "$context.identity.sourceIp",
"requestTime": "$context.requestTime",
"httpMethod": "$context.httpMethod",
"routeKey": "$context.routeKey",
"status": "$context.status",
"protocol": "$context.protocol",
"responseLength": "$context.responseLength"
}
```

It can be overriden via `format` setting:

```yaml
provider:
logs:
httpApi:
format: '{ "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime" }'
```

See [AWS HTTP API Logging](https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html) documentation for more info on variables that can be used
3 changes: 3 additions & 0 deletions docs/providers/aws/guide/serverless.yml.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ provider:
roleManagedExternally: false # Specifies whether the ApiGateway CloudWatch Logs role setting is not managed by Serverless. Defaults to false.
websocket: # Optional configuration which specifies if Websocket logs are used. This can either be set to `true` to use defaults, or configured via subproperties.
level: INFO # Optional configuration which specifies the log level to use for execution logging. May be set to either INFO or ERROR.
httpApi: # Optional configuration which specifies if HTTP API logs are used. This can either be set to `true` (to use defaults as below) or specific log format configuraiton can be provided
format: '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }'

frameworkLambda: true # Optional, whether to write CloudWatch logs for custom resource lambdas as added by the framework

package: # Optional deployment packaging configuration
Expand Down
6 changes: 6 additions & 0 deletions lib/plugins/aws/lib/naming.js
Original file line number Diff line number Diff line change
Expand Up @@ -585,4 +585,10 @@ module.exports = {
getHttpApiAuthorizerLogicalId(authorizerName) {
return `HttpApiAuthorizer${this.getNormalizedFunctionName(authorizerName)}`;
},
getHttpApiLogGroupLogicalId() {
return 'HttpApiLogGroup';
},
getHttpApiLogGroupName() {
return `/aws/http-api/${this.getStackName()}`;
},
};
48 changes: 41 additions & 7 deletions lib/plugins/aws/package/compile/events/httpApi/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class HttpApiEvents {
if (!this.config.routes.size) return;
this.cfTemplate = this.serverless.service.provider.compiledCloudFormationTemplate;
this.compileApi();
this.compileLogGroup();
this.compileStage();
this.compileAuthorizers();
this.compileEndpoints();
Expand Down Expand Up @@ -67,15 +68,32 @@ class HttpApiEvents {
Properties: properties,
};
}
compileLogGroup() {
if (!this.config.accessLogFormat) return;
this.cfTemplate.Resources[this.provider.naming.getHttpApiLogGroupLogicalId()] = {
Type: 'AWS::Logs::LogGroup',
Properties: { LogGroupName: this.provider.naming.getHttpApiLogGroupName() },
};
}
compileStage() {
this.cfTemplate.Resources[this.provider.naming.getHttpApiStageLogicalId()] = {
Type: 'AWS::ApiGatewayV2::Stage',
Properties: {
ApiId: { Ref: this.provider.naming.getHttpApiLogicalId() },
StageName: '$default',
AutoDeploy: true,
},
const properties = {
ApiId: { Ref: this.provider.naming.getHttpApiLogicalId() },
StageName: '$default',
AutoDeploy: true,
};
const resource = (this.cfTemplate.Resources[this.provider.naming.getHttpApiStageLogicalId()] = {
Type: 'AWS::ApiGatewayV2::Stage',
Properties: properties,
});
if (this.config.accessLogFormat) {
properties.AccessLogSettings = {
DestinationArn: {
'Fn::GetAtt': [this.provider.naming.getHttpApiLogGroupLogicalId(), 'Arn'],
},
Format: this.config.accessLogFormat,
};
resource.DependsOn = this.provider.naming.getHttpApiLogGroupLogicalId();
}
this.cfTemplate.Outputs.HttpApiUrl = {
Description: 'URL of the HTTP API',
Value: {
Expand Down Expand Up @@ -196,6 +214,22 @@ Object.defineProperties(
});
}
}

const userLogsConfig = providerConfig.logs && providerConfig.logs.httpApi;
if (userLogsConfig) {
this.config.accessLogFormat =
userLogsConfig.format ||
`{${JSON.stringify({
requestId: '$context.requestId',
ip: '$context.identity.sourceIp',
requestTime: '$context.requestTime',
httpMethod: '$context.httpMethod',
routeKey: '$context.routeKey',
status: '$context.status',
protocol: '$context.protocol',
responseLength: '$context.responseLength',
})}}`;
}
for (const [functionName, functionData] of _.entries(this.serverless.service.functions)) {
const routeTargetData = {
functionName,
Expand Down
39 changes: 39 additions & 0 deletions lib/plugins/aws/package/compile/events/httpApi/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ describe('HttpApiEvents', () => {
const resource = cfResources[naming.getHttpApiLogicalId()];
expect(resource.Properties).to.not.have.property('CorsConfiguration');
});
it('Should not configure logs when not asked to', () => {
expect(cfResources).to.not.have.property(naming.getHttpApiLogGroupLogicalId());
});
it('Should configure stage resource', () => {
const resource = cfResources[naming.getHttpApiStageLogicalId()];
expect(resource.Type).to.equal('AWS::ApiGatewayV2::Stage');
Expand Down Expand Up @@ -364,4 +367,40 @@ describe('HttpApiEvents', () => {
expect(routeResourceProps.AuthorizationScopes).to.deep.equal(['foo']);
});
});

describe('Access logs', () => {
let cfResources;
let naming;
before(() =>
fixtures
.extend('httpApi', {
provider: {
logs: {
httpApi: true,
},
},
})
.then(fixturePath =>
runServerless({
cwd: fixturePath,
cliArgs: ['package'],
}).then(serverless => {
cfResources = serverless.service.provider.compiledCloudFormationTemplate.Resources;
naming = serverless.getProvider('aws').naming;
})
)
);

it('Should configure log group resource', () => {
const resource = cfResources[naming.getHttpApiLogGroupLogicalId()];
expect(resource.Type).to.equal('AWS::Logs::LogGroup');
expect(resource.Properties).to.have.property('LogGroupName');
});

it('Should setup logs format on stage', () => {
const stageResourceProps = cfResources[naming.getHttpApiStageLogicalId()].Properties;

expect(stageResourceProps.AccessLogSettings).to.have.property('Format');
});
});
});
10 changes: 10 additions & 0 deletions tests/integration-all/http-api/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { expect } = require('chai');
const log = require('log').get('serverless:test');
const awsRequest = require('@serverless/test/aws-request');
const fixtures = require('../../fixtures');
const { confirmCloudWatchLogs } = require('../../utils/misc');

const { getTmpDirPath } = require('../../utils/fs');
const {
Expand Down Expand Up @@ -76,6 +77,7 @@ describe('HTTP API Integration Test', function() {
},
},
},
logs: { httpApi: true },
},
functions: {
foo: {
Expand Down Expand Up @@ -167,6 +169,14 @@ describe('HTTP API Integration Test', function() {
const json = await responseAuthorized.json();
expect(json).to.deep.equal({ method: 'GET', path: '/foo' });
});

it('should expose access logs when configured to', () =>
confirmCloudWatchLogs(`/aws/http-api/${stackName}`, async () => {
const response = await fetch(`${endpoint}/some-post`, { method: 'POST' });
await response.json();
}).then(events => {
expect(events.length > 0).to.equal(true);
}));
});

describe('Catch-all endpoints', () => {
Expand Down

0 comments on commit f2cb89a

Please sign in to comment.