├── .gitignore ├── LICENSE ├── README.md ├── backend ├── package-lock.json ├── package.json ├── resources │ ├── glue-db.yml │ ├── glue-table.yml │ ├── kinesis-cloudwatch-logs.yml │ ├── kinesis-firehose-delivery-stream.yml │ ├── outputs.yml │ └── s3-bucket.yml └── serverless.yml ├── client ├── .npmignore ├── ApiCallRecorder.js ├── README.md ├── package-lock.json ├── package.json ├── server │ └── ApiCallRecorderServer.js └── test │ ├── handler.js │ └── test.js └── example ├── .DS_Store ├── functions └── test.js ├── package-lock.json ├── package.json └── serverless.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tobi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-lambda-api-call-recorder 2 | Client and infrastructure for the recording of AWS API calls in Node-based Lambda functions, leveraging AWS Client Side Monitoring. 3 | 4 | ## Why does this project exist? 5 | 6 | Mainly curiosity. But also because AWS CloudTrail doesn't record all API calls, thus it's possible to gather more insights how your Lambda functions behave. 7 | 8 | Also, it's possible to determine the latencies of the API calls of different AWS services. 9 | 10 | ## Client 11 | 12 | To be able to use the client library, you need to have the backend installed in the AWS account you want to record the AWS SDK API calls. Please see the appropriate backend section in this README. 13 | 14 | ### Installation 15 | 16 | To install the client library, just do the following: 17 | 18 | ```bash 19 | $ npm i --save aws-lambda-api-call-recorder 20 | ``` 21 | 22 | ### Usage 23 | 24 | To use the client library, you can wrap you handler function with `ApiCallRecorder`: 25 | 26 | ```javascript 27 | const AWS = require('aws-sdk'); 28 | const ApiCallRecorder = require('aws-lambda-api-call-recorder'); 29 | 30 | const sts = new AWS.STS(); 31 | 32 | const handler = async (event, context) => { 33 | console.log(event); 34 | console.log(context); 35 | const callerIdentity = await sts.getCallerIdentity({}).promise(); 36 | console.log(callerIdentity); 37 | return { 38 | statusCode: 200, 39 | headers: { 40 | 'Content-Type': 'application/json' 41 | }, 42 | body: JSON.stringify({ 43 | ok: true 44 | }) 45 | }; 46 | } 47 | 48 | exports.handler = ApiCallRecorder(handler); 49 | ``` 50 | 51 | Also, you will have to set the following environment variables to enable the detailled AWS SDK API Call recording: 52 | 53 | * `AWS_CSM_ENABLED`: This environment variable should be set to `true` 54 | * Either `NODE_ENV` needs to be set to `development`, or, in case you want `production`, you can additionally set `API_CALL_RECORDER_IS_ACTIVATED` to `true` 55 | * `API_CALL_RECORDER_DELIVERY_STREAM_NAME` needs to be set to the name of the Kinesis DeliveryStream. In case you use the Serverless framework and already installed the backend, you should be able to use the backed stack's output for this: `${cf:api-call-recorder-backend-${self:provider.stage}.ApiCallRecorderDeliveryStreamName}` 56 | 57 | Also, make sure that the IAM execution role of your Lambda function has the following permissions to write to the Kinesis Firehose DeliveryStream (this assumes you already have deployed the backend, and the stage is the same as of your Lambda function): 58 | 59 | ```yaml 60 | - Effect: Allow 61 | Action: 62 | - firehose:PutRecordBatch 63 | Resource: '${cf:api-call-recorder-backend-${self:provider.stage}.ApiCallRecorderDeliveryStreamArn}' 64 | ``` 65 | 66 | **Hints**: 67 | * Running production workloads with the `APICallRecorder` enabled is not advise, because it will add latency (and hence, costs) 68 | * The `APICallRecorder` will **NOT** log the final upload of the recorded events to the Firehose DeliveryStream 69 | 70 | ### How it works 71 | 72 | The client will wrap the Node Lambda function handler, start a UDP server on `localhost` on port `31000`, so that it can receive the published messages of the [AWS Client Side Monitoring](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/metrics.html) (CSM). Once the wrapped handler functions completes, the UDP server will be kept running another 25ms to make sure all API calls can be recorded. After it's shut down, the API calls will be pushed to a Kinesis Firehose DeliveryStream, which will batch the events to gzipped files in S3. 73 | 74 | Those files in S3 are then made queryable in Athena by an appropriate Glue Table definition. 75 | 76 | ## Backend 77 | 78 | The backend needs to be deployed before you can record the API calls. 79 | 80 | ### Installation 81 | 82 | Please clone the git repo: 83 | 84 | ```bash 85 | $ git clone git@github.com:tobilg/aws-lambda-api-call-recorder.git 86 | ``` 87 | 88 | Then, change to the backend folder: 89 | 90 | ```bash 91 | $ cd backend 92 | ``` 93 | 94 | The deployment of the backend requires the presence of the [Serverless framework](https://www.serverless.com) on the machine from where the backend should be deployed. Also, you'll need to have your AWS credentials setup accordingly. 95 | 96 | If it's present, you can use a single command to deploy the backend: 97 | 98 | ```bash 99 | $ sls deploy 100 | ``` 101 | 102 | ### Query the API call logs 103 | 104 | To query the API call logs, you can use Athena. Please go to the Athena service in the AWS Console, and select the appropriate database from the selection (`api_calls_$STAGE`). Once you've done that, you should see the table `calls` on the left-hand side. 105 | 106 | Example query to get the most use API calls filtered by a specific Lambda function: 107 | 108 | ```sql 109 | SELECT 110 | service, 111 | api, 112 | avg(latency) as avg_latency, 113 | count(*) as call_cnt 114 | FROM 115 | api_calls_dev.calls 116 | WHERE 117 | functionname = 'api-call-recorder-example-dev-test' -- Example 118 | AND 119 | type = 'ApiCall' 120 | GROUP BY 121 | service, 122 | api 123 | ``` 124 | 125 | **Hint:** 126 | If you don't see any data, make sure to run the following SQL command to update the table partitions: 127 | 128 | ```sql 129 | MSCK REPAIR TABLE api_calls_dev.calls 130 | ``` 131 | 132 | ## Example project 133 | 134 | Please clone the git repo if you haven't done so already: 135 | 136 | ```bash 137 | $ git clone git@github.com:tobilg/aws-lambda-api-call-recorder.git 138 | ``` 139 | 140 | Then, change to the backend folder: 141 | 142 | ```bash 143 | $ cd example 144 | ``` 145 | 146 | The deployment of the example stack requires the presence of the [Serverless framework](https://www.serverless.com) on the machine from where the backend should be deployed. Also, you'll need to have your AWS credentials setup accordingly. 147 | 148 | If it's present, you can use a single command to deploy the example stack: 149 | 150 | ```bash 151 | $ sls deploy 152 | ``` 153 | 154 | The example for a wrapped AWS Lambda function handler can be found at [functions/test.js](example/functions/test.js). 155 | -------------------------------------------------------------------------------- /backend/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-call-recorder-backend", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "serverless-pseudo-parameters": { 8 | "version": "2.5.0", 9 | "resolved": "https://registry.npmjs.org/serverless-pseudo-parameters/-/serverless-pseudo-parameters-2.5.0.tgz", 10 | "integrity": "sha512-A/O49AR8LL6jlnPSmnOTYgL1KqVgskeRla4sVDeS/r5dHFJlwOU5MgFilc7aaQP8NWAwRJANaIS9oiSE3I+VUA==", 11 | "dev": true 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-call-recorder-backend", 3 | "version": "0.1.0", 4 | "description": "Backend infrastructure for the AWS SDK API Call Recorder", 5 | "scripts": { 6 | "cfn-lint": "cfn-lint .serverless/cloudformation-template-update-stack.json", 7 | "package": "sls package", 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "qa": "npm run package && npm run cfn-lint", 10 | "deploy": "sls deploy" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:tobilg/aws-lambda-api-call-recorder.git" 15 | }, 16 | "author": "TobiLG ", 17 | "license": "UNLICENSED", 18 | "bugs": { 19 | "url": "https://github.com/tobilg/aws-lambda-api-call-recorder/issues" 20 | }, 21 | "homepage": "https://github.com/tobilg/aws-lambda-api-call-recorder#readme", 22 | "devDependencies": { 23 | "serverless-pseudo-parameters": "^2.5.0" 24 | }, 25 | "dependencies": {} 26 | } 27 | -------------------------------------------------------------------------------- /backend/resources/glue-db.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | ApiCallRecorderDatabase: 3 | Type: AWS::Glue::Database 4 | Properties: 5 | CatalogId: '#{AWS::AccountId}' 6 | DatabaseInput: 7 | Name: '${self:custom.glue.databaseName}' -------------------------------------------------------------------------------- /backend/resources/glue-table.yml: -------------------------------------------------------------------------------- 1 | Resources: 2 | CallsTable: 3 | Type: AWS::Glue::Table 4 | Properties: 5 | CatalogId: '#{AWS::AccountId}' 6 | DatabaseName: '#{ApiCallRecorderDatabase}' 7 | TableInput: 8 | Name: '${self:custom.glue.tableName}' 9 | Description: 'Gzipped JSON incoming events via MQTT' 10 | TableType: EXTERNAL_TABLE 11 | PartitionKeys: 12 | - Name: date 13 | Type: string 14 | StorageDescriptor: 15 | OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat 16 | Columns: 17 | - Name: version 18 | Type: int 19 | - Name: clientid 20 | Type: string 21 | - Name: type 22 | Type: string 23 | - Name: service 24 | Type: string 25 | - Name: api 26 | Type: string 27 | - Name: timestamp 28 | Type: bigint 29 | - Name: attemptlatency 30 | Type: int 31 | - Name: fqdn 32 | Type: string 33 | - Name: useragent 34 | Type: string 35 | - Name: accesskey 36 | Type: string 37 | - Name: region 38 | Type: string 39 | - Name: httpstatuscode 40 | Type: int 41 | - Name: xamzrequestid 42 | Type: string 43 | - Name: xamzid2 44 | Type: string 45 | - Name: finalhttpstatuscode 46 | Type: int 47 | - Name: latency 48 | Type: int 49 | - Name: maxretriesexceeded 50 | Type: int 51 | - Name: functionname 52 | Type: string 53 | InputFormat: org.apache.hadoop.mapred.TextInputFormat 54 | Location: 's3://#{ApiCallsS3Bucket}/${self:custom.s3.rawPrefix}' 55 | SerdeInfo: 56 | SerializationLibrary: org.openx.data.jsonserde.JsonSerDe -------------------------------------------------------------------------------- /backend/resources/kinesis-cloudwatch-logs.yml: -------------------------------------------------------------------------------- 1 | # Create a LogGroup 2 | Resources: 3 | CallDataLogGroup: 4 | Type: 'AWS::Logs::LogGroup' 5 | Properties: 6 | LogGroupName: ${self:custom.logs.groupName} 7 | RetentionInDays: ${self:custom.logs.retentionInDays} 8 | 9 | # Create a LogStream 10 | ApiCallRecorderCWLogStream: 11 | Type: 'AWS::Logs::LogStream' 12 | DependsOn: 13 | - CallDataLogGroup 14 | Properties: 15 | LogGroupName: ${self:custom.logs.groupName} 16 | LogStreamName: ${self:custom.logs.streamName} 17 | -------------------------------------------------------------------------------- /backend/resources/kinesis-firehose-delivery-stream.yml: -------------------------------------------------------------------------------- 1 | # Create a Kinesis DeliveryStream to write the incoming data to S3 2 | # See docs at https://docs.aws.amazon.com/firehose/latest/APIReference/API_CreateDeliveryStream.html 3 | Resources: 4 | ApiCallRecorderKinesisFirehose: 5 | Type: 'AWS::KinesisFirehose::DeliveryStream' 6 | Properties: 7 | DeliveryStreamName: ${self:custom.kinesis.name} 8 | DeliveryStreamType: DirectPut 9 | S3DestinationConfiguration: 10 | BucketARN: '#{ApiCallsS3Bucket.Arn}' 11 | CloudWatchLoggingOptions: 12 | Enabled: true 13 | LogGroupName: ${self:custom.logs.groupName} 14 | LogStreamName: ${self:custom.logs.streamName} 15 | BufferingHints: 16 | IntervalInSeconds: ${self:custom.kinesis.limits.intervalInSeconds} 17 | SizeInMBs: ${self:custom.kinesis.limits.sizeInMB} 18 | CompressionFormat: ${self:custom.kinesis.raw.compressionFormat} 19 | Prefix: ${self:custom.s3.rawPrefix}${self:custom.kinesis.s3.partitionPrefix} 20 | ErrorOutputPrefix: '${self:custom.s3.errorPrefix}${self:custom.kinesis.s3.errorPrefix}' 21 | RoleARN: '#{ApiCallRecorderKinesisFirehoseRole.Arn}' 22 | 23 | ApiCallRecorderKinesisFirehoseRole: 24 | Type: 'AWS::IAM::Role' 25 | Properties: 26 | RoleName: ${self:service.name}-firehose-role-${self:provider.stage} 27 | AssumeRolePolicyDocument: 28 | Version: '2012-10-17' 29 | Statement: 30 | - Sid: '' 31 | Effect: Allow 32 | Principal: 33 | Service: firehose.amazonaws.com 34 | Action: 'sts:AssumeRole' 35 | Condition: 36 | StringEquals: 37 | 'sts:ExternalId': 38 | Ref: 'AWS::AccountId' 39 | 40 | ApiCallRecorderPolicyKinesis: 41 | Type: AWS::IAM::Policy 42 | Properties: 43 | PolicyName: ${self:service.name}-firehose-policy-${self:provider.stage} 44 | Roles: 45 | - Ref: ApiCallRecorderKinesisFirehoseRole 46 | PolicyDocument: 47 | Version: '2012-10-17' 48 | Statement: 49 | - Effect: Allow 50 | Action: 51 | - glue:GetTable 52 | - glue:GetTableVersions 53 | Resource: 54 | - '*' 55 | - Effect: Allow 56 | Action: 57 | - s3:AbortMultipartUpload 58 | - s3:GetBucketLocation 59 | - s3:GetObject 60 | - s3:ListBucket 61 | - s3:ListBucketMultipartUploads 62 | - s3:PutObject 63 | Resource: 64 | - 'arn:aws:s3:::${self:custom.s3.bucketName}' 65 | - 'arn:aws:s3:::${self:custom.s3.bucketName}/*' 66 | - Effect: Allow 67 | Action: 68 | - logs:CreateLogGroup 69 | - logs:CreateLogStream 70 | - logs:PutLogEvents 71 | - logs:DescribeLogStreams 72 | Resource: 73 | - '#{CallDataLogGroup.Arn}' 74 | -------------------------------------------------------------------------------- /backend/resources/outputs.yml: -------------------------------------------------------------------------------- 1 | Outputs: 2 | ApiCallRecorderDeliveryStreamName: 3 | Description: The name of the API Call Recorder Kinesis Firehose DeliveryStream 4 | Value: '#{ApiCallRecorderKinesisFirehose}' 5 | Export: 6 | Name: 'ApiCallRecorderDeliveryStreamName' 7 | ApiCallRecorderDeliveryStreamArn: 8 | Description: The ARN of the API Call Recorder Kinesis Firehose DeliveryStream 9 | Value: '#{ApiCallRecorderKinesisFirehose.Arn}' 10 | Export: 11 | Name: 'ApiCallRecorderDeliveryStreamArn' 12 | -------------------------------------------------------------------------------- /backend/resources/s3-bucket.yml: -------------------------------------------------------------------------------- 1 | # Create an encrypted S3 bucket 2 | Resources: 3 | ApiCallsS3Bucket: 4 | Type: 'AWS::S3::Bucket' 5 | Properties: 6 | BucketName: ${self:custom.s3.bucketName} 7 | BucketEncryption: 8 | ServerSideEncryptionConfiguration: 9 | - ServerSideEncryptionByDefault: 10 | SSEAlgorithm: 'AES256' 11 | -------------------------------------------------------------------------------- /backend/serverless.yml: -------------------------------------------------------------------------------- 1 | # Service name 2 | service: 3 | name: api-call-recorder-backend 4 | 5 | # List all plugins used 6 | plugins: 7 | - serverless-pseudo-parameters 8 | 9 | # Custom config 10 | custom: 11 | # Define defaults 12 | defaults: 13 | # Define the default stage 14 | stage: dev 15 | # Define the default region 16 | region: us-east-1 17 | 18 | # Define the default CloudWatch logs settings 19 | logs: 20 | retentionInDays: 14 21 | groupName: 'kinesis/api-call-recorder-backend/${self:provider.stage}' 22 | streamName: 'KinesisFirehoseLogStream' 23 | # Define the default Kinesis settings 24 | kinesis: 25 | name: '${self:service.name}-delivery-stream-${self:provider.stage}' 26 | limits: 27 | intervalInSeconds: 60 28 | sizeInMB: 128 29 | raw: 30 | compressionFormat: GZIP 31 | s3: 32 | # NOTE: 33 | # 1. prefixes are appended to s3 prefix configuration 34 | # 2. If the partition layout in s3 is changed the partitionMetaDataStore function accordingly 35 | partitionPrefix: 'date=!{timestamp:yyyy}-!{timestamp:MM}-!{timestamp:dd}/' 36 | errorPrefix: 'error/!{timestamp:yyyy}-!{timestamp:MM}-!{timestamp:dd}/!{firehose:error-output-type}/' 37 | # Define the S3 defaults 38 | s3: 39 | bucketName: api-call-recorder-#{AWS::AccountId}-${self:provider.stage} 40 | rawPrefix: 'incoming/' 41 | errorPrefix: 'firehose-errors/' 42 | # Define the Glue defaults 43 | glue: 44 | databaseName: api_calls_${self:provider.stage} 45 | tableName: calls 46 | 47 | provider: 48 | name: aws 49 | runtime: nodejs12.x 50 | region: ${opt:region, self:custom.defaults.region} 51 | stage: ${opt:stage, self:custom.defaults.stage} 52 | 53 | resources: 54 | # Create Glue database 55 | - ${file(./resources/glue-db.yml)} 56 | 57 | # Create Glue table 58 | - ${file(./resources/glue-table.yml)} 59 | 60 | # Kinesis Firehose Delivery Stream for writing out to the S3 data management storage 61 | - ${file(./resources/kinesis-firehose-delivery-stream.yml)} 62 | 63 | # S3 bucket for data management storage and metadata 64 | - ${file(./resources/s3-bucket.yml)} 65 | 66 | # LogGroup and LogStream for the Kinesis Delivery Stream 67 | - ${file(./resources/kinesis-cloudwatch-logs.yml)} 68 | 69 | # Stack outputs 70 | - ${file(./resources/outputs.yml)} 71 | -------------------------------------------------------------------------------- /client/.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /client/ApiCallRecorder.js: -------------------------------------------------------------------------------- 1 | const ApiCallRecorderServer = require('./server/ApiCallRecorderServer'); 2 | 3 | module.exports = (handler) => ( 4 | event, 5 | context 6 | ) => { 7 | const wrappedHandler = async (event, context)=> { 8 | // Instantiate API Call Recorder Server 9 | const apiCallRecorderServer = new ApiCallRecorderServer(); 10 | // Store expection(s) 11 | let exception; 12 | // Statistics 13 | let handlerStartTimestamp, handlerStopTimestamp, recordingStartTimestamp, recordingStopTimestamp, 14 | uploadStartTimestamp, uploadStopTimestamp; 15 | recordingStartTimestamp = new Date().getTime(); 16 | try { 17 | // Start API Call Recorder Server 18 | await apiCallRecorderServer.start(); 19 | // Execute original handler function 20 | handlerStartTimestamp = new Date().getTime(); 21 | const response = await handler(event, context); 22 | handlerStopTimestamp = new Date().getTime(); 23 | return response; 24 | } catch (e) { 25 | exception = e; 26 | } finally { 27 | try { 28 | // Wait for last API calls to arrive 29 | await new Promise(resolve => setTimeout(resolve, 25)); 30 | // Stop API Call Recorder Server 31 | await apiCallRecorderServer.stop(); 32 | recordingStopTimestamp = new Date().getTime(); 33 | // Upload API Call Recorder Server logged calls 34 | uploadStartTimestamp = new Date().getTime(); 35 | await apiCallRecorderServer.upload(); 36 | uploadStopTimestamp = new Date().getTime(); 37 | // Log statistics 38 | console.log({ 39 | recordingDuration: (recordingStopTimestamp-recordingStartTimestamp), 40 | handlerDuration: (handlerStopTimestamp-handlerStartTimestamp), 41 | uploadingDuration: (uploadStopTimestamp-uploadStartTimestamp) 42 | }); 43 | } catch (e) { 44 | console.log('Error occured: ', e); 45 | } 46 | } 47 | if (exception) { 48 | throw exception; 49 | } 50 | return; 51 | }; 52 | return wrappedHandler(event, context); 53 | } 54 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # aws-lambda-api-call-recorder 2 | Client for the recording of AWS API calls in Node-based Lambda functions, leveraging AWS Client Side Monitoring. 3 | 4 | ## Why does this project exist? 5 | 6 | Mainly curiosity. But also because AWS CloudTrail doesn't record all API calls, thus it's possible to gather more insights how your Lambda functions behave. 7 | 8 | Also, it's possible to determine the latencies of the API calls of different AWS services. 9 | 10 | ## Preconditions 11 | 12 | To be able to use the client library, you need to have the [backend installed](https://github.com/tobilg/aws-lambda-api-call-recorder) in the AWS account you want to record the AWS SDK API calls. Please see the appropriate backend section in the [README](https://github.com/tobilg/aws-lambda-api-call-recorder/README.md). 13 | 14 | ### Installation 15 | 16 | To install the client library, just do the following: 17 | 18 | ```bash 19 | $ npm i --save aws-lambda-api-call-recorder 20 | ``` 21 | 22 | ### Usage 23 | 24 | To use the client library, you can wrap you handler function with `ApiCallRecorder`: 25 | 26 | ```javascript 27 | const AWS = require('aws-sdk'); 28 | const ApiCallRecorder = require('aws-lambda-api-call-recorder'); 29 | 30 | const sts = new AWS.STS(); 31 | 32 | const handler = async (event, context) => { 33 | console.log(event); 34 | console.log(context); 35 | const callerIdentity = await sts.getCallerIdentity({}).promise(); 36 | console.log(callerIdentity); 37 | return { 38 | statusCode: 200, 39 | headers: { 40 | 'Content-Type': 'application/json' 41 | }, 42 | body: JSON.stringify({ 43 | ok: true 44 | }) 45 | }; 46 | } 47 | 48 | exports.handler = ApiCallRecorder(handler); 49 | ``` 50 | 51 | Also, you will have to set the following environment variables to enable the detailled AWS SDK API Call recording: 52 | 53 | * `AWS_CSM_ENABLED`: This environment variable should be set to `true` 54 | * Either `NODE_ENV` needs to be set to `development`, or, in case you want `production`, you can additionally set `API_CALL_RECORDER_IS_ACTIVATED` to `true` 55 | * `API_CALL_RECORDER_DELIVERY_STREAM_NAME` needs to be set to the name of the Kinesis DeliveryStream. In case you use the Serverless framework and already installed the backend, you should be able to use the backed stack's output for this: `${cf:api-call-recorder-backend-${self:provider.stage}.ApiCallRecorderDeliveryStreamName}` 56 | 57 | Also, make sure that the IAM execution role of your Lambda function has the following permissions to write to the Kinesis Firehose DeliveryStream (this assumes you already have deployed the backend, and the stage is the same as of your Lambda function): 58 | 59 | ```yaml 60 | - Effect: Allow 61 | Action: 62 | - firehose:PutRecordBatch 63 | Resource: '${cf:api-call-recorder-backend-${self:provider.stage}.ApiCallRecorderDeliveryStreamArn}' 64 | ``` 65 | 66 | **Hints**: 67 | * Running production workloads with the `APICallRecorder` enabled is not advise, because it will add latency (and hence, costs) 68 | * The `APICallRecorder` will **NOT** log the final upload of the recorded events to the Firehose DeliveryStream 69 | 70 | ### How it works 71 | 72 | The client will wrap the Node Lambda function handler, start a UDP server on `localhost` on port `31000`, so that it can receive the published messages of the [AWS Client Side Monitoring](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/metrics.html) (CSM). Once the wrapped handler functions completes, the UDP server will be kept running another 25ms to make sure all API calls can be recorded. After it's shut down, the API calls will be pushed to a Kinesis Firehose DeliveryStream, which will batch the events to gzipped files in S3. 73 | 74 | Those files in S3 are then made queryable in Athena by an appropriate Glue Table definition. 75 | -------------------------------------------------------------------------------- /client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-lambda-api-call-recorder", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "aws-sdk": { 8 | "version": "2.751.0", 9 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.751.0.tgz", 10 | "integrity": "sha512-0lo7YKTfjEwoP+2vK7F7WGNigvwFxqiM96PzBaseOpOelfhFHPKEJpk2Poa12JI89c8dGoc1PhTQ1TSTJK3ZqQ==", 11 | "requires": { 12 | "buffer": "4.9.2", 13 | "events": "1.1.1", 14 | "ieee754": "1.1.13", 15 | "jmespath": "0.15.0", 16 | "querystring": "0.2.0", 17 | "sax": "1.2.1", 18 | "url": "0.10.3", 19 | "uuid": "3.3.2", 20 | "xml2js": "0.4.19" 21 | } 22 | }, 23 | "base64-js": { 24 | "version": "1.3.1", 25 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 26 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 27 | }, 28 | "buffer": { 29 | "version": "4.9.2", 30 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 31 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 32 | "requires": { 33 | "base64-js": "^1.0.2", 34 | "ieee754": "^1.1.4", 35 | "isarray": "^1.0.0" 36 | } 37 | }, 38 | "events": { 39 | "version": "1.1.1", 40 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 41 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 42 | }, 43 | "ieee754": { 44 | "version": "1.1.13", 45 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 46 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 47 | }, 48 | "isarray": { 49 | "version": "1.0.0", 50 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 51 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 52 | }, 53 | "jmespath": { 54 | "version": "0.15.0", 55 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 56 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 57 | }, 58 | "punycode": { 59 | "version": "1.3.2", 60 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 61 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 62 | }, 63 | "querystring": { 64 | "version": "0.2.0", 65 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 66 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 67 | }, 68 | "sax": { 69 | "version": "1.2.1", 70 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 71 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 72 | }, 73 | "url": { 74 | "version": "0.10.3", 75 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 76 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 77 | "requires": { 78 | "punycode": "1.3.2", 79 | "querystring": "0.2.0" 80 | } 81 | }, 82 | "uuid": { 83 | "version": "3.3.2", 84 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 85 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 86 | }, 87 | "xml2js": { 88 | "version": "0.4.19", 89 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 90 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 91 | "requires": { 92 | "sax": ">=0.6.0", 93 | "xmlbuilder": "~9.0.1" 94 | } 95 | }, 96 | "xmlbuilder": { 97 | "version": "9.0.7", 98 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 99 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-lambda-api-call-recorder", 3 | "version": "0.0.1", 4 | "description": "A recorder of AWS API calls for Lambda functions", 5 | "main": "ApiCallRecorder.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/tobilg/aws-lambda-api-call-recorder.git" 12 | }, 13 | "keywords": [ 14 | "aws", 15 | "api", 16 | "sdk", 17 | "recorder", 18 | "iam", 19 | "permissions" 20 | ], 21 | "author": "TobiLG ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/tobilg/aws-lambda-api-call-recorder/issues" 25 | }, 26 | "homepage": "https://github.com/tobilg/aws-lambda-api-call-recorder#readme", 27 | "dependencies": { 28 | "aws-sdk": "^2.751.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /client/server/ApiCallRecorderServer.js: -------------------------------------------------------------------------------- 1 | const udp = require('dgram'); 2 | const { Firehose } = require('aws-sdk'); 3 | 4 | const firehose = new Firehose({ 5 | apiVersion: '2015-08-04', 6 | region: process.env.AWS_REGION || 'us-east-1' 7 | }); 8 | 9 | class ApiCallRecorderServer { 10 | constructor (firehoseDeliveryStreamName, logger, port = 31000) { 11 | // Create UDP server socket 12 | this.udpServer = udp.createSocket('udp4'); 13 | 14 | // Set UDP server port 15 | this.port = port; 16 | 17 | // Define logger 18 | this.logger = logger || { 19 | info: console.log, 20 | warn: console.log, 21 | error: console.error, 22 | trace: console.log, 23 | debug: console.log 24 | } 25 | 26 | // Check if the recording is activated 27 | this.isActivated = process.env.NODE_ENV === 'development' || !!(process.env.API_CALL_RECORDER_IS_ACTIVATED === 'true'); 28 | 29 | // Store Firehose DeliveryStream name 30 | this.firehoseDeliveryStreamName = firehoseDeliveryStreamName || process.env.API_CALL_RECORDER_DELIVERY_STREAM_NAME; 31 | 32 | // Store messages 33 | this.queue = []; 34 | 35 | // Event handlers 36 | this.udpServer.on('message', this.handleMessage.bind(this)); 37 | this.udpServer.on('error', this.handleError.bind(this)); 38 | } 39 | 40 | handleError (error) { 41 | this.logger.error('Error: ' + error); 42 | this.udpServer.close(); 43 | } 44 | 45 | async handleMessage (msg, info) { 46 | // Add to queue 47 | this.queue.push(JSON.parse(msg.toString())); 48 | } 49 | 50 | async start () { 51 | let self = this; 52 | if (!self.isActivated) { 53 | return Promise.resolve(); 54 | } else { 55 | return new Promise((resolve, reject) => { 56 | // Startup event listener 57 | self.udpServer.on('listening', function() { 58 | const address = self.udpServer.address(); 59 | self.logger.debug(`UDP server started on port ${address.port}`); 60 | resolve(); 61 | }); 62 | // Start server 63 | self.udpServer.bind(self.port); 64 | }); 65 | } 66 | } 67 | 68 | async stop () { 69 | let self = this; 70 | if (!self.isActivated) { 71 | return Promise.resolve(); 72 | } else { 73 | return new Promise((resolve, reject) => { 74 | // Shutdown event listener 75 | this.udpServer.on('close',function(){ 76 | self.logger.debug(`UDP server stopped on port ${self.port}`); 77 | resolve(); 78 | }); 79 | // Shutdown server 80 | self.udpServer.close(); 81 | }); 82 | } 83 | } 84 | 85 | async upload () { 86 | // Check if something is in the queue, if not resolve 87 | if (this.queue.length === 0 || !this.isActivated) { 88 | this.logger.debug('No API calls to upload'); 89 | return Promise.resolve(); 90 | } else { 91 | this.logger.debug(`${this.queue.length} API calls to upload`); 92 | return firehose.putRecordBatch({ 93 | DeliveryStreamName: this.firehoseDeliveryStreamName, 94 | Records: this.queue.map(item => { 95 | // Add Lambda function name 96 | item.functionName = process.env.AWS_LAMBDA_FUNCTION_NAME || null; 97 | return { 98 | Data: `${JSON.stringify(item)}\n` 99 | } 100 | }) 101 | }).promise(); 102 | } 103 | } 104 | } 105 | 106 | module.exports = ApiCallRecorderServer; 107 | -------------------------------------------------------------------------------- /client/test/handler.js: -------------------------------------------------------------------------------- 1 | const ApiCallRecorder = require('../ApiCallRecorder'); 2 | 3 | const handler = async (event, context) => { 4 | console.log("In handler"); 5 | const a = await new Promise((resolve, reject) => { 6 | setTimeout(() => { 7 | console.log("Timeout finished") 8 | resolve(); 9 | }, 10000) 10 | }); 11 | return a; 12 | } 13 | 14 | module.exports = ApiCallRecorder(handler); 15 | -------------------------------------------------------------------------------- /client/test/test.js: -------------------------------------------------------------------------------- 1 | const test = require('./handler') 2 | 3 | test({}, {}) 4 | -------------------------------------------------------------------------------- /example/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobilg/aws-lambda-api-call-recorder/083e8a879758227f7395dcc8a9dd4d6e974afe58/example/.DS_Store -------------------------------------------------------------------------------- /example/functions/test.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const ApiCallRecorder = require('aws-lambda-api-call-recorder'); 3 | 4 | const sts = new AWS.STS(); 5 | 6 | const handler = async (event, context) => { 7 | console.log(event); 8 | console.log(context); 9 | const callerIdentity = await sts.getCallerIdentity({}).promise(); 10 | console.log(callerIdentity); 11 | return { 12 | statusCode: 200, 13 | headers: { 14 | 'Content-Type': 'application/json' 15 | }, 16 | body: JSON.stringify({ 17 | ok: true 18 | }) 19 | }; 20 | } 21 | 22 | exports.handler = ApiCallRecorder(handler); 23 | -------------------------------------------------------------------------------- /example/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-call-recorder-example", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "aws-sdk": { 8 | "version": "2.751.0", 9 | "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.751.0.tgz", 10 | "integrity": "sha512-0lo7YKTfjEwoP+2vK7F7WGNigvwFxqiM96PzBaseOpOelfhFHPKEJpk2Poa12JI89c8dGoc1PhTQ1TSTJK3ZqQ==", 11 | "requires": { 12 | "buffer": "4.9.2", 13 | "events": "1.1.1", 14 | "ieee754": "1.1.13", 15 | "jmespath": "0.15.0", 16 | "querystring": "0.2.0", 17 | "sax": "1.2.1", 18 | "url": "0.10.3", 19 | "uuid": "3.3.2", 20 | "xml2js": "0.4.19" 21 | } 22 | }, 23 | "base64-js": { 24 | "version": "1.3.1", 25 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 26 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" 27 | }, 28 | "buffer": { 29 | "version": "4.9.2", 30 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", 31 | "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", 32 | "requires": { 33 | "base64-js": "^1.0.2", 34 | "ieee754": "^1.1.4", 35 | "isarray": "^1.0.0" 36 | } 37 | }, 38 | "events": { 39 | "version": "1.1.1", 40 | "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", 41 | "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" 42 | }, 43 | "ieee754": { 44 | "version": "1.1.13", 45 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 46 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" 47 | }, 48 | "isarray": { 49 | "version": "1.0.0", 50 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 51 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 52 | }, 53 | "jmespath": { 54 | "version": "0.15.0", 55 | "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", 56 | "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" 57 | }, 58 | "lodash": { 59 | "version": "4.17.20", 60 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 61 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", 62 | "dev": true 63 | }, 64 | "punycode": { 65 | "version": "1.3.2", 66 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", 67 | "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" 68 | }, 69 | "querystring": { 70 | "version": "0.2.0", 71 | "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", 72 | "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" 73 | }, 74 | "sax": { 75 | "version": "1.2.1", 76 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", 77 | "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" 78 | }, 79 | "serverless-iam-roles-per-function": { 80 | "version": "2.0.2", 81 | "resolved": "https://registry.npmjs.org/serverless-iam-roles-per-function/-/serverless-iam-roles-per-function-2.0.2.tgz", 82 | "integrity": "sha512-3zv4Af3x7KJHC4fuJMHniEEstCljMghReaYIg94Spdsqkhiy8GJ3yfJ5Jy+wiG/DvyObK91JJNMjLhDnDc+vCQ==", 83 | "dev": true, 84 | "requires": { 85 | "lodash": "^4.17.15" 86 | } 87 | }, 88 | "url": { 89 | "version": "0.10.3", 90 | "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", 91 | "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", 92 | "requires": { 93 | "punycode": "1.3.2", 94 | "querystring": "0.2.0" 95 | } 96 | }, 97 | "uuid": { 98 | "version": "3.3.2", 99 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 100 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 101 | }, 102 | "xml2js": { 103 | "version": "0.4.19", 104 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", 105 | "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", 106 | "requires": { 107 | "sax": ">=0.6.0", 108 | "xmlbuilder": "~9.0.1" 109 | } 110 | }, 111 | "xmlbuilder": { 112 | "version": "9.0.7", 113 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", 114 | "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api-call-recorder-example", 3 | "version": "0.1.0", 4 | "description": "Example AWS Lambda project to showcase the API Call Recorder functionalities", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "bugs": { 9 | "url": "https://github.com/tobilg/aws-lambda-api-call-recorder/issues" 10 | }, 11 | "homepage": "https://github.com/tobilg/aws-lambda-api-call-recorder#readme", 12 | "keywords": [], 13 | "author": "TobiLG ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "aws-sdk": "^2.751.0" 17 | }, 18 | "devDependencies": { 19 | "serverless-iam-roles-per-function": "^2.0.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/serverless.yml: -------------------------------------------------------------------------------- 1 | service: 2 | name: api-call-recorder-example 3 | 4 | plugins: 5 | - serverless-iam-roles-per-function 6 | 7 | provider: 8 | name: aws 9 | region: ${opt:region, 'us-east-1'} 10 | stage: ${opt:stage, 'dev'} 11 | 12 | functions: 13 | 14 | test: 15 | handler: functions/test.handler 16 | runtime: nodejs12.x 17 | memorySize: 256 18 | timeout: 3 19 | environment: 20 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1' # Enable HTTP keep-alive connections for the AWS SDK 21 | AWS_CSM_ENABLED: 'true' 22 | API_CALL_RECORDER_IS_ACTIVATED: 'true' 23 | API_CALL_RECORDER_DELIVERY_STREAM_NAME: ${cf:api-call-recorder-backend-${self:provider.stage}.ApiCallRecorderDeliveryStreamName} 24 | iamRoleStatements: 25 | - Effect: Allow 26 | Action: 27 | - firehose:PutRecordBatch 28 | Resource: '${cf:api-call-recorder-backend-${self:provider.stage}.ApiCallRecorderDeliveryStreamArn}' 29 | events: 30 | - httpApi: 31 | method: GET 32 | path: /test 33 | 34 | package: 35 | exclude: 36 | - docs/** 37 | - LICENSE 38 | - README.md 39 | --------------------------------------------------------------------------------