├── .coveralls.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── cf-template-basic-enabled-both.json ├── cf-template-basic-enabled-one.json ├── cf-template-basic.json ├── cf-template-custom-name-enabled.json ├── cf-template-custom-name-not-changed.json ├── cf-template-custom-name.json └── test.js /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: DJGAL2yp0e1J24hc7m5Mlot2zKP0Y9vh4 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | env: 4 | node: true 5 | es6: true 6 | mocha: true 7 | 8 | extends: 9 | "eslint:recommended" 10 | 11 | ecmaFeatures: 12 | generators: true 13 | -------------------------------------------------------------------------------- /.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 (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | coverage/ 3 | .editorconfig 4 | .eslintignore 5 | .eslintrc 6 | .gitignore 7 | .npmignore 8 | .travis.yml 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexandr Murashkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-plugin-tracing 2 | 3 | [![npm version](https://badge.fury.io/js/serverless-plugin-tracing.svg)](https://badge.fury.io/js/serverless-plugin-tracing) 4 | [![CircleCI](https://circleci.com/gh/alex-murashkin/serverless-plugin-tracing.svg?style=shield)](https://circleci.com/gh/alex-murashkin/serverless-plugin-tracing) 5 | [![Coverage Status](https://coveralls.io/repos/github/AlexanderMS/serverless-plugin-tracing/badge.svg)](https://coveralls.io/github/alex-murashkin/serverless-plugin-tracing) 6 | 7 | Enables AWS X-Ray (https://aws.amazon.com/xray/) for the entire Serverless stack or individual functions. 8 | 9 | **Update**: as of `2.0.0`, the plugin uses Cloud Formation to update `TracingConfig` and no longer 10 | makes additional AWS SDK calls. No change to YAML contract: stays same as in 1.x. Tested with `serverless@1.22.0`. 11 | 12 | Note: this plugin is currently **Beta**. 13 | 14 | Note: 1.x was tested to work well with `serverless@1.13.2`. Some older versions of `serverless` 15 | may not work due to outdated Javascript SDK that 16 | does not support `TracingConfig`. 17 | 18 | `npm install --save-dev serverless-plugin-tracing` 19 | 20 | Example `serverless.yml`: 21 | 22 | ```yaml 23 | service: my-great-service 24 | 25 | provider: 26 | name: aws 27 | stage: test 28 | tracing: true # enable tracing 29 | iamRoleStatements: 30 | - Effect: "Allow" # xray permissions (required) 31 | Action: 32 | - "xray:PutTraceSegments" 33 | - "xray:PutTelemetryRecords" 34 | Resource: 35 | - "*" 36 | 37 | plugins: 38 | - serverless-plugin-tracing 39 | 40 | functions: 41 | mainFunction: # inherits tracing settings from "provider" 42 | handler: src/app/index.handler 43 | healthCheckFunction: 44 | tracing: false # overrides provider settings (opt out) 45 | ``` 46 | 47 | Output after `serverless deploy`: 48 | ``` 49 | Serverless: Tracing ENABLED for function 50 | "my-great-service-test-mainFunction" 51 | Serverless: Tracing DISABLED for function 52 | "my-great-service-test-healthcheck" 53 | ``` 54 | 55 | **Important**: in addition to using the plugin, you need to enable capturing 56 | traces in the code as well: 57 | 58 | ```javascript 59 | const awsXRay = require('aws-xray-sdk'); 60 | const awsSdk = awsXRay.captureAWS(require('aws-sdk')); 61 | ``` 62 | 63 | The plugin only controls the checkbox that be viewed in AWS Console: 64 | go to AWS Lambda -> select a Lambda function -> Configuration tab -> Advanced settings -> 65 | "Enable active tracing". If `tracing` ends up being `true` for a function, 66 | the checkbox will be checked for that function. 67 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = class TracingConfigPlugin { 4 | constructor(serverless, options) { 5 | this.serverless = serverless; 6 | this.options = options; 7 | this.hooks = { 8 | 'package:compileEvents': this.processTracing.bind(this) 9 | }; 10 | } 11 | 12 | processTracing() { 13 | const service = this.serverless.service; 14 | this.functionResources = Object.keys(service.provider.compiledCloudFormationTemplate.Resources) 15 | .reduce((acc, resourceId) => { 16 | const resource = service.provider.compiledCloudFormationTemplate.Resources[resourceId]; 17 | if (resource.Type === "AWS::Lambda::Function") { 18 | if (resource.Properties) { 19 | acc[resource.Properties.FunctionName] = resource; 20 | } 21 | } 22 | return acc; 23 | }, {}); 24 | const stage = this.options.stage; 25 | const providerLevelTracingEnabled = (service.provider.tracing === true || service.provider.tracing === 'true'); 26 | return Promise.all(Object.keys(service.functions).map(functionName => { 27 | return this.toggleTracing( 28 | service.functions[functionName].name || `${service.service}-${stage}-${functionName}`, 29 | service.functions[functionName].tracing === true 30 | || service.functions[functionName].tracing === 'true' 31 | || (providerLevelTracingEnabled && (service.functions[functionName].tracing !== false && service.functions[functionName].tracing !== 'false')) 32 | ); 33 | })); 34 | } 35 | 36 | toggleTracing(functionName, isEnabled) { 37 | if (!this.functionResources[functionName]) { 38 | this.serverless.cli.log(`Tracing NOT SET for function "${functionName}" as couldn't find it in Cloud Formation template`); 39 | return; 40 | } 41 | this.serverless.cli.log(`Tracing ${isEnabled ? 'ENABLED' : 'DISABLED'} for function "${functionName}"`); 42 | this.functionResources[functionName].Properties.TracingConfig = { 43 | Mode: isEnabled === true ? 'Active' : 'PassThrough' 44 | }; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-plugin-tracing", 3 | "version": "2.0.0", 4 | "description": "Enables AWS X-Ray for entire Serverless stack or individual functions", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=4.0" 8 | }, 9 | "directories": { 10 | "test": "test" 11 | }, 12 | "scripts": { 13 | "coverage": "nyc report --reporter=lcov", 14 | "coveralls-coverage": "nyc report --reporter=text-lcov | coveralls", 15 | "lint": "eslint .", 16 | "test": "nyc --all mocha --recursive ./test", 17 | "posttest": "eslint ." 18 | }, 19 | "keywords": [ 20 | "serverless", 21 | "aws", 22 | "tracing", 23 | "trace", 24 | "xray", 25 | "x-ray" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/AlexanderMS/serverless-plugin-tracing.git" 30 | }, 31 | "author": "Alex Murashkin", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/AlexanderMS/serverless-plugin-tracing/issues" 35 | }, 36 | "homepage": "https://github.com/AlexanderMS/serverless-plugin-tracing#readme", 37 | "dependencies": {}, 38 | "devDependencies": { 39 | "chai": "^3.5.0", 40 | "coveralls": "^2.13.0", 41 | "eslint": "^3.18.0", 42 | "mocha": "^3.2.0", 43 | "nyc": "^10.2.0", 44 | "sinon": "^2.1.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/cf-template-basic-enabled-both.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "MainLambdaFunction": { 6 | "Type": "AWS::Lambda::Function", 7 | "Properties": { 8 | "FunctionName": "myService-test-mainFunction", 9 | "TracingConfig": { 10 | "Mode": "Active" 11 | } 12 | } 13 | }, 14 | "HealthcheckLambdaFunction": { 15 | "Type": "AWS::Lambda::Function", 16 | "Properties": { 17 | "FunctionName": "myService-test-healthcheck", 18 | "TracingConfig": { 19 | "Mode": "Active" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/cf-template-basic-enabled-one.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "MainLambdaFunction": { 6 | "Type": "AWS::Lambda::Function", 7 | "Properties": { 8 | "FunctionName": "myService-test-mainFunction", 9 | "TracingConfig": { 10 | "Mode": "Active" 11 | } 12 | } 13 | }, 14 | "HealthcheckLambdaFunction": { 15 | "Type": "AWS::Lambda::Function", 16 | "Properties": { 17 | "FunctionName": "myService-test-healthcheck", 18 | "TracingConfig": { 19 | "Mode": "PassThrough" 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/cf-template-basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "MainLambdaFunction": { 6 | "Type": "AWS::Lambda::Function", 7 | "Properties": { 8 | "FunctionName": "myService-test-mainFunction" 9 | } 10 | }, 11 | "HealthcheckLambdaFunction": { 12 | "Type": "AWS::Lambda::Function", 13 | "Properties": { 14 | "FunctionName": "myService-test-healthcheck" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/cf-template-custom-name-enabled.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "MainLambdaFunction": { 6 | "Type": "AWS::Lambda::Function", 7 | "Properties": { 8 | "FunctionName": "custom-name", 9 | "TracingConfig": { 10 | "Mode": "Active" 11 | } 12 | } 13 | }, 14 | "HealthcheckLambdaFunction": { 15 | "Type": "AWS::Lambda::Function", 16 | "Properties": { 17 | "FunctionName": "myService-test-healthcheck" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/cf-template-custom-name-not-changed.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "MainLambdaFunction": { 6 | "Type": "AWS::Lambda::Function", 7 | "Properties": { 8 | "FunctionName": "custom-name", 9 | "TracingConfig": { 10 | "Mode": "Active" 11 | } 12 | } 13 | }, 14 | "HealthcheckLambdaFunction": { 15 | "Type": "AWS::Lambda::Function", 16 | "Properties": { 17 | "FunctionName": "myService-test-healthcheck" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/cf-template-custom-name.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "MainLambdaFunction": { 6 | "Type": "AWS::Lambda::Function", 7 | "Properties": { 8 | "FunctionName": "custom-name" 9 | } 10 | }, 11 | "HealthcheckLambdaFunction": { 12 | "Type": "AWS::Lambda::Function", 13 | "Properties": { 14 | "FunctionName": "myService-test-healthcheck" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const 4 | sinon = require('sinon'), 5 | assert = require('chai').assert, 6 | Plugin = require('../index'); 7 | 8 | describe('serverless-plugin-tracing', function() { 9 | let sandbox, logSpy, serverlessBaseInstance; 10 | beforeEach(function() { 11 | sandbox = sinon.sandbox.create(); 12 | logSpy = sandbox.spy(); 13 | serverlessBaseInstance = { 14 | service: { 15 | service: 'myService', 16 | functions: { 17 | }, 18 | provider: { 19 | stage: 'test' 20 | } 21 | }, 22 | cli: { 23 | log: logSpy 24 | } 25 | }; 26 | }); 27 | 28 | function runPlugin(params) { 29 | const serverlessInstance = Object.assign({}, serverlessBaseInstance); 30 | serverlessInstance.service.provider = Object.assign({}, 31 | serverlessInstance.service.provider, 32 | params.provider); 33 | 34 | serverlessInstance.service.functions = params.functions; 35 | const options = { stage: 'test', noDeploy: params.noDeploy }; 36 | const plugin = new Plugin(serverlessInstance, options); 37 | plugin.hooks['package:compileEvents'].call(plugin); 38 | return plugin; 39 | } 40 | 41 | afterEach(function() { 42 | sandbox.verifyAndRestore(); 43 | }); 44 | 45 | it('enables tracing when function.tracing=true', function() { 46 | const plugin = runPlugin({ 47 | functions: { 48 | healthcheck: { 49 | }, 50 | mainFunction: { 51 | tracing: true 52 | } 53 | }, 54 | provider: { 55 | compiledCloudFormationTemplate: require('./cf-template-basic.json') 56 | } 57 | }); 58 | 59 | assert.deepEqual(logSpy.getCall(0).args[0], 'Tracing DISABLED for function "myService-test-healthcheck"'); 60 | assert.deepEqual(logSpy.getCall(1).args[0], 'Tracing ENABLED for function "myService-test-mainFunction"'); 61 | assert.deepEqual(plugin.serverless.service.provider.compiledCloudFormationTemplate, require('./cf-template-basic-enabled-one.json')); 62 | }); 63 | 64 | it('enables tracing when provider.tracing=true', function() { 65 | const plugin = runPlugin({ 66 | functions: { 67 | healthcheck: { 68 | }, 69 | mainFunction: { 70 | tracing: true 71 | } 72 | }, 73 | provider: { 74 | tracing: true, 75 | compiledCloudFormationTemplate: require('./cf-template-basic.json') 76 | } 77 | }); 78 | 79 | assert.deepEqual(logSpy.getCall(0).args[0], 'Tracing ENABLED for function "myService-test-healthcheck"'); 80 | assert.deepEqual(logSpy.getCall(1).args[0], 'Tracing ENABLED for function "myService-test-mainFunction"'); 81 | assert.deepEqual(plugin.serverless.service.provider.compiledCloudFormationTemplate, require('./cf-template-basic-enabled-both.json')); 82 | }); 83 | 84 | it('enables tracing when opt variable is "true"', function() { 85 | const plugin = runPlugin({ 86 | functions: { 87 | healthcheck: { 88 | tracing: 'false' 89 | }, 90 | mainFunction: { 91 | tracing: 'true' 92 | } 93 | }, 94 | provider: { 95 | tracing: 'true', 96 | compiledCloudFormationTemplate: require('./cf-template-basic.json') 97 | } 98 | }); 99 | 100 | assert.deepEqual(logSpy.getCall(0).args[0], 'Tracing DISABLED for function "myService-test-healthcheck"'); 101 | assert.deepEqual(logSpy.getCall(1).args[0], 'Tracing ENABLED for function "myService-test-mainFunction"'); 102 | assert.deepEqual(plugin.serverless.service.provider.compiledCloudFormationTemplate, require('./cf-template-basic-enabled-one.json')); 103 | }); 104 | 105 | it('does not enable tracing when provider.tracing=true but function.tracing=false', function() { 106 | const plugin = runPlugin({ 107 | functions: { 108 | healthcheck: { 109 | tracing: false 110 | }, 111 | mainFunction: { 112 | tracing: true 113 | } 114 | }, 115 | provider: { 116 | tracing: true, 117 | compiledCloudFormationTemplate: require('./cf-template-basic.json') 118 | } 119 | }); 120 | 121 | assert.deepEqual(logSpy.getCall(0).args[0], 'Tracing DISABLED for function "myService-test-healthcheck"'); 122 | assert.deepEqual(logSpy.getCall(1).args[0], 'Tracing ENABLED for function "myService-test-mainFunction"'); 123 | assert.deepEqual(plugin.serverless.service.provider.compiledCloudFormationTemplate, require('./cf-template-basic-enabled-one.json')); 124 | }); 125 | 126 | it('respects the name property', function() { 127 | const plugin = runPlugin({ 128 | functions: { 129 | mainFunction: { 130 | name: 'custom-name', 131 | tracing: true 132 | } 133 | }, 134 | provider: { 135 | compiledCloudFormationTemplate: require('./cf-template-custom-name.json') 136 | } 137 | }); 138 | 139 | assert.deepEqual(logSpy.getCall(0).args[0], 'Tracing ENABLED for function "custom-name"'); 140 | assert.deepEqual(plugin.serverless.service.provider.compiledCloudFormationTemplate, require('./cf-template-custom-name-enabled.json')); 141 | }); 142 | 143 | it('does not change functions that could not be found', function() { 144 | const plugin = runPlugin({ 145 | functions: { 146 | mainFunction: { 147 | name: 'notFound', 148 | tracing: true 149 | } 150 | }, 151 | provider: { 152 | compiledCloudFormationTemplate: require('./cf-template-custom-name.json') 153 | } 154 | }); 155 | 156 | assert.deepEqual(logSpy.getCall(0).args[0], 'Tracing NOT SET for function "notFound" as couldn\'t find it in Cloud Formation template'); 157 | assert.deepEqual(plugin.serverless.service.provider.compiledCloudFormationTemplate, require('./cf-template-custom-name-not-changed.json')); 158 | }); 159 | }); 160 | --------------------------------------------------------------------------------