├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── globals.ts ├── index.ts ├── logging.ts └── types.ts ├── test ├── integration-tests │ ├── base.ts │ ├── configs │ │ ├── create-perm-off │ │ │ ├── logs_producer.py │ │ │ └── serverless.yml │ │ ├── multiple-one-disabled │ │ │ ├── logs_producer.py │ │ │ └── serverless.yml │ │ ├── multiple-producers │ │ │ ├── logs_producer.py │ │ │ └── serverless.yml │ │ ├── single-filter │ │ │ ├── logs_producer.py │ │ │ └── serverless.yml │ │ ├── single-producer │ │ │ ├── logs_producer.py │ │ │ └── serverless.yml │ │ ├── stage-match │ │ │ ├── logs_producer.py │ │ │ └── serverless.yml │ │ └── stage-no-match │ │ │ ├── logs_producer.py │ │ │ └── serverless.yml │ ├── fixtures │ │ ├── logs-receiver.js │ │ └── logs-receiver.zip │ ├── integration-tests.ts │ └── utils │ │ ├── aws │ │ ├── iam-wrap.ts │ │ ├── lambda-wrap.ts │ │ └── log-wrap.ts │ │ ├── log-receiver.ts │ │ └── test-utilities.ts └── unit-tests │ └── index-test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "commonjs": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "extends": [ 10 | "standard", 11 | "eslint:recommended", 12 | "plugin:@typescript-eslint/eslint-recommended" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 2020, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint" 21 | ], 22 | "rules": { 23 | "quotes": [ 24 | "error", 25 | "double" 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ], 31 | "arrow-parens": [ 32 | "error", 33 | "always" 34 | ], 35 | "complexity": [ 36 | "error", 37 | 15 38 | ], 39 | "guard-for-in": "error" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Test, Lint, Release and Publish 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | main: 8 | name: Release and publish Node package 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v4 13 | 14 | - name: Install and test 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 18.x 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm install 20 | - run: npm run build 21 | - run: npm run lint 22 | - run: npm test 23 | 24 | - name: Extract version 25 | id: extract_version 26 | uses: Saionaro/extract-package-version@v1.2.1 27 | 28 | - name: Get latest tag 29 | id: get_latest_tag 30 | uses: actions-ecosystem/action-get-latest-tag@v1 31 | with: 32 | initial_version: 'v${{ steps.extract_version.outputs.version }}' 33 | 34 | - name: See if version changed 35 | run: | 36 | if [[ "v${{ steps.extract_version.outputs.version }}" == "${{ steps.get_latest_tag.outputs.tag }}" ]]; then 37 | echo "VERSION_CHANGED=false" >> "$GITHUB_ENV" 38 | else 39 | echo "VERSION_CHANGED=true" >> "$GITHUB_ENV" 40 | fi 41 | 42 | - name: Create tag 43 | uses: rickstaa/action-create-tag@v1 44 | if: env.VERSION_CHANGED == 'true' 45 | with: 46 | tag: "v${{ steps.extract_version.outputs.version }}" 47 | 48 | - name: Create release 49 | if: env.VERSION_CHANGED == 'true' 50 | uses: softprops/action-gh-release@v1 51 | with: 52 | tag_name: 'v${{ steps.extract_version.outputs.version }}' 53 | generate_release_notes: true 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | 57 | - name: Publish new version 58 | if: env.VERSION_CHANGED == 'true' 59 | # Note: Setting NODE_AUTH_TOKEN as job|workspace wide env var won't work 60 | # as it appears actions/setup-node sets own value 61 | env: 62 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | run: npm publish --verbose 64 | 65 | - name: Print dirs 66 | if: failure() 67 | run: find /home/runner/.npm/_logs/ -type f -exec cat {} \; 68 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test and Lint for PRs 2 | on: 3 | pull_request: 4 | types: 5 | - opened 6 | - reopened 7 | - synchronize 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | node-version: [ 16.x, 18.x, 20.x, 22.x ] 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: 'npm' 23 | - run: npm install 24 | - run: npm run lint 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | coverage 4 | npm-debug.log 5 | .idea 6 | .vscode 7 | dist/ 8 | .nyc_output 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [4.0.0] - 2024-11-27 9 | 10 | ### Added 11 | - Serverless V4 support. 12 | 13 | ### Changed 14 | 15 | - Dropped Node14.x support. 16 | - Dropped Serverless 2.x support. 17 | - Updated npm dependencies to fix security vulnerabilities. 18 | 19 | ## [3.2.0] - 2024-01-08 20 | 21 | ### Fixed 22 | 23 | - Added plugin schema to fix serverless warning. Thank you @crunchie84 ([74](https://github.com/amplify-education/serverless-log-forwarding/pull/74)) 24 | - Refactored integration tests 25 | - Updated linting 26 | - Updated packages 27 | 28 | ## [3.1.0] - 2023-02-17 29 | 30 | ### Added 31 | 32 | - Node version minimal requirements upgraded from 12.x to 14.x. 33 | - Upgraded aws-sdk, eslint, mocha, serverless, typescript and some other packages. 34 | - npm bin changed to npx because of first command's deprecation. 35 | 36 | ## [3.0.2] - 2023-02-09 37 | 38 | ### Added 39 | 40 | - Github workflow adjustment 41 | 42 | ## [3.0.1] - 2022-02-28 43 | 44 | ### Added 45 | 46 | - Fixes for build 47 | 48 | ## [3.0.0] - 2022-02-03 49 | 50 | ### Added 51 | 52 | - Moved the plugin and tests to TypeScript. 53 | - Moved from istanbul to nyc. 54 | 55 | ## [2.0.0] - 2022-02-03 56 | 57 | ### Added 58 | 59 | - Added support of Serverless Framework 3. 60 | - Moved from Travis CI to Github Actions. 61 | - Added integration tests. 62 | - Updated npm dev dependencies. 63 | 64 | ## [1.4.0] - 2019-03-17 65 | 66 | ### Added 67 | 68 | - Added support for disabling automatic creation of a AWS::Lambda::Permission. createLambdaPermission flag is enabled by default. 69 | 70 | 71 | ## [1.3.0] - 2018-12-11 72 | 73 | ### Added 74 | 75 | - Added support for a role arn parameter which allows for AWS Kinesis streams to be added as subscription filters. 76 | 77 | ## [1.2.1] - 2018-10-09 78 | 79 | ### Changed 80 | 81 | - Updated npm dependencies to fix security vulnerabilities 82 | 83 | ## [1.2.0] - 2018-9-18 84 | 85 | ### Added 86 | 87 | - Added support for per-function exclusion by setting `functions.function.logForwarding.enabled = false` for the given function. 88 | 89 | ## [1.1.8] - 2018-07-30 90 | 91 | ### Added 92 | 93 | - Added option to support filename as filter logic id, called `normalizedFilterID`. True by default. 94 | 95 | 96 | ## [1.1.7] - 2018-07-30 97 | 98 | ### Added 99 | 100 | - This CHANGELOG file to make it easier for future updates to be documented. Sadly, will not be going back to document changes made for previous versions. 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Amplify Education, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-log-forwarding 2 | 3 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 4 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/amplify-education/serverless-domain-manager/master/LICENSE) 5 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/bb1e50c048434012bd57eb73225a089e)](https://www.codacy.com/app/CFER/serverless-log-forwarding?utm_source=github.com&utm_medium=referral&utm_content=amplify-education/serverless-log-forwarding&utm_campaign=badger) 6 | [![Build Status](https://travis-ci.org/amplify-education/serverless-log-forwarding.svg?branch=master)](https://travis-ci.org/amplify-education/serverless-log-forwarding) 7 | [![npm version](https://badge.fury.io/js/serverless-log-forwarding.svg)](https://badge.fury.io/js/serverless-log-forwarding) 8 | [![npm downloads](https://img.shields.io/npm/dt/serverless-log-forwarding.svg?style=flat)](https://www.npmjs.com/package/serverless-log-forwarding) 9 | 10 | Serverless plugin for forwarding CloudWatch logs to another Lambda function. 11 | 12 | # About Amplify 13 | 14 | Amplify builds innovative and compelling digital educational products that empower teachers and students across the country. We have a long history as the leading innovator in K-12 education - and have been described as the best tech company in education and the best education company in tech. While others try to shrink the learning experience into the technology, we use technology to expand what is possible in real classrooms with real students and teachers. 15 | 16 | # Getting Started 17 | 18 | ## Prerequisites 19 | 20 | Make sure you have the following installed before starting: 21 | * [nodejs](https://nodejs.org/en/download/) 22 | * [npm](https://www.npmjs.com/get-npm) 23 | * [serverless](https://serverless.com/framework/docs/providers/aws/guide/installation/) 24 | 25 | ## Installing 26 | 27 | To install the plugin, run: 28 | 29 | ```shell 30 | npm install serverless-log-forwarding 31 | ``` 32 | 33 | Then make the following edits to your `serverless.yaml` file: 34 | 35 | ```yaml 36 | plugins: 37 | - serverless-log-forwarding 38 | 39 | custom: 40 | logForwarding: 41 | destinationARN: '[ARN of Lambda Function to forward logs to]' 42 | # optional: 43 | roleArn: '[ARN of the IAM role that grants Cloudwatch Logs permissions]' 44 | filterPattern: '[filter pattern for logs that are sent to Lambda function]' 45 | normalizedFilterID: true # whether to use normalized function name as filter ID 46 | stages: 47 | - '[name of the stage to apply log forwarding]' 48 | - '[another stage name to filter]' 49 | createLambdaPermission: true # whether to create the AWS::Lambda::Permission for the destinationARN (when policy size limits are a concern) 50 | 51 | functions: 52 | myFunction: 53 | handler: src/someHandler 54 | # optional properties for per-function configuration: 55 | logForwarding: 56 | # set enabled to false to disable log forwarding for a single given function 57 | enabled: false 58 | 59 | ``` 60 | 61 | ## Running Tests 62 | To run unit tests: 63 | ``` 64 | npm run test 65 | ``` 66 | 67 | For running integration tests you will need to log into you AWS account 68 | and set AWS_PROFILE environment variable, 69 | it will be used to create AWS entities for testing purposes 70 | ``` 71 | export AWS_PROFILE= 72 | export SERVERLESS_LICENSE_KEY= 73 | npx npm run build 74 | npx npm run integration-test 75 | ``` 76 | 77 | All tests should pass. All unit tests should pass before merging. 78 | Integration tests will probably take some time 79 | 80 | If there is an error update the node_modules inside the root folder of the directory: 81 | ``` 82 | npm install 83 | ``` 84 | 85 | ## Writing Integration Tests 86 | Unit tests are found in `test/unit-tests`. 87 | Integration tests are found in `test/integration-tests`. 88 | 89 | `test/integration-tests` contains configs folder, 90 | for every test there is a folder with `serverless.yml` configuration and `logs_producer.py`. 91 | 92 | To add another test create a folder for your test with the folder name that corresponds to test name 93 | and add code to run test to `test/integration-tests/integration-tests.ts` 94 | 95 | 96 | ## Deploying 97 | 98 | --------- 99 | The plugin will be packaged with the lambda when deployed as normal using Serverless: 100 | 101 | ```shell 102 | serverless deploy 103 | ``` 104 | 105 | # Responsible Disclosure 106 | 107 | If you have any security issue to report, contact project maintainers privately. 108 | You can reach us at 109 | 110 | # Contributing 111 | 112 | We welcome pull requests! For your pull request to be accepted smoothly, we suggest that you: 113 | 1. For any sizable change, first open a GitHub issue to discuss your idea. 114 | 2. Create a pull request. Explain why you want to make the change and what it’s for. 115 | We’ll try to answer any PRs promptly. 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-log-forwarding", 3 | "version": "4.0.0", 4 | "description": "a serverless plugin to forward logs to given lambda function", 5 | "keywords": [ 6 | "serverless", 7 | "plugin", 8 | "logs" 9 | ], 10 | "homepage": "https://github.com/amplify-education/serverless-log-forwarding#readme", 11 | "bugs": { 12 | "url": "https://github.com/amplify-education/serverless-log-forwarding/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/amplify-education/serverless-log-forwarding.git" 17 | }, 18 | "license": "MIT", 19 | "author": "Amplify Education", 20 | "main": "dist/src/index.js", 21 | "directories": { 22 | "test": "test" 23 | }, 24 | "scripts": { 25 | "build": "tsc --project .", 26 | "integration-test": "nyc mocha -r ts-node/register --project tsconfig.json test/integration-tests/integration-tests.ts && nyc report --reporter=text-summary", 27 | "lint": "eslint . --ext .ts", 28 | "lint:fix": "npx npm run lint -- --fix", 29 | "test": "nyc mocha -r ts-node/register --project tsconfig.json test/unit-tests/index-test.ts && nyc report --reporter=text-summary" 30 | }, 31 | "dependencies": { 32 | "underscore": "^1.13.7" 33 | }, 34 | "devDependencies": { 35 | "@aws-sdk/client-cloudwatch-logs": "^3.699.0", 36 | "@aws-sdk/client-iam": "^3.699.0", 37 | "@aws-sdk/client-lambda": "^3.699.0", 38 | "@types/async-retry": "^1.4.9", 39 | "@types/chai": "^5.0.1", 40 | "@types/mocha": "^10.0.10", 41 | "@types/randomstring": "^1.3.0", 42 | "@types/serverless": "^3.12.23", 43 | "@types/shelljs": "^0.8.15", 44 | "@types/underscore": "^1.13.0", 45 | "@typescript-eslint/eslint-plugin": "^8.16.0", 46 | "@typescript-eslint/parser": "^8.16.0", 47 | "chai": "^4.5.0", 48 | "eslint": "^8.57.1", 49 | "eslint-config-standard": "^17.1.0", 50 | "eslint-plugin-import": "^2.31.0", 51 | "eslint-plugin-node": "^11.1.0", 52 | "eslint-plugin-promise": "^6.6.0", 53 | "mocha": "^10.8.2", 54 | "nyc": "^17.1.0", 55 | "randomstring": "^1.3.0", 56 | "serverless": "^4.4.12", 57 | "shelljs": "^0.8.5", 58 | "ts-node": "^10.9.2", 59 | "typescript": "^5.7.2" 60 | }, 61 | "peerDependencies": { 62 | "serverless": ">=3" 63 | }, 64 | "engines": { 65 | "node": ">=16" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/globals.ts: -------------------------------------------------------------------------------- 1 | import { ServerlessInstance, ServerlessUtils } from "./types"; 2 | 3 | export default class Globals { 4 | public static pluginName = "Serverless Log Forwarding"; 5 | 6 | public static serverless: ServerlessInstance; 7 | public static v3Utils?: ServerlessUtils; 8 | } 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as _ from "underscore"; 2 | import type { 3 | AWSProvider, 4 | ServerlessInstance, 5 | ServerlessConfig, 6 | PluginConfig, 7 | ResourcesCF, 8 | SubscriptionFilterCF, 9 | LambdaPermissionCF, 10 | ServerlessUtils 11 | } from "./types"; 12 | import Globals from "./globals"; 13 | import Logging from "./logging"; 14 | 15 | const CONFIG_DEFAULTS = { 16 | filterPattern: "", 17 | normalizedFilterID: true, 18 | createLambdaPermission: true 19 | }; 20 | 21 | class LogForwardingPlugin { 22 | options: ServerlessConfig; 23 | 24 | provider: AWSProvider; 25 | 26 | serverless: ServerlessInstance; 27 | 28 | config: PluginConfig | null = null; 29 | 30 | hooks: Record void>; 31 | 32 | // The key of a lambda permission in CloudFormation resources 33 | permissionId = "LogForwardingLambdaPermission"; 34 | 35 | constructor (serverless: ServerlessInstance, options: ServerlessConfig, v3Utils?: ServerlessUtils) { 36 | this.serverless = serverless; 37 | this.options = options; 38 | this.provider = this.serverless.getProvider("aws"); 39 | this.hooks = { 40 | "package:initialize": this.updateResources.bind(this) 41 | }; 42 | 43 | Globals.serverless = serverless; 44 | if (v3Utils?.log) { 45 | Globals.v3Utils = v3Utils; 46 | } 47 | 48 | // schema for the function section of serverless.yml 49 | this.serverless.configSchemaHandler.defineFunctionProperties("aws", { 50 | properties: { 51 | logForwarding: { 52 | type: "object", 53 | properties: { 54 | enabled: { 55 | type: "boolean" 56 | } 57 | }, 58 | required: ["enabled"] 59 | } 60 | }, 61 | required: [] 62 | }); 63 | 64 | // schema for the custom props section of serverless.yml 65 | this.serverless.configSchemaHandler.defineCustomProperties({ 66 | properties: { 67 | logForwarding: { 68 | type: "object", 69 | properties: { 70 | destinationARN: { type: "string" }, 71 | roleArn: { type: "string" }, 72 | filterPattern: { type: "string" }, 73 | normalizedFilterID: { type: "string" }, 74 | stages: { 75 | type: "array", 76 | uniqueItems: true, 77 | items: { type: "string" } 78 | }, 79 | createLambdaPermission: { type: "boolean" } 80 | }, 81 | required: ["destinationARN"] 82 | } 83 | }, 84 | required: ["logForwarding"] 85 | }); 86 | } 87 | 88 | loadConfig (): void { 89 | const { service } = this.serverless; 90 | if (!service.custom || !service.custom.logForwarding) { 91 | throw new Error("Serverless-log-forwarding configuration not provided."); 92 | } 93 | this.config = { ...CONFIG_DEFAULTS, ...service.custom.logForwarding }; 94 | if (this.config.destinationARN === undefined) { 95 | throw new Error("Serverless-log-forwarding is not configured correctly. Please see README for proper setup."); 96 | } 97 | } 98 | 99 | updateResources (): void { 100 | this.loadConfig(); 101 | const stage = this.getStage(); 102 | if (this.config.stages && !this.config.stages.includes(stage)) { 103 | Logging.logWarning(`Log Forwarding is ignored for ${stage} stage`); 104 | return; 105 | } 106 | Logging.logInfo("Updating Log Forwarding Resources..."); 107 | const resourceObj = this.createResourcesObj(); 108 | const cfResources = this.getResources(); 109 | _.extend(cfResources, resourceObj); 110 | Logging.logInfo("Log Forwarding Resources Updated"); 111 | } 112 | 113 | private getResources (): ResourcesCF { 114 | const { service } = this.serverless; 115 | if (service.resources === undefined) { 116 | service.resources = { 117 | Resources: {} 118 | }; 119 | } 120 | if (service.resources.Resources === undefined) { 121 | service.resources.Resources = {}; 122 | } 123 | return service.resources.Resources; 124 | } 125 | 126 | createResourcesObj (): ResourcesCF { 127 | const { service } = this.serverless; 128 | const resourceObj = {}; 129 | 130 | const createLambdaPermission = this.config.createLambdaPermission && !this.config.roleArn; 131 | if (createLambdaPermission) { 132 | const permission = this.makeLambdaPermission(); 133 | resourceObj[this.permissionId] = permission; 134 | } 135 | 136 | _.keys(service.functions) 137 | .filter((functionName) => { 138 | const { logForwarding = {} } = this.serverless.service.getFunction(functionName); 139 | if (logForwarding.enabled === undefined) { 140 | return true; 141 | } 142 | return !!logForwarding.enabled; 143 | }) 144 | .forEach((functionName) => { 145 | const filterId = this.getFilterId(functionName); 146 | const dependsOn = createLambdaPermission ? [this.permissionId] : []; 147 | const filter = this.makeSubsctiptionFilter(functionName, dependsOn); 148 | resourceObj[filterId] = filter; 149 | }); 150 | return resourceObj; 151 | } 152 | 153 | getFilterId (functionName: string): string { 154 | const filterName = this.config.normalizedFilterID 155 | ? this.provider.naming.getNormalizedFunctionName(functionName) 156 | : functionName; 157 | return `SubscriptionFilter${filterName}`; 158 | } 159 | 160 | getStage (): string { 161 | const { stage } = this.options; 162 | if (stage && stage !== "") { 163 | return stage; 164 | } 165 | return this.serverless.service.provider.stage; 166 | } 167 | 168 | makeSubsctiptionFilter (functionName: string, deps?: string[]): SubscriptionFilterCF { 169 | const functionObject = this.serverless.service.getFunction(functionName); 170 | const logGroupName = this.provider.naming.getLogGroupName(functionObject.name); 171 | const logGroupId = this.provider.naming.getLogGroupLogicalId(functionName); 172 | const roleObject = this.config.roleArn ? { RoleArn: this.config.roleArn } : {}; 173 | return { 174 | Type: "AWS::Logs::SubscriptionFilter", 175 | Properties: { 176 | DestinationArn: this.config.destinationARN, 177 | FilterPattern: this.config.filterPattern, 178 | LogGroupName: logGroupName, 179 | ...roleObject 180 | }, 181 | DependsOn: _.union(deps, [logGroupId]) 182 | }; 183 | } 184 | 185 | makeLambdaPermission (): LambdaPermissionCF { 186 | const { region } = this.serverless.service.provider; 187 | const principal = `logs.${region}.amazonaws.com`; 188 | return { 189 | Type: "AWS::Lambda::Permission", 190 | Properties: { 191 | FunctionName: this.config.destinationARN, 192 | Action: "lambda:InvokeFunction", 193 | Principal: principal 194 | } 195 | }; 196 | } 197 | } 198 | 199 | export = LogForwardingPlugin; 200 | -------------------------------------------------------------------------------- /src/logging.ts: -------------------------------------------------------------------------------- 1 | import Globals from "./globals"; 2 | 3 | export default class Logging { 4 | public static cliLog (prefix: string, message: string): void { 5 | Globals.serverless.cli.log(`${prefix} ${message}`, Globals.pluginName); 6 | } 7 | 8 | /** 9 | * Logs info message 10 | */ 11 | public static logInfo (message: string): void { 12 | if (Globals.v3Utils) { 13 | Globals.v3Utils.log.verbose(message); 14 | } else { 15 | Logging.cliLog("[Info]", message); 16 | } 17 | } 18 | 19 | /** 20 | * Logs warning message 21 | */ 22 | public static logWarning (message: string): void { 23 | if (Globals.v3Utils) { 24 | Globals.v3Utils.log.warning(message); 25 | } else { 26 | Logging.cliLog("[WARNING]", message); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ObjectCF { 2 | Type: string; 3 | DependsOn?: string[]; 4 | Properties?: TProps; 5 | } 6 | 7 | export type ResourcesCF = Record>; 8 | 9 | export interface AWSProvider { 10 | naming: { 11 | getLogGroupName (name: string): string, 12 | getNormalizedFunctionName (name: string): string, 13 | getLogGroupLogicalId (name: string): string 14 | } 15 | } 16 | 17 | export interface SlsFunction { 18 | name: string; 19 | logForwarding?: { 20 | enabled?: boolean; 21 | } 22 | } 23 | 24 | export interface PluginConfig { 25 | stages?: string[]; 26 | roleArn?: string; 27 | filterPattern: string; 28 | normalizedFilterID: boolean; 29 | createLambdaPermission: boolean; 30 | destinationARN: string; 31 | } 32 | 33 | /** 34 | * If your plugin adds new properties to any level in the serverless.yml 35 | * you can use these functions to add JSON ajv based schema validation for 36 | * those properties 37 | * 38 | * @see https://www.serverless.com/framework/docs/guides/plugins/custom-configuration 39 | */ 40 | export interface ConfigSchemaHandler { 41 | /** 42 | * If your plugin requires additional top-level properties (like provider, custom, service...) 43 | * you can use the defineTopLevelProperty helper to add their definition. 44 | * @see https://www.serverless.com/framework/docs/guides/plugins/custom-configuration#top-level-properties-via-definetoplevelproperty 45 | */ 46 | defineTopLevelProperty ( 47 | providerName: string, 48 | schema: Record 49 | ): void; 50 | 51 | /** 52 | * If your plugin depends on properties defined in the custom: section, you can use the 53 | * defineCustomProperties helper 54 | * @see https://www.serverless.com/framework/docs/guides/plugins/custom-configuration#properties-in-custom-via-definecustomproperties 55 | */ 56 | defineCustomProperties (jsonSchema: object): void; 57 | 58 | /** 59 | * If your plugin adds support to a new function event, you can use the 60 | * defineFunctionEvent helper 61 | * @see https://www.serverless.com/framework/docs/guides/plugins/custom-configuration#function-events-via-definefunctionevent 62 | */ 63 | defineFunctionEvent ( 64 | providerName: string, 65 | event: string, 66 | jsonSchema: Record 67 | ): void; 68 | 69 | /** 70 | * If your plugin adds new properties to a function event, you can use the 71 | * defineFunctionEventProperties helper 72 | * @see https://www.serverless.com/framework/docs/guides/plugins/custom-configuration#function-event-properties-via-definefunctioneventproperties 73 | */ 74 | defineFunctionEventProperties ( 75 | providerName: string, 76 | existingEvent: string, 77 | jsonSchema: object 78 | ): void; 79 | 80 | /** 81 | * If your plugin adds new properties to functions, you can use the 82 | * defineFunctionProperties helper. 83 | * @see https://www.serverless.com/framework/docs/guides/plugins/custom-configuration#function-properties-via-definefunctionproperties 84 | */ 85 | defineFunctionProperties (providerName: string, schema: object): void; 86 | 87 | /** 88 | * If your plugin provides support for a new provider, register it via defineProvider 89 | * @see https://www.serverless.com/framework/docs/guides/plugins/custom-configuration#new-provider-via-defineprovider 90 | */ 91 | defineProvider (providerName: string, options?: Record): void; 92 | } 93 | 94 | export interface ServerlessInstance { 95 | service: { 96 | service: string 97 | resources: { 98 | Resources: ResourcesCF 99 | }, 100 | provider: { 101 | stage: string, 102 | region: string 103 | }, 104 | functions?: Record, 105 | getFunction (name: string): SlsFunction, 106 | custom: { 107 | logForwarding?: PluginConfig 108 | }, 109 | }, 110 | providers: { 111 | aws?: { 112 | getRegion (): string, 113 | }, 114 | }, 115 | 116 | getProvider (name: string): AWSProvider, 117 | 118 | configSchemaHandler: ConfigSchemaHandler; 119 | cli: { 120 | log (str: string, entity?: string): void 121 | } 122 | } 123 | 124 | export interface ServerlessConfig { 125 | commands: string[]; 126 | options: Record; 127 | stage: string | null; 128 | } 129 | 130 | export interface LambdaPermissionProps { 131 | Action: string; 132 | Principal: string; 133 | FunctionName: string; 134 | } 135 | 136 | export interface SubscriptionFilterProps { 137 | DestinationArn: string; 138 | FilterPattern: string; 139 | LogGroupName: string; 140 | RoleArn?: string; 141 | } 142 | 143 | interface ServerlessProgress { 144 | update (message: string): void 145 | 146 | remove (): void 147 | } 148 | 149 | export interface ServerlessProgressFactory { 150 | get (name: string): ServerlessProgress; 151 | } 152 | 153 | export interface ServerlessUtils { 154 | writeText: (message: string) => void, 155 | log: ((message: string) => void) & { 156 | error (message: string): void 157 | verbose (message: string): void 158 | warning (message: string): void 159 | } 160 | progress: ServerlessProgressFactory 161 | } 162 | 163 | export type LambdaPermissionCF = ObjectCF; 164 | 165 | export type SubscriptionFilterCF = ObjectCF; 166 | -------------------------------------------------------------------------------- /test/integration-tests/base.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const randomstring = require("randomstring"); 3 | 4 | export const PLUGIN_IDENTIFIER = "slf"; 5 | export const RANDOM_STRING = randomstring.generate({ 6 | capitalization: "lowercase", 7 | charset: "alphanumeric", 8 | length: 5 9 | }); 10 | export const TEMP_DIR = `~/tmp/serveless-log-forwarding-integration-tests/${RANDOM_STRING}`; 11 | 12 | process.env.PLUGIN_IDENTIFIER = PLUGIN_IDENTIFIER; 13 | process.env.RANDOM_STRING = RANDOM_STRING; 14 | 15 | module.exports = { 16 | PLUGIN_IDENTIFIER, 17 | RANDOM_STRING, 18 | TEMP_DIR 19 | }; 20 | -------------------------------------------------------------------------------- /test/integration-tests/configs/create-perm-off/logs_producer.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, _): 5 | print("Event", event) 6 | body = { 7 | "message": "Go Serverless! Your function executed successfully!", 8 | "input": event 9 | } 10 | response = { 11 | "statusCode": 200, 12 | "body": json.dumps(body) 13 | } 14 | return response 15 | -------------------------------------------------------------------------------- /test/integration-tests/configs/create-perm-off/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ${env:PLUGIN_IDENTIFIER}-create-perm-off 2 | 3 | frameworkVersion: "4.2.5" 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.8 8 | region: us-west-2 9 | stage: test 10 | 11 | package: 12 | patterns: 13 | - '!node_modules/**' 14 | 15 | functions: 16 | logs-producer: 17 | handler: logs_producer.handler 18 | 19 | plugins: 20 | - serverless-log-forwarding 21 | 22 | custom: 23 | logForwarding: 24 | destinationARN: ${env:LOGS_RECEIVER_ARN} 25 | createLambdaPermission: false 26 | -------------------------------------------------------------------------------- /test/integration-tests/configs/multiple-one-disabled/logs_producer.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, _): 5 | print("Event", event) 6 | body = { 7 | "message": "Go Serverless! Your function executed successfully!", 8 | "input": event 9 | } 10 | response = { 11 | "statusCode": 200, 12 | "body": json.dumps(body) 13 | } 14 | return response 15 | -------------------------------------------------------------------------------- /test/integration-tests/configs/multiple-one-disabled/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ${env:PLUGIN_IDENTIFIER}-multiple-one-disabled 2 | 3 | frameworkVersion: "4.2.5" 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.8 8 | region: us-west-2 9 | stage: test 10 | 11 | package: 12 | patterns: 13 | - '!node_modules/**' 14 | 15 | functions: 16 | logs-producer-1: 17 | handler: logs_producer.handler 18 | logs-producer-2: 19 | handler: logs_producer.handler 20 | logForwarding: 21 | enabled: false 22 | 23 | plugins: 24 | - serverless-log-forwarding 25 | 26 | custom: 27 | logForwarding: 28 | destinationARN: ${env:LOGS_RECEIVER_ARN} 29 | -------------------------------------------------------------------------------- /test/integration-tests/configs/multiple-producers/logs_producer.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, _): 5 | print("Event", event) 6 | body = { 7 | "message": "Go Serverless! Your function executed successfully!", 8 | "input": event 9 | } 10 | response = { 11 | "statusCode": 200, 12 | "body": json.dumps(body) 13 | } 14 | return response 15 | -------------------------------------------------------------------------------- /test/integration-tests/configs/multiple-producers/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ${env:PLUGIN_IDENTIFIER}-multiple-producers 2 | 3 | frameworkVersion: "4.2.5" 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.8 8 | region: us-west-2 9 | stage: test 10 | 11 | package: 12 | patterns: 13 | - '!node_modules/**' 14 | 15 | functions: 16 | logs-producer-1: 17 | handler: logs_producer.handler 18 | logs-producer-2: 19 | handler: logs_producer.handler 20 | 21 | plugins: 22 | - serverless-log-forwarding 23 | 24 | custom: 25 | logForwarding: 26 | destinationARN: ${env:LOGS_RECEIVER_ARN} 27 | -------------------------------------------------------------------------------- /test/integration-tests/configs/single-filter/logs_producer.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, _): 5 | print("Event", event) 6 | body = { 7 | "message": "Go Serverless! Your function executed successfully!", 8 | "input": event 9 | } 10 | response = { 11 | "statusCode": 200, 12 | "body": json.dumps(body) 13 | } 14 | return response 15 | -------------------------------------------------------------------------------- /test/integration-tests/configs/single-filter/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ${env:PLUGIN_IDENTIFIER}-single-filter 2 | 3 | frameworkVersion: "4.2.5" 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.8 8 | region: us-west-2 9 | stage: test 10 | 11 | package: 12 | patterns: 13 | - '!node_modules/**' 14 | 15 | functions: 16 | logs-producer: 17 | handler: logs_producer.handler 18 | 19 | plugins: 20 | - serverless-log-forwarding 21 | 22 | custom: 23 | logForwarding: 24 | destinationARN: ${env:LOGS_RECEIVER_ARN} 25 | filterPattern: "helloWorld" 26 | -------------------------------------------------------------------------------- /test/integration-tests/configs/single-producer/logs_producer.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, _): 5 | print("Event", event) 6 | body = { 7 | "message": "Go Serverless! Your function executed successfully!", 8 | "input": event 9 | } 10 | response = { 11 | "statusCode": 200, 12 | "body": json.dumps(body) 13 | } 14 | return response 15 | -------------------------------------------------------------------------------- /test/integration-tests/configs/single-producer/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ${env:PLUGIN_IDENTIFIER}-single-producer 2 | 3 | frameworkVersion: "4.2.5" 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.8 8 | region: us-west-2 9 | stage: test 10 | 11 | package: 12 | patterns: 13 | - '!node_modules/**' 14 | 15 | functions: 16 | logs-producer: 17 | handler: logs_producer.handler 18 | 19 | plugins: 20 | - serverless-log-forwarding 21 | 22 | custom: 23 | logForwarding: 24 | destinationARN: ${env:LOGS_RECEIVER_ARN} 25 | -------------------------------------------------------------------------------- /test/integration-tests/configs/stage-match/logs_producer.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, _): 5 | print("Event", event) 6 | body = { 7 | "message": "Go Serverless! Your function executed successfully!", 8 | "input": event 9 | } 10 | response = { 11 | "statusCode": 200, 12 | "body": json.dumps(body) 13 | } 14 | return response 15 | -------------------------------------------------------------------------------- /test/integration-tests/configs/stage-match/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ${env:PLUGIN_IDENTIFIER}-stage-match 2 | 3 | frameworkVersion: "4.2.5" 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.8 8 | region: us-west-2 9 | stage: test 10 | 11 | package: 12 | patterns: 13 | - '!node_modules/**' 14 | 15 | functions: 16 | logs-producer: 17 | handler: logs_producer.handler 18 | 19 | plugins: 20 | - serverless-log-forwarding 21 | 22 | custom: 23 | logForwarding: 24 | destinationARN: ${env:LOGS_RECEIVER_ARN} 25 | stages: 26 | - dev 27 | - test 28 | - ci 29 | -------------------------------------------------------------------------------- /test/integration-tests/configs/stage-no-match/logs_producer.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def handler(event, _): 5 | print("Event", event) 6 | body = { 7 | "message": "Go Serverless! Your function executed successfully!", 8 | "input": event 9 | } 10 | response = { 11 | "statusCode": 200, 12 | "body": json.dumps(body) 13 | } 14 | return response 15 | -------------------------------------------------------------------------------- /test/integration-tests/configs/stage-no-match/serverless.yml: -------------------------------------------------------------------------------- 1 | service: ${env:PLUGIN_IDENTIFIER}-stage-no-match 2 | 3 | frameworkVersion: "4.2.5" 4 | 5 | provider: 6 | name: aws 7 | runtime: python3.8 8 | region: us-west-2 9 | stage: test 10 | 11 | package: 12 | patterns: 13 | - '!node_modules/**' 14 | 15 | functions: 16 | logs-producer: 17 | handler: logs_producer.handler 18 | 19 | plugins: 20 | - serverless-log-forwarding 21 | 22 | custom: 23 | logForwarding: 24 | destinationARN: ${env:LOGS_RECEIVER_ARN} 25 | stages: 26 | - test1 27 | - dev 28 | - ci 29 | -------------------------------------------------------------------------------- /test/integration-tests/fixtures/logs-receiver.js: -------------------------------------------------------------------------------- 1 | exports.handler = function (_, __) { }; 2 | -------------------------------------------------------------------------------- /test/integration-tests/fixtures/logs-receiver.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amplify-education/serverless-log-forwarding/1d3b9a5bfbc4c68043b1a5a0fbab920b1659a3e6/test/integration-tests/fixtures/logs-receiver.zip -------------------------------------------------------------------------------- /test/integration-tests/integration-tests.ts: -------------------------------------------------------------------------------- 1 | import { assert } from "chai"; 2 | import * as utils from "./utils/test-utilities"; 3 | import LogReceiver from "./utils/log-receiver"; 4 | import type { LogsReceiverData } from "./utils/log-receiver"; 5 | import { PLUGIN_IDENTIFIER, RANDOM_STRING } from "./base"; 6 | import LambdaWrap from "./utils/aws/lambda-wrap"; 7 | import LogWrap from "./utils/aws/log-wrap"; 8 | 9 | const region = "us-west-2"; 10 | const logReceiver = new LogReceiver(region, `${PLUGIN_IDENTIFIER}-${RANDOM_STRING}`); 11 | const lambdaWrap = new LambdaWrap(region); 12 | const logWrap = new LogWrap(region); 13 | 14 | const testTimeOutMinutes = 15; 15 | describe("Integration Tests", function () { 16 | this.timeout(testTimeOutMinutes * 60 * 1000); 17 | 18 | let logsReceiverFixture: LogsReceiverData; 19 | 20 | beforeEach(async () => { 21 | logsReceiverFixture = await logReceiver.setUpLogsReceiver(); 22 | process.env.LOGS_RECEIVER_ARN = logsReceiverFixture.functionArn; 23 | }); 24 | 25 | afterEach(async () => { 26 | await logReceiver.tearDownLogsReceiver(); 27 | }); 28 | 29 | it("Single producer", async () => { 30 | const testName = "single-producer"; 31 | const receiverArn = logsReceiverFixture.functionArn; 32 | const producerLogGroup = utils.getLogGroup(testName, "test", "logs-producer"); 33 | await utils.runTest(testName, async () => { 34 | assert.isNotNull(await logWrap.getSubscriptionFilter(producerLogGroup, receiverArn)); 35 | assert.isNotNull(await lambdaWrap.getFunctionPolicy(receiverArn)); 36 | }); 37 | }); 38 | 39 | it("Multiple producers", async () => { 40 | const testName = "multiple-producers"; 41 | const logGroup1 = utils.getLogGroup(testName, "test", "logs-producer-1"); 42 | const logGroup2 = utils.getLogGroup(testName, "test", "logs-producer-2"); 43 | const receiverArn = logsReceiverFixture.functionArn; 44 | await utils.runTest(testName, async () => { 45 | assert.isNotNull(await logWrap.getSubscriptionFilter(logGroup1, receiverArn)); 46 | assert.isNotNull(await logWrap.getSubscriptionFilter(logGroup2, receiverArn)); 47 | assert.isNotNull(await lambdaWrap.getFunctionPolicy(receiverArn)); 48 | }); 49 | }); 50 | 51 | it("Multiple producers one disabled", async () => { 52 | const testName = "multiple-one-disabled"; 53 | const logGroup1 = utils.getLogGroup(testName, "test", "logs-producer-1"); 54 | const logGroup2 = utils.getLogGroup(testName, "test", "logs-producer-2"); 55 | const receiverArn = logsReceiverFixture.functionArn; 56 | await utils.runTest(testName, async () => { 57 | assert.isNotNull(await logWrap.getSubscriptionFilter(logGroup1, receiverArn)); 58 | assert.isNull(await logWrap.getSubscriptionFilter(logGroup2, receiverArn)); 59 | assert.isNotNull(await lambdaWrap.getFunctionPolicy(receiverArn)); 60 | }); 61 | }); 62 | 63 | it("Single producer stage filter pass", async () => { 64 | const testName = "stage-match"; 65 | const receiverArn = logsReceiverFixture.functionArn; 66 | const producerLogGroup = utils.getLogGroup(testName, "test", "logs-producer"); 67 | await utils.runTest(testName, async () => { 68 | assert.isNotNull(await logWrap.getSubscriptionFilter(producerLogGroup, receiverArn)); 69 | assert.isNotNull(await lambdaWrap.getFunctionPolicy(receiverArn)); 70 | }); 71 | }); 72 | 73 | it("Single producer stage filter fail", async () => { 74 | const testName = "stage-no-match"; 75 | const receiverArn = logsReceiverFixture.functionArn; 76 | const producerLogGroup = utils.getLogGroup(testName, "test", "logs-producer"); 77 | await utils.runTest(testName, async () => { 78 | assert.isNull(await logWrap.getSubscriptionFilter(producerLogGroup, receiverArn)); 79 | assert.isNull(await lambdaWrap.getFunctionPolicy(receiverArn)); 80 | }); 81 | }); 82 | 83 | it("Single producer filter pattern", async () => { 84 | const testName = "single-filter"; 85 | const receiverArn = logsReceiverFixture.functionArn; 86 | const producerLogGroup = utils.getLogGroup(testName, "test", "logs-producer"); 87 | await utils.runTest(testName, async () => { 88 | const filter = await logWrap.getSubscriptionFilter(producerLogGroup, receiverArn); 89 | assert.isNotNull(filter); 90 | assert.equal(filter.filterPattern, "helloWorld"); 91 | assert.isNotNull(await lambdaWrap.getFunctionPolicy(receiverArn)); 92 | }); 93 | }); 94 | 95 | describe("With custom lambda permission", async () => { 96 | const statementId = `invokeLambdaStatement${RANDOM_STRING}`; 97 | 98 | beforeEach(async () => { 99 | await lambdaWrap.setUpCustomPolicy(logsReceiverFixture.functionName, statementId); 100 | }); 101 | 102 | afterEach(async () => { 103 | await lambdaWrap.tearDownCustomPolicy(logsReceiverFixture.functionName, statementId); 104 | }); 105 | 106 | it("Single producer custom lambda permission", async () => { 107 | const testName = "create-perm-off"; 108 | const receiverArn = logsReceiverFixture.functionArn; 109 | const receiverName = logsReceiverFixture.functionName; 110 | const producerLogGroup = utils.getLogGroup(testName, "test", "logs-producer"); 111 | await utils.runTest(testName, async () => { 112 | assert.isNotNull(await logWrap.getSubscriptionFilter(producerLogGroup, receiverArn)); 113 | const functionPolicy = await lambdaWrap.getFunctionPolicy(receiverName); 114 | assert.isNotNull(functionPolicy); 115 | assert.equal(functionPolicy.Statement.length, 1); 116 | assert.equal(functionPolicy.Statement[0].Sid.toString(), statementId); 117 | }); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /test/integration-tests/utils/aws/iam-wrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AttachRolePolicyCommand, 3 | CreateRoleCommand, 4 | DeleteRoleCommand, 5 | DetachRolePolicyCommand, 6 | IAMClient, 7 | Role 8 | } from "@aws-sdk/client-iam"; 9 | 10 | const EXECUTION_POLICY_ARN = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"; 11 | const ASSUME_ROLE_POLICY = { 12 | Version: "2012-10-17", 13 | Statement: [{ 14 | Sid: "", 15 | Effect: "Allow", 16 | Principal: { 17 | Service: "lambda.amazonaws.com" 18 | }, 19 | Action: "sts:AssumeRole" 20 | } 21 | ] 22 | }; 23 | 24 | export default class IamWrap { 25 | private iamClient: IAMClient; 26 | 27 | constructor (region: string) { 28 | this.iamClient = new IAMClient({ region }); 29 | } 30 | 31 | /** 32 | * Creates a role with basic lambda permissions. Returns the role. 33 | */ 34 | async setupLambdaRole (roleName: string): Promise { 35 | const { Role } = await this.iamClient.send(new CreateRoleCommand({ 36 | RoleName: roleName, 37 | AssumeRolePolicyDocument: JSON.stringify(ASSUME_ROLE_POLICY) 38 | })); 39 | 40 | await this.iamClient.send(new AttachRolePolicyCommand({ 41 | PolicyArn: EXECUTION_POLICY_ARN, 42 | RoleName: Role.RoleName 43 | })); 44 | 45 | return Role; 46 | } 47 | 48 | async removeLambdaRole (roleName: string) { 49 | await this.iamClient.send(new DetachRolePolicyCommand({ 50 | RoleName: roleName, 51 | PolicyArn: EXECUTION_POLICY_ARN 52 | })); 53 | await this.iamClient.send(new DeleteRoleCommand({ 54 | RoleName: roleName 55 | })); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/integration-tests/utils/aws/lambda-wrap.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | import { 3 | AddPermissionCommand, 4 | CreateFunctionCommand, 5 | DeleteFunctionCommand, 6 | FunctionConfiguration, 7 | GetFunctionCommand, 8 | GetFunctionResponse, 9 | GetPolicyCommand, 10 | GetPolicyResponse, 11 | LambdaClient, RemovePermissionCommand 12 | } from "@aws-sdk/client-lambda"; 13 | import { sleep } from "../test-utilities"; 14 | 15 | const FUNC_ZIP_PATH = "test/integration-tests/fixtures/logs-receiver.zip"; 16 | const MAX_FUNC_ACTIVE_RETRY = 3; 17 | const FUNC_ACTIVE_TIMEOUT_SEC = 10; 18 | 19 | export default class LambdaWrap { 20 | private lambdaClient: LambdaClient; 21 | 22 | constructor (region: string) { 23 | this.lambdaClient = new LambdaClient({ region }); 24 | } 25 | 26 | /** 27 | * Creates a lambda function with basic lambda permissions. 28 | * Returns Arn of the created function. 29 | */ 30 | async createDummyFunction (funcName: string, roleArn: string): Promise { 31 | return await this.lambdaClient.send(new CreateFunctionCommand({ 32 | FunctionName: funcName, 33 | Runtime: "nodejs18.x", 34 | Code: { 35 | ZipFile: readFileSync(FUNC_ZIP_PATH) 36 | }, 37 | Architectures: ["x86_64"], 38 | Handler: "index.handler", 39 | Role: roleArn 40 | })); 41 | } 42 | 43 | async ensureFunctionActive (funcName: string, retry: number = 1): Promise { 44 | if (retry > MAX_FUNC_ACTIVE_RETRY) { 45 | throw new Error("Max retry reached"); 46 | } 47 | 48 | const resp: GetFunctionResponse = await this.lambdaClient.send(new GetFunctionCommand({ 49 | FunctionName: funcName 50 | })); 51 | 52 | if (resp.Configuration.State !== "Active") { 53 | await sleep(FUNC_ACTIVE_TIMEOUT_SEC * 1000); 54 | await this.ensureFunctionActive(funcName, retry + 1); 55 | } 56 | } 57 | 58 | async removeFunction (funcName: string) { 59 | await this.lambdaClient.send(new DeleteFunctionCommand({ 60 | FunctionName: funcName 61 | })); 62 | } 63 | 64 | async getFunctionPolicy (funcName: string): Promise { 65 | try { 66 | const resp: GetPolicyResponse = await this.lambdaClient.send(new GetPolicyCommand({ 67 | FunctionName: funcName 68 | })); 69 | return JSON.parse(resp.Policy); 70 | } catch (e) { 71 | if (e.name === "ResourceNotFoundException") { 72 | return null; 73 | } 74 | throw e; 75 | } 76 | } 77 | 78 | async setUpCustomPolicy (functionName: string, statementId: string) { 79 | await this.lambdaClient.send(new AddPermissionCommand({ 80 | Action: "lambda:InvokeFunction", 81 | FunctionName: functionName, 82 | Principal: "logs.us-west-2.amazonaws.com", 83 | StatementId: statementId 84 | })); 85 | } 86 | 87 | async tearDownCustomPolicy (functionName: string, statementId: string) { 88 | try { 89 | await this.lambdaClient.send(new RemovePermissionCommand({ 90 | FunctionName: functionName, 91 | StatementId: statementId 92 | })); 93 | console.debug("Custom policy was deleted"); 94 | } catch (e) { 95 | console.debug("Failed to delete a custom policy"); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/integration-tests/utils/aws/log-wrap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CloudWatchLogsClient, 3 | DescribeSubscriptionFiltersCommand, 4 | SubscriptionFilter 5 | } from "@aws-sdk/client-cloudwatch-logs"; 6 | 7 | export default class LogWrap { 8 | private cwlClient: CloudWatchLogsClient; 9 | 10 | constructor (region: string) { 11 | this.cwlClient = new CloudWatchLogsClient({ region }); 12 | } 13 | 14 | async getSubscriptionFilter (logGroupName: string, destinationArn: string): Promise { 15 | const { subscriptionFilters } = await this.cwlClient.send(new DescribeSubscriptionFiltersCommand({ 16 | logGroupName 17 | })); 18 | const filter = subscriptionFilters.find((s) => s.destinationArn === destinationArn); 19 | if (filter === undefined) { 20 | return null; 21 | } 22 | return filter; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/integration-tests/utils/log-receiver.ts: -------------------------------------------------------------------------------- 1 | // logs-receiver.ts 2 | /** 3 | * This file implements a fixture to safely create and delete a lambda function, 4 | * it's execution role and a log group. 5 | * @module 6 | */ 7 | 8 | import IamWrap from "./aws/iam-wrap"; 9 | import LambdaWrap from "./aws/lambda-wrap"; 10 | import LogWrap from "./aws/log-wrap"; 11 | import { sleep } from "./test-utilities"; 12 | 13 | /** 14 | * Interface for the immutable objects with information about 15 | * the lambda and the execution role that was created by the fixture. 16 | */ 17 | export interface LogsReceiverData { 18 | readonly functionName: string, 19 | readonly functionArn: string, 20 | } 21 | 22 | export default class LogReceiver { 23 | private readonly resourceName: string; 24 | private lambdaWrap: LambdaWrap; 25 | private iamWrap: IamWrap; 26 | private logWrap: LogWrap; 27 | 28 | constructor (region: string, resourceName: string) { 29 | this.resourceName = resourceName; 30 | // init AWS resources 31 | this.iamWrap = new IamWrap(region); 32 | this.lambdaWrap = new LambdaWrap(region); 33 | this.logWrap = new LogWrap(region); 34 | } 35 | 36 | /** 37 | * Creates the execution role for a lambda, lambda itself and ensures 38 | * that it's active. Returns the information about objects. 39 | */ 40 | public async setUpLogsReceiver (): Promise { 41 | // setup lambda role 42 | const role = await this.iamWrap.setupLambdaRole(this.resourceName); 43 | // we need to wait around 10 seconds to use the role 44 | await sleep(10 * 1000); 45 | const funcData = await this.lambdaWrap.createDummyFunction(this.resourceName, role.Arn); 46 | await this.lambdaWrap.ensureFunctionActive(funcData.FunctionName); 47 | 48 | return { 49 | functionName: funcData.FunctionName, 50 | functionArn: funcData.FunctionArn 51 | }; 52 | } 53 | 54 | /** 55 | * Removes all resources that could possibly be created by 56 | * the fixture in a safe way. 57 | */ 58 | async tearDownLogsReceiver (): Promise { 59 | try { 60 | await this.lambdaWrap.removeFunction(this.resourceName); 61 | console.debug("Logs receiver lambda was deleted"); 62 | } catch (e) { 63 | console.debug("Failed to delete a logs receiver"); 64 | } 65 | try { 66 | await this.iamWrap.removeLambdaRole(this.resourceName); 67 | console.log("Log receiver's role was deleted"); 68 | } catch (e) { 69 | console.debug("Failed to delete the execution role"); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/integration-tests/utils/test-utilities.ts: -------------------------------------------------------------------------------- 1 | import * as shell from "shelljs"; 2 | import { PLUGIN_IDENTIFIER, TEMP_DIR } from "../base"; 3 | 4 | /** 5 | * Executes given shell command. 6 | */ 7 | async function exec (cmd: string): Promise { 8 | console.debug(`\tRunning command: ${cmd}`); 9 | return new Promise((resolve, reject) => { 10 | shell.exec(cmd, { silent: false }, (errorCode, stdout, stderr) => { 11 | if (errorCode === 0) { 12 | return resolve(stdout); 13 | } 14 | return reject(stderr); 15 | }); 16 | }); 17 | } 18 | 19 | /** 20 | * Move item in folderName to created tempDir 21 | */ 22 | async function createTempDir (tempDir: string, folderName: string) { 23 | await exec(`rm -rf ${tempDir}`); 24 | await exec(`mkdir -p ${tempDir} && cp -R test/integration-tests/${folderName}/. ${tempDir}`); 25 | await exec(`mkdir -p ${tempDir}/node_modules/.bin`); 26 | await exec(`ln -s $(pwd) ${tempDir}/node_modules/`); 27 | 28 | await exec(`ln -s $(pwd)/node_modules/serverless ${tempDir}/node_modules/`); 29 | // link serverless to the bin directory so we can use $(npm bin) to get the path to serverless 30 | await exec(`ln -s $(pwd)/node_modules/serverless/bin/serverless.js ${tempDir}/node_modules/.bin/serverless`); 31 | } 32 | 33 | /** 34 | * Runs `sls deploy` for the given folder 35 | */ 36 | function slsDeploy (tempDir: string): Promise { 37 | return exec(`cd ${tempDir} && npx serverless deploy`); 38 | } 39 | 40 | /** 41 | * Runs `sls remove` for the given folder 42 | */ 43 | function slsRemove (tempDir: string): Promise { 44 | return exec(`cd ${tempDir} && npx serverless remove`); 45 | } 46 | 47 | /** 48 | * Wraps creation of testing resources. 49 | */ 50 | export async function createResources (folderName: string, testName: string): Promise { 51 | console.debug(`\tCreating Resources for ${testName} \tUsing tmp directory ${TEMP_DIR}`); 52 | try { 53 | await createTempDir(TEMP_DIR, folderName); 54 | await slsDeploy(TEMP_DIR); 55 | console.debug("\tResources Created"); 56 | } catch (e) { 57 | console.debug("\tResources Failed to Create"); 58 | } 59 | } 60 | 61 | /** 62 | * Wraps deletion of testing resources. 63 | */ 64 | export async function destroyResources (testName: string): Promise { 65 | try { 66 | console.debug(`\tCleaning Up Resources for ${testName}`); 67 | await slsRemove(TEMP_DIR); 68 | await exec(`rm -rf ${TEMP_DIR}`); 69 | console.debug("\tResources Cleaned Up"); 70 | } catch (e) { 71 | console.debug("\tFailed to Clean Up Resources"); 72 | } 73 | } 74 | 75 | /** 76 | * Returns a name of a CloudWatch log group of a function by it's 77 | * sls identifier. 78 | * 79 | * @param testName - name of a sls config 80 | * @param stage - deployment stage of sls 81 | * @param functionId - sls identifier of a function 82 | * @returns name of a function's log group 83 | */ 84 | export function getLogGroup (testName: string, stage: string, functionId: string): string { 85 | const functionName = `${PLUGIN_IDENTIFIER}-${testName}-${stage}-${functionId}`; 86 | return `/aws/lambda/${functionName}`; 87 | } 88 | 89 | export function sleep (ms: number) { 90 | return new Promise((resolve) => setTimeout(resolve, ms)); 91 | } 92 | 93 | export async function runTest (testName: string, assertFunc): Promise { 94 | try { 95 | const configFolder = `configs/${testName}`; 96 | await createResources(configFolder, testName); 97 | await assertFunc(); 98 | } finally { 99 | await destroyResources(testName); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /test/unit-tests/index-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { ServerlessConfig, ServerlessInstance } from "../../src/types"; 3 | import LogForwardingPlugin = require("../../src"); 4 | 5 | const TEST_DESTINATION_ARN = "arn:aws:lambda:us-moon-1:314159265358:function:testforward-test-forward"; 6 | 7 | const createServerless = (service, options) => { 8 | const funcLowerName = (name) => name.charAt(0).toLowerCase() + name.slice(1); 9 | const funcUpperName = (name) => name.charAt(0).toUpperCase() + name.slice(1); 10 | 11 | service.getFunction = (name: string) => { 12 | return { 13 | name: funcUpperName(name), 14 | ...service.functions[name] 15 | }; 16 | }; 17 | return { 18 | cli: { 19 | log (str: string) { 20 | } 21 | }, 22 | providers: {}, 23 | getProvider (name: string) { 24 | const state = options.stage || service.provider.stage; 25 | return { 26 | naming: { 27 | getLogGroupName: (name: string) => `/aws/lambda/${service.service}-${state}-${funcLowerName(name)}`, 28 | getNormalizedFunctionName: (name: string) => funcUpperName(name), 29 | getLogGroupLogicalId: (name: string) => `${funcUpperName(name)}LogGroup` 30 | } 31 | }; 32 | }, 33 | configSchemaHandler: { 34 | defineTopLevelProperty: () => null, 35 | defineCustomProperties: () => null, 36 | defineFunctionEvent: () => null, 37 | defineFunctionEventProperties: () => null, 38 | defineFunctionProperties: () => null, 39 | defineProvider: () => null 40 | }, 41 | service 42 | }; 43 | }; 44 | 45 | const constructPluginResources = (logForwarding, functions?) => { 46 | const config = { 47 | commands: [], 48 | options: {} 49 | }; 50 | const serverless = createServerless({ 51 | service: "test-service", 52 | provider: { 53 | name: "aws", 54 | region: "us-moon-1", 55 | stage: "test-stage" 56 | }, 57 | custom: { 58 | logForwarding 59 | }, 60 | resources: { 61 | Resources: { 62 | TestExistingFilter: { 63 | Type: "AWS:Test:Filter" 64 | } 65 | } 66 | }, 67 | functions: functions || { 68 | testFunctionOne: { 69 | filterPattern: "Pattern" 70 | }, 71 | testFunctionTwo: {} 72 | } 73 | }, config); 74 | return new LogForwardingPlugin(serverless as ServerlessInstance, config as ServerlessConfig); 75 | }; 76 | const constructPluginNoResources = (logForwarding) => { 77 | const config = { 78 | commands: [], 79 | options: {} 80 | }; 81 | const serverless = createServerless({ 82 | provider: { 83 | region: "us-moon-1", 84 | stage: "test-stage" 85 | }, 86 | custom: { 87 | logForwarding 88 | }, 89 | functions: { 90 | testFunctionOne: {}, 91 | testFunctionTwo: {} 92 | }, 93 | service: "test-service" 94 | }, config); 95 | serverless.service.resources = undefined; 96 | return new LogForwardingPlugin(serverless as ServerlessInstance, config as ServerlessConfig); 97 | }; 98 | const constructPluginResourcesWithParam = (logForwarding) => { 99 | const options = { 100 | commands: [], 101 | options: {}, 102 | stage: "dev" 103 | }; 104 | const serverless = createServerless({ 105 | provider: { 106 | name: "aws", 107 | region: "us-moon-1", 108 | stage: "test-stage" 109 | }, 110 | custom: { 111 | logForwarding 112 | }, 113 | resources: { 114 | Resources: { 115 | TestExistingFilter: { 116 | Type: "AWS:Test:Filter" 117 | } 118 | } 119 | }, 120 | functions: { 121 | testFunctionOne: { 122 | filterPattern: "Pattern" 123 | }, 124 | testFunctionTwo: {} 125 | }, 126 | service: "test-service" 127 | }, options); 128 | return new LogForwardingPlugin(serverless as ServerlessInstance, options); 129 | }; 130 | 131 | describe("Given a serverless config", () => { 132 | it("updates the resources object if it already exists", () => { 133 | const plugin = constructPluginResources({ 134 | destinationARN: TEST_DESTINATION_ARN 135 | }); 136 | const expectedResources = { 137 | Resources: { 138 | TestExistingFilter: { 139 | Type: "AWS:Test:Filter" 140 | }, 141 | LogForwardingLambdaPermission: { 142 | Type: "AWS::Lambda::Permission", 143 | Properties: { 144 | FunctionName: TEST_DESTINATION_ARN, 145 | Action: "lambda:InvokeFunction", 146 | Principal: "logs.us-moon-1.amazonaws.com" 147 | } 148 | }, 149 | SubscriptionFilterTestFunctionOne: { 150 | Type: "AWS::Logs::SubscriptionFilter", 151 | Properties: { 152 | DestinationArn: TEST_DESTINATION_ARN, 153 | FilterPattern: "", 154 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionOne" 155 | }, 156 | DependsOn: [ 157 | "LogForwardingLambdaPermission", 158 | "TestFunctionOneLogGroup" 159 | ] 160 | }, 161 | SubscriptionFilterTestFunctionTwo: { 162 | Type: "AWS::Logs::SubscriptionFilter", 163 | Properties: { 164 | DestinationArn: TEST_DESTINATION_ARN, 165 | FilterPattern: "", 166 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionTwo" 167 | }, 168 | DependsOn: [ 169 | "LogForwardingLambdaPermission", 170 | "TestFunctionTwoLogGroup" 171 | ] 172 | } 173 | } 174 | }; 175 | plugin.updateResources(); 176 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 177 | }); 178 | it("updates the resources object if it already exists with params", () => { 179 | const plugin = constructPluginResourcesWithParam({ 180 | destinationARN: TEST_DESTINATION_ARN 181 | }); 182 | const expectedResources = { 183 | Resources: { 184 | TestExistingFilter: { 185 | Type: "AWS:Test:Filter" 186 | }, 187 | LogForwardingLambdaPermission: { 188 | Type: "AWS::Lambda::Permission", 189 | Properties: { 190 | FunctionName: TEST_DESTINATION_ARN, 191 | Action: "lambda:InvokeFunction", 192 | Principal: "logs.us-moon-1.amazonaws.com" 193 | } 194 | }, 195 | SubscriptionFilterTestFunctionOne: { 196 | Type: "AWS::Logs::SubscriptionFilter", 197 | Properties: { 198 | DestinationArn: TEST_DESTINATION_ARN, 199 | FilterPattern: "", 200 | LogGroupName: "/aws/lambda/test-service-dev-testFunctionOne" 201 | }, 202 | DependsOn: [ 203 | "LogForwardingLambdaPermission", 204 | "TestFunctionOneLogGroup" 205 | ] 206 | }, 207 | SubscriptionFilterTestFunctionTwo: { 208 | Type: "AWS::Logs::SubscriptionFilter", 209 | Properties: { 210 | DestinationArn: TEST_DESTINATION_ARN, 211 | FilterPattern: "", 212 | LogGroupName: "/aws/lambda/test-service-dev-testFunctionTwo" 213 | }, 214 | DependsOn: [ 215 | "LogForwardingLambdaPermission", 216 | "TestFunctionTwoLogGroup" 217 | ] 218 | } 219 | } 220 | }; 221 | plugin.updateResources(); 222 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 223 | }); 224 | it("updates the resources object if it doesn't exist", () => { 225 | const plugin = constructPluginNoResources({ 226 | destinationARN: TEST_DESTINATION_ARN 227 | }); 228 | const expectedResources = { 229 | Resources: { 230 | LogForwardingLambdaPermission: { 231 | Type: "AWS::Lambda::Permission", 232 | Properties: { 233 | FunctionName: TEST_DESTINATION_ARN, 234 | Action: "lambda:InvokeFunction", 235 | Principal: "logs.us-moon-1.amazonaws.com" 236 | } 237 | }, 238 | SubscriptionFilterTestFunctionOne: { 239 | Type: "AWS::Logs::SubscriptionFilter", 240 | Properties: { 241 | DestinationArn: TEST_DESTINATION_ARN, 242 | FilterPattern: "", 243 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionOne" 244 | }, 245 | DependsOn: [ 246 | "LogForwardingLambdaPermission", 247 | "TestFunctionOneLogGroup" 248 | ] 249 | }, 250 | SubscriptionFilterTestFunctionTwo: { 251 | Type: "AWS::Logs::SubscriptionFilter", 252 | Properties: { 253 | DestinationArn: TEST_DESTINATION_ARN, 254 | FilterPattern: "", 255 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionTwo" 256 | }, 257 | DependsOn: [ 258 | "LogForwardingLambdaPermission", 259 | "TestFunctionTwoLogGroup" 260 | ] 261 | } 262 | } 263 | }; 264 | plugin.updateResources(); 265 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 266 | }); 267 | it("uses the filterPattern property if set", () => { 268 | const plugin = constructPluginResources({ 269 | destinationARN: TEST_DESTINATION_ARN, 270 | filterPattern: "Test Pattern", 271 | normalizedFilterID: false 272 | }); 273 | const expectedResources = { 274 | Resources: { 275 | TestExistingFilter: { 276 | Type: "AWS:Test:Filter" 277 | }, 278 | LogForwardingLambdaPermission: { 279 | Type: "AWS::Lambda::Permission", 280 | Properties: { 281 | FunctionName: TEST_DESTINATION_ARN, 282 | Action: "lambda:InvokeFunction", 283 | Principal: "logs.us-moon-1.amazonaws.com" 284 | } 285 | }, 286 | SubscriptionFiltertestFunctionOne: { 287 | Type: "AWS::Logs::SubscriptionFilter", 288 | Properties: { 289 | DestinationArn: TEST_DESTINATION_ARN, 290 | FilterPattern: "Test Pattern", 291 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionOne" 292 | }, 293 | DependsOn: [ 294 | "LogForwardingLambdaPermission", 295 | "TestFunctionOneLogGroup" 296 | ] 297 | }, 298 | SubscriptionFiltertestFunctionTwo: { 299 | Type: "AWS::Logs::SubscriptionFilter", 300 | Properties: { 301 | DestinationArn: TEST_DESTINATION_ARN, 302 | FilterPattern: "Test Pattern", 303 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionTwo" 304 | }, 305 | DependsOn: [ 306 | "LogForwardingLambdaPermission", 307 | "TestFunctionTwoLogGroup" 308 | ] 309 | } 310 | } 311 | }; 312 | plugin.updateResources(); 313 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 314 | }); 315 | it("excludes functions with logForwarding.enabled=false from AWS::Logs::SubscriptionFilter output", () => { 316 | const plugin = constructPluginResources({ 317 | destinationARN: TEST_DESTINATION_ARN, 318 | filterPattern: "Test Pattern", 319 | normalizedFilterID: false 320 | }, { 321 | testFunctionOne: {}, 322 | testFunctionTwo: { 323 | logForwarding: {} 324 | }, 325 | testFunctionThree: { 326 | logForwarding: { 327 | enabled: true 328 | } 329 | }, 330 | testFunctionFour: { 331 | logForwarding: { 332 | enabled: false 333 | } 334 | } 335 | }); 336 | const expectedResources = { 337 | Resources: { 338 | TestExistingFilter: { 339 | Type: "AWS:Test:Filter" 340 | }, 341 | LogForwardingLambdaPermission: { 342 | Type: "AWS::Lambda::Permission", 343 | Properties: { 344 | FunctionName: TEST_DESTINATION_ARN, 345 | Action: "lambda:InvokeFunction", 346 | Principal: "logs.us-moon-1.amazonaws.com" 347 | } 348 | }, 349 | SubscriptionFiltertestFunctionOne: { 350 | Type: "AWS::Logs::SubscriptionFilter", 351 | Properties: { 352 | DestinationArn: TEST_DESTINATION_ARN, 353 | FilterPattern: "Test Pattern", 354 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionOne" 355 | }, 356 | DependsOn: [ 357 | "LogForwardingLambdaPermission", 358 | "TestFunctionOneLogGroup" 359 | ] 360 | }, 361 | SubscriptionFiltertestFunctionTwo: { 362 | Type: "AWS::Logs::SubscriptionFilter", 363 | Properties: { 364 | DestinationArn: TEST_DESTINATION_ARN, 365 | FilterPattern: "Test Pattern", 366 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionTwo" 367 | }, 368 | DependsOn: [ 369 | "LogForwardingLambdaPermission", 370 | "TestFunctionTwoLogGroup" 371 | ] 372 | }, 373 | SubscriptionFiltertestFunctionThree: { 374 | Type: "AWS::Logs::SubscriptionFilter", 375 | Properties: { 376 | DestinationArn: TEST_DESTINATION_ARN, 377 | FilterPattern: "Test Pattern", 378 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionThree" 379 | }, 380 | DependsOn: [ 381 | "LogForwardingLambdaPermission", 382 | "TestFunctionThreeLogGroup" 383 | ] 384 | } 385 | } 386 | }; 387 | plugin.updateResources(); 388 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 389 | }); 390 | it("uses stage filter if set", () => { 391 | const plugin = constructPluginResources({ 392 | destinationARN: TEST_DESTINATION_ARN, 393 | filterPattern: "Test Pattern", 394 | stages: ["production"] 395 | }); 396 | const expectedResources = { 397 | Resources: { 398 | TestExistingFilter: { 399 | Type: "AWS:Test:Filter" 400 | } 401 | } 402 | }; 403 | plugin.updateResources(); 404 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 405 | }); 406 | it("uses the roleArn property if set", () => { 407 | const plugin = constructPluginResources({ 408 | destinationARN: TEST_DESTINATION_ARN, 409 | roleArn: "arn:aws:lambda:us-moon-1:314159265358:role/test-iam-role", 410 | normalizedFilterID: false 411 | }); 412 | 413 | const expectedResources = { 414 | Resources: { 415 | TestExistingFilter: { 416 | Type: "AWS:Test:Filter" 417 | }, 418 | SubscriptionFiltertestFunctionOne: { 419 | Type: "AWS::Logs::SubscriptionFilter", 420 | Properties: { 421 | DestinationArn: TEST_DESTINATION_ARN, 422 | FilterPattern: "", 423 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionOne", 424 | RoleArn: "arn:aws:lambda:us-moon-1:314159265358:role/test-iam-role" 425 | }, 426 | DependsOn: [ 427 | "TestFunctionOneLogGroup" 428 | ] 429 | }, 430 | SubscriptionFiltertestFunctionTwo: { 431 | Type: "AWS::Logs::SubscriptionFilter", 432 | Properties: { 433 | DestinationArn: TEST_DESTINATION_ARN, 434 | FilterPattern: "", 435 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionTwo", 436 | RoleArn: "arn:aws:lambda:us-moon-1:314159265358:role/test-iam-role" 437 | }, 438 | DependsOn: [ 439 | "TestFunctionTwoLogGroup" 440 | ] 441 | } 442 | } 443 | }; 444 | plugin.updateResources(); 445 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 446 | }); 447 | it("uses the disabledLambdaPermission property if set to not include the LogForwardingLambdaPermission", () => { 448 | const plugin = constructPluginResources({ 449 | destinationARN: TEST_DESTINATION_ARN, 450 | normalizedFilterID: false, 451 | createLambdaPermission: false 452 | }); 453 | const expectedResources = { 454 | Resources: { 455 | TestExistingFilter: { 456 | Type: "AWS:Test:Filter" 457 | }, 458 | SubscriptionFiltertestFunctionOne: { 459 | Type: "AWS::Logs::SubscriptionFilter", 460 | Properties: { 461 | DestinationArn: TEST_DESTINATION_ARN, 462 | FilterPattern: "", 463 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionOne" 464 | }, 465 | DependsOn: [ 466 | "TestFunctionOneLogGroup" 467 | ] 468 | }, 469 | SubscriptionFiltertestFunctionTwo: { 470 | Type: "AWS::Logs::SubscriptionFilter", 471 | Properties: { 472 | DestinationArn: TEST_DESTINATION_ARN, 473 | FilterPattern: "", 474 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionTwo" 475 | }, 476 | DependsOn: [ 477 | "TestFunctionTwoLogGroup" 478 | ] 479 | } 480 | } 481 | }; 482 | plugin.updateResources(); 483 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 484 | }); 485 | it("uses the roleArn even if disabledLambdaPermission property is set", () => { 486 | const plugin = constructPluginResources({ 487 | destinationARN: TEST_DESTINATION_ARN, 488 | roleArn: "arn:aws:lambda:us-moon-1:314159265358:role/test-iam-role", 489 | normalizedFilterID: false, 490 | createLambdaPermission: false 491 | }); 492 | const expectedResources = { 493 | Resources: { 494 | TestExistingFilter: { 495 | Type: "AWS:Test:Filter" 496 | }, 497 | SubscriptionFiltertestFunctionOne: { 498 | Type: "AWS::Logs::SubscriptionFilter", 499 | Properties: { 500 | DestinationArn: TEST_DESTINATION_ARN, 501 | FilterPattern: "", 502 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionOne", 503 | RoleArn: "arn:aws:lambda:us-moon-1:314159265358:role/test-iam-role" 504 | }, 505 | DependsOn: [ 506 | "TestFunctionOneLogGroup" 507 | ] 508 | }, 509 | SubscriptionFiltertestFunctionTwo: { 510 | Type: "AWS::Logs::SubscriptionFilter", 511 | Properties: { 512 | DestinationArn: TEST_DESTINATION_ARN, 513 | FilterPattern: "", 514 | LogGroupName: "/aws/lambda/test-service-test-stage-testFunctionTwo", 515 | RoleArn: "arn:aws:lambda:us-moon-1:314159265358:role/test-iam-role" 516 | }, 517 | DependsOn: [ 518 | "TestFunctionTwoLogGroup" 519 | ] 520 | } 521 | } 522 | }; 523 | plugin.updateResources(); 524 | expect(plugin.serverless.service.resources).to.eql(expectedResources); 525 | }); 526 | }); 527 | 528 | describe("Catching errors in serverless config ", () => { 529 | it("missing custom log forwarding options", () => { 530 | const emptyConfig = {}; 531 | const plugin = constructPluginResources(emptyConfig); 532 | const expectedError = "Serverless-log-forwarding is not configured correctly. Please see README for proper setup."; 533 | expect(plugin.updateResources.bind(plugin)).to.throw(expectedError); 534 | }); 535 | }); 536 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "rootDir": ".", 6 | "target": "es6", 7 | "sourceMap": false, 8 | "outDir": "dist", 9 | "types": [ 10 | "mocha", 11 | "node" 12 | ], 13 | "allowSyntheticDefaultImports": true, 14 | "resolveJsonModule": true, 15 | "esModuleInterop": true 16 | }, 17 | "include": [ 18 | "src/**/*.ts" 19 | ], 20 | "exclude": [ 21 | "test/**/*.ts" 22 | ] 23 | } 24 | --------------------------------------------------------------------------------