├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── commitlint.config.js ├── integration-test ├── nodejs-esbuild │ ├── .gitignore │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── nodejs-esm │ ├── .gitignore │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── nodejs-layers │ ├── .gitignore │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── nodejs-pnpm │ ├── .gitignore │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── nodejs │ ├── .gitignore │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── python-layers │ ├── .gitignore │ ├── handler.py │ ├── package.json │ ├── requirements.txt │ └── serverless.yml └── python │ ├── .gitignore │ ├── 01_handler_with_numbers.py │ ├── handler.py │ ├── package.json │ ├── requirements.txt │ └── serverless.yml ├── jest.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── scripts └── checks.sh └── src ├── index.js └── index.test.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | workflows: 4 | build-deploy: 5 | jobs: 6 | - build: 7 | context: 8 | - common 9 | - node.js 10 | 11 | - deploy: 12 | context: 13 | - common 14 | - node.js 15 | filters: 16 | branches: 17 | only: master 18 | requires: 19 | - build 20 | 21 | jobs: 22 | build: 23 | docker: 24 | - image: cimg/node:16.13.2 25 | working_directory: ~/serverless-lumigo-plugin 26 | steps: 27 | - checkout 28 | - run: wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh 29 | - run: bash miniconda.sh -b -p $HOME/miniconda 30 | - run: echo 'export PATH=$PATH:$HOME/miniconda/bin' >> $BASH_ENV 31 | - run: conda install python==3.10 32 | - run: conda install virtualenv 33 | - run: npm ci 34 | - run: npm install pnpm 35 | - run: npm run test:all 36 | - run: npm run codecov 37 | 38 | deploy: 39 | docker: 40 | - image: cimg/node:16.13.2 41 | working_directory: ~/serverless-lumigo 42 | steps: 43 | - checkout 44 | - run: npm ci 45 | - run: npm run test 46 | - run: npm run codecov 47 | - run: 48 | name: release 49 | command: | 50 | release_output=$(npm run semantic-release) 51 | echo "$release_output" 52 | echo $release_output | grep "Published release" || exit 1 53 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | charset = utf-8 11 | indent_style = space 12 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | docs -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "es6": true, 5 | "mocha": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2017, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "indent": ["error", "tab"], 15 | "linebreak-style": ["error", "unix"], 16 | "quotes": ["error", "double", { "avoidEscape": true }], 17 | "semi": ["error", "always"], 18 | "no-console": 0 19 | }, 20 | "extends": ["eslint:recommended", "prettier"], 21 | "globals": { 22 | "console": true, 23 | "require": true, 24 | "module": true, 25 | "process": true, 26 | "setTimeout": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | package 39 | 40 | # TypeScript v1 declaration files 41 | typings/ 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional eslint cache 47 | .eslintcache 48 | 49 | # Optional REPL history 50 | .node_repl_history 51 | 52 | # Output of 'npm pack' 53 | *.tgz 54 | 55 | # Yarn Integrity file 56 | .yarn-integrity 57 | 58 | # dotenv environment variables file 59 | .env 60 | 61 | # next.js build output 62 | .next 63 | 64 | # OSX 65 | .DS_Store 66 | 67 | # Python 68 | venv 69 | 70 | # InteliJ 71 | .idea 72 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.19.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2019] [Lumigo] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-lumigo 2 | 3 | [![CircleCI](https://dl.circleci.com/status-badge/img/gh/lumigo-io/serverless-lumigo-plugin/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/lumigo-io/serverless-lumigo-plugin/tree/master) 4 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 5 | [![version](https://badge.fury.io/js/serverless-lumigo.svg)](https://www.npmjs.com/package/serverless-lumigo) 6 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) 7 | [![codecov](https://codecov.io/gh/lumigo-io/serverless-lumigo-plugin/branch/master/graph/badge.svg?token=8mXE2G04ZO)](https://codecov.io/gh/lumigo-io/serverless-lumigo-plugin) 8 | 9 | Serverless framework plugin to auto-install the Lumigo tracer for Node.js and Python functions. 10 | 11 | ## TOC 12 | 13 | - [Install](#install) 14 | - [Node.js functions](#nodejs-functions) 15 | - [Python functions](#python-functions) 16 | - [Configuring the tracer](#configuration) 17 | 18 | ## Install 19 | 20 | Run `npm install` in your Serverless project. 21 | 22 | `$ npm install --save-dev serverless-lumigo` 23 | 24 | Add the plugin to your serverless.yml file 25 | 26 | ```yaml 27 | plugins: 28 | - serverless-lumigo 29 | ``` 30 | 31 | ## Node.js functions 32 | 33 | For Node.js functions, the plugin would install the latest version of the Lumigo tracer for Node.js during `serverless package` and `serverless deploy`. It would also wrap your functions as well, so you only need to configure your Lumigo token in a `custom` section inside the `serverless.yml`. 34 | 35 | For example: 36 | 37 | ```yaml 38 | provider: 39 | name: aws 40 | runtime: nodejs12.x 41 | 42 | custom: 43 | lumigo: 44 | token: 45 | nodePackageManager: 46 | ``` 47 | 48 | In case you want to pin the specific tracer version use `pinVersion` attribute. 49 | 50 | For example 51 | 52 | ```yaml 53 | provider: 54 | name: aws 55 | runtime: nodejs12.x 56 | 57 | custom: 58 | lumigo: 59 | token: 60 | pinVersion: 1.31.1 61 | ``` 62 | 63 | In case you want to manage the Lumigo tracer dependency yourself - e.g. you want to use Lerna or Webpack, and can't have this plugin install the Lumigo tracer on your behalf on every deployment - then you can also disable the NPM install process altogether. 64 | 65 | ```yaml 66 | provider: 67 | name: aws 68 | runtime: nodejs12.x 69 | 70 | custom: 71 | lumigo: 72 | token: 73 | skipInstallNodeTracer: true # defaults to false 74 | ``` 75 | 76 | In case you are using ES Modules for Lambda handlers. 77 | 78 | ```yaml 79 | provider: 80 | name: aws 81 | runtime: nodejs14.x 82 | 83 | custom: 84 | lumigo: 85 | token: 86 | nodeUseESModule: true 87 | nodeModuleFileExtension: js 88 | ``` 89 | 90 | ## Python functions 91 | 92 | For Python functions, we recommend using the [serverless-python-requirements](https://www.npmjs.com/package/serverless-python-requirements) plugin to help you manage your dependencies. You should have the following in your `requirements.txt`: 93 | 94 | ```txt 95 | lumigo_tracer or lumigo-tracer 96 | ``` 97 | 98 | This installs the Lumigo tracer for Python, and this plugin would wrap your functions during `serverless package` and `serverless deploy`. 99 | 100 | You also need to configure the Lumigo token in a `custom` section in the `serverless.yml`. 101 | 102 | ```yaml 103 | provider: 104 | name: aws 105 | runtime: python3.10 106 | custom: 107 | lumigo: 108 | token: 109 | ``` 110 | 111 | In case you are not using `requirements.txt` to manage your requirements then you can add `skipReqCheck` and set it to `true` 112 | 113 | ```yaml 114 | custom: 115 | lumigo: 116 | token: 1234 117 | skipReqCheck: true 118 | ``` 119 | 120 | ## Configuration 121 | 122 | In order to pass parameters to the tracer, just add them as keys to lumigo custom configuration. For example: 123 | 124 | ```yaml 125 | custom: 126 | lumigo: 127 | token: 128 | step_function: true 129 | ``` 130 | 131 | ### Function Scope Configuration 132 | 133 | You can configure lumigo behavior for individual functions as well: 134 | 135 | - `enabled` - Allows one to enable or disable lumigo for specific a specific function 136 | 137 | ```yaml 138 | functions: 139 | foo: 140 | lumigo: 141 | enabled: false 142 | 143 | bar: 144 | lumigo: 145 | enabled: ${self:custom.enabledLumigo} 146 | ``` 147 | 148 | ## How to test 149 | 150 | Run `npm run test:all` 151 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ["@commitlint/config-conventional"] }; 2 | -------------------------------------------------------------------------------- /integration-test/nodejs-esbuild/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | /package-lock.json 8 | -------------------------------------------------------------------------------- /integration-test/nodejs-esbuild/handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports.hello = async event => { 4 | return { message: "Hello Lumigo!", event }; 5 | }; 6 | -------------------------------------------------------------------------------- /integration-test/nodejs-esbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "esbuild": "^0.14.25", 13 | "serverless-esbuild": "^1.30.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /integration-test/nodejs-esbuild/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-plugin-nodejs-layers 2 | 3 | custom: 4 | lumigo: 5 | token: 1234 6 | 7 | provider: 8 | name: aws 9 | runtime: nodejs14.x 10 | environment: 11 | LUMIGO_TRACER_HOST: "test.execute-api.us-west-2.amazonaws.com" 12 | LUMIGO_DEBUG: "TRUE" 13 | 14 | functions: 15 | test: 16 | handler: handler.hello 17 | 18 | plugins: 19 | - ./../../src/index 20 | - serverless-esbuild 21 | -------------------------------------------------------------------------------- /integration-test/nodejs-esm/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | /package-lock.json 8 | -------------------------------------------------------------------------------- /integration-test/nodejs-esm/handler.js: -------------------------------------------------------------------------------- 1 | export const hello = async event => { 2 | return { message: "Hello Lumigo!", event }; 3 | }; 4 | -------------------------------------------------------------------------------- /integration-test/nodejs-esm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /integration-test/nodejs-esm/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-plugin-nodejs-esm 2 | 3 | custom: 4 | lumigo: 5 | token: t_b9222642efc14985baaed 6 | nodePackageManager: npm 7 | nodeUseESModule: true 8 | nodeModuleFileExtension: js 9 | edgeHost: test.execute-api.us-west-2.amazonaws.com 10 | pinVersion: 1.30.0 11 | 12 | provider: 13 | name: aws 14 | runtime: nodejs14.x 15 | environment: 16 | LUMIGO_TRACER_HOST: "test.execute-api.us-west-2.amazonaws.com" 17 | LUMIGO_DEBUG: "TRUE" 18 | 19 | functions: 20 | test: 21 | handler: handler.hello 22 | 23 | plugins: 24 | - ./../../src/index 25 | -------------------------------------------------------------------------------- /integration-test/nodejs-layers/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | /package-lock.json 8 | -------------------------------------------------------------------------------- /integration-test/nodejs-layers/handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports.hello = async event => { 4 | return { message: "Hello Lumigo!", event }; 5 | }; 6 | -------------------------------------------------------------------------------- /integration-test/nodejs-layers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": {} 12 | } 13 | -------------------------------------------------------------------------------- /integration-test/nodejs-layers/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-plugin-nodejs-layers 2 | 3 | custom: 4 | lumigo: 5 | token: 1234 6 | useLayers: true 7 | 8 | provider: 9 | name: aws 10 | runtime: nodejs14.x 11 | environment: 12 | LUMIGO_TRACER_HOST: "test.execute-api.us-west-2.amazonaws.com" 13 | LUMIGO_DEBUG: "TRUE" 14 | 15 | functions: 16 | test: 17 | handler: handler.hello 18 | 19 | plugins: 20 | - ./../../src/index 21 | -------------------------------------------------------------------------------- /integration-test/nodejs-pnpm/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | /package-lock.json 8 | -------------------------------------------------------------------------------- /integration-test/nodejs-pnpm/handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports.hello = async event => { 4 | return { message: "Hello Lumigo!", event }; 5 | }; 6 | -------------------------------------------------------------------------------- /integration-test/nodejs-pnpm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /integration-test/nodejs-pnpm/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-plugin-nodejs 2 | 3 | custom: 4 | lumigo: 5 | token: t_b9222642efc14985baaed 6 | nodePackageManager: pnpm 7 | edgeHost: test.execute-api.us-west-2.amazonaws.com 8 | pinVersion: 1.30.0 9 | 10 | provider: 11 | name: aws 12 | runtime: nodejs14.x 13 | environment: 14 | LUMIGO_TRACER_HOST: "test.execute-api.us-west-2.amazonaws.com" 15 | LUMIGO_DEBUG: "TRUE" 16 | 17 | functions: 18 | test: 19 | handler: handler.hello 20 | 21 | plugins: 22 | - ./../../src/index 23 | -------------------------------------------------------------------------------- /integration-test/nodejs/.gitignore: -------------------------------------------------------------------------------- 1 | # package directories 2 | node_modules 3 | jspm_packages 4 | 5 | # Serverless directories 6 | .serverless 7 | /package-lock.json 8 | -------------------------------------------------------------------------------- /integration-test/nodejs/handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports.hello = async event => { 4 | return { message: "Hello Lumigo!", event }; 5 | }; 6 | -------------------------------------------------------------------------------- /integration-test/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "handler.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /integration-test/nodejs/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-plugin-nodejs 2 | 3 | custom: 4 | lumigo: 5 | token: t_b9222642efc14985baaed 6 | nodePackageManager: npm 7 | edgeHost: test.execute-api.us-west-2.amazonaws.com 8 | pinVersion: 1.30.0 9 | 10 | provider: 11 | name: aws 12 | runtime: nodejs14.x 13 | environment: 14 | LUMIGO_TRACER_HOST: "test.execute-api.us-west-2.amazonaws.com" 15 | LUMIGO_DEBUG: "TRUE" 16 | 17 | functions: 18 | test: 19 | handler: handler.hello 20 | 21 | plugins: 22 | - ./../../src/index 23 | -------------------------------------------------------------------------------- /integration-test/python-layers/.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | *.pyc 4 | env/ 5 | build/ 6 | develop-eggs/ 7 | dist/ 8 | downloads/ 9 | eggs/ 10 | .eggs/ 11 | lib/ 12 | lib64/ 13 | parts/ 14 | sdist/ 15 | var/ 16 | *.egg-info/ 17 | .installed.cfg 18 | *.egg 19 | 20 | # Serverless directories 21 | .serverless 22 | /package-lock.json 23 | -------------------------------------------------------------------------------- /integration-test/python-layers/handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def hello(event, context): 5 | return { 6 | "message": "Hello Lumigo!", 7 | "event": event 8 | } 9 | 10 | -------------------------------------------------------------------------------- /integration-test/python-layers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "serverless-python-requirements": "^4.2.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration-test/python-layers/requirements.txt: -------------------------------------------------------------------------------- 1 | lumigo_tracer 2 | -------------------------------------------------------------------------------- /integration-test/python-layers/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-plugin-python-layers 2 | 3 | custom: 4 | lumigo: 5 | token: 1234 6 | useLayers: true 7 | 8 | provider: 9 | name: aws 10 | runtime: python3.10 11 | environment: 12 | LUMIGO_DEBUG: "TRUE" 13 | 14 | functions: 15 | test: 16 | handler: handler.hello 17 | 18 | plugins: 19 | - ./../../src/index 20 | - serverless-python-requirements 21 | -------------------------------------------------------------------------------- /integration-test/python/.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | *.pyc 4 | env/ 5 | build/ 6 | develop-eggs/ 7 | dist/ 8 | downloads/ 9 | eggs/ 10 | .eggs/ 11 | lib/ 12 | lib64/ 13 | parts/ 14 | sdist/ 15 | var/ 16 | *.egg-info/ 17 | .installed.cfg 18 | *.egg 19 | 20 | # Serverless directories 21 | .serverless 22 | /package-lock.json 23 | -------------------------------------------------------------------------------- /integration-test/python/01_handler_with_numbers.py: -------------------------------------------------------------------------------- 1 | def hello(event, context): 2 | return { 3 | "message": "Hello Lumigo!", 4 | "event": event 5 | } 6 | -------------------------------------------------------------------------------- /integration-test/python/handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def hello(event, context): 5 | return { 6 | "message": "Hello Lumigo!", 7 | "event": event 8 | } 9 | 10 | -------------------------------------------------------------------------------- /integration-test/python/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "python", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "serverless-python-requirements": "^4.2.5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integration-test/python/requirements.txt: -------------------------------------------------------------------------------- 1 | lumigo_tracer 2 | -------------------------------------------------------------------------------- /integration-test/python/serverless.yml: -------------------------------------------------------------------------------- 1 | service: serverless-plugin-python 2 | 3 | custom: 4 | lumigo: 5 | token: t_b9222642efc14985baaed 6 | skipReqCheck: true 7 | 8 | provider: 9 | name: aws 10 | runtime: python3.10 11 | environment: 12 | LUMIGO_DEBUG: "TRUE" 13 | LUMIGO_TRACER_HOST: "test.execute-api.us-west-2.amazonaws.com" 14 | 15 | functions: 16 | test: 17 | handler: handler.hello 18 | 19 | test-with-numbers: 20 | handler: 01_handler_with_numbers.hello 21 | 22 | plugins: 23 | - ./../../src/index 24 | - serverless-python-requirements 25 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | coverageReporters: [ 4 | "text", 5 | "html", 6 | "lcov" 7 | ], 8 | testEnvironment: "node" 9 | }; 10 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { "typeAcquisition": { "include": [ "jest" ] } } 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-lumigo", 3 | "version": "0.0.0-dev", 4 | "description": "Serverless framework plugin to auto-install the Lumigo tracer", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "codecov": "codecov", 8 | "test": "jest --config=jest.config.js", 9 | "test:all": "./scripts/checks.sh", 10 | "test:lint": "eslint .", 11 | "semantic-release": "semantic-release", 12 | "prettier:ci": "prettier --list-different \"src/**/*.js\"", 13 | "prettier:fix": "prettier --write \"./src/**/*.js\"" 14 | }, 15 | "files": [ 16 | "src/index.js", 17 | "LICENSE", 18 | "package.json", 19 | "README.md" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/lumigo-io/serverless-lumigo.git" 24 | }, 25 | "keywords": [ 26 | "serverless", 27 | "python", 28 | "node", 29 | "nodejs", 30 | "serverless framework plugin", 31 | "serverless applications", 32 | "serverless plugins", 33 | "api gateway", 34 | "lambda", 35 | "aws", 36 | "aws lambda", 37 | "amazon", 38 | "amazon web services", 39 | "serverless.com" 40 | ], 41 | "author": "Lumigo LTD (https://lumigo.io)", 42 | "license": "Apache 2", 43 | "bugs": { 44 | "url": "https://github.com/lumigo-io/serverless-lumigo/issues" 45 | }, 46 | "homepage": "https://github.com/lumigo-io/serverless-lumigo#readme", 47 | "dependencies": { 48 | "axios": "^0.21.1", 49 | "bluebird": "^3.5.5", 50 | "fs-extra": "^8.1.0", 51 | "lodash": "^4.17.21", 52 | "pnpm": "^7.27.1" 53 | }, 54 | "devDependencies": { 55 | "@commitlint/cli": "^16.0.1", 56 | "@commitlint/config-conventional": "^16.0.0", 57 | "@types/jest": "^26.0.10", 58 | "codecov": "^3.5.0", 59 | "coveralls": "^3.0.2", 60 | "eslint": "^5.13.0", 61 | "eslint-config-prettier": "^6.0.0", 62 | "eslint-config-standard": "^12.0.0", 63 | "eslint-plugin-import": "^2.16.0", 64 | "eslint-plugin-node": "^9.1.0", 65 | "eslint-plugin-promise": "^4.0.1", 66 | "eslint-plugin-standard": "^4.0.0", 67 | "husky": "^3.0.0", 68 | "jest": "^27.4.5", 69 | "lint-staged": "^9.1.0", 70 | "prettier": "^1.18.2", 71 | "semantic-release": "^19.0.2", 72 | "serverless": "^3.8.0" 73 | }, 74 | "prettier": { 75 | "useTabs": true, 76 | "tabWidth": 4, 77 | "printWidth": 90 78 | }, 79 | "greenkeeper": { 80 | "ignore": [ 81 | "eslint" 82 | ] 83 | }, 84 | "husky": { 85 | "hooks": { 86 | "pre-commit": "lint-staged", 87 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 88 | } 89 | }, 90 | "lint-staged": { 91 | "*.js": [ 92 | "eslint" 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /scripts/checks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -o pipefail 4 | 5 | if [ -n "$STAGING_AWS_ACCESS_KEY_ID" ]; then 6 | echo "Staging credentials detected, overriding default credentials." 7 | export AWS_ACCESS_KEY_ID="$STAGING_AWS_ACCESS_KEY_ID" 8 | export AWS_SECRET_ACCESS_KEY="$STAGING_AWS_SECRET_ACCESS_KEY" 9 | fi 10 | 11 | npm run test:lint 12 | npm run prettier:ci 13 | npm run test 14 | 15 | pushd integration-test/nodejs 16 | 17 | random=$RANDOM 18 | 19 | echo 20 | echo "********************" 21 | echo "** Testing NodeJS **" 22 | echo "********************" 23 | echo 24 | 25 | echo 26 | echo "** Deploying **" 27 | echo 28 | npx serverless deploy --force --stage $random 29 | 30 | echo 31 | echo "** Testing **" 32 | echo 33 | npx serverless invoke -l -f test --stage $random | grep "#LUMIGO#" 34 | 35 | echo 36 | echo "** Removing stack **" 37 | echo 38 | npx serverless remove --stage $random 39 | popd 40 | 41 | 42 | pushd integration-test/nodejs-esbuild 43 | 44 | random=$RANDOM 45 | 46 | echo 47 | echo "**********************************" 48 | echo "** Testing NodeJS with es-build **" 49 | echo "**********************************" 50 | echo 51 | 52 | echo 53 | echo "** Install packages **" 54 | echo 55 | npm i 56 | 57 | echo 58 | echo "** Deploying **" 59 | echo 60 | npx serverless deploy --force --stage $random 61 | 62 | echo 63 | echo "** Testing **" 64 | echo 65 | npx serverless invoke -l -f test --stage $random | grep "#LUMIGO#" 66 | 67 | echo 68 | echo "** Removing stack **" 69 | echo 70 | npx serverless remove --stage $random 71 | popd 72 | 73 | 74 | pushd integration-test/nodejs-pnpm 75 | 76 | random=$RANDOM 77 | 78 | echo 79 | echo "******************************" 80 | echo "** Testing NodeJS with PNPM **" 81 | echo "******************************" 82 | echo 83 | 84 | echo 85 | echo "** Deploying **" 86 | echo 87 | npx serverless deploy --force --stage $random 88 | 89 | echo 90 | echo "** Testing **" 91 | echo 92 | npx serverless invoke -l -f test --stage $random | grep "#LUMIGO#" 93 | 94 | echo 95 | echo "** Removing stack **" 96 | echo 97 | npx serverless remove --stage $random 98 | popd 99 | 100 | pushd integration-test/nodejs-esm 101 | 102 | random=$RANDOM 103 | 104 | echo 105 | echo "***********************************" 106 | echo "** Testing NodeJS with ES Module **" 107 | echo "***********************************" 108 | echo 109 | 110 | echo 111 | echo "** Deploying **" 112 | echo 113 | npx serverless deploy --force --stage $random 114 | 115 | echo 116 | echo "** Testing **" 117 | echo 118 | npx serverless invoke -l -f test --stage $random | grep "#LUMIGO#" 119 | 120 | echo 121 | echo "** Removing stack **" 122 | echo 123 | npx serverless remove --stage $random 124 | popd 125 | 126 | echo 127 | echo "********************" 128 | echo "** Testing Python **" 129 | echo "********************" 130 | echo 131 | echo 132 | pushd integration-test 133 | rm -rf venv || true 134 | virtualenv venv -p python3.10 135 | . venv/bin/activate 136 | popd 137 | 138 | echo 139 | echo "** Deploying **" 140 | echo 141 | pushd integration-test/python 142 | npm i 143 | npx serverless deploy --force --stage $random 144 | echo 145 | echo "** Testing #1 **" 146 | echo 147 | npx serverless invoke -l -f test --stage $random | grep "#LUMIGO#" 148 | echo 149 | echo "** Testing #2 **" 150 | echo 151 | npx serverless invoke -l -f test --stage $random | grep "#LUMIGO#" 152 | echo 153 | echo "** Removing stack **" 154 | echo 155 | npx serverless remove --stage $random 156 | popd 157 | 158 | echo 159 | echo "** Tests completed successfully. **" 160 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const _ = require("lodash"); 2 | const http = require("axios"); 3 | const fs = require("fs-extra"); 4 | const BbPromise = require("bluebird"); 5 | const childProcess = BbPromise.promisifyAll(require("child_process")); 6 | const path = require("path"); 7 | 8 | const nodeLayerVersionsUrl = 9 | "https://raw.githubusercontent.com/lumigo-io/lumigo-node/master/layers/LAYERS12x.md"; 10 | const pythonLayerVersionsUrl = 11 | "https://raw.githubusercontent.com/lumigo-io/python_tracer/master/layers/LAYERS37.md"; 12 | 13 | const NodePackageManagers = { 14 | NPM: "npm", 15 | Yarn: "yarn", 16 | PNPM: "pnpm" 17 | }; 18 | 19 | const LayerArns = { 20 | node: null, 21 | python: null 22 | }; 23 | 24 | class LumigoPlugin { 25 | constructor(serverless, options) { 26 | this.serverless = serverless; 27 | this.options = options; 28 | this.log = msg => this.serverless.cli.log(`serverless-lumigo: ${msg}`); 29 | this.verboseLog = msg => { 30 | if (process.env.SLS_DEBUG) { 31 | this.log(msg); 32 | } 33 | }; 34 | this.folderPath = path.join(this.serverless.config.servicePath, "_lumigo"); 35 | 36 | this.hooks = { 37 | "after:package:initialize": this.afterPackageInitialize.bind(this), 38 | "after:deploy:function:initialize": this.afterDeployFunctionInitialize.bind( 39 | this 40 | ), 41 | "after:package:createDeploymentArtifacts": this.afterCreateDeploymentArtifacts.bind( 42 | this 43 | ) 44 | }; 45 | this.extendServerlessSchema(); 46 | } 47 | 48 | extendServerlessSchema() { 49 | if ( 50 | this.serverless.configSchemaHandler && 51 | typeof this.serverless.configSchemaHandler.defineFunctionProperties === 52 | "function" 53 | ) { 54 | this.serverless.configSchemaHandler.defineFunctionProperties("aws", { 55 | type: "object", 56 | properties: { 57 | lumigo: { 58 | type: "object", 59 | properties: { 60 | token: { type: "string" }, 61 | enabled: { type: "boolean" }, 62 | pinVersion: { type: "string" }, 63 | skipInstallNodeTracer: { type: "boolean" }, 64 | skipReqCheck: { type: "boolean" }, 65 | step_function: { type: "boolean" }, 66 | useLayers: { type: "boolean" }, 67 | nodePackageManager: { type: "string" }, 68 | nodeLayerVersion: { type: "string" }, 69 | nodeUseESModule: { type: "boolean" }, 70 | nodeModuleFileExtension: { type: "string" }, 71 | pythonLayerVersion: { type: "string" } 72 | }, 73 | additionalProperties: false 74 | } 75 | } 76 | }); 77 | } 78 | } 79 | 80 | get nodeModuleFileExtension() { 81 | return _.get( 82 | this.serverless.service, 83 | "custom.lumigo.nodeModuleFileExtension", 84 | "js" 85 | ).toLowerCase(); 86 | } 87 | 88 | get nodeUseESModule() { 89 | return _.get(this.serverless.service, "custom.lumigo.nodeUseESModule", false); 90 | } 91 | 92 | get nodePackageManager() { 93 | return _.get( 94 | this.serverless.service, 95 | "custom.lumigo.nodePackageManager", 96 | NodePackageManagers.NPM 97 | ).toLowerCase(); 98 | } 99 | 100 | get useServerlessEsbuild() { 101 | const plugins = _.get(this.serverless.service, "plugins", []); 102 | const modulesPlugins = _.get(this.serverless.service, "plugins.modules", []); // backward compatible 103 | const isServerlessEsbuildInList = list => 104 | list.find(plugin => plugin === "serverless-esbuild"); 105 | return ( 106 | (Array.isArray(plugins) && isServerlessEsbuildInList(plugins)) || 107 | (Array.isArray(modulesPlugins) && isServerlessEsbuildInList(modulesPlugins)) 108 | ); 109 | } 110 | 111 | get useLayers() { 112 | return ( 113 | _.get(this.serverless.service, "custom.lumigo.useLayers", false) || 114 | this.useServerlessEsbuild 115 | ); 116 | } 117 | 118 | get pinnedNodeLayerVersion() { 119 | return _.get(this.serverless.service, "custom.lumigo.nodeLayerVersion", null); 120 | } 121 | 122 | get pinnedPythonLayerVersion() { 123 | return _.get(this.serverless.service, "custom.lumigo.pythonLayerVersion", null); 124 | } 125 | 126 | async afterDeployFunctionInitialize() { 127 | await this.wrapFunctions([this.options.function]); 128 | } 129 | 130 | async afterPackageInitialize() { 131 | await this.wrapFunctions(); 132 | } 133 | 134 | async getLatestNodeLayerVersionArn(layerArn) { 135 | const resp = await http.get(nodeLayerVersionsUrl); 136 | const pattern = `${layerArn}:\\d+`; 137 | const regex = new RegExp(pattern, "gm"); 138 | const matches = regex.exec(resp.data); 139 | return matches[0]; 140 | } 141 | 142 | async getLatestPythonLayerVersionArn(layerArn) { 143 | const resp = await http.get(pythonLayerVersionsUrl); 144 | const pattern = `${layerArn}:\\d+`; 145 | const regex = new RegExp(pattern, "gm"); 146 | const matches = regex.exec(resp.data); 147 | return matches[0]; 148 | } 149 | 150 | async getLayerArn(runtime) { 151 | const region = this.serverless.service.provider.region; 152 | if (runtime.startsWith("nodejs")) { 153 | if (this.pinnedNodeLayerVersion) { 154 | return `arn:aws:lambda:${region}:114300393969:layer:lumigo-node-tracer:${this.pinnedNodeLayerVersion}`; 155 | } else if (LayerArns.node) { 156 | return LayerArns.node; 157 | } else { 158 | const nodeLayerArn = `arn:aws:lambda:${region}:114300393969:layer:lumigo-node-tracer`; 159 | LayerArns.node = await this.getLatestNodeLayerVersionArn(nodeLayerArn); 160 | return LayerArns.node; 161 | } 162 | } else if (runtime.startsWith("python")) { 163 | if (this.pinnedPythonLayerVersion) { 164 | return `arn:aws:lambda:${region}:114300393969:layer:lumigo-python-tracer:${this.pinnedPythonLayerVersion}`; 165 | } else if (LayerArns.python) { 166 | return LayerArns.python; 167 | } else { 168 | const pythonLayerArn = `arn:aws:lambda:${region}:114300393969:layer:lumigo-python-tracer`; 169 | LayerArns.python = await this.getLatestPythonLayerVersionArn( 170 | pythonLayerArn 171 | ); 172 | return LayerArns.python; 173 | } 174 | } 175 | } 176 | 177 | async wrapFunctions(functionNames) { 178 | const { runtime, functions } = this.getFunctionsToWrap( 179 | this.serverless.service, 180 | functionNames 181 | ); 182 | 183 | this.log(`there are ${functions.length} function(s) to wrap...`); 184 | functions.forEach(fn => this.verboseLog(JSON.stringify(fn))); 185 | 186 | if (functions.length === 0) { 187 | return; 188 | } 189 | 190 | const token = _.get(this.serverless.service, "custom.lumigo.token"); 191 | if (!token) { 192 | throw new this.serverless.classes.Error( 193 | "serverless-lumigo: Unable to find token. Please follow https://github.com/lumigo-io/serverless-lumigo" 194 | ); 195 | } 196 | 197 | if (!this.useLayers) { 198 | const pinVersion = _.get(this.serverless.service, "custom.lumigo.pinVersion"); 199 | const skipInstallNodeTracer = _.get( 200 | this.serverless.service, 201 | "custom.lumigo.skipInstallNodeTracer", 202 | false 203 | ); 204 | let skipReqCheck = _.get( 205 | this.serverless.service, 206 | "custom.lumigo.skipReqCheck", 207 | false 208 | ); 209 | 210 | let parameters = _.get(this.serverless.service, "custom.lumigo", {}); 211 | parameters = _.omit(parameters, [ 212 | "pinVersion", 213 | "skipReqCheck", 214 | "skipInstallNodeTracer" 215 | ]); 216 | 217 | if (runtime === "nodejs") { 218 | if (!skipInstallNodeTracer) { 219 | await this.installLumigoNodejs(pinVersion); 220 | } 221 | 222 | for (const func of functions) { 223 | const handler = await this.createWrappedNodejsFunction( 224 | func, 225 | token, 226 | parameters 227 | ); 228 | // replace the function handler to the wrapped function 229 | this.verboseLog( 230 | `setting [${func.localName}]'s handler to [${handler}]...` 231 | ); 232 | this.serverless.service.functions[func.localName].handler = handler; 233 | } 234 | } else if (runtime === "python") { 235 | if (skipReqCheck !== true) { 236 | await this.ensureLumigoPythonIsInstalled(); 237 | } else { 238 | this.log("Skipping requirements.txt check"); 239 | } 240 | 241 | const { isZip } = await this.getPythonPluginConfiguration(); 242 | this.verboseLog(`Python plugin zip status ${isZip}`); 243 | for (const func of functions) { 244 | const handler = await this.createWrappedPythonFunction( 245 | func, 246 | token, 247 | parameters, 248 | isZip 249 | ); 250 | // replace the function handler to the wrapped function 251 | this.verboseLog( 252 | `setting [${func.localName}]'s handler to [${handler}]...` 253 | ); 254 | this.serverless.service.functions[func.localName].handler = handler; 255 | } 256 | } 257 | 258 | if (this.serverless.service.package) { 259 | const include = this.serverless.service.package.include || []; 260 | include.push("_lumigo/*"); 261 | this.serverless.service.package.include = include; 262 | } 263 | } 264 | } 265 | 266 | async afterCreateDeploymentArtifacts() { 267 | if (this.useLayers) { 268 | const token = _.get(this.serverless.service, "custom.lumigo.token"); 269 | const { runtime, functions } = this.getFunctionsToWrap( 270 | this.serverless.service 271 | ); 272 | 273 | for (const func of functions) { 274 | const funcRuntime = func.runtime || runtime; 275 | func.layers = func.layers || [ 276 | ...(this.serverless.service.provider.layers || []) 277 | ]; 278 | const layer = await this.getLayerArn(funcRuntime); 279 | func.layers.push(layer); 280 | func.environment = func.environment || {}; 281 | func.environment["LUMIGO_ORIGINAL_HANDLER"] = func.handler; 282 | func.environment["LUMIGO_TRACER_TOKEN"] = token; 283 | 284 | if (funcRuntime.startsWith("nodejs")) { 285 | func.handler = "lumigo-auto-instrument.handler"; 286 | } else if (funcRuntime.startsWith("python")) { 287 | func.handler = "/opt/python/lumigo_tracer._handler"; 288 | } 289 | 290 | // replace the function handler to the wrapped function 291 | this.verboseLog(`adding Lumigo tracer layer to [${func.localName}]...`); 292 | this.serverless.service.functions[func.localName].handler = func.handler; 293 | this.serverless.service.functions[func.localName].environment = 294 | func.environment; 295 | this.serverless.service.functions[func.localName].layers = func.layers; 296 | } 297 | return; 298 | } 299 | 300 | const { runtime, functions } = this.getFunctionsToWrap(this.serverless.service); 301 | 302 | if (functions.length === 0) { 303 | return; 304 | } 305 | 306 | await this.cleanFolder(); 307 | 308 | if (runtime === "nodejs") { 309 | const skipInstallNodeTracer = _.get( 310 | this.serverless.service, 311 | "custom.lumigo.skipInstallNodeTracer", 312 | false 313 | ); 314 | if (!skipInstallNodeTracer) { 315 | await this.uninstallLumigoNodejs(); 316 | } 317 | } 318 | } 319 | 320 | getFunctionsToWrap(service, functionNames) { 321 | functionNames = functionNames || this.serverless.service.getAllFunctions(); 322 | 323 | const functions = service 324 | .getAllFunctions() 325 | .filter(localName => functionNames.includes(localName)) 326 | .filter(localName => { 327 | const { lumigo = {} } = this.serverless.service.getFunction(localName); 328 | return lumigo.enabled == undefined || lumigo.enabled === true; 329 | }) 330 | .map(localName => { 331 | const x = _.cloneDeep(service.getFunction(localName)); 332 | x.localName = localName; 333 | return x; 334 | }); 335 | 336 | if (service.provider.runtime.startsWith("nodejs")) { 337 | return { runtime: "nodejs", functions }; 338 | } else if (service.provider.runtime.startsWith("python3")) { 339 | return { runtime: "python", functions }; 340 | } else { 341 | this.log(`unsupported runtime: [${service.provider.runtime}], skipped...`); 342 | return { runtime: "unsupported", functions: [] }; 343 | } 344 | } 345 | 346 | async installLumigoNodejs(pinVersion) { 347 | const finalVersion = pinVersion || "latest"; 348 | this.log(`installing @lumigo/tracer@${finalVersion}...`); 349 | let installCommand; 350 | if (this.nodePackageManager === NodePackageManagers.NPM) { 351 | installCommand = `npm install @lumigo/tracer@${finalVersion}`; 352 | } else if (this.nodePackageManager === NodePackageManagers.Yarn) { 353 | installCommand = `yarn add @lumigo/tracer@${finalVersion}`; 354 | } else if (this.nodePackageManager === NodePackageManagers.PNPM) { 355 | installCommand = `pnpm add @lumigo/tracer@${finalVersion}`; 356 | } else { 357 | throw new this.serverless.classes.Error( 358 | "No Node.js package manager found. Please install either NPM, PNPM or Yarn." 359 | ); 360 | } 361 | 362 | const installDetails = childProcess.execSync(installCommand, "utf8"); 363 | this.verboseLog(installDetails); 364 | } 365 | 366 | async uninstallLumigoNodejs() { 367 | this.log("uninstalling @lumigo/tracer..."); 368 | let uninstallCommand; 369 | if (this.nodePackageManager === NodePackageManagers.NPM) { 370 | uninstallCommand = "npm uninstall @lumigo/tracer"; 371 | } else if (this.nodePackageManager === NodePackageManagers.Yarn) { 372 | uninstallCommand = "yarn remove @lumigo/tracer"; 373 | } else if (this.nodePackageManager === NodePackageManagers.PNPM) { 374 | uninstallCommand = "pnpm remove @lumigo/tracer"; 375 | } else { 376 | throw new this.serverless.classes.Error( 377 | "No Node.js package manager found. Please install either NPM, PNPM or Yarn." 378 | ); 379 | } 380 | 381 | const uninstallDetails = childProcess.execSync(uninstallCommand, "utf8"); 382 | this.verboseLog(uninstallDetails); 383 | } 384 | 385 | async getPythonPluginConfiguration() { 386 | const isZip = _.get( 387 | this.serverless.service, 388 | "custom.pythonRequirements.zip", 389 | false 390 | ); 391 | 392 | return { isZip }; 393 | } 394 | 395 | async ensureLumigoPythonIsInstalled() { 396 | this.log("checking if lumigo_tracer is installed..."); 397 | 398 | const pluginsSection = _.get(this.serverless.service, "plugins", []); 399 | const plugins = Array.isArray(pluginsSection) 400 | ? pluginsSection 401 | : pluginsSection.modules; 402 | const slsPythonInstalled = plugins.includes("serverless-python-requirements"); 403 | 404 | const ensureTracerInstalled = async fileName => { 405 | const requirementsExists = fs.pathExistsSync(fileName); 406 | 407 | if (!requirementsExists) { 408 | let errorMessage = `${fileName} is not found.`; 409 | if (!slsPythonInstalled) { 410 | errorMessage += ` 411 | Consider using the serverless-python-requirements plugin to help you package Python dependencies.`; 412 | } 413 | throw new this.serverless.classes.Error(errorMessage); 414 | } 415 | 416 | const requirements = await fs.readFile(fileName, "utf8"); 417 | if ( 418 | !requirements.includes("lumigo_tracer") && 419 | !requirements.includes("lumigo-tracer") 420 | ) { 421 | const errorMessage = `lumigo_tracer is not installed. Please check ${fileName}.`; 422 | throw new this.serverless.classes.Error(errorMessage); 423 | } 424 | }; 425 | 426 | const packageIndividually = _.get( 427 | this.serverless.service, 428 | "package.individually", 429 | false 430 | ); 431 | 432 | if (packageIndividually) { 433 | this.log( 434 | "functions are packed individually, ensuring each function has a requirement.txt..." 435 | ); 436 | const { functions } = this.getFunctionsToWrap(this.serverless.service); 437 | 438 | for (const fn of functions) { 439 | // functions/hello.world.handler -> functions 440 | const dir = path.dirname(fn.handler); 441 | 442 | // there should be a requirements.txt in each function's folder 443 | // unless there's an override 444 | const defaultRequirementsFilename = path.join(dir, "requirements.txt"); 445 | const requirementsFilename = _.get( 446 | this.serverless.service, 447 | "custom.pythonRequirements.fileName", 448 | defaultRequirementsFilename 449 | ); 450 | await ensureTracerInstalled(requirementsFilename); 451 | } 452 | } else { 453 | this.log("ensuring there is a requirement.txt or equivalent..."); 454 | const requirementsFilename = _.get( 455 | this.serverless.service, 456 | "custom.pythonRequirements.fileName", 457 | "requirements.txt" 458 | ); 459 | await ensureTracerInstalled(requirementsFilename); 460 | } 461 | } 462 | 463 | getTracerParameters( 464 | token, 465 | options, 466 | equalityToken = ":", 467 | trueValue = "true", 468 | falseValue = "false" 469 | ) { 470 | if (token === undefined) { 471 | throw new this.serverless.classes.Error("Lumigo's tracer token is undefined"); 472 | } 473 | let configuration = []; 474 | options = _.omit(options, [ 475 | "nodePackageManager", 476 | "nodeUseESModule", 477 | "nodeModuleFileExtension" 478 | ]); 479 | for (const [key, value] of Object.entries(options)) { 480 | if (String(value).toLowerCase() === "true") { 481 | configuration.push(`${key}${equalityToken}${trueValue}`); 482 | } else if (String(value).toLowerCase() === "false") { 483 | configuration.push(`${key}${equalityToken}${falseValue}`); 484 | } else { 485 | configuration.push(`${key}${equalityToken}'${value}'`); 486 | } 487 | } 488 | return configuration.join(","); 489 | } 490 | 491 | getNodeTracerParameters(token, options) { 492 | return this.getTracerParameters(token, options, ":", "true", "false"); 493 | } 494 | 495 | getPythonTracerParameters(token, options) { 496 | return this.getTracerParameters(token, options, "=", "True", "False"); 497 | } 498 | 499 | async createWrappedNodejsFunction(func, token, options) { 500 | this.verboseLog(`wrapping [${func.handler}]...`); 501 | 502 | const localName = func.localName; 503 | 504 | // e.g. functions/hello.world.handler -> hello.world.handler 505 | const handler = path.basename(func.handler); 506 | 507 | // e.g. functions/hello.world.handler -> functions/hello.world 508 | const handlerModulePath = func.handler.substr(0, func.handler.lastIndexOf(".")); 509 | // e.g. functions/hello.world.handler -> handler 510 | const handlerFuncName = handler.substr(handler.lastIndexOf(".") + 1); 511 | 512 | // too shorten the file extension ref for prettier during test:all 513 | const fileExt = this.nodeModuleFileExtension; 514 | 515 | const wrappedESMFunction = ` 516 | import lumigo from '@lumigo/tracer' 517 | import {${handlerFuncName} as originalHandler} from '../${handlerModulePath}.${fileExt}' 518 | const tracer = lumigo({ ${this.getNodeTracerParameters(token, options)} }) 519 | 520 | export const ${handlerFuncName} = tracer.trace(originalHandler);`; 521 | 522 | const wrappedCJSFunction = ` 523 | const tracer = require("@lumigo/tracer")({ 524 | ${this.getNodeTracerParameters(token, options)} 525 | }); 526 | const handler = require('../${handlerModulePath}').${handlerFuncName}; 527 | 528 | module.exports.${handlerFuncName} = tracer.trace(handler);`; 529 | 530 | const wrappedFunction = this.nodeUseESModule 531 | ? wrappedESMFunction 532 | : wrappedCJSFunction; 533 | 534 | const fileName = localName + ".js"; 535 | // e.g. hello.world.js -> /Users/username/source/project/_lumigo/hello.world.js 536 | const filePath = path.join(this.folderPath, fileName); 537 | this.verboseLog(`writing wrapper function to [${filePath}]...`); 538 | await fs.outputFile(filePath, wrappedFunction); 539 | 540 | // convert from abs path to relative path, e.g. 541 | // /Users/username/source/project/_lumigo/hello.world.js -> _lumigo/hello.world.js 542 | // Make sure to support windows paths 543 | const newFilePath = path 544 | .relative(this.serverless.config.servicePath, filePath) 545 | .replace("\\", "/"); 546 | // e.g. _lumigo/hello.world.js -> _lumigo/hello.world.handler 547 | return newFilePath.substr(0, newFilePath.lastIndexOf(".") + 1) + handlerFuncName; 548 | } 549 | 550 | async createWrappedPythonFunction(func, token, options, isZip) { 551 | this.verboseLog(`wrapping [${func.handler}]...`); 552 | 553 | const localName = func.localName; 554 | 555 | // e.g. functions/hello.world.handler -> hello.world.handler 556 | const handler = path.basename(func.handler); 557 | 558 | // e.g. functions/hello.world.handler -> functions.hello.world 559 | const handlerModulePath = func.handler 560 | .substr(0, func.handler.lastIndexOf(".")) 561 | .split("/") // replace all occurances of "/"" 562 | .join("."); 563 | // e.g. functions/hello.world.handler -> handler 564 | const handlerFuncName = handler.substr(handler.lastIndexOf(".") + 1); 565 | let addZipConstruct = ""; 566 | if (isZip) { 567 | addZipConstruct = ` 568 | try: 569 | import unzip_requirements 570 | except ImportError: 571 | pass 572 | `; 573 | } 574 | const wrappedFunction = ` 575 | ${addZipConstruct} 576 | import importlib 577 | from lumigo_tracer import lumigo_tracer 578 | userHandler = getattr(importlib.import_module("${handlerModulePath}"), "${handlerFuncName}") 579 | 580 | @lumigo_tracer(${this.getPythonTracerParameters(token, options)}) 581 | def ${handlerFuncName}(event, context): 582 | return userHandler(event, context) 583 | `; 584 | 585 | const fileName = localName + ".py"; 586 | // e.g. hello.world.py -> /Users/username/source/project/_lumigo/hello.world.py 587 | const filePath = path.join(this.folderPath, fileName); 588 | this.verboseLog(`writing wrapper function to [${filePath}]...`); 589 | await fs.outputFile(filePath, wrappedFunction); 590 | 591 | // convert from abs path to relative path, e.g. 592 | // /Users/username/source/project/_lumigo/hello.world.py -> _lumigo/hello.world.py 593 | const newFilePath = path.relative(this.serverless.config.servicePath, filePath); 594 | // e.g. _lumigo/hello.world.py -> _lumigo/hello.world.handler 595 | return newFilePath.substr(0, newFilePath.lastIndexOf(".") + 1) + handlerFuncName; 596 | } 597 | 598 | async cleanFolder() { 599 | this.verboseLog(`removing the temporary folder [${this.folderPath}]...`); 600 | return fs.remove(this.folderPath); 601 | } 602 | } 603 | 604 | module.exports = LumigoPlugin; 605 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-extra"); 2 | const childProcess = require("child_process"); 3 | const Serverless = require("serverless/lib/serverless"); 4 | const AwsProvider = require("serverless/lib/plugins/aws/provider"); 5 | 6 | jest.mock("fs-extra"); 7 | jest.mock("child_process"); 8 | 9 | const token = "test-token"; 10 | const edgeHost = "edge-host"; 11 | 12 | expect.extend({ 13 | toContainAllStrings(received, ...strings) { 14 | const pass = strings.every(s => received.includes(s)); 15 | return { 16 | message: () => 17 | `expected ${received} contain the strings [${strings.join(",")}]`, 18 | pass 19 | }; 20 | } 21 | }); 22 | 23 | let serverless; 24 | let lumigo; 25 | let options; 26 | 27 | const log = jest.fn(); 28 | 29 | beforeEach(() => { 30 | options = {}; 31 | serverless = new Serverless({ commands: [], options }); 32 | serverless.servicePath = true; 33 | serverless.service.service = "lumigo-test"; 34 | serverless.service.provider.compiledCloudFormationTemplate = { Resources: {} }; 35 | serverless.setProvider("aws", new AwsProvider(serverless)); 36 | serverless.cli = { log: log }; 37 | serverless.service.provider.region = "us-east-1"; 38 | serverless.service.functions = { 39 | hello: { 40 | handler: "hello.world", 41 | events: [] 42 | }, 43 | "hello.world": { 44 | handler: "hello.world.handler", // . in the filename 45 | events: [] 46 | }, 47 | foo: { 48 | handler: "foo_bar.handler", // both pointing to same handler 49 | events: [] 50 | }, 51 | bar: { 52 | handler: "foo_bar.handler", // both pointing to same handler 53 | events: [] 54 | }, 55 | jet: { 56 | handler: "foo/foo/bar.handler", // / in the path 57 | events: [] 58 | }, 59 | pack: { 60 | handler: "foo.bar/zoo.handler", // . in file name and / in the path 61 | events: [] 62 | }, 63 | skippy: { 64 | handler: "will.skip", 65 | lumigo: { 66 | enabled: false 67 | } 68 | } 69 | }; 70 | serverless.service.custom = { 71 | lumigo: { 72 | token: token 73 | } 74 | }; 75 | serverless.config.servicePath = __dirname; 76 | childProcess.execSync.mockImplementation(() => ""); 77 | const LumigoPlugin = require("./index"); 78 | lumigo = new LumigoPlugin(serverless, options); 79 | delete process.env.SLS_DEBUG; 80 | }); 81 | 82 | afterEach(() => jest.resetAllMocks()); 83 | 84 | describe("Invalid plugin configuration", () => { 85 | beforeEach(() => { 86 | serverless.service.provider.runtime = "nodejs8.10"; 87 | }); 88 | 89 | test("Token is not present, exception is thrown", async () => { 90 | delete serverless.service.custom.lumigo["token"]; 91 | // https://github.com/facebook/jest/issues/1700 92 | let error; 93 | try { 94 | await lumigo.afterPackageInitialize(); 95 | } catch (e) { 96 | error = e; 97 | } 98 | expect(error).toBeTruthy(); 99 | }); 100 | }); 101 | 102 | describe("Old serverless compatibility", () => { 103 | test("Schema validation", async () => { 104 | // This is the case in serverless version 1.83.3 105 | serverless.configSchemaHandler = {}; 106 | expect(lumigo.extendServerlessSchema()).resolves; 107 | }); 108 | }); 109 | 110 | describe("Lumigo plugin (node.js)", () => { 111 | const runtimes = [["nodejs14.x"], ["nodejs12.x"], ["nodejs10.x"]]; 112 | describe.each(runtimes)("when using runtime %s", runtime => { 113 | beforeEach(() => { 114 | serverless.service.provider.runtime = runtime; 115 | }); 116 | 117 | test("edgeHost configuration present, should appear in the wrapped code", async () => { 118 | serverless.service.custom.lumigo["edgeHost"] = edgeHost; 119 | await lumigo.afterPackageInitialize(); 120 | 121 | expect(fs.outputFile).toBeCalledWith( 122 | __dirname + "/_lumigo/hello.js", 123 | expect.toContainAllStrings(`edgeHost:'${edgeHost}'`) 124 | ); 125 | }); 126 | 127 | test("edgeHost configuration not present, should not appear in the wrapped code", async () => { 128 | await lumigo.afterPackageInitialize(); 129 | expect(fs.outputFile).toBeCalledWith( 130 | __dirname + "/_lumigo/hello.js", 131 | expect.not.toContainAllStrings(`edgeHost:'${edgeHost}'`) 132 | ); 133 | }); 134 | 135 | if (runtime === "nodejs14.x") { 136 | describe("when nodeUseESModule is true", () => { 137 | beforeEach(() => { 138 | serverless.service.custom.lumigo.nodeUseESModule = true; 139 | }); 140 | 141 | test("it should wrap all non-skipped functions after package initialize ES style", async () => { 142 | await lumigo.afterPackageInitialize(); 143 | assertNodejsFunctionsAreWrappedES(); 144 | }); 145 | }); 146 | 147 | describe("when nodeModuleFileExtension is mjs", () => { 148 | beforeEach(async () => { 149 | serverless.service.custom.lumigo.nodeUseESModule = true; 150 | serverless.service.custom.lumigo.nodeModuleFileExtension = "mjs"; 151 | options.function = "hello"; 152 | await lumigo.afterDeployFunctionInitialize(); 153 | }); 154 | 155 | test("should add mjs as file extension", async () => { 156 | assertFileOutputES({ 157 | filename: "hello.js", 158 | importStatement: 159 | "import {world as originalHandler} from '../hello.mjs'", 160 | exportStatement: 161 | "export const world = tracer.trace(originalHandler);" 162 | }); 163 | expect(serverless.service.functions.hello.handler).toBe( 164 | "_lumigo/hello.world" 165 | ); 166 | }); 167 | }); 168 | } 169 | 170 | describe("when nodeUseESModule is false", () => { 171 | beforeEach(() => { 172 | serverless.service.custom.lumigo.nodeUseESModule = false; 173 | }); 174 | 175 | test("it should wrap all non-skipped functions after package initialize CJS style", async () => { 176 | await lumigo.afterPackageInitialize(); 177 | assertNodejsFunctionsAreWrappedCJS(); 178 | }); 179 | }); 180 | 181 | describe("when nodeUseESModule is not set", () => { 182 | test("it should wrap all non-skipped functions after package initialize CJS style", async () => { 183 | await lumigo.afterPackageInitialize(); 184 | assertNodejsFunctionsAreWrappedCJS(); 185 | }); 186 | }); 187 | 188 | test("it should clean up after deployment artifact is created", async () => { 189 | await lumigo.afterCreateDeploymentArtifacts(); 190 | assertNodejsFunctionsAreCleanedUp(); 191 | }); 192 | 193 | describe("there are no functions", () => { 194 | beforeEach(() => { 195 | serverless.service.functions = {}; 196 | }); 197 | 198 | test("it shouldn't wrap any function after package initialize", async () => { 199 | await lumigo.afterPackageInitialize(); 200 | assertFunctionsAreNotWrapped(); 201 | }); 202 | 203 | test("it does nothing after deployment artifact is created", async () => { 204 | await lumigo.afterCreateDeploymentArtifacts(); 205 | assertNothingHappens(); 206 | }); 207 | }); 208 | 209 | describe("when functions are packaged individually", () => { 210 | beforeEach(() => { 211 | serverless.service.package = { 212 | individually: true 213 | }; 214 | }); 215 | 216 | test("if package.include is not set, it's initialized with _lumigo/*", async () => { 217 | await lumigo.afterPackageInitialize(); 218 | assertLumigoIsIncluded(); 219 | }); 220 | 221 | test("if package.include is set, it adds _lumigo/* to the array", async () => { 222 | Object.values(serverless.service.functions).forEach(fun => { 223 | fun.package = { 224 | include: ["node_modules/**/*"] 225 | }; 226 | }); 227 | 228 | await lumigo.afterPackageInitialize(); 229 | assertLumigoIsIncluded(); 230 | }); 231 | }); 232 | 233 | describe("if verbose logging is enabled", () => { 234 | beforeEach(() => { 235 | process.env.SLS_DEBUG = "*"; 236 | }); 237 | 238 | test("it should publish debug messages", async () => { 239 | await lumigo.afterPackageInitialize(); 240 | 241 | const logs = log.mock.calls.map(x => x[0]); 242 | expect(logs).toContain( 243 | "serverless-lumigo: setting [hello]'s handler to [_lumigo/hello.world]..." 244 | ); 245 | }); 246 | }); 247 | 248 | describe("when nodePackageManager is Yarn", () => { 249 | beforeEach(() => { 250 | serverless.service.custom.lumigo.nodePackageManager = "yarn"; 251 | }); 252 | 253 | test("it should install with Yarn", async () => { 254 | await lumigo.afterPackageInitialize(); 255 | 256 | expect(childProcess.execSync).toBeCalledWith( 257 | "yarn add @lumigo/tracer@latest", 258 | "utf8" 259 | ); 260 | }); 261 | 262 | test("it should uninstall with Yarn", async () => { 263 | await lumigo.afterCreateDeploymentArtifacts(); 264 | 265 | expect(childProcess.execSync).toBeCalledWith( 266 | "yarn remove @lumigo/tracer", 267 | "utf8" 268 | ); 269 | }); 270 | 271 | test("Pin version", async () => { 272 | serverless.service.custom.lumigo.pinVersion = "1.0.3"; 273 | await lumigo.afterPackageInitialize(); 274 | 275 | expect(childProcess.execSync).toBeCalledWith( 276 | "yarn add @lumigo/tracer@1.0.3", 277 | "utf8" 278 | ); 279 | }); 280 | }); 281 | 282 | describe("when nodePackageManager is PNPM", () => { 283 | beforeEach(() => { 284 | serverless.service.custom.lumigo.nodePackageManager = "pnpm"; 285 | }); 286 | 287 | test("it should install with PNPM", async () => { 288 | await lumigo.afterPackageInitialize(); 289 | 290 | expect(childProcess.execSync).toBeCalledWith( 291 | "pnpm add @lumigo/tracer@latest", 292 | "utf8" 293 | ); 294 | }); 295 | 296 | test("it should uninstall with PNPM", async () => { 297 | await lumigo.afterCreateDeploymentArtifacts(); 298 | 299 | expect(childProcess.execSync).toBeCalledWith( 300 | "pnpm remove @lumigo/tracer", 301 | "utf8" 302 | ); 303 | }); 304 | 305 | test("Pin version", async () => { 306 | serverless.service.custom.lumigo.pinVersion = "1.0.3"; 307 | await lumigo.afterPackageInitialize(); 308 | 309 | expect(childProcess.execSync).toBeCalledWith( 310 | "pnpm add @lumigo/tracer@1.0.3", 311 | "utf8" 312 | ); 313 | }); 314 | }); 315 | 316 | describe("when nodePackageManager is NPM", () => { 317 | beforeEach(() => { 318 | serverless.service.custom.lumigo.nodePackageManager = "npm"; 319 | }); 320 | 321 | test("Pin version", async () => { 322 | serverless.service.custom.lumigo.pinVersion = "1.0.3"; 323 | await lumigo.afterPackageInitialize(); 324 | 325 | expect(childProcess.execSync).toBeCalledWith( 326 | "npm install @lumigo/tracer@1.0.3", 327 | "utf8" 328 | ); 329 | }); 330 | }); 331 | 332 | describe("when nodePackageManager is not NPM or Yarn", () => { 333 | beforeEach(() => { 334 | serverless.service.custom.lumigo.nodePackageManager = "whatever"; 335 | }); 336 | 337 | test("it should error on install", async () => { 338 | await expect(lumigo.afterPackageInitialize()).rejects.toThrow( 339 | "No Node.js package manager found. Please install either NPM, PNPM or Yarn" 340 | ); 341 | }); 342 | 343 | test("it should error on uninstall", async () => { 344 | await expect(lumigo.afterCreateDeploymentArtifacts()).rejects.toThrow( 345 | "No Node.js package manager found. Please install either NPM, PNPM or Yarn" 346 | ); 347 | }); 348 | }); 349 | 350 | describe("when deploying a single function using 'sls deploy -f'", () => { 351 | beforeEach(async () => { 352 | options.function = "hello"; 353 | await lumigo.afterDeployFunctionInitialize(); 354 | }); 355 | 356 | it("should only wrap one function", () => { 357 | expect(fs.outputFile).toBeCalledTimes(1); 358 | assertFileOutputCJS({ 359 | filename: "hello.js", 360 | requireHandler: "require('../hello').world" 361 | }); 362 | expect(serverless.service.functions.hello.handler).toBe( 363 | "_lumigo/hello.world" 364 | ); 365 | }); 366 | }); 367 | 368 | describe("when skipInstallNodeTracer is true", () => { 369 | beforeEach(() => { 370 | serverless.service.custom.lumigo.skipInstallNodeTracer = true; 371 | }); 372 | 373 | test("it should not install Node tracer", async () => { 374 | await lumigo.afterPackageInitialize(); 375 | 376 | expect(childProcess.execSync).not.toBeCalledWith( 377 | "npm install @lumigo/tracer@latest", 378 | "utf8" 379 | ); 380 | }); 381 | 382 | test("it should not uninstall Node tracer", async () => { 383 | await lumigo.afterCreateDeploymentArtifacts(); 384 | 385 | expect(childProcess.execSync).not.toBeCalledWith( 386 | "npm uninstall @lumigo/tracer", 387 | "utf8" 388 | ); 389 | }); 390 | }); 391 | 392 | describe("when using esbuild plugin", () => { 393 | beforeEach(() => { 394 | serverless.service.plugins = ["serverless-esbuild"]; 395 | }); 396 | 397 | test("layers are added during after:package:initialize", async () => { 398 | await lumigo.afterCreateDeploymentArtifacts(); 399 | 400 | assertNodejsFunctionsHaveLayers(); 401 | }); 402 | 403 | test("custom layers configured at provider level are retained", async () => { 404 | serverless.service.provider.layers = ["custom-layer"]; 405 | 406 | await lumigo.afterCreateDeploymentArtifacts(); 407 | 408 | console.log(serverless.service.functions); 409 | const wrappedFunction = serverless.service.functions.hello; 410 | expect(wrappedFunction.layers).not.toBeUndefined(); 411 | expect(wrappedFunction.layers).toHaveLength(2); 412 | expect(wrappedFunction.layers[0]).toEqual("custom-layer"); 413 | expect(wrappedFunction.layers[1]).toEqual( 414 | expect.stringMatching( 415 | /arn:aws:lambda:us-east-1:114300393969:layer:lumigo-node-tracer:\d+/ 416 | ) 417 | ); 418 | }); 419 | }); 420 | 421 | describe("when useLayers is true", () => { 422 | beforeEach(() => { 423 | serverless.service.custom.lumigo.useLayers = true; 424 | }); 425 | 426 | test("functions are not wrapped during after:package:initialize", async () => { 427 | await lumigo.afterPackageInitialize(); 428 | 429 | expect(childProcess.execSync).not.toBeCalledWith( 430 | "npm install @lumigo/tracer", 431 | "utf8" 432 | ); 433 | }); 434 | 435 | test("functions are not wrapped during after:deploy:function:initialize", async () => { 436 | await lumigo.afterDeployFunctionInitialize(); 437 | 438 | expect(childProcess.execSync).not.toBeCalledWith( 439 | "npm install @lumigo/tracer", 440 | "utf8" 441 | ); 442 | }); 443 | 444 | test("layers are added during after:package:initialize", async () => { 445 | await lumigo.afterCreateDeploymentArtifacts(); 446 | 447 | assertNodejsFunctionsHaveLayers(); 448 | }); 449 | 450 | test("layers are added during after:package:createDeploymentArtifacts", async () => { 451 | options.function = "hello"; 452 | await lumigo.afterCreateDeploymentArtifacts(); 453 | 454 | const functions = serverless.service.functions; 455 | expect(functions.hello.handler).toBe("lumigo-auto-instrument.handler"); 456 | expect(functions.hello.layers).toHaveLength(1); 457 | expect(functions.hello.layers[0]).toEqual( 458 | expect.stringMatching( 459 | /arn:aws:lambda:us-east-1:114300393969:layer:lumigo-node-tracer:\d*/ 460 | ) 461 | ); 462 | expect(functions.hello.environment).toHaveProperty( 463 | "LUMIGO_ORIGINAL_HANDLER" 464 | ); 465 | }); 466 | 467 | describe("if pinned to version 87 of layer", () => { 468 | beforeEach(() => { 469 | serverless.service.custom.lumigo.nodeLayerVersion = 87; 470 | }); 471 | 472 | test("layer version 87 are added during after:package:createDeploymentArtifacts", async () => { 473 | await lumigo.afterPackageInitialize(); 474 | await lumigo.afterCreateDeploymentArtifacts(); 475 | 476 | assertNodejsFunctionsHaveLayers(87); 477 | }); 478 | 479 | test("layers are added during after:package:createDeploymentArtifacts", async () => { 480 | options.function = "hello"; 481 | await lumigo.afterCreateDeploymentArtifacts(); 482 | 483 | const functions = serverless.service.functions; 484 | expect(functions.hello.handler).toBe( 485 | "lumigo-auto-instrument.handler" 486 | ); 487 | expect(functions.hello.layers).toHaveLength(1); 488 | expect(functions.hello.layers[0]).toEqual( 489 | "arn:aws:lambda:us-east-1:114300393969:layer:lumigo-node-tracer:87" 490 | ); 491 | expect(functions.hello.environment).toHaveProperty( 492 | "LUMIGO_ORIGINAL_HANDLER" 493 | ); 494 | }); 495 | }); 496 | }); 497 | }); 498 | }); 499 | 500 | describe("Lumigo plugin (python)", () => { 501 | describe("python2.7", () => { 502 | beforeEach(() => { 503 | serverless.service.provider.runtime = "python2.7"; 504 | }); 505 | 506 | test("it shouldn't wrap any function after package initialize", async () => { 507 | await lumigo.afterPackageInitialize(); 508 | assertFunctionsAreNotWrapped(); 509 | }); 510 | 511 | test("it does nothing after deployment artifact is created", async () => { 512 | await lumigo.afterCreateDeploymentArtifacts(); 513 | assertNothingHappens(); 514 | }); 515 | }); 516 | 517 | describe("python3.10", () => { 518 | beforeEach(() => { 519 | serverless.service.provider.runtime = "python3.10"; 520 | }); 521 | 522 | describe("when useLayers is true", () => { 523 | beforeEach(() => { 524 | serverless.service.custom.lumigo.useLayers = true; 525 | }); 526 | 527 | afterEach(() => { 528 | delete serverless.service.custom.lumigo.useLayers; 529 | }); 530 | 531 | test("layers are added during after:package:createDeploymentArtifacts", async () => { 532 | await lumigo.afterCreateDeploymentArtifacts(); 533 | 534 | assertPythonFunctionsHaveLayers(); 535 | }); 536 | 537 | test("layers are added during after:package:createDeploymentArtifacts", async () => { 538 | options.function = "hello"; 539 | await lumigo.afterCreateDeploymentArtifacts(); 540 | 541 | const functions = serverless.service.functions; 542 | expect(functions.hello.handler).toBe( 543 | "/opt/python/lumigo_tracer._handler" 544 | ); 545 | expect(functions.hello.layers).toHaveLength(1); 546 | expect(functions.hello.layers[0]).toEqual( 547 | expect.stringMatching( 548 | /arn:aws:lambda:us-east-1:114300393969:layer:lumigo-python-tracer:\d+/ 549 | ) 550 | ); 551 | expect(functions.hello.environment).toHaveProperty( 552 | "LUMIGO_ORIGINAL_HANDLER" 553 | ); 554 | }); 555 | 556 | describe("if pinned to version 87 of layer", () => { 557 | beforeEach(() => { 558 | serverless.service.custom.lumigo.pythonLayerVersion = 87; 559 | }); 560 | 561 | test("layer version 87 are added during after:package:createDeploymentArtifacts", async () => { 562 | await lumigo.afterCreateDeploymentArtifacts(); 563 | 564 | assertPythonFunctionsHaveLayers(87); 565 | }); 566 | 567 | test("layers are added during after:package:createDeploymentArtifacts", async () => { 568 | options.function = "hello"; 569 | await lumigo.afterCreateDeploymentArtifacts(); 570 | 571 | const functions = serverless.service.functions; 572 | expect(functions.hello.handler).toBe( 573 | "/opt/python/lumigo_tracer._handler" 574 | ); 575 | expect(functions.hello.layers).toHaveLength(1); 576 | expect(functions.hello.layers[0]).toEqual( 577 | "arn:aws:lambda:us-east-1:114300393969:layer:lumigo-python-tracer:87" 578 | ); 579 | expect(functions.hello.environment).toHaveProperty( 580 | "LUMIGO_ORIGINAL_HANDLER" 581 | ); 582 | }); 583 | }); 584 | }); 585 | 586 | describe("Using zip configuration", () => { 587 | beforeEach(() => { 588 | serverless.service.functions = { 589 | hello: { 590 | handler: "hello.world", 591 | events: [] 592 | } 593 | }; 594 | serverless.service.custom.pythonRequirements = { zip: true }; 595 | fs.pathExistsSync.mockReturnValue(true); 596 | fs.readFile.mockReturnValue(` 597 | --index-url https://1wmWND-GD5RPAwKgsdvb6DphXCj0vPLs@pypi.fury.io/lumigo/ 598 | --extra-index-url https://pypi.org/simple/ 599 | lumigo_tracer`); 600 | }); 601 | 602 | test("When zip is set then add special construct", async () => { 603 | await lumigo.afterPackageInitialize(); 604 | expect(fs.outputFile).toBeCalledWith( 605 | __dirname + "/_lumigo/hello.py", 606 | expect.toContainAllStrings( 607 | "try:", 608 | "import unzip_requirements", 609 | "except ImportError:", 610 | "pass", 611 | "from lumigo_tracer import lumigo_tracer", 612 | getPythonImportLine("hello", "world") 613 | ) 614 | ); 615 | }); 616 | }); 617 | 618 | describe("there are no functions", () => { 619 | beforeEach(() => { 620 | serverless.service.functions = {}; 621 | }); 622 | 623 | test("it shouldn't wrap any function after package initialize", async () => { 624 | await lumigo.afterPackageInitialize(); 625 | assertFunctionsAreNotWrapped(); 626 | }); 627 | 628 | test("it does nothing after deployment artifact is created", async () => { 629 | await lumigo.afterCreateDeploymentArtifacts(); 630 | assertNothingHappens(); 631 | }); 632 | }); 633 | 634 | describe("given the requirement.txt file exists", () => { 635 | beforeEach(() => { 636 | serverless.service.plugins = ["a-module"]; 637 | fs.pathExistsSync.mockReturnValue(true); 638 | fs.readFile.mockReturnValue(` 639 | --index-url https://1wmWND-GD5RPAwKgsdvb6DphXCj0vPLs@pypi.fury.io/lumigo/ 640 | --extra-index-url https://pypi.org/simple/ 641 | lumigo_tracer`); 642 | }); 643 | 644 | test("it should wrap all functions after package initialize", async () => { 645 | await lumigo.afterPackageInitialize(); 646 | assertPythonFunctionsAreWrapped({ token: `'${token}'` }); 647 | }); 648 | 649 | test("it should wrap all functions after package initialize with modules in plugins", async () => { 650 | serverless.service.plugins = { modules: ["a-module"] }; 651 | await lumigo.afterPackageInitialize(); 652 | assertPythonFunctionsAreWrapped({ token: `'${token}'` }); 653 | }); 654 | 655 | test("enhance_print configuration present with true, should appear in the wrapped code", async () => { 656 | serverless.service.custom.lumigo["enhance_print"] = true; 657 | await lumigo.afterPackageInitialize(); 658 | 659 | assertPythonFunctionsAreWrapped({ 660 | token: `'${token}'`, 661 | enhance_print: "True" 662 | }); 663 | }); 664 | 665 | test("enhance_print configuration present with false, should appear in the wrapped code", async () => { 666 | serverless.service.custom.lumigo["enhance_print"] = "false"; 667 | await lumigo.afterPackageInitialize(); 668 | 669 | assertPythonFunctionsAreWrapped({ 670 | token: `'${token}'`, 671 | enhance_print: "False" 672 | }); 673 | }); 674 | }); 675 | 676 | test("it should clean up after deployment artifact is created", async () => { 677 | await lumigo.afterCreateDeploymentArtifacts(); 678 | assertPythonFunctionsAreCleanedUp({ token: `'${token}'` }); 679 | }); 680 | 681 | describe("if there is override file name for requirements.txt (for the serverless-python-requirements plugin)", () => { 682 | beforeEach(() => { 683 | serverless.service.custom.pythonRequirements = { 684 | fileName: "requirements-dev.txt" 685 | }; 686 | }); 687 | 688 | test("it should check the requirements for the override file", async () => { 689 | fs.pathExistsSync.mockReturnValue(true); 690 | fs.readFile.mockReturnValue(` 691 | --index-url https://1wmWND-GD5RPAwKgsdvb6DphXCj0vPLs@pypi.fury.io/lumigo/ 692 | --extra-index-url https://pypi.org/simple/ 693 | lumigo_tracer`); 694 | 695 | await lumigo.afterPackageInitialize(); 696 | assertPythonFunctionsAreWrapped({ token: `'${token}'` }); 697 | expect(fs.pathExistsSync).toBeCalledWith("requirements-dev.txt"); 698 | expect(fs.readFile).toBeCalledWith("requirements-dev.txt", "utf8"); 699 | }); 700 | }); 701 | 702 | describe("if the requirements.txt is missing", () => { 703 | beforeEach(() => { 704 | fs.pathExistsSync.mockReturnValue(false); 705 | }); 706 | 707 | test("it should error", async () => { 708 | await expect(lumigo.afterPackageInitialize()).rejects.toThrow(); 709 | expect(fs.pathExistsSync).toBeCalledWith("requirements.txt"); 710 | }); 711 | }); 712 | 713 | describe("if the requirements.txt does not have lumigo_tracer", () => { 714 | beforeEach(() => { 715 | fs.pathExistsSync.mockReturnValue(true); 716 | fs.readFile.mockReturnValue("some_other_package"); 717 | }); 718 | 719 | test("it should error", async () => { 720 | await expect(lumigo.afterPackageInitialize()).rejects.toThrow(); 721 | expect(fs.pathExistsSync).toBeCalledWith("requirements.txt"); 722 | expect(fs.readFile).toBeCalledWith("requirements.txt", "utf8"); 723 | }); 724 | }); 725 | 726 | describe("if functions are packed individually", () => { 727 | beforeEach(() => { 728 | serverless.service.package = { 729 | individually: true 730 | }; 731 | serverless.service.functions = { 732 | hello: { 733 | handler: "functions/hello/hello.world", 734 | events: [] 735 | }, 736 | world: { 737 | handler: "functions/world/world.handler", 738 | events: [] 739 | } 740 | }; 741 | 742 | fs.pathExistsSync.mockReturnValue(true); 743 | fs.readFile.mockReturnValue(` 744 | --index-url https://1wmWND-GD5RPAwKgsdvb6DphXCj0vPLs@pypi.fury.io/lumigo/ 745 | --extra-index-url https://pypi.org/simple/ 746 | lumigo_tracer`); 747 | }); 748 | 749 | test("it should check the requirements.txt in each function's folder", async () => { 750 | await lumigo.afterPackageInitialize(); 751 | expect(fs.pathExistsSync).toBeCalledTimes(2); 752 | expect(fs.pathExistsSync).toBeCalledWith( 753 | "functions/hello/requirements.txt" 754 | ); 755 | expect(fs.pathExistsSync).toBeCalledWith( 756 | "functions/world/requirements.txt" 757 | ); 758 | expect(fs.readFile).toBeCalledTimes(2); 759 | expect(fs.readFile).toBeCalledWith( 760 | "functions/hello/requirements.txt", 761 | "utf8" 762 | ); 763 | expect(fs.readFile).toBeCalledWith( 764 | "functions/world/requirements.txt", 765 | "utf8" 766 | ); 767 | }); 768 | 769 | describe("if there is override file name for requirements.txt", () => { 770 | beforeEach(() => { 771 | serverless.service.custom.pythonRequirements = { 772 | fileName: "requirements-dev.txt" 773 | }; 774 | }); 775 | 776 | test("it should check the requirements for the override file", async () => { 777 | await lumigo.afterPackageInitialize(); 778 | 779 | expect(fs.pathExistsSync).toBeCalledTimes(2); 780 | expect(fs.pathExistsSync).toBeCalledWith("requirements-dev.txt"); 781 | expect(fs.readFile).toBeCalledWith("requirements-dev.txt", "utf8"); 782 | 783 | expect(fs.pathExistsSync).not.toBeCalledWith( 784 | "functions/hello/requirements.txt" 785 | ); 786 | expect(fs.pathExistsSync).not.toBeCalledWith( 787 | "functions/world/requirements.txt" 788 | ); 789 | 790 | expect(fs.readFile).not.toBeCalledWith( 791 | "functions/hello/requirements.txt", 792 | "utf8" 793 | ); 794 | expect(fs.readFile).not.toBeCalledWith( 795 | "functions/world/requirements.txt", 796 | "utf8" 797 | ); 798 | }); 799 | }); 800 | 801 | test("if package.include is not set, it's initialized with _lumigo/*", async () => { 802 | await lumigo.afterPackageInitialize(); 803 | assertLumigoIsIncluded(); 804 | }); 805 | 806 | test("if package.include is set, it adds _lumigo/* to the array", async () => { 807 | Object.values(serverless.service.functions).forEach(fun => { 808 | fun.package = { 809 | include: ["functions/**/*"] 810 | }; 811 | }); 812 | 813 | await lumigo.afterPackageInitialize(); 814 | assertLumigoIsIncluded(); 815 | }); 816 | }); 817 | 818 | describe("if verbose logging is enabled", () => { 819 | beforeEach(() => { 820 | process.env.SLS_DEBUG = "*"; 821 | }); 822 | 823 | test("it should publish debug messages", async () => { 824 | fs.pathExistsSync.mockReturnValue(true); 825 | fs.readFile.mockReturnValue(` 826 | --index-url https://1wmWND-GD5RPAwKgsdvb6DphXCj0vPLs@pypi.fury.io/lumigo/ 827 | --extra-index-url https://pypi.org/simple/ 828 | lumigo_tracer`); 829 | 830 | await lumigo.afterPackageInitialize(); 831 | 832 | const logs = log.mock.calls.map(x => x[0]); 833 | expect(logs).toContain( 834 | "serverless-lumigo: setting [hello]'s handler to [_lumigo/hello.world]..." 835 | ); 836 | }); 837 | }); 838 | }); 839 | }); 840 | 841 | describe("is not nodejs or python", () => { 842 | beforeEach(() => { 843 | serverless.service.provider.runtime = "java8"; 844 | }); 845 | 846 | test("it shouldn't wrap any function after package initialize", async () => { 847 | await lumigo.afterPackageInitialize(); 848 | assertFunctionsAreNotWrapped(); 849 | }); 850 | 851 | test("it does nothing after deployment artifact is created", async () => { 852 | await lumigo.afterCreateDeploymentArtifacts(); 853 | assertNothingHappens(); 854 | }); 855 | }); 856 | 857 | function assertFileOutputCJS({ filename, requireHandler }) { 858 | expect(fs.outputFile).toBeCalledWith( 859 | `${__dirname}/_lumigo/${filename}`, 860 | expect.toContainAllStrings( 861 | 'const tracer = require("@lumigo/tracer")', 862 | `const handler = ${requireHandler}`, 863 | `token:'${token}'` 864 | ) 865 | ); 866 | } 867 | 868 | function assertFileOutputES({ filename, importStatement, exportStatement }) { 869 | expect(fs.outputFile).toBeCalledWith( 870 | `${__dirname}/_lumigo/${filename}`, 871 | expect.toContainAllStrings( 872 | "import lumigo from '@lumigo/tracer'", 873 | importStatement, 874 | exportStatement, 875 | `token:'${token}'` 876 | ) 877 | ); 878 | } 879 | 880 | function assertTracerInstall() { 881 | expect(childProcess.execSync).toBeCalledWith( 882 | "npm install @lumigo/tracer@latest", 883 | "utf8" 884 | ); 885 | } 886 | 887 | function assertNodejsFunctionsHaveLayers(version) { 888 | const functions = serverless.service.functions; 889 | const wrappedFunctions = [ 890 | functions.hello, 891 | functions["hello.world"], 892 | functions.foo, 893 | functions.bar, 894 | functions.jet, 895 | functions.pack 896 | ]; 897 | const skippedFunctions = [functions.skippy]; 898 | 899 | wrappedFunctions.forEach(func => { 900 | expect(func.handler).toBe("lumigo-auto-instrument.handler"); 901 | expect(func.layers).toHaveLength(1); 902 | if (version) { 903 | expect(func.layers[0]).toEqual( 904 | `arn:aws:lambda:us-east-1:114300393969:layer:lumigo-node-tracer:${version}` 905 | ); 906 | } else { 907 | expect(func.layers[0]).toEqual( 908 | expect.stringMatching( 909 | /arn:aws:lambda:us-east-1:114300393969:layer:lumigo-node-tracer:\d+/ 910 | ) 911 | ); 912 | } 913 | expect(func.environment).toHaveProperty("LUMIGO_ORIGINAL_HANDLER"); 914 | }); 915 | 916 | skippedFunctions.forEach(func => { 917 | expect(func.handler).not.toBe("lumigo-auto-instrument.handler"); 918 | expect(func.layers).toBeUndefined(); 919 | expect(func.environment).toBeUndefined(); 920 | }); 921 | } 922 | 923 | function assertNodejsFunctionsAreWrappedCJS() { 924 | assertTracerInstall(); 925 | 926 | expect(fs.outputFile).toBeCalledTimes(6); 927 | [ 928 | { filename: "hello.js", requireHandler: "require('../hello').world" }, 929 | { 930 | filename: "hello.world.js", 931 | requireHandler: "require('../hello.world').handler" 932 | }, 933 | { filename: "foo.js", requireHandler: "require('../foo_bar').handler" }, 934 | { filename: "bar.js", requireHandler: "require('../foo_bar').handler" }, 935 | { filename: "jet.js", requireHandler: "require('../foo/foo/bar').handler" }, 936 | { filename: "pack.js", requireHandler: "require('../foo.bar/zoo').handler" } 937 | ].forEach(assertFileOutputCJS); 938 | 939 | const functions = serverless.service.functions; 940 | expect(functions.hello.handler).toBe("_lumigo/hello.world"); 941 | expect(functions["hello.world"].handler).toBe("_lumigo/hello.world.handler"); 942 | expect(functions.foo.handler).toBe("_lumigo/foo.handler"); 943 | expect(functions.bar.handler).toBe("_lumigo/bar.handler"); 944 | expect(functions.jet.handler).toBe("_lumigo/jet.handler"); 945 | expect(functions.pack.handler).toBe("_lumigo/pack.handler"); 946 | } 947 | 948 | function assertNodejsFunctionsAreWrappedES() { 949 | assertTracerInstall(); 950 | 951 | expect(fs.outputFile).toBeCalledTimes(6); 952 | [ 953 | { 954 | filename: "hello.js", 955 | importStatement: "import {world as originalHandler} from '../hello.js'", 956 | exportStatement: "export const world = tracer.trace(originalHandler);" 957 | }, 958 | { 959 | filename: "hello.world.js", 960 | importStatement: 961 | "import {handler as originalHandler} from '../hello.world.js'", 962 | exportStatement: "export const handler = tracer.trace(originalHandler);" 963 | }, 964 | { 965 | filename: "foo.js", 966 | importStatement: "import {handler as originalHandler} from '../foo_bar.js'", 967 | exportStatement: "export const handler = tracer.trace(originalHandler);" 968 | }, 969 | { 970 | filename: "jet.js", 971 | importStatement: 972 | "import {handler as originalHandler} from '../foo/foo/bar.js'", 973 | exportStatement: "export const handler = tracer.trace(originalHandler);" 974 | }, 975 | { 976 | filename: "pack.js", 977 | importStatement: 978 | "import {handler as originalHandler} from '../foo.bar/zoo.js'", 979 | exportStatement: "export const handler = tracer.trace(originalHandler);" 980 | } 981 | ].forEach(assertFileOutputES); 982 | 983 | const functions = serverless.service.functions; 984 | expect(functions.hello.handler).toBe("_lumigo/hello.world"); 985 | expect(functions["hello.world"].handler).toBe("_lumigo/hello.world.handler"); 986 | expect(functions.foo.handler).toBe("_lumigo/foo.handler"); 987 | expect(functions.bar.handler).toBe("_lumigo/bar.handler"); 988 | expect(functions.jet.handler).toBe("_lumigo/jet.handler"); 989 | expect(functions.pack.handler).toBe("_lumigo/pack.handler"); 990 | } 991 | 992 | function assertPythonFunctionsHaveLayers(version) { 993 | const functions = serverless.service.functions; 994 | const wrappedFunctions = [ 995 | functions.hello, 996 | functions["hello.world"], 997 | functions.foo, 998 | functions.bar, 999 | functions.jet, 1000 | functions.pack 1001 | ]; 1002 | const skippedFunctions = [functions.skippy]; 1003 | 1004 | wrappedFunctions.forEach(func => { 1005 | expect(func.handler).toBe("/opt/python/lumigo_tracer._handler"); 1006 | expect(func.layers).toHaveLength(1); 1007 | if (version) { 1008 | expect(func.layers[0]).toEqual( 1009 | `arn:aws:lambda:us-east-1:114300393969:layer:lumigo-python-tracer:${version}` 1010 | ); 1011 | } else { 1012 | expect(func.layers[0]).toEqual( 1013 | expect.stringMatching( 1014 | /arn:aws:lambda:us-east-1:114300393969:layer:lumigo-python-tracer:\d*/ 1015 | ) 1016 | ); 1017 | } 1018 | expect(func.environment).toHaveProperty("LUMIGO_ORIGINAL_HANDLER"); 1019 | }); 1020 | 1021 | skippedFunctions.forEach(func => { 1022 | expect(func.handler).not.toBe("/opt/python/lumigo_tracer._handler"); 1023 | expect(func.layers).toBeUndefined(); 1024 | expect(func.environment).toBeUndefined(); 1025 | }); 1026 | } 1027 | 1028 | function getPythonImportLine(handlerModulePath, handlerFuncName) { 1029 | return `userHandler = getattr(importlib.import_module("${handlerModulePath}"), "${handlerFuncName}")`; 1030 | } 1031 | 1032 | function assertPythonFunctionsAreWrapped(parameters) { 1033 | let endParams = []; 1034 | for (const [key, value] of Object.entries(parameters)) { 1035 | endParams.push(`${key}=${value}`); 1036 | } 1037 | expect(fs.outputFile).toBeCalledTimes(6); 1038 | expect(fs.outputFile).toBeCalledWith( 1039 | __dirname + "/_lumigo/hello.py", 1040 | expect.toContainAllStrings( 1041 | "from lumigo_tracer import lumigo_tracer", 1042 | getPythonImportLine("hello", "world"), 1043 | `@lumigo_tracer(${endParams.join(",")})` 1044 | ) 1045 | ); 1046 | expect(fs.outputFile).toBeCalledWith( 1047 | __dirname + "/_lumigo/hello.world.py", 1048 | expect.toContainAllStrings( 1049 | "from lumigo_tracer import lumigo_tracer", 1050 | getPythonImportLine("hello.world", "handler"), 1051 | `@lumigo_tracer(${endParams.join(",")})` 1052 | ) 1053 | ); 1054 | expect(fs.outputFile).toBeCalledWith( 1055 | __dirname + "/_lumigo/foo.py", 1056 | expect.toContainAllStrings( 1057 | "from lumigo_tracer import lumigo_tracer", 1058 | getPythonImportLine("foo_bar", "handler"), 1059 | `@lumigo_tracer(${endParams.join(",")})` 1060 | ) 1061 | ); 1062 | expect(fs.outputFile).toBeCalledWith( 1063 | __dirname + "/_lumigo/bar.py", 1064 | expect.toContainAllStrings( 1065 | "from lumigo_tracer import lumigo_tracer", 1066 | getPythonImportLine("foo_bar", "handler"), 1067 | `@lumigo_tracer(${endParams.join(",")})` 1068 | ) 1069 | ); 1070 | expect(fs.outputFile).toBeCalledWith( 1071 | __dirname + "/_lumigo/jet.py", 1072 | expect.toContainAllStrings( 1073 | "from lumigo_tracer import lumigo_tracer", 1074 | getPythonImportLine("foo.foo.bar", "handler"), 1075 | `@lumigo_tracer(${endParams.join(",")})` 1076 | ) 1077 | ); 1078 | expect(fs.outputFile).toBeCalledWith( 1079 | __dirname + "/_lumigo/pack.py", 1080 | expect.toContainAllStrings( 1081 | "from lumigo_tracer import lumigo_tracer", 1082 | getPythonImportLine("foo.bar.zoo", "handler"), 1083 | `@lumigo_tracer(${endParams.join(",")})` 1084 | ) 1085 | ); 1086 | 1087 | const functions = serverless.service.functions; 1088 | expect(functions.hello.handler).toBe("_lumigo/hello.world"); 1089 | expect(functions["hello.world"].handler).toBe("_lumigo/hello.world.handler"); 1090 | expect(functions.foo.handler).toBe("_lumigo/foo.handler"); 1091 | expect(functions.bar.handler).toBe("_lumigo/bar.handler"); 1092 | expect(functions.jet.handler).toBe("_lumigo/jet.handler"); 1093 | expect(functions.pack.handler).toBe("_lumigo/pack.handler"); 1094 | } 1095 | 1096 | function assertFunctionsAreNotWrapped() { 1097 | expect(childProcess.exec).not.toBeCalled(); 1098 | expect(fs.outputFile).not.toBeCalled(); 1099 | } 1100 | 1101 | function assertNodejsFunctionsAreCleanedUp() { 1102 | expect(fs.remove).toBeCalledWith(__dirname + "/_lumigo"); 1103 | expect(childProcess.execSync).toBeCalledWith("npm uninstall @lumigo/tracer", "utf8"); 1104 | } 1105 | 1106 | function assertPythonFunctionsAreCleanedUp() { 1107 | expect(fs.remove).toBeCalledWith(__dirname + "/_lumigo"); 1108 | } 1109 | 1110 | function assertNothingHappens() { 1111 | expect(fs.remove).not.toBeCalled(); 1112 | expect(childProcess.exec).not.toBeCalled(); 1113 | } 1114 | 1115 | function assertLumigoIsIncluded() { 1116 | expect(serverless.service.package.include).toContain("_lumigo/*"); 1117 | } 1118 | --------------------------------------------------------------------------------