├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github ├── CODEOWNERS └── workflows │ └── npm-publish-github-packages.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── examples ├── .eslintrc ├── batch_example.js ├── hello-world-typescript │ ├── package.json │ ├── src │ │ ├── handler.ts │ │ └── queries │ │ │ └── addSomethingQuery.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── webpack.config.js ├── hello-world │ ├── handler.js │ ├── package.json │ └── serverless.yml ├── label_example.js ├── node_wrapper_example.js └── openwhisk_wrapper_example.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts ├── build_peg.sh ├── publish_layers.sh └── run_tests.sh ├── src ├── config.js ├── consts.js ├── containers │ ├── azure.js │ ├── ec2.js │ ├── ecs.js │ └── k8s.js ├── event.js ├── events │ ├── amazon_dax_client.js │ ├── amqp.js │ ├── amqplib.js │ ├── aws_sdk.js │ ├── aws_sdk_v3.js │ ├── azure_sdk.js │ ├── bunyan.js │ ├── cassandra-driver.js │ ├── console.js │ ├── dns.js │ ├── fs.js │ ├── google_cloud.js │ ├── http.js │ ├── http2.js │ ├── ioredis.js │ ├── kafka-node.js │ ├── kafkajs.js │ ├── ldap.js │ ├── memcached.js │ ├── module_utils.js │ ├── mongodb.js │ ├── mqtt.js │ ├── mysql.js │ ├── nats.js │ ├── neo4j.js │ ├── openwhisk.js │ ├── pg.js │ ├── pino.js │ ├── redis.js │ ├── sql.js │ ├── tencent-cos.js │ ├── winston.js │ └── winston_cloudwatch.js ├── helpers │ ├── events.js │ └── http.js ├── index.d.ts ├── index.js ├── patcher.js ├── peg │ └── sql.pegjs ├── proto │ ├── error_code_pb.js │ ├── event_pb.js │ ├── exception_pb.js │ ├── timestamp_pb.js │ └── trace_pb.js ├── resource_utils │ └── sqs_utils.js ├── runners │ ├── aws_batch.js │ ├── aws_lambda.js │ └── tencent_function.js ├── trace_object.js ├── trace_queue.js ├── trace_senders │ ├── http.js │ └── logs.js ├── tracer.js ├── triggers │ ├── aws_lambda.js │ └── tencent_function.js ├── try_require.js ├── utils.js └── wrappers │ ├── batch.js │ ├── google_cloud_function.js │ ├── lambda.js │ ├── lambda_env.js │ ├── node.js │ ├── openwhisk.js │ └── tencent.js ├── test ├── .eslintrc ├── acceptance │ ├── acceptance.js │ ├── lambda-handlers │ │ ├── .gitignore │ │ ├── handler.js │ │ ├── handler_tracer_test.js │ │ └── serverless.yml │ └── run.sh └── unit_tests │ ├── consts.js │ ├── events │ ├── google_cloud.test.js │ ├── ldap-server-mock │ │ ├── ldap-server-mock-conf.json │ │ └── users.json │ ├── ldapjs.js │ ├── oauth2-server-mock │ │ └── server.js │ └── simple-oauth2.js │ ├── fixtures │ └── bigtrace.json │ ├── runners │ └── test_aws_batch.js │ ├── test_config.js │ ├── test_trace_queue.js │ ├── test_tracer.js │ └── wrappers │ ├── test_batch.js │ ├── test_lambda.js │ ├── test_node.js │ ├── test_openwhisk.js │ └── test_openwhisk_traces.js └── trace.png /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.js] 2 | indent_style=space 3 | indent_size=4 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/** 2 | **/proto/** 3 | examples/** 4 | /src/resource_utils/sql_utils.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "node": true, 5 | "browser": false 6 | }, 7 | "plugins": [ 8 | "json" 9 | ], 10 | "rules": { 11 | "no-console": "off", 12 | "indent": ["error", 4], 13 | "function-paren-newline": ["error", "consistent"], 14 | "require-jsdoc": ["error", { 15 | "require": { 16 | "FunctionDeclaration": true, 17 | "MethodDefinition": true, 18 | "ClassDeclaration": true, 19 | "ArrowFunctionExpression": false, 20 | "FunctionExpression": true 21 | } 22 | }], 23 | "operator-linebreak": ["error", "after"], 24 | "valid-jsdoc": ["error", { 25 | "requireReturn": false 26 | }], 27 | "comma-dangle": ["error", { 28 | "arrays": "always-multiline", 29 | "objects": "always-multiline", 30 | "imports": "never", 31 | "exports": "never", 32 | "functions": "never" 33 | }] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | * @epsagon/team-miso 3 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-github-packages.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | [workflow_dispatch] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 14 17 | - run: npm ci 18 | - run: npm test 19 | 20 | publish: 21 | needs: build 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 14 31 | registry-url: https://npm.pkg.github.com/ 32 | - run: npm ci 33 | - run: npm run semantic-release 34 | env: 35 | GH_TOKEN: ${{secrets.GH_TOKEN}} 36 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 37 | 38 | publish-layers: 39 | needs: publish 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v3 43 | - uses: actions/setup-node@v3 44 | with: 45 | node-version: 14 46 | - run: ./scripts/publish_layers.sh 47 | env: 48 | GH_TOKEN: ${{secrets.GH_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | dist/ 4 | issues/ 5 | src/resource_utils/sql_utils.js 6 | **/.DS_Store 7 | .vscode 8 | .env 9 | .pytest_cache 10 | *.cpuprofile 11 | tenna_releases 12 | .nyc_output/* 13 | examples/* -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | branches: 4 | only: 5 | - master 6 | - develop 7 | 8 | install: 9 | - npm install 10 | 11 | node_js: 12 | - "14.17" 13 | - "16" 14 | 15 | script: 16 | - ./scripts/build_peg.sh 17 | - npm run lint 18 | - npm run test 19 | 20 | 21 | jobs: 22 | include: 23 | - stage: acceptance 24 | node_js: "14.17" 25 | script: 26 | - npm install -g serverless@2.45.2 27 | - npm run build 28 | - ./test/acceptance/run.sh $TRAVIS_BUILD_NUMBER 29 | 30 | - stage: build-and-deploy 31 | node_js: "14.17" 32 | script: 33 | - npm run build && npm run semantic-release && ./scripts/publish_layers.sh 34 | 35 | stages: 36 | - Test 37 | - name: acceptance 38 | if: branch = master AND type = pull_request 39 | - name: build-and-deploy 40 | if: branch = master AND type = push 41 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at dev@epsagon.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Epsagon 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. -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-angular']} 2 | -------------------------------------------------------------------------------- /examples/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "import/no-unresolved": 0 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/batch_example.js: -------------------------------------------------------------------------------- 1 | const epsagon = require('../src/index'); 2 | const http = require('http'); 3 | const { Console } = require('console'); 4 | 5 | 6 | epsagon.init({ 7 | token: process.env.EPSAGON_TOKEN, 8 | appName: 'batch-test', 9 | metadataOnly: false, 10 | sendBatch: true, 11 | batchSize: 5, 12 | maxBatchSizeBytes: 5000000, 13 | }); 14 | 15 | function doRequest(options) { 16 | return new Promise ((resolve, reject) => { 17 | let req = http.request(options); 18 | 19 | req.on('response', res => { 20 | resolve(res); 21 | }); 22 | 23 | req.on('error', err => { 24 | resolve(err); 25 | }); 26 | }); 27 | } 28 | 29 | 30 | async function testAsyncFunction() { 31 | const options = { 32 | host: 'localhost', 33 | method: 'GET', 34 | }; 35 | doRequest(options) 36 | console.log("logging something") 37 | } 38 | 39 | 40 | const wrappedAsyncTestFunction = epsagon.nodeWrapper(testAsyncFunction); 41 | 42 | async function main (){ 43 | await wrappedAsyncTestFunction() 44 | await wrappedAsyncTestFunction() 45 | 46 | await wrappedAsyncTestFunction() 47 | 48 | await wrappedAsyncTestFunction() 49 | 50 | await wrappedAsyncTestFunction() 51 | await wrappedAsyncTestFunction() 52 | await wrappedAsyncTestFunction() 53 | await wrappedAsyncTestFunction() 54 | await wrappedAsyncTestFunction() 55 | await wrappedAsyncTestFunction() 56 | await wrappedAsyncTestFunction() 57 | await wrappedAsyncTestFunction() 58 | 59 | await Promise.all([ 60 | wrappedAsyncTestFunction(), 61 | wrappedAsyncTestFunction()] 62 | 63 | ) 64 | } 65 | 66 | 67 | 68 | 69 | main() -------------------------------------------------------------------------------- /examples/hello-world-typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "tsc", 7 | "build:deploy": "tsc -p tsconfig.build.json", 8 | "deploy": "npm run build:deploy && webpack" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "epsagon": "^1.105.1" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^14.14.25", 17 | "aws-sdk": "^2.841.0", 18 | "esbuild-loader": "^2.9.1", 19 | "ts-loader": "^8.0.17", 20 | "ts-node": "^9.1.1", 21 | "typescript": "^4.1.5", 22 | "webpack": "^5.21.2", 23 | "webpack-cli": "^4.5.0", 24 | "zip-webpack-plugin": "4.0.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/hello-world-typescript/src/handler.ts: -------------------------------------------------------------------------------- 1 | import epsagon from "epsagon"; 2 | import DynamoDB from "aws-sdk/clients/dynamodb"; 3 | import { AddSomethingQuery } from "./queries/addSomethingQuery"; 4 | 5 | epsagon.init({ 6 | token: "token", 7 | appName: "myApp" 8 | }); 9 | 10 | const dynamoDbClient = new DynamoDB.DocumentClient(); 11 | const addSomethingQuery = new AddSomethingQuery(dynamoDbClient); 12 | 13 | export const handler = epsagon.lambdaWrapper(async (event: any): Promise => { 14 | 15 | await addSomethingQuery.execute("id", "something"); 16 | return "OK"; 17 | }); -------------------------------------------------------------------------------- /examples/hello-world-typescript/src/queries/addSomethingQuery.ts: -------------------------------------------------------------------------------- 1 | import DynamoDB from "aws-sdk/clients/dynamodb"; 2 | 3 | export class AddSomethingQuery { 4 | constructor(private readonly dynamoDbClient: DynamoDB.DocumentClient) { } 5 | 6 | public async execute(id: string, payload: string): Promise { 7 | const params = { 8 | Item: { 9 | "id": id, 10 | "payload": payload, 11 | }, 12 | TableName: "myTb", 13 | }; 14 | 15 | await this.dynamoDbClient.put(params).promise(); 16 | } 17 | } -------------------------------------------------------------------------------- /examples/hello-world-typescript/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "inlineSourceMap": false, 5 | "sourceMap": true, 6 | } 7 | } -------------------------------------------------------------------------------- /examples/hello-world-typescript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": false, 7 | "noImplicitAny": false, 8 | "noImplicitThis": false, 9 | "inlineSourceMap": true, 10 | "esModuleInterop": true, 11 | "sourceMap": false, 12 | "target": "es2019", 13 | "baseUrl": "src" 14 | }, 15 | "include": [ 16 | "src/**/*.ts" 17 | ], 18 | "exclude": [ 19 | "tests/**/*.ts", 20 | "node_modules" 21 | ] 22 | } -------------------------------------------------------------------------------- /examples/hello-world-typescript/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const { ESBuildPlugin } = require('esbuild-loader'); 4 | const ZipPlugin = require('zip-webpack-plugin'); 5 | 6 | module.exports = { 7 | entry: './src/handler.ts', 8 | mode: "production", 9 | target: 'node', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts?$/, 14 | use: 'ts-loader', 15 | exclude: /node_modules/ 16 | } 17 | ] 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.js', '.json'] 21 | }, 22 | externals: [ 23 | 'aws-sdk/clients/dynamodb' 24 | ], 25 | plugins: [ 26 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), 27 | new ESBuildPlugin(), 28 | new ZipPlugin({ 29 | filename: `hello.zip` 30 | }) 31 | ], 32 | output: { 33 | filename: 'hello.js', 34 | path: path.resolve(__dirname, '../resources/'), 35 | libraryTarget: 'commonjs2' 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /examples/hello-world/handler.js: -------------------------------------------------------------------------------- 1 | const epsagon = require('epsagon'); 2 | 3 | epsagon.init({ 4 | token: 'my-secret-token', 5 | appName: 'my-app-name', 6 | metadataOnly: false, 7 | }); 8 | 9 | module.exports.hello = epsagon.lambdaWrapper((event, context, callback) => { 10 | const response = { 11 | statusCode: 200, 12 | body: JSON.stringify({ 13 | message: 'It Worked!', 14 | input: event, 15 | }), 16 | }; 17 | 18 | callback(null, response); 19 | }); 20 | -------------------------------------------------------------------------------- /examples/hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "EpsagonHelloWorldExample", 3 | "version": "1.0.0", 4 | "description": "Epsagon library basic usage example", 5 | "dependencies": { 6 | "epsagon": "^1.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/hello-world/serverless.yml: -------------------------------------------------------------------------------- 1 | service: hello-world 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs6.10 6 | 7 | functions: 8 | hello: 9 | handler: handler.hello 10 | -------------------------------------------------------------------------------- /examples/label_example.js: -------------------------------------------------------------------------------- 1 | const epsagon = require('epsagon'); 2 | 3 | epsagon.init({ 4 | token: 'my-secret-token', 5 | appName: 'my-app-name', 6 | metadataOnly: false, 7 | }); 8 | 9 | module.exports.test = epsagon.lambdaWrapper(async () => { 10 | epsagon.label('myFirstLabel', 'customValue1'); 11 | epsagon.label('mySecondLabel', 'customValue2'); 12 | 13 | return 'success'; 14 | }); 15 | -------------------------------------------------------------------------------- /examples/node_wrapper_example.js: -------------------------------------------------------------------------------- 1 | const epsagon = require('epsagon'); 2 | 3 | epsagon.init({ 4 | token: 'my-secret-token', 5 | appName: 'my-app-name', 6 | metadataOnly: false, 7 | }); 8 | 9 | /** 10 | * Node wrapper test function 11 | * @param {function} callback: callback function 12 | */ 13 | function test(callback) { // eslint-disable-line no-unused-vars 14 | // eslint-disable-next-line no-console 15 | console.log('hello world from node function'); 16 | } 17 | 18 | const testFunction = epsagon.nodeWrapper(test); 19 | 20 | testFunction(() => {}); 21 | -------------------------------------------------------------------------------- /examples/openwhisk_wrapper_example.js: -------------------------------------------------------------------------------- 1 | const epsagon = require('epsagon'); 2 | 3 | epsagon.init({ 4 | token: 'my-secret-token', 5 | appName: 'my-app-name', 6 | metadataOnly: false, 7 | }); 8 | 9 | /** 10 | * OpenWhisk wrapper test function 11 | * @param {function} callback: callback function 12 | */ 13 | function main(params) { // eslint-disable-line no-unused-vars 14 | // eslint-disable-next-line no-console 15 | console.log('hello world from node function'); 16 | } 17 | 18 | exports.main = epsagon.openWhiskWrapper(main); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "epsagon", 3 | "version": "0.0.0-development", 4 | "description": "Epsagon Instrumentation for Node.js", 5 | "keywords": [ 6 | "serverless", 7 | "epsagon", 8 | "tracing", 9 | "distributed-tracing", 10 | "lambda", 11 | "aws-lambda", 12 | "debugging", 13 | "monitoring" 14 | ], 15 | "author": "Epsagon Team ", 16 | "license": "MIT", 17 | "scripts": { 18 | "pretest": "./scripts/build_peg.sh", 19 | "test": "nyc --reporter=text --reporter=text-summary ./scripts/run_tests.sh", 20 | "lint:js": "eslint --max-warnings=0 ./src/ ./examples/ ./test/unit_tests ./index.js -f table --ext .js --ext .jsx", 21 | "lint:js:fix": "eslint --max-warnings=0 ./src/ ./examples/ ./test/unit_tests ./index.js -f table --ext .js --ext .jsx --fix", 22 | "lint": "npm run lint:js", 23 | "build:dev": "./scripts/build_peg.sh && rollup -c", 24 | "build": "./scripts/build_peg.sh && NODE_ENV=production rollup -c", 25 | "clean": "rm -r dist/", 26 | "prepublishOnly": "npm run build", 27 | "semantic-release": "semantic-release" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/epsagon/epsagon-node/issues" 31 | }, 32 | "homepage": "https://github.com/epsagon/epsagon-node#readme", 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/epsagon/epsagon-node.git" 36 | }, 37 | "main": "dist/bundle.js", 38 | "files": [ 39 | "dist" 40 | ], 41 | "husky": { 42 | "hooks": { 43 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 44 | } 45 | }, 46 | "nyc": { 47 | "check-coverage": true, 48 | "statements": 17, 49 | "branches": 13, 50 | "functions": 25, 51 | "lines": 17 52 | }, 53 | "devDependencies": { 54 | "@babel/runtime": "^7.4.5", 55 | "@commitlint/cli": "^13.2.1", 56 | "@commitlint/config-angular": "^7.1.2", 57 | "@commitlint/config-conventional": "^7.1.2", 58 | "aws-sdk": "^2.197.0", 59 | "body-parser": "^1.19.0", 60 | "chai": "^4.1.2", 61 | "chai-as-promised": "^7.1.1", 62 | "dotenv": "^8.2.0", 63 | "eslint": "^4.18.0", 64 | "eslint-config-airbnb": "^17.1.0", 65 | "eslint-plugin-chai-friendly": "^0.4.1", 66 | "eslint-plugin-import": "^2.14.0", 67 | "eslint-plugin-json": "^1.2.1", 68 | "eslint-plugin-jsx-a11y": "^6.1.1", 69 | "eslint-plugin-mocha": "^4.11.0", 70 | "eslint-plugin-react": "^7.11.0", 71 | "express": "^4.17.1", 72 | "express-session": "^1.17.1", 73 | "husky": "^1.1.0", 74 | "ldap-server-mock": "^3.0.0", 75 | "ldapjs": "^2.1.0", 76 | "lolex": "^3.0.0", 77 | "memcached": "^2.2.2", 78 | "mocha": "^9.1.2", 79 | "mongodb": "^3.1.13", 80 | "mysql": "^2.16.0", 81 | "mysql2": "^1.6.4", 82 | "nyc": "^15.1.0", 83 | "pegjs": "^0.10.0", 84 | "pg": "^7.6.0", 85 | "pg-pool": "^2.0.3", 86 | "proxyquire": "^2.0.1", 87 | "randomstring": "^1.1.5", 88 | "redis": "^3.1.2", 89 | "request": "^2.88.2", 90 | "request-promise-native": "^1.0.7", 91 | "rollup": "^0.66.6", 92 | "rollup-plugin-commonjs": "^9.1.8", 93 | "rollup-plugin-copy": "^3.1.0", 94 | "rollup-plugin-eslint": "^5.0.0", 95 | "rollup-plugin-json": "^3.1.0", 96 | "rollup-plugin-terser": "^7.0.1", 97 | "semantic-release": "^18.0.0", 98 | "semver": "^7.3.4", 99 | "simple-oauth2": "^4.2.0", 100 | "sinon": "^4.3.0", 101 | "uglify-es": "^3.3.9", 102 | "underscore": "^1.12.0" 103 | }, 104 | "dependencies": { 105 | "@aws-sdk/client-sns": "^3.41.0", 106 | "@aws-sdk/util-dynamodb": "^3.49.0", 107 | "axios-minified": "^1.0.7", 108 | "google-protobuf-minified": "^1.0.8", 109 | "json-stringify-safe": "^5.0.1", 110 | "md5": "^2.2.1", 111 | "require-in-the-middle": "^5.0.3", 112 | "shimmer": "^1.2.1", 113 | "sort-json": "^2.0.0", 114 | "uuid-parse": "^1.1.0", 115 | "uuid4": "^1.0.0" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import copy from 'rollup-plugin-copy'; 2 | 3 | const commonjs = require('rollup-plugin-commonjs'); 4 | const { eslint } = require('rollup-plugin-eslint'); 5 | const { terser } = require('rollup-plugin-terser'); 6 | const json = require('rollup-plugin-json'); 7 | 8 | module.exports = { 9 | input: 'src/index.js', 10 | output: { 11 | file: 'dist/bundle.js', 12 | format: 'cjs', 13 | }, 14 | plugins: [ 15 | (process.env.NODE_ENV === 'production' ? eslint({ 16 | throwOnError: true, 17 | throwOnWarning: true, 18 | }) : null), 19 | commonjs({ 20 | ignore: ['conditional-runtime-dependency'], 21 | }), 22 | json(), 23 | (process.env.NODE_ENV === 'production' ? terser({ 24 | warnings: 'verbose', 25 | compress: { 26 | warnings: 'verbose', 27 | }, 28 | mangle: { 29 | keep_fnames: true, 30 | }, 31 | output: { 32 | beautify: false, 33 | }, 34 | }) : null), 35 | copy({ 36 | targets: [{ 37 | src: 'src/index.d.ts', dest: 'dist', rename: 'bundle.d.ts', 38 | }], 39 | }), 40 | ], 41 | }; 42 | -------------------------------------------------------------------------------- /scripts/build_peg.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./node_modules/pegjs/bin/pegjs -o ./src/resource_utils/sql_utils.js ./src/peg/sql.pegjs -------------------------------------------------------------------------------- /scripts/publish_layers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | declare -a regions=("ap-northeast-1" "ap-northeast-2" "ap-south-1" "ap-southeast-1" "ap-southeast-2" "ca-central-1" "eu-central-1" "eu-west-1" "eu-west-2" "eu-west-3" "sa-east-1" "us-east-1" "us-east-2" "us-west-1" "us-west-2") 3 | pip install --user awscli jq 4 | mkdir layer 5 | cd layer 6 | npm init -f 7 | npm i epsagon@latest 8 | mkdir nodejs 9 | mv node_modules nodejs/ 10 | zip -r epsagon-node-layer.zip nodejs -x ".*" -x "__MACOSX" 11 | 12 | for region in "${regions[@]}" 13 | do 14 | echo ${region} 15 | aws s3 cp epsagon-node-layer.zip s3://epsagon-layers-${region}/ 16 | LAYER_VERSION=$(aws lambda publish-layer-version --layer-name epsagon-node-layer --description "Epsagon Node.js layer that includes pre-installed packages to get up and running with monitoring and distributed tracing" --content S3Bucket=epsagon-layers-${region},S3Key=epsagon-node-layer.zip --compatible-runtimes nodejs16.x nodejs14.x nodejs12.x nodejs10.x nodejs8.10 --compatible-architectures "arm64" "x86_64" --license-info MIT --region ${region} | jq '.Version') 17 | sleep 3 18 | aws lambda add-layer-version-permission --layer-name epsagon-node-layer --version-number ${LAYER_VERSION} --statement-id sid1 --action lambda:GetLayerVersion --principal \* --region ${region} 19 | done 20 | -------------------------------------------------------------------------------- /scripts/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | if [[ `node -v | cut -d '.' -f1` =~ ^(v6|v8)$ ]]; then 3 | mocha --recursive test/unit_tests --exclude test/unit_tests/wrappers/test_openwhisk.js --exclude test/unit_tests/wrappers/test_openwhisk_traces.js 4 | else 5 | mocha --recursive test/unit_tests 6 | fi 7 | -------------------------------------------------------------------------------- /src/consts.js: -------------------------------------------------------------------------------- 1 | module.exports.VERSION = require('../package.json').version; 2 | 3 | const DEFAULT_REGION = 'us-east-1'; 4 | let REGION = process.env.AWS_REGION; 5 | 6 | // Check that we got region from env. 7 | if (REGION === undefined) { 8 | REGION = DEFAULT_REGION; 9 | } 10 | 11 | module.exports.REGION = REGION; 12 | module.exports.LOCAL_URL = 'http://localhost:3000'; 13 | module.exports.TRACE_COLLECTOR_URL = `https://${REGION}.tc.epsagon.com`; 14 | 15 | module.exports.COLD_START = true; 16 | 17 | /** 18 | * The identifier of the injected step data in the step machine result dict 19 | */ 20 | module.exports.STEP_ID_NAME = 'Epsagon'; 21 | 22 | module.exports.LOG_PREFIX = '[EPSAGON]'; 23 | 24 | module.exports.EPSAGON_EVENT_ID_KEY = '_epsagon_event_id'; 25 | 26 | module.exports.MAX_VALUE_CHARS = 3 * 1024; 27 | 28 | module.exports.MAX_LABEL_SIZE = 10 * 1024; 29 | 30 | module.exports.MAX_HTTP_VALUE_SIZE = 10 * 1024; 31 | 32 | module.exports.MAX_TRACE_SIZE_BYTES = process.env.EPSAGON_MAX_TRACE_SIZE ? 33 | parseInt(process.env.EPSAGON_MAX_TRACE_SIZE, 10) : 64 * 1024; 34 | 35 | module.exports.DEFAULT_SAMPLE_RATE = 1; 36 | 37 | module.exports.DEFAULT_BATCH_SIZE = 5; 38 | 39 | module.exports.MAX_TRACE_WAIT = 5000; // miliseconds 40 | 41 | module.exports.BATCH_SIZE_BYTES_HARD_LIMIT = 10 * 64 * 1024; // 650KB 42 | 43 | module.exports.QUEUE_SIZE_BYTES_HARD_LIMIT = 10 * 1024 * 1024; // 10MB 44 | 45 | module.exports.MAX_QUERY_ELEMENTS = 100; 46 | 47 | // Key name to inject epsagon correlation ID 48 | module.exports.EPSAGON_HEADER = 'epsagon-trace-id'; 49 | 50 | // In some cases we manually add the Lambda node_modules path, where it is not found by default 51 | module.exports.LAMBDA_DEFAULT_NODE_MODULES_PATH = '/var/task/node_modules'; 52 | 53 | module.exports.STRONG_ID_KEYS = [ 54 | 'key', 55 | 'request_id', 56 | 'requestid', 57 | 'request-id', 58 | 'steps_dict', 59 | 'message_id', 60 | 'etag', 61 | 'item_hash', 62 | 'sequence_number', 63 | 'trace_id', 64 | 'job_id', 65 | 'activation_id', 66 | 'http_trace_id', 67 | 'id', 68 | 'aws.sqs.message_id', 69 | 'x-amz-request-id', 70 | 'object_key', 71 | 'object_etag', 72 | 'aws.requestId', 73 | 'aws.s3.key', 74 | 'aws.s3.etag', 75 | 'aws.kinesis.sequence_number', 76 | 'request_trace_id', 77 | 'logging_tracing_enabled', 78 | 'CLOUDWATCH_LOG_GROUP_NAME', 79 | 'CLOUDWATCH_LOG_STREAM_NAME', 80 | 'log_stream_name', 81 | 'log_group_name', 82 | 'function_version', 83 | 'memory', 84 | 'aws_account', 85 | 'cold_start', 86 | 'region', 87 | 'status_code', 88 | 'epsagon-trace-id', 89 | 'epsagon.trace_ids', 90 | ]; 91 | 92 | module.exports.traceUrl = (id, requestTime) => `https://app.epsagon.com/trace/${id}?timestamp=${requestTime}`; 93 | module.exports.lambdaTraceUrl = (awsAccount, region, functionName, requestId, requestTime) => `https://app.epsagon.com/functions/${awsAccount}/${region}/${functionName}?requestId=${requestId}&requestTime=${requestTime}`; 94 | -------------------------------------------------------------------------------- /src/containers/azure.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios-minified'); 2 | const utils = require('../utils'); 3 | const eventIterface = require('../event'); 4 | 5 | let currentAzureLabels = null; 6 | 7 | const AZURE_CHECK_HOST = process.env.AZURE_HOST || 'http://169.254.169.254'; 8 | const PATH = process.env.AZURE_PATH || '/metadata/instance?api-version=2019-06-01'; 9 | const URL = `${AZURE_CHECK_HOST}${PATH}`; 10 | const AZURE_REQUEST_TIMEOUT = process.env.AZURE_REQUEST_TIMEOUT || 3000; 11 | 12 | const parseAzureTags = (tags) => { 13 | const splittedTags = tags.split(';'); 14 | const parsedTags = splittedTags.reduce((result, currentTag) => { 15 | const [key, value] = currentTag.split(':'); 16 | return (key) ? 17 | { 18 | ...result, 19 | [key]: value, 20 | } : result; 21 | }, {}); 22 | 23 | return parsedTags; 24 | }; 25 | 26 | /** 27 | * Load Azure metadata and store it 28 | * @param {Object} cb callback that fired when load is finished. 29 | * @returns {Promise} when resolved will contain the Azure metadata 30 | */ 31 | module.exports.loadAzureMetadata = function loadAzureMetadata(cb) { 32 | if (currentAzureLabels) return Promise.resolve(currentAzureLabels); 33 | 34 | utils.debugLog(`loading azure metadata, url: (${URL})`); 35 | const source = axios.CancelToken.source(); 36 | setTimeout(() => { 37 | source.cancel(); 38 | // Timeout Logic 39 | }, AZURE_REQUEST_TIMEOUT); 40 | 41 | const options = { 42 | headers: { 43 | Metadata: 'True', 44 | }, 45 | timeout: AZURE_REQUEST_TIMEOUT, 46 | cancelToken: source.token, 47 | }; 48 | 49 | return axios.get(URL, options).then((response) => { 50 | utils.debugLog(`Received response: ${response}`); 51 | if (response.status === 200) { 52 | const { 53 | location, 54 | subscriptionId, 55 | tags, 56 | publisher, 57 | } = response.data.compute; 58 | 59 | currentAzureLabels = { 60 | 'azure.location': location, 61 | 'azure.subscription_id': subscriptionId, 62 | 'azure.tags': parseAzureTags(tags), 63 | 'azure.publisher': publisher, 64 | }; 65 | 66 | if (cb) { 67 | cb({ 68 | traceCollectorURL: `http://${location}.atc.epsagon.com`, 69 | }); 70 | } 71 | 72 | utils.debugLog(`Received metadata: ${currentAzureLabels}`); 73 | } 74 | }).catch(() => { 75 | utils.debugLog('Could not load azure metadata'); 76 | }); 77 | }; 78 | 79 | /** 80 | * If the current process is running in azure cloud, 81 | * it will add metadata to trace 82 | * 83 | * @param {Object} runner runner object to add the metadata 84 | */ 85 | module.exports.addAzureMetadata = function addAzureMetadata(runner) { 86 | if (!runner || !currentAzureLabels) return; 87 | eventIterface.addToMetadata(runner, Object.assign({}, currentAzureLabels)); 88 | }; 89 | -------------------------------------------------------------------------------- /src/containers/ec2.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios-minified'); 2 | const utils = require('../utils'); 3 | const eventIterface = require('../event'); 4 | 5 | let currentEC2Labels = null; 6 | 7 | const URL = 'http://169.254.169.254/latest/meta-data/'; 8 | const RESPONSE_LEN_THRESHOLD = 100; 9 | const attributeToGet = ['instance-id', 'instance-type', 'local-ipv4', 'public-hostname', 'public-ipv4']; 10 | const EPSAGON_EC2_REQUEST_TIMEOUT = process.env.EPSAGON_EC2_REQUEST_TIMEOUT || 3000; 11 | 12 | 13 | /** 14 | * Load EC2 metadata and store it 15 | * @returns {Promise} when resolved will contain the EC2 metadata 16 | */ 17 | module.exports.loadEC2Metadata = function loadEC2Metadata() { 18 | if (currentEC2Labels) return Promise.resolve(currentEC2Labels); 19 | 20 | const promises = []; 21 | utils.debugLog('Loading EC2 metadata'); 22 | attributeToGet.forEach((attribute) => { 23 | const source = axios.CancelToken.source(); 24 | setTimeout(() => { 25 | source.cancel(); 26 | }, EPSAGON_EC2_REQUEST_TIMEOUT); 27 | promises.push(axios.get(URL + attribute, { 28 | timeout: EPSAGON_EC2_REQUEST_TIMEOUT, 29 | cancelToken: source.token, 30 | }).then((response) => { 31 | utils.debugLog(`Received response for ${attribute}`); 32 | // In some cases a long, irrelevant HTML response is being returned 33 | if (response.status === 200 && response.data.length < RESPONSE_LEN_THRESHOLD) { 34 | const attributeKey = attribute.replace('-', '_'); 35 | const attributeData = response.data; 36 | if (!currentEC2Labels) currentEC2Labels = {}; 37 | currentEC2Labels[`aws.ec2.${attributeKey}`] = attributeData; 38 | utils.debugLog(`${attributeKey} stored with: ${attributeData}`); 39 | } 40 | return attribute; 41 | }) 42 | .catch(() => { 43 | utils.debugLog(`Could not load EC2 metadata for ${attribute}`); 44 | })); 45 | }); 46 | 47 | return Promise.all(promises); 48 | }; 49 | /** 50 | * If the current process is running in EC2 cloud, 51 | * it will add metadata to trace 52 | * 53 | * @param {Object} runner runner object to add the metadata 54 | */ 55 | module.exports.addEC2Metadata = function addEC2Metadata(runner) { 56 | if (!runner || !currentEC2Labels) return; 57 | eventIterface.addToMetadata(runner, Object.assign({}, currentEC2Labels)); 58 | }; 59 | -------------------------------------------------------------------------------- /src/containers/ecs.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios-minified'); 2 | const utils = require('../utils'); 3 | const eventInterface = require('../event'); 4 | 5 | let currentECSLabels = null; 6 | let currentECSAccount = null; 7 | 8 | 9 | /** 10 | * Check if the current process is running inside 11 | * an ECS container, if so return the ECS_CONTAINER_METADATA_URI 12 | * @returns {string | boolean} ECS_CONTAINER_METADATA_URI if in ECS else false 13 | */ 14 | module.exports.hasECSMetadata = function hasECSMetadata() { 15 | return process.env.ECS_CONTAINER_METADATA_URI || false; 16 | }; 17 | 18 | /** 19 | * Load ECS metadata and store it 20 | * @param {string} uri metadata uri to load, @see {@link hasECSMetadata} to get the uri 21 | * @returns {Promise} when resolved will contain the ECS metadata 22 | */ 23 | module.exports.loadECSMetadata = function loadECSMetadata(uri) { 24 | if (currentECSLabels) return Promise.resolve(currentECSLabels); 25 | 26 | utils.debugLog(`loading ecs meta, url: (${uri})`); 27 | const promises = []; 28 | const labelsPromise = axios.get(uri).then(res => res.data).then((metadata) => { 29 | utils.debugLog(`Received metadata: ${JSON.stringify(metadata)}`); 30 | currentECSLabels = metadata && metadata.Labels; 31 | const cluster = currentECSLabels && currentECSLabels['com.amazonaws.ecs.cluster']; 32 | if (cluster) { 33 | // eslint-disable-next-line prefer-destructuring 34 | currentECSAccount = cluster.split(':')[4]; 35 | } 36 | return currentECSLabels; 37 | }).catch((e) => { 38 | utils.debugLog('error fetching ecs metadata: ', e); 39 | }); 40 | promises.push(labelsPromise); 41 | 42 | return Promise.all(promises); 43 | }; 44 | 45 | /** 46 | * If the current process is running under an ECS container, 47 | * it will add the task-arn of the current task to the metadata field 48 | * of the trace, if its not running under ECS the trace will return unchanged 49 | * 50 | * @param {Object} runner runner object to add the metadata 51 | */ 52 | module.exports.addECSMetadata = function addECSMetadata(runner) { 53 | if (!runner || !currentECSLabels) return; 54 | eventInterface.addToMetadata(runner, { ECS: currentECSLabels }); 55 | eventInterface.addToMetadata(runner, { 'aws.account_id': currentECSAccount }); 56 | }; 57 | -------------------------------------------------------------------------------- /src/containers/k8s.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const fs = require('fs'); 3 | const utils = require('../utils'); 4 | const eventIterface = require('../event'); 5 | 6 | let k8sHostname = null; 7 | let k8sContainerId = null; 8 | 9 | /** 10 | * log invocations of functions 11 | * @param {Function} fn the function to log 12 | * @returns {Any} the result of the function 13 | */ 14 | function logInvocation(fn) { 15 | return (...args) => { 16 | utils.debugLog(`[K8S-LOGS] invoking function: ${fn && fn.name}, time: ${new Date().toUTCString()}`); 17 | const returnValue = fn.apply(this, args); 18 | 19 | utils.debugLog(`[K8S-LOGS] function: ${fn && fn.name} finished execution, result: ${returnValue}, time: ${new Date().toUTCString()}`); 20 | return returnValue; 21 | }; 22 | } 23 | 24 | 25 | /** 26 | * @returns {boolean} true if the current process is running inside 27 | * a K8S container, false otherwise 28 | */ 29 | /* eslint-disable-next-line prefer-arrow-callback */ 30 | module.exports.hasK8sMetadata = logInvocation(function hasK8sMetadata() { 31 | utils.debugLog(`[K8S-LOGS] process.env.KUBERNETES_SERVICE_HOST: ${process.env.KUBERNETES_SERVICE_HOST}`); 32 | return !!(process.env.KUBERNETES_SERVICE_HOST); 33 | }); 34 | 35 | /** 36 | * Load K8S metadata and store it 37 | */ 38 | /* eslint-disable-next-line prefer-arrow-callback */ 39 | module.exports.loadK8sMetadata = logInvocation(function loadK8sMetadata() { 40 | if (!k8sHostname) { 41 | k8sHostname = os.hostname(); 42 | } 43 | 44 | if (!k8sContainerId) { 45 | try { 46 | utils.debugLog('[K8S-LOGS] calling readFile on /proc/self/cgroup'); 47 | 48 | const data = fs.readFileSync('/proc/self/cgroup'); 49 | const firstLineParts = data.toString('utf-8').split('\n')[0].split('/'); 50 | 51 | k8sContainerId = firstLineParts[firstLineParts.length - 1]; 52 | 53 | utils.debugLog('[K8S-LOGS] finished loading K8s metadata'); 54 | } catch (err) { 55 | utils.debugLog('Error loading k8s container id - cannot read cgroup file', err); 56 | } 57 | } 58 | }); 59 | 60 | /** 61 | * If the current process is running under an ECS container, 62 | * it will add the task-arn of the current task to the metadata field 63 | * of the trace, if its not running under ECS the trace will return unchanged 64 | * 65 | * @param {Object} runner runner object to add the metadata 66 | */ 67 | /* eslint-disable-next-line prefer-arrow-callback */ 68 | module.exports.addK8sMetadata = logInvocation(function addK8sMetadata(runner) { 69 | if (!runner || !k8sHostname) return; 70 | const payload = { 71 | is_k8s: true, 72 | k8s_pod_name: k8sHostname, 73 | }; 74 | if (k8sContainerId) { 75 | payload.k8s_container_id = k8sContainerId; 76 | } 77 | 78 | utils.debugLog('[K8S-LOGS] adding K8s metadata to trace'); 79 | eventIterface.addToMetadata(runner, payload); 80 | utils.debugLog('[K8S-LOGS] finished adding K8s metadata to trace'); 81 | }); 82 | -------------------------------------------------------------------------------- /src/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview an interface to {@link proto.event_pb.Event} objects, with useful methods to 3 | * manipulate them 4 | */ 5 | const uuid4 = require('uuid4'); 6 | const utils = require('./utils'); 7 | const errorCode = require('./proto/error_code_pb.js'); 8 | const exception = require('./proto/exception_pb.js'); 9 | const config = require('./config.js'); 10 | const tracer = require('./tracer.js'); 11 | const consts = require('./consts.js'); 12 | const serverlessEvent = require('./proto/event_pb.js'); 13 | 14 | /** 15 | * Sets an event's exception to the given error 16 | * @param {proto.event_pb.Event} event The event the exception is set on 17 | * @param {Error} error The error to set as the exception 18 | * @param {boolean} handled False if the exception was raised by the wrapped function 19 | * @param {boolean} warning True if this exception marked as warning. 20 | */ 21 | module.exports.setException = function setException(event, error, handled = true, warning = false) { 22 | try { 23 | event.setErrorCode(warning ? errorCode.ErrorCode.OK : errorCode.ErrorCode.EXCEPTION); 24 | const userException = new exception.Exception([ 25 | error.name, 26 | error.message, 27 | error.stack, 28 | utils.createTimestamp(), 29 | ]); 30 | event.setException(userException); 31 | userException.getAdditionalDataMap().set('handled', handled); 32 | userException.getAdditionalDataMap().set('warning', warning); 33 | } catch (err) { 34 | tracer.addException(err); 35 | } 36 | }; 37 | 38 | /** 39 | * Add timeout indication to a given event 40 | * @param {proto.event_pb.Event} event The event the timeout is set on 41 | */ 42 | module.exports.markAsTimeout = function setTimeout(event) { 43 | event.setErrorCode(errorCode.ErrorCode.TIMEOUT); 44 | }; 45 | 46 | /** 47 | * Adds items from a map to a resource Metadata 48 | * @param {serverlessEvent.Event} event The event to add the items to 49 | * @param {object} map The map containing the objects 50 | * @param {object} [fullDataMap={}] Additional data to add only if {@link config.metadataOnly} 51 | * is False 52 | */ 53 | module.exports.addToMetadata = function addToMetadata(event, map, fullDataMap = {}) { 54 | const resource = event.getResource(); 55 | const metadataMap = resource && resource.getMetadataMap(); 56 | if (!metadataMap) { 57 | return; 58 | } 59 | 60 | Object.keys(map).forEach((key) => { 61 | metadataMap.set(key, map[key]); 62 | }); 63 | if (!config.getConfig().metadataOnly) { 64 | Object.keys(fullDataMap).forEach((key) => { 65 | metadataMap.set(key, fullDataMap[key]); 66 | }); 67 | } 68 | }; 69 | 70 | 71 | /** 72 | * Adds JSON serialized object to a resource Metadata 73 | * @param {proto.event_pb.Event} event The event to add the items to 74 | * @param {string} key The name of field that is added 75 | * @param {object} object The object to add 76 | * @param {array} [dataFields=[]] List of data fields that should be filtered out 77 | * only if {@link config.metadataOnly} is True 78 | */ 79 | module.exports.addObjectToMetadata = function addObjectToMetadata( 80 | event, 81 | key, 82 | object, 83 | dataFields = [] 84 | ) { 85 | let objectToAdd = object; 86 | if (!config.getConfig().metadataOnly && dataFields.length > 0) { 87 | const fields = Object.getOwnPropertyNames(object).filter( 88 | field => dataFields.includes(field) 89 | ); 90 | objectToAdd = Object.assign(...(fields.map(field => ({ [field]: object[field] })))); 91 | event.getResource().getMetadataMap().set(key, JSON.stringify(objectToAdd)); 92 | } 93 | }; 94 | 95 | /** 96 | * Adds a given label to the metadata map 97 | * @param {proto.event_pb.Event} event The event to add the items to 98 | * @param {string} key key for the added label 99 | * @param {string} value value for the added label 100 | */ 101 | module.exports.addLabelToMetadata = function addLabelToMetadata(event, key, value) { 102 | const currLabels = event.getResource().getMetadataMap().get('labels'); 103 | let labels = null; 104 | if (currLabels !== undefined) { 105 | labels = JSON.parse(currLabels); 106 | labels[key] = value; 107 | } else { 108 | labels = { [key]: value }; 109 | } 110 | 111 | const labelsJson = JSON.stringify(labels); 112 | if (labelsJson.length <= consts.MAX_LABEL_SIZE) { 113 | event.getResource().getMetadataMap().set('labels', labelsJson); 114 | } else { 115 | utils.debugLog(`Max label size exceeded for: ${key}`); 116 | } 117 | }; 118 | 119 | /** 120 | * Create and initialize a new serverless event in the epsagon format. 121 | * @param {string} resourceType resourceType name 122 | * @param {string} name Event name 123 | * @param {string} operation Operation name 124 | * @param {string} origin Origin name 125 | * @returns {Object} Object with serverlessEvent and event started time. 126 | */ 127 | module.exports.initializeEvent = function initializeEvent(resourceType, name, operation, origin) { 128 | const startTime = Date.now(); 129 | const resource = new serverlessEvent.Resource([ 130 | name, 131 | resourceType, 132 | operation, 133 | ]); 134 | const slsEvent = new serverlessEvent.Event([ 135 | `${resourceType}-${uuid4()}`, 136 | utils.createTimestampFromTime(startTime), 137 | null, 138 | origin, 139 | 0, 140 | errorCode.ErrorCode.OK, 141 | ]); 142 | slsEvent.setResource(resource); 143 | return { slsEvent, startTime }; 144 | }; 145 | 146 | /** 147 | * Adding callback data/error to event, and finalize event. 148 | * @param {serverlessEvent.Event} slsEvent Serverless event. 149 | * @param {number} startTime Event start time. 150 | * @param {Error} error Callback error. 151 | * @param {string[] | Object[] | Object} metadata Callback metadata. 152 | * @param {string[] | Object[] | Object} payload Payload(Will only be added when 153 | * metaDataOnly=FALSE). 154 | */ 155 | module.exports.finalizeEvent = function finalizeEvent( 156 | slsEvent, 157 | startTime, 158 | error, 159 | metadata = {}, 160 | payload = {} 161 | ) { 162 | try { 163 | if (error) { 164 | this.setException(slsEvent, error); 165 | } 166 | this.addToMetadata(slsEvent, metadata, payload); 167 | slsEvent.setDuration(utils.createDurationTimestamp(startTime)); 168 | } catch (err) { 169 | tracer.addException(err); 170 | } 171 | }; 172 | 173 | 174 | /** 175 | * Creates a UUID as a trace identifier and adds it to a resource's Metadata. 176 | * @param {proto.event_pb.Event} event The event to add the items to 177 | */ 178 | module.exports.createTraceIdMetadata = function createTraceIdMetadata(event) { 179 | module.exports.addToMetadata(event, { 180 | trace_id: uuid4(), 181 | }); 182 | }; 183 | -------------------------------------------------------------------------------- /src/events/amazon_dax_client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Handlers for the amazon-dax-client js library instrumantation. 3 | */ 4 | const tracer = require('../tracer'); 5 | const serverlessEvent = require('../proto/event_pb.js'); 6 | const eventInterface = require('../event.js'); 7 | const errorCode = require('../proto/error_code_pb.js'); 8 | const utils = require('../utils.js'); 9 | const { dynamoDBEventCreator } = require('./aws_sdk.js'); 10 | const moduleUtils = require('./module_utils'); 11 | 12 | /** 13 | * Wraps the Dax client request methods with tracing 14 | * @param {Function} wrappedFunction The function to wrap 15 | * @returns {Function} The wrapped function 16 | */ 17 | function DAXWrapper(wrappedFunction) { 18 | return function internalDAXWrapper(opname, params, operation, callback) { 19 | const resource = new serverlessEvent.Resource([ 20 | '', 21 | 'dax', 22 | `${opname}`, 23 | ]); 24 | const startTime = Date.now(); 25 | const daxEvent = new serverlessEvent.Event([ 26 | '', 27 | utils.createTimestampFromTime(startTime), 28 | null, 29 | 'amazon-dax-client', 30 | 0, 31 | errorCode.ErrorCode.OK, 32 | ]); 33 | daxEvent.setResource(resource); 34 | try { 35 | dynamoDBEventCreator.requestHandler( 36 | { 37 | params, 38 | operation: opname, 39 | }, 40 | daxEvent 41 | ); 42 | } catch (e) { 43 | tracer.addException(e); 44 | } 45 | const request = wrappedFunction.apply(this, [opname, params, operation, callback]); 46 | try { 47 | const responsePromise = new Promise((resolve) => { 48 | request.once('error', (error) => { 49 | try { 50 | eventInterface.setException(daxEvent, error); 51 | } catch (e) { 52 | tracer.addException(e); 53 | } 54 | if (request.listenerCount('error') === 0) { 55 | throw error; // no error listener, we should explode 56 | } 57 | }).on('complete', (response) => { 58 | try { 59 | daxEvent.setId(`${response.requestId}`); 60 | daxEvent.setDuration(utils.createDurationTimestamp(startTime)); 61 | 62 | if (response.data !== null) { 63 | daxEvent.setErrorCode(errorCode.ErrorCode.OK); 64 | eventInterface.addToMetadata(daxEvent, { 65 | request_id: `${response.requestId}`, 66 | retry_attempts: `${response.retryCount}`, 67 | status_code: `${response.httpResponse.statusCode}`, 68 | }); 69 | 70 | dynamoDBEventCreator.responseHandler( 71 | response, 72 | daxEvent 73 | ); 74 | } 75 | 76 | if (response.error !== null) { 77 | if (daxEvent.getErrorCode() !== errorCode.ErrorCode.EXCEPTION) { 78 | daxEvent.setErrorCode(errorCode.ErrorCode.ERROR); 79 | } 80 | 81 | eventInterface.addToMetadata(daxEvent, { 82 | request_id: `${response.requestId}`, 83 | error_message: `${response.error.message}`, 84 | error_code: `${response.error.code}`, 85 | }); 86 | } 87 | } catch (e) { 88 | tracer.addException(e); 89 | } finally { 90 | resolve(); 91 | } 92 | }); 93 | }); 94 | tracer.addEvent(daxEvent, responsePromise); 95 | } catch (error) { 96 | tracer.addException(error); 97 | } 98 | return request; 99 | }; 100 | } 101 | 102 | module.exports = { 103 | /** 104 | * Initializes the dax tracer 105 | */ 106 | init() { 107 | moduleUtils.patchModule( 108 | 'amazon-dax-client', 109 | '_makeWriteRequestWithRetries', 110 | DAXWrapper, 111 | AmazonDaxClient => AmazonDaxClient.prototype 112 | ); 113 | 114 | moduleUtils.patchModule( 115 | 'amazon-dax-client', 116 | '_makeReadRequestWithRetries', 117 | DAXWrapper, 118 | AmazonDaxClient => AmazonDaxClient.prototype 119 | ); 120 | }, 121 | }; 122 | -------------------------------------------------------------------------------- /src/events/amqp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Handlers for amqp instrumentation 3 | */ 4 | 5 | const tracer = require('../tracer.js'); 6 | const eventInterface = require('../event.js'); 7 | const moduleUtils = require('./module_utils.js'); 8 | const { EPSAGON_HEADER } = require('../consts.js'); 9 | const { generateEpsagonTraceId } = require('../helpers/http'); 10 | 11 | 12 | /** 13 | * Wraps the amqp producer creation and wrapping send function. 14 | * @param {Function} sendFunction The amqp producer function 15 | * @returns {Function} The wrapped function 16 | */ 17 | function amqpProducerWrapper(sendFunction) { 18 | return function internalamqpProducerWrapper(routingKey, data, options, callback) { 19 | let sendResponse; 20 | let sendEvent; 21 | let eventStartTime; 22 | const epsagonId = generateEpsagonTraceId(); 23 | try { 24 | const { slsEvent, startTime } = eventInterface.initializeEvent( 25 | 'rabbitmq', 26 | routingKey, 27 | 'SendMessage', 28 | 'amqplib' 29 | ); 30 | sendEvent = slsEvent; 31 | eventStartTime = startTime; 32 | if (!options || !options.headers) { 33 | // eslint-disable-next-line no-param-reassign 34 | options.headers = {}; 35 | } 36 | // eslint-disable-next-line no-param-reassign 37 | options.headers[EPSAGON_HEADER] = epsagonId; 38 | } catch (err) { 39 | tracer.addException(err); 40 | } 41 | try { 42 | sendResponse = sendFunction.apply(this, [routingKey, data, options, callback]); 43 | } catch (err) { 44 | if (sendEvent) { 45 | eventInterface.setException(sendEvent, err); 46 | tracer.addEvent(sendEvent); 47 | } 48 | throw err; 49 | } 50 | 51 | eventInterface.finalizeEvent( 52 | sendEvent, 53 | eventStartTime, 54 | undefined, 55 | { 56 | exchange: this.name, 57 | host: this.connection.options.host, 58 | vhost: this.connection.options.vhost, 59 | [EPSAGON_HEADER]: epsagonId, 60 | 'messaging.message_payload_size_bytes': data.toString().length, 61 | }, 62 | { 63 | headers: options.headers, 64 | message: JSON.stringify(data), 65 | } 66 | ); 67 | tracer.addEvent(sendEvent); 68 | return sendResponse; 69 | }; 70 | } 71 | 72 | module.exports = { 73 | /** 74 | * Initializes the amqp tracer 75 | */ 76 | init() { 77 | moduleUtils.patchModule( 78 | 'amqp/lib/exchange.js', 79 | 'publish', 80 | amqpProducerWrapper, 81 | amqp => amqp.prototype 82 | ); 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /src/events/amqplib.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Handlers for amqplib instrumentation 3 | */ 4 | 5 | const tracer = require('../tracer.js'); 6 | const eventInterface = require('../event.js'); 7 | const moduleUtils = require('./module_utils.js'); 8 | const { EPSAGON_HEADER } = require('../consts.js'); 9 | const { generateEpsagonTraceId } = require('../helpers/http'); 10 | 11 | 12 | /** 13 | * Wraps the amqplib producer creation and wrapping send function. 14 | * @param {Function} sendFunction The amqplib producer function 15 | * @returns {Function} The wrapped function 16 | */ 17 | function amqplibProducerWrapper(sendFunction) { 18 | return function internalamqplibProducerWrapper(fields, properties, content) { 19 | let sendResponse; 20 | let sendEvent; 21 | let eventStartTime; 22 | const epsagonId = generateEpsagonTraceId(); 23 | try { 24 | const { slsEvent, startTime } = eventInterface.initializeEvent( 25 | 'rabbitmq', 26 | fields.routingKey, 27 | 'SendMessage', 28 | 'amqplib' 29 | ); 30 | sendEvent = slsEvent; 31 | eventStartTime = startTime; 32 | // eslint-disable-next-line no-param-reassign 33 | fields.headers[EPSAGON_HEADER] = epsagonId; 34 | } catch (err) { 35 | tracer.addException(err); 36 | } 37 | try { 38 | sendResponse = sendFunction.apply(this, [fields, properties, content]); 39 | } catch (err) { 40 | if (sendEvent) { 41 | eventInterface.setException(sendEvent, err); 42 | tracer.addEvent(sendEvent); 43 | } 44 | throw err; 45 | } 46 | 47 | eventInterface.finalizeEvent( 48 | sendEvent, 49 | eventStartTime, 50 | undefined, 51 | { 52 | exchange: fields.exchange, 53 | host: this.connection.stream._host, // eslint-disable-line no-underscore-dangle 54 | [EPSAGON_HEADER]: epsagonId, 55 | 'messaging.message_payload_size_bytes': content.toString().length, 56 | }, 57 | { 58 | headers: fields.headers, 59 | message: content.toString(), 60 | } 61 | ); 62 | tracer.addEvent(sendEvent); 63 | return sendResponse; 64 | }; 65 | } 66 | 67 | module.exports = { 68 | /** 69 | * Initializes the amqplib tracer 70 | */ 71 | init() { 72 | moduleUtils.patchModule( 73 | 'amqplib/lib/channel.js', 74 | 'sendMessage', 75 | amqplibProducerWrapper, 76 | amqplib => amqplib.Channel.prototype 77 | ); 78 | }, 79 | }; 80 | -------------------------------------------------------------------------------- /src/events/azure_sdk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Handlers for the azure-sdk js library instrumentation. 3 | */ 4 | const utils = require('../utils.js'); 5 | const tracer = require('../tracer.js'); 6 | const eventInterface = require('../event.js'); 7 | const moduleUtils = require('./module_utils.js'); 8 | 9 | /** 10 | * Wraps the BlockBlobClient upload method. 11 | * @param {Function} wrappedFunction The function to wrap 12 | * @returns {Function} The wrapped function 13 | */ 14 | function blobUploadWrapper(wrappedFunction) { 15 | return function internalUploadWrapper(content, size) { 16 | const { accountName, containerName } = this; 17 | const { slsEvent: uploadEvent, startTime } = eventInterface.initializeEvent( 18 | 'blob_storage', 19 | containerName, 20 | 'upload', 21 | 'azure-sdk' 22 | ); 23 | eventInterface.addToMetadata(uploadEvent, { 24 | 'azure.blob.account_name': accountName, 25 | 'azure.blob.content_size': size, 26 | }, { 'azure.blob.content': content }); 27 | const request = wrappedFunction.apply(this, [content, size]); 28 | const requestPromise = request.then((res) => { 29 | eventInterface.addToMetadata(uploadEvent, { 'azure.blob.error_code': res.errorCode }); 30 | uploadEvent.setDuration(utils.createDurationTimestamp(startTime)); 31 | return res; 32 | }).catch((err) => { 33 | eventInterface.setException(uploadEvent, err); 34 | throw err; 35 | }); 36 | 37 | tracer.addEvent(uploadEvent, requestPromise); 38 | return requestPromise; 39 | }; 40 | } 41 | 42 | /** 43 | * Wraps the BlockBlobClient download method. 44 | * @param {Function} wrappedFunction The function to wrap 45 | * @returns {Function} The wrapped function 46 | */ 47 | function blobDownloadWrapper(wrappedFunction) { 48 | return function internalDownloadWrapper(offset, count, options) { 49 | const { accountName, containerName } = this; 50 | const { slsEvent: downloadEvent, startTime } = eventInterface.initializeEvent( 51 | 'blob_storage', 52 | containerName, 53 | 'download', 54 | 'azure-sdk' 55 | ); 56 | eventInterface.addToMetadata(downloadEvent, { 57 | 'azure.blob.account_name': accountName, 58 | 'azure.blob.container_name': containerName, 59 | 'azure.blob.offset': offset, 60 | }); 61 | const request = wrappedFunction.apply(this, [offset, count, options]); 62 | const requestPromise = request.then((res) => { 63 | eventInterface.addToMetadata(downloadEvent, { 'azure.blob.content_length': res.contentLength }); 64 | downloadEvent.setDuration(utils.createDurationTimestamp(startTime)); 65 | return res; 66 | }).catch((err) => { 67 | eventInterface.setException(downloadEvent, err); 68 | throw err; 69 | }); 70 | tracer.addEvent(downloadEvent, requestPromise); 71 | return requestPromise; 72 | }; 73 | } 74 | 75 | 76 | /** 77 | * Wraps the CosmosDB Item create method. 78 | * @param {Function} wrappedFunction The function to wrap 79 | * @returns {Function} The wrapped function 80 | */ 81 | function cosmosCreateItemWrapper(wrappedFunction) { 82 | return function internalCreateWrapper(body, options) { 83 | const { id: itemId, content } = body; 84 | const { container, clientContext } = this; 85 | const { database } = container; 86 | const name = container.id; 87 | const { slsEvent: createEvent, startTime } = eventInterface.initializeEvent( 88 | 'cosmos_db', 89 | name, 90 | 'create', 91 | 'azure-sdk' 92 | ); 93 | eventInterface.addToMetadata(createEvent, { 94 | 'azure.cosmos.endpoint': clientContext.cosmosClientOptions.endpoint, 95 | 'azure.cosmos.database_id': database.id, 96 | 'azure.cosmos.item_id': itemId, 97 | }, 98 | { 'azure.cosmos.item_content': content }); 99 | const request = wrappedFunction.apply(this, [body, options]); 100 | const requestPromise = request.then((res) => { 101 | eventInterface.addToMetadata(createEvent, { 'azure.cosmos.status_code': res.statusCode }); 102 | createEvent.setDuration(utils.createDurationTimestamp(startTime)); 103 | return res; 104 | }).catch((err) => { 105 | eventInterface.setException(createEvent, err); 106 | throw err; 107 | }); 108 | tracer.addEvent(createEvent, requestPromise); 109 | return requestPromise; 110 | }; 111 | } 112 | 113 | module.exports = { 114 | /** 115 | * Patch Azure SDK methods. 116 | */ 117 | init() { 118 | moduleUtils.patchModule( 119 | '@azure/storage-blob', 120 | 'upload', 121 | blobUploadWrapper, 122 | Clients => Clients.BlockBlobClient.prototype 123 | ); 124 | moduleUtils.patchModule( 125 | '@azure/storage-blob', 126 | 'download', 127 | blobDownloadWrapper, 128 | Clients => Clients.BlockBlobClient.prototype 129 | ); 130 | moduleUtils.patchModule( 131 | '@azure/cosmos', 132 | 'create', 133 | cosmosCreateItemWrapper, 134 | index => index.Items.prototype 135 | ); 136 | }, 137 | }; 138 | -------------------------------------------------------------------------------- /src/events/bunyan.js: -------------------------------------------------------------------------------- 1 | const tracer = require('../tracer.js'); 2 | const moduleUtils = require('./module_utils.js'); 3 | 4 | 5 | /** 6 | * Wrap bunyan logs 7 | * @param {Function} wrappedFunction The function to wrap from bunyan 8 | * @returns {function} emit wrapper function 9 | */ 10 | function emitWrapper(wrappedFunction) { 11 | return function internalEmitWrapper(rec, ...args) { 12 | if (!tracer.isLoggingTracingEnabled()) { 13 | return wrappedFunction.apply(this, [rec].concat(args)); 14 | } 15 | const traceId = tracer.getTraceId(); 16 | if (!traceId) { 17 | return wrappedFunction.apply(this, [rec].concat(args)); 18 | } 19 | 20 | const newRec = { 21 | epsagon: { 22 | trace_id: traceId, 23 | }, 24 | }; 25 | 26 | if (!rec) { 27 | return wrappedFunction.apply(this, [rec].concat(args)); 28 | } 29 | /* eslint-disable guard-for-in, no-restricted-syntax */ 30 | for (const key in rec) { 31 | newRec[key] = rec[key]; 32 | } 33 | 34 | /* eslint-disable no-restricted-syntax */ 35 | for (const symbol of Object.getOwnPropertySymbols(rec)) { 36 | newRec[symbol] = rec[symbol]; 37 | } 38 | 39 | tracer.addLoggingTracingEnabledMetadata(); 40 | 41 | return wrappedFunction.apply(this, [newRec].concat(args)); 42 | }; 43 | } 44 | 45 | module.exports = { 46 | /** 47 | * Initializes the bunyan log tracer 48 | */ 49 | init() { 50 | moduleUtils.patchModule( 51 | 'bunyan', 52 | '_emit', 53 | emitWrapper, 54 | bunyan => bunyan.prototype 55 | ); 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/events/cassandra-driver.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js'); 2 | const tracer = require('../tracer.js'); 3 | const eventInterface = require('../event.js'); 4 | const moduleUtils = require('./module_utils.js'); 5 | const { parse } = require('../resource_utils/sql_utils.js'); 6 | 7 | /** 8 | * Wraps the cassandra send command function with tracing 9 | * @param {Function} wrappedFunction The wrapped function from cassandra module 10 | * @returns {Function} The wrapped function 11 | */ 12 | function cassandraClientWrapper(wrappedFunction) { 13 | return function internalCassandraClientWrapper(query, params, execOptions, cb) { 14 | let executeResponse; 15 | let cassandraEvent; 16 | let eventStartTime; 17 | let table; 18 | let patchedCallback; 19 | let operation = 'execute'; 20 | try { 21 | const parsedQuery = parse(query); 22 | operation = parsedQuery.type; 23 | table = parsedQuery.from.length && parsedQuery.from[0].table; 24 | } catch (err) { 25 | utils.debugLog(`could not extract cassandra operation ${err}`); 26 | } 27 | try { 28 | const { slsEvent, startTime } = eventInterface.initializeEvent( 29 | 'cassandra', 30 | this.options.contactPoints[0], 31 | operation, 32 | 'cassandra-driver' 33 | ); 34 | cassandraEvent = slsEvent; 35 | eventStartTime = startTime; 36 | } catch (err) { 37 | tracer.addException(err); 38 | } 39 | 40 | if (this.options.keyspace) { 41 | eventInterface.addToMetadata(cassandraEvent, { 42 | 'db.cassandra.keyspace': this.options.keyspace, 43 | }); 44 | } 45 | if (this.options.localDataCenter) { 46 | eventInterface.addToMetadata(cassandraEvent, { 47 | 'db.cassandra.coordinator.dc': this.options.localDataCenter, 48 | }); 49 | } 50 | if (table) { 51 | eventInterface.addToMetadata(cassandraEvent, { 'db.cassandra.table': table }); 52 | } 53 | 54 | const responsePromise = new Promise((resolve) => { 55 | patchedCallback = (err, data) => { 56 | let callbackResult; 57 | try { 58 | if (!cassandraEvent) { 59 | utils.debugLog('Could not initialize cassandra, skipping response.'); 60 | return callbackResult; 61 | } 62 | eventInterface.finalizeEvent( 63 | cassandraEvent, 64 | eventStartTime, 65 | err, 66 | { 67 | 'db.name': this.options.contactPoints[0], 68 | 'db.operation': operation, 69 | }, 70 | { 71 | 'db.statement': query, 72 | 'db.cassandra.params': params, 73 | } 74 | ); 75 | } catch (callbackErr) { 76 | tracer.addException(callbackErr); 77 | } finally { 78 | if (cb && typeof cb === 'function') { 79 | callbackResult = cb(err, data); 80 | } 81 | } 82 | resolve(); 83 | return callbackResult; 84 | }; 85 | }); 86 | 87 | try { 88 | executeResponse = wrappedFunction.apply( 89 | this, 90 | [query, params, execOptions, patchedCallback] 91 | ); 92 | } catch (err) { 93 | if (cassandraEvent) { 94 | eventInterface.setException(cassandraEvent, err); 95 | tracer.addEvent(cassandraEvent); 96 | } 97 | throw err; 98 | } 99 | 100 | if (cassandraEvent) { 101 | tracer.addEvent(cassandraEvent, responsePromise); 102 | } 103 | 104 | return executeResponse; 105 | }; 106 | } 107 | 108 | module.exports = { 109 | /** 110 | * Initializes the cassandra tracer 111 | */ 112 | init() { 113 | moduleUtils.patchModule( 114 | 'cassandra-driver/lib/client', 115 | 'execute', 116 | cassandraClientWrapper, 117 | cassandra => cassandra.prototype 118 | ); 119 | }, 120 | }; 121 | -------------------------------------------------------------------------------- /src/events/console.js: -------------------------------------------------------------------------------- 1 | 2 | const config = require('../config.js'); 3 | const tracer = require('../tracer.js'); 4 | const moduleUtils = require('./module_utils'); 5 | const tryRequire = require('../try_require'); 6 | const utils = require('../utils.js'); 7 | const consts = require('../consts.js'); 8 | 9 | const console = tryRequire('console'); 10 | 11 | /** 12 | * Wrap console stdout methods. 13 | * @param {Function} originalFunc the Original stdout function 14 | * @returns {Function} the wrapped function 15 | */ 16 | function wrapConsoleStdoutFunction(originalFunc) { 17 | return function internalWrapConsoleStdoutFunction(...args) { 18 | originalFunc.apply(this, args); 19 | if (args.length === 2) { 20 | const [k, v] = args; 21 | 22 | if (k === consts.LOG_PREFIX) { 23 | return; 24 | } 25 | 26 | if (config.isKeyMatched(config.getConfig().keysToAllow, k)) { 27 | tracer.label(k, v); 28 | } 29 | } 30 | }; 31 | } 32 | 33 | module.exports = { 34 | /** 35 | * Patch Node.JS console functions. 36 | */ 37 | init() { 38 | if (console) { 39 | utils.debugLog('Patching console module'); 40 | [ 41 | 'log', 42 | 'warn', 43 | 'error', 44 | ] 45 | .forEach((method) => { 46 | moduleUtils.patchSingle(console, method, wrapConsoleStdoutFunction); 47 | }); 48 | } 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/events/fs.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const tracer = require('../tracer.js'); 3 | const moduleUtils = require('./module_utils'); 4 | const eventInterface = require('../event.js'); 5 | 6 | /** 7 | * Calling to the fs writeFileSync function without callback, And record the error if thrown. 8 | * @param {Function} original The node fs function. 9 | * @param {number} startTime Event start time. 10 | * @param {serverlessEvent.Event} fsEvent FS event. 11 | * @param {Array} args Array of function arguments. 12 | * @returns {Object} original function response. 13 | */ 14 | function handleFunctionWithoutCallback(original, startTime, fsEvent, args) { 15 | try { 16 | tracer.addEvent(fsEvent); 17 | return original.apply(this, args); 18 | } catch (err) { 19 | eventInterface.finalizeEvent(fsEvent, startTime, err); 20 | throw err; 21 | } 22 | } 23 | 24 | /** 25 | * Wrap node fs requset. 26 | * @param {Function} original The node fs function. 27 | * @param {Function} originalName The node fs function name. 28 | * @returns {Function} The wrapped function 29 | */ 30 | function wrapFsWriteFileFunction(original, originalName) { 31 | return function internalWrapFsWriteFileFunction(file, data, options, callback) { 32 | const fileName = typeof file === 'object' ? file.toString() : file; 33 | const fsCallback = typeof (callback || options) === 'function' && (callback || options); 34 | const { slsEvent: fsEvent, startTime } = eventInterface.initializeEvent('file_system', fileName, originalName, 'file_system'); 35 | 36 | eventInterface.addToMetadata(fsEvent, { 'fs.file': fileName }); 37 | if (!!options && typeof options === 'object') { 38 | eventInterface.addToMetadata(fsEvent, { options }); 39 | } 40 | if (!fsCallback) { 41 | return handleFunctionWithoutCallback(original, startTime, fsEvent, [ 42 | fileName, 43 | data, 44 | options, 45 | ]); 46 | } 47 | let patchedCallback; 48 | let clientRequest; 49 | let clientRequestHasBeenCalled; 50 | try { 51 | const responsePromise = new Promise((resolve) => { 52 | patchedCallback = (err) => { 53 | eventInterface.finalizeEvent(fsEvent, startTime, err); 54 | resolve(); 55 | fsCallback(err); 56 | }; 57 | }); 58 | if (typeof callback === 'function') { 59 | clientRequestHasBeenCalled = true; 60 | clientRequest = original.apply(this, [file, data, options, patchedCallback]); 61 | } else if (typeof options === 'function') { 62 | clientRequestHasBeenCalled = true; 63 | clientRequest = original.apply(this, [file, data, patchedCallback]); 64 | } 65 | tracer.addEvent(fsEvent, responsePromise); 66 | } catch (err) { 67 | tracer.addException(err); 68 | } 69 | 70 | return clientRequest || clientRequestHasBeenCalled ? 71 | clientRequest : 72 | original.apply(this, [file, data, options, callback]); 73 | }; 74 | } 75 | 76 | 77 | module.exports = { 78 | /** 79 | * Patch Node fs methods. 80 | * process.env.EPSAGON_FS_INSTRUMENTATION=true is requird. 81 | */ 82 | init() { 83 | if ((process.env.EPSAGON_FS_INSTRUMENTATION || '').toUpperCase() === 'TRUE') { 84 | moduleUtils.patchSingle(fs, 'writeFile', () => wrapFsWriteFileFunction(fs.writeFile, 'writeFile')); 85 | moduleUtils.patchSingle(fs, 'writeFileSync', () => wrapFsWriteFileFunction(fs.writeFileSync, 'writeFileSync')); 86 | } 87 | }, 88 | }; 89 | -------------------------------------------------------------------------------- /src/events/ioredis.js: -------------------------------------------------------------------------------- 1 | const uuid4 = require('uuid4'); 2 | const serverlessEvent = require('../proto/event_pb.js'); 3 | const utils = require('../utils.js'); 4 | const tracer = require('../tracer.js'); 5 | const errorCode = require('../proto/error_code_pb.js'); 6 | const eventInterface = require('../event.js'); 7 | const moduleUtils = require('./module_utils.js'); 8 | 9 | /** 10 | * Wraps the redis' send command function with tracing 11 | * @param {Function} wrappedFunction The wrapped function from redis module 12 | * @returns {Function} The wrapped function 13 | */ 14 | function redisClientWrapper(wrappedFunction) { 15 | return function internalRedisClientWrapper(command, stream) { 16 | try { 17 | if (this.status !== 'ready') { 18 | // Skipping such events since they are irrelevant / duplicated 19 | return wrappedFunction.apply(this, [command, stream]); 20 | } 21 | 22 | const host = this.options.host || 'local'; 23 | const resource = new serverlessEvent.Resource([ 24 | host, 25 | 'redis', 26 | command.name, 27 | ]); 28 | 29 | const startTime = Date.now(); 30 | 31 | const dbapiEvent = new serverlessEvent.Event([ 32 | `ioredis-${uuid4()}`, 33 | utils.createTimestampFromTime(startTime), 34 | null, 35 | 'redis', 36 | 0, 37 | errorCode.ErrorCode.OK, 38 | ]); 39 | 40 | dbapiEvent.setResource(resource); 41 | 42 | const commandArgs = Array.isArray(command.args) ? command.args : []; 43 | 44 | eventInterface.addToMetadata(dbapiEvent, { 45 | 'Redis Host': host, 46 | 'Redis Port': this.options.port, 47 | 'Redis DB Index': this.options.db || '0', 48 | }, { 49 | 'Command Arguments': commandArgs.map(arg => arg.toString()), 50 | }); 51 | 52 | 53 | const responsePromise = new Promise((resolve) => { 54 | command.promise.then((result) => { 55 | if (result) { 56 | eventInterface.addToMetadata(dbapiEvent, { 57 | 'redis.response': result.toString(), 58 | }); 59 | } 60 | }).catch((err) => { 61 | eventInterface.setException(dbapiEvent, err); 62 | }).finally(() => { 63 | dbapiEvent.setDuration(utils.createDurationTimestamp(startTime)); 64 | resolve(); 65 | }); 66 | }); 67 | tracer.addEvent(dbapiEvent, responsePromise); 68 | } catch (error) { 69 | tracer.addException(error); 70 | } 71 | 72 | return wrappedFunction.apply(this, [command, stream]); 73 | }; 74 | } 75 | 76 | module.exports = { 77 | /** 78 | * Initializes the ioredis tracer 79 | */ 80 | init() { 81 | moduleUtils.patchModule( 82 | 'ioredis', 83 | 'sendCommand', 84 | redisClientWrapper, 85 | redis => redis.prototype 86 | ); 87 | moduleUtils.patchModule( 88 | 'ioredis-move', 89 | 'sendCommand', 90 | redisClientWrapper, 91 | redis => redis.prototype 92 | ); 93 | moduleUtils.patchModule( 94 | 'dy-ioredis/lib/redis.js', 95 | 'sendCommand', 96 | redisClientWrapper, 97 | redis => redis.prototype 98 | ); 99 | }, 100 | }; 101 | -------------------------------------------------------------------------------- /src/events/kafka-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Handlers for kafka-node instrumentation 3 | */ 4 | 5 | const tracer = require('../tracer.js'); 6 | const eventInterface = require('../event.js'); 7 | const utils = require('../utils.js'); 8 | const moduleUtils = require('./module_utils.js'); 9 | const { EPSAGON_HEADER } = require('../consts.js'); 10 | const { generateEpsagonTraceId } = require('../helpers/http'); 11 | 12 | 13 | /** 14 | * Wrap kafka-node producer send function 15 | * @param {Function} sendFunction kafka producer send function. 16 | * @returns {Promise} sendFunction response. 17 | */ 18 | function wrapKafkaSendFunction(sendFunction) { 19 | return function internalKafkaSendFunction(messages, callback) { 20 | let kafkaSendEvent; 21 | let kafkaSendStartTime; 22 | let kafkaSendResponse; 23 | let originalHandlerAsyncError; 24 | let patchedCallback; 25 | const producer = this; 26 | const epsagonId = generateEpsagonTraceId(); 27 | 28 | // Each send operation can contain multiple messages to different topics. At the moment 29 | // we support just one. 30 | const payload = messages[0]; 31 | 32 | try { 33 | const { slsEvent, startTime } = eventInterface.initializeEvent( 34 | 'kafka', 35 | payload.topic, 36 | 'produce', 37 | 'kafka-node' 38 | ); 39 | kafkaSendEvent = slsEvent; 40 | kafkaSendStartTime = startTime; 41 | // eslint-disable-next-line no-param-reassign 42 | messages = messages.map((message) => { 43 | // kafka-node doesn't support headers, 44 | // so we're checking if Epsagon found in a JSON value 45 | try { 46 | if (typeof message.messages === 'string') { 47 | const jsonData = JSON.parse(message.messages); 48 | jsonData[EPSAGON_HEADER] = epsagonId; 49 | // eslint-disable-next-line no-param-reassign 50 | message.messages = JSON.stringify(jsonData); 51 | } else { 52 | const jsonData = JSON.parse(message.messages[0]); 53 | jsonData[EPSAGON_HEADER] = epsagonId; 54 | // eslint-disable-next-line no-param-reassign 55 | message.messages[0] = JSON.stringify(jsonData); 56 | } 57 | } catch (err) { 58 | utils.debugLog('kafka-node - Could not extract epsagon header'); 59 | } 60 | return message; 61 | }); 62 | } catch (err) { 63 | tracer.addException(err); 64 | } 65 | 66 | const responsePromise = new Promise((resolve) => { 67 | patchedCallback = (err, data) => { 68 | let callbackResult; 69 | try { 70 | if (!kafkaSendEvent) { 71 | utils.debugLog('Could not initialize kafka-node, skipping response.'); 72 | return callbackResult; 73 | } 74 | eventInterface.finalizeEvent( 75 | kafkaSendEvent, 76 | kafkaSendStartTime, 77 | originalHandlerAsyncError, 78 | { 79 | [EPSAGON_HEADER]: epsagonId, 80 | host: producer.client.options.kafkaHost, 81 | }, 82 | { 83 | messages: payload.messages, 84 | } 85 | ); 86 | } catch (callbackErr) { 87 | tracer.addException(callbackErr); 88 | } finally { 89 | if (callback && typeof callback === 'function') { 90 | callbackResult = callback(err, data); 91 | } 92 | } 93 | resolve(); 94 | return callbackResult; 95 | }; 96 | }); 97 | 98 | try { 99 | kafkaSendResponse = sendFunction.apply(this, [messages, patchedCallback]); 100 | } catch (err) { 101 | if (kafkaSendEvent) { 102 | eventInterface.setException(kafkaSendEvent, err); 103 | tracer.addEvent(kafkaSendEvent); 104 | } 105 | throw err; 106 | } 107 | 108 | if (kafkaSendEvent) { 109 | tracer.addEvent(kafkaSendEvent, responsePromise); 110 | } 111 | return kafkaSendResponse; 112 | }; 113 | } 114 | 115 | module.exports = { 116 | /** 117 | * Initializes the kafka-node tracer 118 | */ 119 | init() { 120 | moduleUtils.patchModule( 121 | 'kafka-node', 122 | 'send', 123 | wrapKafkaSendFunction, 124 | kafka => kafka.Producer.prototype 125 | ); 126 | }, 127 | }; 128 | -------------------------------------------------------------------------------- /src/events/ldap.js: -------------------------------------------------------------------------------- 1 | const uuid4 = require('uuid4'); 2 | const serverlessEvent = require('../proto/event_pb.js'); 3 | const utils = require('../utils.js'); 4 | const tracer = require('../tracer.js'); 5 | const errorCode = require('../proto/error_code_pb.js'); 6 | const eventInterface = require('../event.js'); 7 | const moduleUtils = require('./module_utils.js'); 8 | 9 | /** 10 | * Wraps the ldap.js bind command function with tracing 11 | * @param {Function} bindFunction The wrapped bind function from ldap.js module 12 | * @returns {Function} The wrapped function 13 | */ 14 | function bindWrapper(bindFunction) { 15 | return function internalBindWrapper(a, b, c, d) { 16 | let callback; 17 | let controls; 18 | let patchedCallback; 19 | 20 | try { 21 | const name = a; 22 | 23 | if (typeof (c) === 'function') { 24 | callback = c; 25 | controls = []; 26 | } else if (typeof (d) === 'function') { 27 | callback = d; 28 | controls = c; 29 | } 30 | utils.debugLog(`LDAP.js bind() wrapper - name: ${name}`); 31 | const resource = new serverlessEvent.Resource([ 32 | this.url.hostname, 33 | 'ldap', 34 | 'bind', 35 | ]); 36 | const startTime = Date.now(); 37 | const bindEvent = new serverlessEvent.Event([ 38 | `ldap-${uuid4()}`, 39 | utils.createTimestampFromTime(startTime), 40 | null, 41 | 'ldap', 42 | 0, 43 | errorCode.ErrorCode.OK, 44 | ]); 45 | bindEvent.setResource(resource); 46 | const tags = utils.flatten({ 47 | enduser: { id: name }, 48 | ldap: { strict_dn: utils.getValueIfExist(this.url, 'strictDN') }, 49 | net: { 50 | transport: 'IP.TCP', 51 | protocol: utils.getValueIfExist(this.url, 'protocol'), 52 | socket_path: utils.getValueIfExist(this.url, 'socketPath'), 53 | timeout: utils.getValueIfExist(this.url, 'timeout'), 54 | connect_timeout: utils.getValueIfExist(this.url, 'connectTimeout'), 55 | tls_options: utils.getValueIfExist(this.url, 'tlsOptions'), 56 | idle_timeout: utils.getValueIfExist(this.url, 'idleTimeout'), 57 | pathname: utils.getValueIfExist(this.url, 'pathname'), 58 | secure: utils.getValueIfExist(this.url, 'secure'), 59 | peer: { 60 | address: utils.getValueIfExist(this.url, 'href'), 61 | hostname: utils.getValueIfExist(this.url, 'hostname'), 62 | port: utils.getValueIfExist(this.url, 'port'), 63 | service: 'ldap', 64 | }, 65 | }, 66 | }); 67 | eventInterface.addToMetadata(bindEvent, tags); 68 | const responsePromise = new Promise((resolve) => { 69 | patchedCallback = (err, res) => { // eslint-disable-line no-param-reassign 70 | // The callback is run when the response for the command is received 71 | bindEvent.setDuration(utils.createDurationTimestamp(startTime)); 72 | 73 | // Note: currently not saving the response 74 | if (err) { 75 | eventInterface.setException(bindEvent, err); 76 | } 77 | 78 | // Resolving to mark this event as complete 79 | resolve(); 80 | if (callback) { 81 | callback(err, res); 82 | } 83 | }; 84 | }); 85 | tracer.addEvent(bindEvent, responsePromise); 86 | } catch (error) { 87 | tracer.addException(error); 88 | } 89 | return bindFunction.apply(this, [a, b, controls, patchedCallback]); 90 | }; 91 | } 92 | 93 | 94 | module.exports = { 95 | /** 96 | * Initializes the ldap.js tracer 97 | */ 98 | init() { 99 | moduleUtils.patchModule( 100 | 'ldapjs', 101 | 'bind', 102 | bindWrapper, 103 | ldapjs => ldapjs.Client.prototype 104 | ); 105 | }, 106 | }; 107 | -------------------------------------------------------------------------------- /src/events/memcached.js: -------------------------------------------------------------------------------- 1 | const uuid4 = require('uuid4'); 2 | const serverlessEvent = require('../proto/event_pb.js'); 3 | const utils = require('../utils.js'); 4 | const tracer = require('../tracer.js'); 5 | const errorCode = require('../proto/error_code_pb.js'); 6 | const eventInterface = require('../event.js'); 7 | const moduleUtils = require('./module_utils.js'); 8 | 9 | /** 10 | * Wraps memcached 'command' function with tracing 11 | * @param {Function} wrappedFunction The wrapped function from memcached module 12 | * @returns {Function} The wrapped function 13 | */ 14 | function memcachedClientWrapper(wrappedFunction) { 15 | return function internalMemcachedClientWrapper(...args) { 16 | try { 17 | const startTime = Date.now(); 18 | const commandObj = args[0]; 19 | const { callback } = commandObj; 20 | const cmdArgs = commandObj(() => ({ 21 | // eslint-disable-next-line no-undef 22 | key: fullkey, 23 | validate: [['key', String], ['callback', Function]], 24 | type: 'get', 25 | // eslint-disable-next-line no-undef 26 | command: `get ${fullkey}`, 27 | })); 28 | const host = this.servers && this.servers.length > 0 ? this.servers[0].split(':') : ['local', 0]; 29 | const hostname = host[0]; 30 | const port = host.length > 1 ? host[1] : 0; 31 | const resource = new serverlessEvent.Resource([ 32 | hostname, 33 | 'memcached', 34 | commandObj.name, 35 | ]); 36 | 37 | const dbapiEvent = new serverlessEvent.Event([ 38 | `memcached-${uuid4()}`, 39 | utils.createTimestampFromTime(startTime), 40 | null, 41 | 'memcached', 42 | 0, 43 | errorCode.ErrorCode.OK, 44 | ]); 45 | 46 | dbapiEvent.setResource(resource); 47 | 48 | eventInterface.addToMetadata(dbapiEvent, { 49 | memcached_hostname: hostname, 50 | memcached_host: port, 51 | }, { 52 | 'Command Arguments': cmdArgs, 53 | }); 54 | 55 | const responsePromise = new Promise((resolve) => { 56 | commandObj.callback = (err, res) => { // eslint-disable-line no-param-reassign 57 | // The callback is run when the response for the command is received 58 | dbapiEvent.setDuration(utils.createDurationTimestamp(startTime)); 59 | 60 | if (err) { 61 | eventInterface.setException(dbapiEvent, err); 62 | } 63 | 64 | // Resolving to mark this event as complete 65 | resolve(); 66 | 67 | if (callback) { 68 | callback(err, res); 69 | } 70 | }; 71 | }); 72 | 73 | tracer.addEvent(dbapiEvent, responsePromise); 74 | } catch (error) { 75 | utils.debugLog('memcahced catch error', error); 76 | tracer.addException(error); 77 | } 78 | 79 | return wrappedFunction.apply(this, [...args]); 80 | }; 81 | } 82 | 83 | module.exports = { 84 | /** 85 | * Initializes the memcached tracer 86 | */ 87 | init() { 88 | moduleUtils.patchModule( 89 | 'memcached/lib/memcached.js', 90 | 'command', 91 | memcachedClientWrapper, 92 | memcached => memcached.prototype 93 | ); 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /src/events/module_utils.js: -------------------------------------------------------------------------------- 1 | const shimmer = require('shimmer'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const tryRequire = require('../try_require'); 5 | const utils = require('../utils'); 6 | const { LAMBDA_DEFAULT_NODE_MODULES_PATH } = require('../consts'); 7 | 8 | let autoNodePaths; 9 | /** 10 | * finds recursively all node_modules sub folders. 11 | * @param {String} dirPath the root folder to start searching. 12 | * @param {Array} arrayOfNodeModulesPaths array of the founded node_modules paths. 13 | * @return {Array} an array of all the founded node_modules sub folders paths 14 | */ 15 | const getAllNodeModulesPaths = (dirPath, arrayOfNodeModulesPaths = []) => { 16 | let arrayOfNodeModulesPathsCopied = arrayOfNodeModulesPaths; 17 | const files = fs.readdirSync(dirPath); 18 | files.forEach((file) => { 19 | if (fs.statSync(`${dirPath}/${file}`).isDirectory()) { 20 | if (file === 'node_modules') { 21 | arrayOfNodeModulesPathsCopied.push(path.join(dirPath, file)); 22 | } else { 23 | arrayOfNodeModulesPathsCopied = getAllNodeModulesPaths(`${dirPath}/${file}`, arrayOfNodeModulesPathsCopied); 24 | } 25 | } 26 | }); 27 | return arrayOfNodeModulesPathsCopied; 28 | }; 29 | 30 | /** 31 | * finds all the instances of a module in the NODE_PATH 32 | * @param {String} id the id of the module to load 33 | * @return {Array} an array of all the module instances in the PATH 34 | */ 35 | module.exports.getModules = function getModules(id) { 36 | const modules = []; 37 | if (typeof require.resolve.paths !== 'function') { 38 | utils.debugLog('require.resolve.paths is not a function'); 39 | // running in a bundler that doesn't support require.resolve.paths(). e.g. webpack. 40 | const module = tryRequire(id); 41 | if (module) { 42 | modules.push(module); 43 | } 44 | return modules; 45 | } 46 | 47 | const searchPaths = require.resolve.paths(id); 48 | if (process.env.EPSAGON_ADD_NODE_PATH) { 49 | searchPaths.push(...process.env.EPSAGON_ADD_NODE_PATH.split(':').map( 50 | item => path.resolve(item.trim()) 51 | )); 52 | } 53 | if (process.env.EPSAGON_AUTO_ADD_NODE_PATHS && 54 | process.env.EPSAGON_AUTO_ADD_NODE_PATHS.toUpperCase() === 'TRUE' 55 | ) { 56 | const rootFolder = path.dirname(require.main.filename); 57 | if (!autoNodePaths) { 58 | autoNodePaths = getAllNodeModulesPaths(rootFolder); 59 | } 60 | utils.debugLog('Found the following paths', autoNodePaths); 61 | autoNodePaths.forEach((nodePath) => { 62 | if (!searchPaths.includes(nodePath)) { 63 | searchPaths.push(nodePath); 64 | } 65 | }); 66 | } 67 | if (utils.isLambdaEnv && !searchPaths.includes(LAMBDA_DEFAULT_NODE_MODULES_PATH)) { 68 | searchPaths.push(LAMBDA_DEFAULT_NODE_MODULES_PATH); 69 | } 70 | 71 | utils.distinct(searchPaths).forEach((searchPath) => { 72 | const modulePath = path.resolve(`${searchPath}/${id}`); 73 | const module = tryRequire(modulePath); 74 | 75 | if (module) { 76 | utils.debugLog('Loaded module', id, searchPath); 77 | modules.push(module); 78 | } 79 | }); 80 | return modules; 81 | }; 82 | 83 | const shimmerPatches = []; 84 | 85 | /** 86 | * Patches all instances of a module 87 | * @param {String} id The module id 88 | * @param {String} methodName the method name 89 | * @param {Function} wrapper The wrapper function 90 | * @param {Function} memberExtractor Extracts the wrapped member from the module 91 | */ 92 | module.exports.patchModule = function patchModule( 93 | id, 94 | methodName, 95 | wrapper, 96 | memberExtractor = (mod => mod) 97 | ) { 98 | utils.debugLog('patching module:', id); 99 | const modules = module.exports.getModules(id); 100 | utils.debugLog('found module copies:', modules.length); 101 | modules.forEach((module) => { 102 | const extracted = memberExtractor(module); 103 | shimmerPatches.push({ id, methodName, module: extracted }); 104 | shimmer.wrap(extracted, methodName, wrapper); 105 | }); 106 | utils.debugLog('done patching module:', id); 107 | }; 108 | 109 | /** 110 | * Patch single module 111 | * @param {any} module the module 112 | * @param {String} methodName the method to patch 113 | * @param {Function} wrapper the wrapper to apply 114 | */ 115 | module.exports.patchSingle = function patchSingle(module, methodName, wrapper) { 116 | shimmerPatches.push({ id: methodName, methodName, module }); 117 | shimmer.wrap(module, methodName, wrapper); 118 | }; 119 | 120 | /** Unpatch all modules */ 121 | module.exports.unpatchModules = function unpatchModules() { 122 | console.log('unpatching all modules'); 123 | 124 | shimmerPatches.forEach((patch) => { 125 | console.log(`unpatching ${patch.methodName} from ${patch.id}`); 126 | shimmer.unwrap(patch.module, patch.methodName); 127 | }); 128 | 129 | console.log('finished unpatching'); 130 | }; 131 | -------------------------------------------------------------------------------- /src/events/mysql.js: -------------------------------------------------------------------------------- 1 | const moduleUtils = require('./module_utils.js'); 2 | const sqlWrapper = require('./sql.js'); 3 | 4 | /** 5 | * Wraps Connection.query function with tracing 6 | * @param {Function} wrappedFunction The function to wrap from mysql 7 | * @returns {Function} The wrapped function 8 | */ 9 | function mysqlQueryWrapper(wrappedFunction) { 10 | return function internalMySqlQueryWrapper(sql, arg1, arg2) { 11 | let queryString; 12 | let callback; 13 | let params; 14 | let overrideInnerCallback = false; 15 | if (typeof sql !== 'string') { 16 | queryString = sql.sql; 17 | } else { 18 | queryString = sql; 19 | } 20 | 21 | if (sql.onResult) { 22 | params = sql.values; 23 | callback = sql.onResult; 24 | } else { 25 | ({ params, callback } = sqlWrapper.parseQueryArgs(arg1, arg2)); 26 | } 27 | 28 | if (callback === undefined && sql._callback) { // eslint-disable-line no-underscore-dangle 29 | // In pool connection, no callback passed, but _callback is being used. 30 | callback = sql._callback; // eslint-disable-line no-underscore-dangle 31 | overrideInnerCallback = true; 32 | } 33 | 34 | const patchedCallback = sqlWrapper.wrapSqlQuery( 35 | queryString, 36 | params, 37 | callback, 38 | this.config, 39 | 'mysql' 40 | ); 41 | if (sql.onResult) { 42 | sql.onResult = patchedCallback; // eslint-disable-line 43 | } else { 44 | callback = patchedCallback; 45 | } 46 | if (overrideInnerCallback) { 47 | // eslint-disable-next-line no-underscore-dangle,no-param-reassign 48 | sql._callback = patchedCallback; 49 | } 50 | return wrappedFunction.apply(this, [sql, params, callback]); 51 | }; 52 | } 53 | 54 | module.exports = { 55 | /** 56 | * Initializes the mysql tracer 57 | */ 58 | init() { 59 | moduleUtils.patchModule( 60 | 'mysql2', 61 | 'query', 62 | mysqlQueryWrapper, 63 | mysql2 => mysql2.Connection.prototype 64 | ); 65 | 66 | moduleUtils.patchModule( 67 | 'mysql2', 68 | 'execute', 69 | mysqlQueryWrapper, 70 | mysql2 => mysql2.Connection.prototype 71 | ); 72 | 73 | moduleUtils.patchModule( 74 | 'mysql/lib/Connection.js', 75 | 'query', 76 | mysqlQueryWrapper, 77 | mysqlConnection => mysqlConnection.prototype 78 | ); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /src/events/openwhisk.js: -------------------------------------------------------------------------------- 1 | const uuid4 = require('uuid4'); 2 | const utils = require('../utils.js'); 3 | const tracer = require('../tracer.js'); 4 | const serverlessEvent = require('../proto/event_pb.js'); 5 | const eventInterface = require('../event.js'); 6 | const errorCode = require('../proto/error_code_pb.js'); 7 | const moduleUtils = require('./module_utils.js'); 8 | 9 | /** 10 | * Wraps the openwhisk module. 11 | * @param {Function} wrappedFunction The openwhisk module 12 | * @returns {Function} The wrapped function 13 | */ 14 | function openWhiskWrapper(wrappedFunction) { 15 | return function internalOWWrapper(options, callback) { 16 | const { name } = options; 17 | const fullName = `/${process.env['__OW_NAMESPACE']}/${name || options}`; // eslint-disable-line dot-notation 18 | const resource = new serverlessEvent.Resource([ 19 | fullName, 20 | 'openwhisk_action', 21 | 'invoke', 22 | ]); 23 | const startTime = Date.now(); 24 | const invokeEvent = new serverlessEvent.Event([ 25 | `openwhisk-${uuid4()}`, 26 | utils.createTimestampFromTime(startTime), 27 | null, 28 | 'openwhisk', 29 | 0, 30 | errorCode.ErrorCode.OK, 31 | ]); 32 | 33 | invokeEvent.setResource(resource); 34 | eventInterface.addToMetadata(invokeEvent, { 35 | api_host: options.apihost || process.env['__OW_API_HOST'], // eslint-disable-line dot-notation 36 | namespace: options.namespace || process.env['__OW_NAMESPACE'], // eslint-disable-line dot-notation 37 | }, { 38 | params: options.params, 39 | }); 40 | let request; 41 | let response; 42 | if (options.result) { 43 | // action.invoke would return directly `response.result` so we would loose some 44 | // of the information from the response. 45 | const opts = { 46 | ...options, 47 | result: false, 48 | }; 49 | request = wrappedFunction.apply(this, [opts, callback]); 50 | // ensure we return the originally requested form 51 | response = request.then(res => res.response.result); 52 | } else { 53 | request = wrappedFunction.apply(this, [options, callback]); 54 | response = request; 55 | } 56 | 57 | const responsePromise = new Promise((resolve) => { 58 | request.then((res) => { 59 | let resp = res.response; 60 | if (resp && resp.result && resp.result.body && resp.result.body.length > 100) { 61 | // create copy so we can trim the long response body 62 | resp = Object.assign({}, resp); 63 | resp.result = Object.assign({}, resp.result); 64 | resp.result.body = `${resp.result.body.substring(0, 100)}...(truncated)`; 65 | } 66 | const lastActivationId = resp.result && resp.result.headers && resp.result.headers['x-last-activation-id']; 67 | const brief = { 68 | activation_id: lastActivationId || res.activationId, 69 | status: resp.status, 70 | result_statusCode: resp.result && resp.result.statusCode, 71 | }; 72 | eventInterface.addToMetadata( 73 | invokeEvent, 74 | brief, 75 | { 76 | response: resp, 77 | } 78 | ); 79 | invokeEvent.setDuration(utils.createDurationTimestamp(startTime)); 80 | }).catch((err) => { 81 | eventInterface.setException(invokeEvent, err); 82 | }).finally(() => { 83 | resolve(); 84 | }); 85 | }); 86 | 87 | tracer.addEvent(invokeEvent, responsePromise); 88 | return response; 89 | }; 90 | } 91 | 92 | module.exports = { 93 | /** 94 | * Initializes the openwhisk tracer 95 | */ 96 | init() { 97 | moduleUtils.patchModule( 98 | 'openwhisk/lib/actions.js', 99 | 'invoke', 100 | openWhiskWrapper, 101 | actions => actions.prototype 102 | ); 103 | }, 104 | }; 105 | -------------------------------------------------------------------------------- /src/events/pg.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js'); 2 | const sqlWrapper = require('./sql.js'); 3 | const moduleUtils = require('./module_utils.js'); 4 | 5 | const pgPath = process.env.EPSAGON_PG_PATH ? `${process.cwd()}${process.env.EPSAGON_PG_PATH}` : 'pg'; 6 | /** 7 | * Wraps the pg's module request function with tracing 8 | * @param {Function} wrappedFunction The pg's module 9 | * @returns {Function} The wrapped function 10 | */ 11 | function pgClientWrapper(wrappedFunction) { 12 | return function internalPgClientWrapper(queryString, arg1, arg2) { 13 | if (queryString && queryString.submit) { 14 | // this is a Submittable instance, not supported yet - return as is. 15 | return wrappedFunction.apply(this, [queryString, arg1, arg2]); 16 | } 17 | 18 | const parseResult = sqlWrapper.parseQueryArgs(arg1, arg2); 19 | let { params } = parseResult; 20 | const { callback } = parseResult; 21 | 22 | let sqlString = queryString; 23 | let sqlParams = params; 24 | if (queryString && queryString.text) { 25 | // this is a query object, use the values inside it. 26 | sqlString = queryString.text; 27 | if (queryString.values && params && !params.length) { 28 | // values are in the object 29 | params = undefined; 30 | sqlParams = queryString.values; 31 | } 32 | } 33 | 34 | let patchedCallback = sqlWrapper.wrapSqlQuery( 35 | sqlString, 36 | sqlParams, 37 | callback, 38 | this.connectionParameters || this._clients[0], // eslint-disable-line 39 | 'pg' 40 | ); 41 | 42 | 43 | if (callback) { 44 | // it's safe to use callback, user not expecting a Promise. 45 | return wrappedFunction.apply(this, [queryString, params, patchedCallback]); 46 | } 47 | 48 | // verify we have a patched callback; 49 | patchedCallback = patchedCallback || (() => {}); 50 | // we need to return a Promise. we can't pass patchedCallback or a Promise won't be returned 51 | const responsePromise = wrappedFunction.apply(this, [queryString, params]); 52 | 53 | if (!(responsePromise && typeof responsePromise.then === 'function')) { 54 | // the return value is not a promise. This is an old version 55 | // call patchedCallback now or it will never be called 56 | // using empty result 57 | patchedCallback(null, null, null); 58 | } 59 | 60 | // we got a promise. call patchedCallback when it resolves/rejects. 61 | return responsePromise.then((res) => { 62 | patchedCallback(null, res, null); 63 | return res; 64 | }, (err) => { 65 | patchedCallback(err, null, null); 66 | throw err; 67 | }); 68 | }; 69 | } 70 | 71 | module.exports = { 72 | /** 73 | * Initializes the pg tracer 74 | */ 75 | init() { 76 | if (process.env.EPSAGON_PG_PATH) { 77 | utils.debugLog(`EPSAGON_PG_PATH=${process.env.EPSAGON_PG_PATH}`); 78 | utils.debugLog(`cwd=${process.cwd()}`); 79 | } 80 | 81 | moduleUtils.patchModule( 82 | pgPath, 83 | 'query', 84 | pgClientWrapper, 85 | pg => pg.Client.prototype 86 | ); 87 | moduleUtils.patchModule( 88 | 'pg-pool', 89 | 'query', 90 | pgClientWrapper, 91 | Pool => Pool.prototype 92 | ); 93 | }, 94 | }; 95 | -------------------------------------------------------------------------------- /src/events/pino.js: -------------------------------------------------------------------------------- 1 | const tracer = require('../tracer.js'); 2 | const moduleUtils = require('./module_utils.js'); 3 | 4 | 5 | /** 6 | * Wrap pino logs 7 | * @param {Function} wrappedFunction The function to wrap from winston 8 | * @returns {function} asJson wrapper function 9 | */ 10 | function logWrapper(wrappedFunction) { 11 | return function internalLogWrapper(obj, msg, num, time) { 12 | if (!tracer.isLoggingTracingEnabled()) { 13 | return wrappedFunction.apply(this, [obj, msg, num, time]); 14 | } 15 | const traceId = tracer.getTraceId(); 16 | if (!traceId) { 17 | return wrappedFunction.apply(this, [obj, msg, num, time]); 18 | } 19 | 20 | /* eslint-disable no-param-reassign */ 21 | obj.epsagon = { 22 | trace_id: traceId, 23 | }; 24 | 25 | tracer.addLoggingTracingEnabledMetadata(); 26 | 27 | return wrappedFunction.apply(this, [obj, msg, num, time]); 28 | }; 29 | } 30 | 31 | module.exports = { 32 | /** 33 | * Initializes the pino log tracer 34 | */ 35 | init() { 36 | moduleUtils.patchModule( 37 | 'pino/lib/tools', 38 | 'asJson', 39 | logWrapper 40 | ); 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/events/redis.js: -------------------------------------------------------------------------------- 1 | const uuid4 = require('uuid4'); 2 | const serverlessEvent = require('../proto/event_pb.js'); 3 | const utils = require('../utils.js'); 4 | const tracer = require('../tracer.js'); 5 | const errorCode = require('../proto/error_code_pb.js'); 6 | const eventInterface = require('../event.js'); 7 | const moduleUtils = require('./module_utils.js'); 8 | 9 | /** 10 | * Wraps the redis' send command function with tracing 11 | * @param {Function} wrappedFunction The wrapped function from redis module 12 | * @returns {Function} The wrapped function 13 | */ 14 | function redisClientWrapper(wrappedFunction) { 15 | return function internalRedisClientWrapper(commandObj) { 16 | try { 17 | // This is used to prevent duplicates command tracing. In this case, 18 | // the command won't be executed until the client is able to do it, 19 | // and the wrapped internal function will be called again. 20 | if (this.ready === false || this.stream.writable === false) { 21 | return wrappedFunction.apply(this, [commandObj]); 22 | } 23 | 24 | const { callback } = commandObj; 25 | 26 | const host = this.connection_options.host || 'local'; 27 | const resource = new serverlessEvent.Resource([ 28 | this.connection_options.host || 'local', 29 | 'redis', 30 | commandObj.command, 31 | ]); 32 | 33 | const startTime = Date.now(); 34 | 35 | const dbapiEvent = new serverlessEvent.Event([ 36 | `redis-${uuid4()}`, 37 | utils.createTimestampFromTime(startTime), 38 | null, 39 | 'redis', 40 | 0, 41 | errorCode.ErrorCode.OK, 42 | ]); 43 | 44 | dbapiEvent.setResource(resource); 45 | 46 | eventInterface.addToMetadata(dbapiEvent, { 47 | 'Redis Host': host, 48 | 'Redis Port': this.connection_options.port, 49 | 'Redis DB Index': this.connection_options.db || '0', 50 | }, { 51 | 'Command Arguments': commandObj.args, 52 | }); 53 | 54 | const responsePromise = new Promise((resolve) => { 55 | commandObj.callback = (err, res) => { // eslint-disable-line no-param-reassign 56 | // The callback is run when the response for the command is received 57 | dbapiEvent.setDuration(utils.createDurationTimestamp(startTime)); 58 | 59 | // Note: currently not saving the response 60 | if (err) { 61 | eventInterface.setException(dbapiEvent, err); 62 | } 63 | 64 | // Resolving to mark this event as complete 65 | resolve(); 66 | 67 | if (callback) { 68 | callback(err, res); 69 | } 70 | }; 71 | }); 72 | 73 | tracer.addEvent(dbapiEvent, responsePromise); 74 | } catch (error) { 75 | tracer.addException(error); 76 | } 77 | 78 | return wrappedFunction.apply(this, [commandObj]); 79 | }; 80 | } 81 | 82 | module.exports = { 83 | /** 84 | * Initializes the Redis tracer 85 | */ 86 | init() { 87 | moduleUtils.patchModule( 88 | 'redis', 89 | 'internal_send_command', 90 | redisClientWrapper, 91 | redis => redis.RedisClient.prototype 92 | ); 93 | }, 94 | }; 95 | -------------------------------------------------------------------------------- /src/events/sql.js: -------------------------------------------------------------------------------- 1 | const uuid4 = require('uuid4'); 2 | const { parse } = require('../resource_utils/sql_utils.js'); 3 | const utils = require('../utils.js'); 4 | const tracer = require('../tracer.js'); 5 | const serverlessEvent = require('../proto/event_pb.js'); 6 | const eventInterface = require('../event.js'); 7 | const errorCode = require('../proto/error_code_pb.js'); 8 | const epsagonConfig = require('../config.js'); 9 | const consts = require('../consts.js'); 10 | 11 | const MAX_QUERY_SIZE = 2048; 12 | const MAX_PARAMS_LENGTH = 5; 13 | 14 | 15 | /** 16 | * Parse query arguments - get the callback and params 17 | * @param {Array|Function} arg1 First argument 18 | * @param {Function} arg2 Second argument 19 | * @returns {{params: Array, callback: Function}} The callback and params 20 | */ 21 | module.exports.parseQueryArgs = function parseQueryArgs(arg1, arg2) { 22 | const paramNotSet = (arg2 === undefined && arg1 instanceof Function); 23 | const callback = (paramNotSet) ? arg1 : arg2; 24 | const params = (paramNotSet) ? [] : arg1; 25 | 26 | return { params, callback }; 27 | }; 28 | 29 | /** 30 | * Wrap SQL query call with tracing 31 | * @param {string} queryString The executed SQL command 32 | * @param {Array} params The params argument (values) 33 | * @param {Function} callback The callback argument (cb) 34 | * @param {Object} config The connection config object 35 | * @param {string} driver The database driver type (mysql/pg/..) 36 | * @returns {Array} The arguments 37 | */ 38 | module.exports.wrapSqlQuery = function wrapSqlQuery(queryString, params, callback, config, driver) { 39 | let patchedCallback; 40 | 41 | try { 42 | let sqlObj = {}; 43 | try { 44 | // Sanitizing query. 45 | let queryStringSan = queryString.split('`').join(''); 46 | if (queryStringSan.endsWith(';')) { 47 | queryStringSan = queryStringSan.substr(0, queryStringSan.length - 1); 48 | } 49 | sqlObj = parse(queryStringSan); 50 | } catch (error) { 51 | sqlObj.type = 'SQL-Command'; 52 | sqlObj.tables = ['Could not parse table name']; 53 | } 54 | 55 | const { type } = sqlObj; 56 | let { tables } = sqlObj; 57 | if (!tables) { 58 | tables = sqlObj.from.map(f => f.table); 59 | } 60 | 61 | const { database, host } = config; 62 | 63 | let resourceType = 'sql'; 64 | if (host.match('.rds.')) { resourceType = 'rds'; } 65 | if (host.match('.redshift.')) { resourceType = 'redshift'; } 66 | 67 | const resource = new serverlessEvent.Resource([ 68 | database, // name of the database 69 | resourceType, 70 | type, 71 | ]); 72 | 73 | const startTime = Date.now(); 74 | 75 | const dbapiEvent = new serverlessEvent.Event([ 76 | `dbapi-${uuid4()}`, 77 | utils.createTimestampFromTime(startTime), 78 | null, 79 | driver, 80 | 0, 81 | errorCode.ErrorCode.OK, 82 | ]); 83 | 84 | dbapiEvent.setResource(resource); 85 | eventInterface.addToMetadata(dbapiEvent, { 86 | Host: host, 87 | Driver: driver, 88 | Type: type, 89 | 'Table Name': (tables.length === 1 ? tables[0] : tables), 90 | }, { 91 | Query: queryString.substring(0, MAX_QUERY_SIZE), 92 | }); 93 | if (params && params.length) { 94 | eventInterface.addToMetadata(dbapiEvent, { }, { 95 | Params: params.slice(0, MAX_PARAMS_LENGTH), 96 | }); 97 | } 98 | 99 | const responsePromise = new Promise((resolve) => { 100 | patchedCallback = (err, res, fields) => { 101 | utils.debugLog('SQL Patched callback was called'); 102 | dbapiEvent.setDuration(utils.createDurationTimestamp(startTime)); 103 | 104 | if (err) { 105 | eventInterface.setException(dbapiEvent, err); 106 | } else { 107 | let { rowCount, rows } = res; 108 | if (!rowCount && res instanceof Array) { 109 | rowCount = res.length; 110 | rows = res; 111 | } 112 | eventInterface.addToMetadata(dbapiEvent, { rowCount }); 113 | const ignoredTables = epsagonConfig.getConfig().ignoredDBTables; 114 | if (rowCount && 115 | rows instanceof Array && 116 | rows.length && 117 | !tables.some(t => epsagonConfig.isKeyMatched(ignoredTables, t)) 118 | ) { 119 | if (rows.length > consts.MAX_QUERY_ELEMENTS) { 120 | eventInterface.addToMetadata(dbapiEvent, { is_trimmed: true }); 121 | } 122 | eventInterface.addToMetadata( 123 | dbapiEvent, 124 | { 'sql.rows': rows.slice(0, consts.MAX_QUERY_ELEMENTS) } 125 | ); 126 | } 127 | } 128 | 129 | resolve(); 130 | 131 | if (callback) { 132 | callback(err, res, fields); 133 | } 134 | }; 135 | }); 136 | 137 | tracer.addEvent(dbapiEvent, responsePromise); 138 | } catch (error) { 139 | tracer.addException(error); 140 | } 141 | 142 | return patchedCallback || callback; 143 | }; 144 | -------------------------------------------------------------------------------- /src/events/tencent-cos.js: -------------------------------------------------------------------------------- 1 | const utils = require('../utils.js'); 2 | const tracer = require('../tracer.js'); 3 | const eventInterface = require('../event.js'); 4 | const moduleUtils = require('./module_utils.js'); 5 | 6 | /** 7 | * Wraps the Tencent COS module. 8 | * @param {Function} wrappedFunction original functions 9 | * @returns {Function} The wrapped function 10 | */ 11 | function tencentCOSWrapper(wrappedFunction) { 12 | return function internalTencentCOSWrapper(cos) { 13 | const result = wrappedFunction.apply(this, [cos]); 14 | // eslint-disable-next-line no-underscore-dangle 15 | const originalAddTask = cos._addTask; 16 | // eslint-disable-next-line no-underscore-dangle, no-param-reassign 17 | cos._addTask = (api, params, callback, ignoreAddEvent) => { 18 | const { slsEvent, startTime } = eventInterface.initializeEvent( 19 | 'cos', 20 | params.Bucket, 21 | api, 22 | 'tencent-cos' 23 | ); 24 | eventInterface.addToMetadata(slsEvent, { 25 | 'tencent.region': params.Region, 26 | 'tencent.cos.object_key': params.Key, 27 | 'tencent.cos.object_path': params.FilePath, 28 | }); 29 | let patchedCallback = callback; 30 | const responsePromise = new Promise((resolve) => { 31 | patchedCallback = (err, data) => { 32 | slsEvent.setDuration(utils.createDurationTimestamp(startTime)); 33 | eventInterface.addToMetadata(slsEvent, { 34 | 'tencent.cos.request_id': data.headers['x-cos-request-id'], 35 | 'tencent.status_code': data.statusCode, 36 | }); 37 | if (err) { 38 | eventInterface.setException(slsEvent, err); 39 | } 40 | resolve(); 41 | return callback(err, data); 42 | }; 43 | }); 44 | tracer.addEvent(slsEvent, responsePromise); 45 | return originalAddTask(api, params, patchedCallback, ignoreAddEvent); 46 | }; 47 | return result; 48 | }; 49 | } 50 | 51 | module.exports = { 52 | /** 53 | * Initializes the Tencent COS tracer 54 | */ 55 | init() { 56 | moduleUtils.patchModule( 57 | 'cos-nodejs-sdk-v5/sdk/task.js', 58 | 'init', 59 | tencentCOSWrapper 60 | ); 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/events/winston.js: -------------------------------------------------------------------------------- 1 | const tracer = require('../tracer.js'); 2 | const moduleUtils = require('./module_utils.js'); 3 | const utils = require('../utils'); 4 | 5 | /** 6 | * returns the trace id if message should be traced, or null if not. 7 | * @param {Object} chunk the chunk to add id to 8 | * @return {string|null} The trace id, or null if the message shouldn't 9 | * be traced 10 | */ 11 | function getTraceIdIfShouldTrace(chunk) { 12 | if (!chunk || typeof chunk !== 'object' || !tracer.isLoggingTracingEnabled()) { 13 | return null; 14 | } 15 | 16 | return tracer.getTraceId(); 17 | } 18 | /** 19 | * Wrap bunyan logs 20 | * @param {Function} wrappedFunction The function to wrap from bunyan 21 | * @returns {function} emit wrapper function 22 | */ 23 | function writeWrapper(wrappedFunction) { 24 | return function internalWriteWrapper(chunk, encoding, callback) { 25 | utils.debugLog('in internal winston write'); 26 | const traceId = getTraceIdIfShouldTrace(chunk); 27 | utils.debugLog(`winston traceId=${traceId}`); 28 | if (!traceId) { 29 | return wrappedFunction.apply(this, [chunk, encoding, callback]); 30 | } 31 | 32 | const newChunk = { 33 | epsagon: { 34 | trace_id: traceId, 35 | }, 36 | }; 37 | /* eslint-disable guard-for-in, no-restricted-syntax */ 38 | for (const key in chunk) { 39 | newChunk[key] = chunk[key]; 40 | } 41 | 42 | /* eslint-disable no-restricted-syntax */ 43 | for (const symbol of Object.getOwnPropertySymbols(chunk)) { 44 | newChunk[symbol] = chunk[symbol]; 45 | } 46 | 47 | tracer.addLoggingTracingEnabledMetadata(); 48 | utils.debugLog('finish internal winston write'); 49 | return wrappedFunction.apply(this, [newChunk, encoding, callback]); 50 | }; 51 | } 52 | 53 | module.exports = { 54 | /** 55 | * Initializes the bunyan log tracer 56 | */ 57 | init() { 58 | moduleUtils.patchModule( 59 | 'winston/lib/winston/logger', 60 | 'write', 61 | writeWrapper, 62 | Logger => Logger.prototype 63 | ); 64 | }, 65 | }; 66 | -------------------------------------------------------------------------------- /src/events/winston_cloudwatch.js: -------------------------------------------------------------------------------- 1 | const Hook = require('require-in-the-middle'); 2 | const utils = require('../utils'); 3 | const tracer = require('../tracer'); 4 | const tryRequire = require('../try_require'); 5 | 6 | const AWS = tryRequire('aws-sdk/global'); 7 | const STS = tryRequire('aws-sdk/clients/sts'); 8 | 9 | const logDestinations = []; 10 | 11 | 12 | /** 13 | * loads data from aws metadata to additional tags 14 | * @param {Object} options options cloudwatch winston was initialized with 15 | * @returns {Promise} resolves when metadata was loaded with the destination 16 | */ 17 | function loadAWSLogDestination(options) { 18 | if (!AWS) { 19 | return Promise.resolve(); 20 | } 21 | 22 | const destination = {}; 23 | destination.log_group_name = options.logGroupName; 24 | destination.log_stream_name = options.logStreamName; 25 | const sts = new STS(); 26 | const cwConfig = (options.cloudWatchLogs && options.cloudWatchLogs.config) || {}; 27 | destination.region = ( 28 | options.awsRegion || 29 | cwConfig.region || 30 | AWS.config.region || 31 | process.env.AWS_REGION 32 | ); 33 | return sts.getCallerIdentity().promise().then((data) => { 34 | destination.account_id = data.Account; 35 | return destination; 36 | }); 37 | } 38 | 39 | 40 | /** 41 | * Capture winston-cloudwatch require 42 | * @param {Function} exports returned from requiring the module 43 | * @returns {Object} overriden exports 44 | */ 45 | function onWinstonCloudwatchRequire(exports) { 46 | utils.debugLog('winston-cloudwatch required'); 47 | if (typeof exports !== 'function') { 48 | utils.debugLog('winston-cloudwatch got non-function exports', typeof exports, exports); 49 | return exports; 50 | } 51 | // eslint-disable-next-line no-underscore-dangle 52 | if (exports.__epsagon_wrapped) { 53 | utils.debugLog('winston-cloudwatch already hooked'); 54 | return exports; 55 | } 56 | 57 | // eslint-disable-next-line no-underscore-dangle,require-jsdoc 58 | function wrapper(...args) { 59 | const [options] = args; 60 | try { 61 | utils.debugLog('winston-cloudwatch instance created'); 62 | loadAWSLogDestination(options) 63 | .then(dest => logDestinations.push(dest)) 64 | .catch(err => tracer.addException(err)); 65 | } catch (e) { 66 | utils.debugLog('failed to set cloudwatch-winston log parameters', e); 67 | tracer.addException(e); 68 | } 69 | return exports.apply(this, args); 70 | } 71 | 72 | wrapper.prototype = Object.create(exports.prototype); 73 | 74 | // eslint-disable-next-line no-param-reassign,no-underscore-dangle 75 | exports.__epsagon_wrapped = true; 76 | 77 | utils.debugLog('winston-cloudwatch require patching done'); 78 | return wrapper; 79 | } 80 | 81 | module.exports = { 82 | /** 83 | * Initializes the bunyan log tracer 84 | */ 85 | init() { 86 | utils.debugLog('hooking winston-cloudwatch'); 87 | Hook(['winston-cloudwatch'], onWinstonCloudwatchRequire); 88 | utils.debugLog('hooked winston-cloudwatch'); 89 | }, 90 | 91 | /** 92 | * @return {Array} log destinations for winston-cloudwatch 93 | */ 94 | additionalMetadata() { 95 | return logDestinations.length ? { 96 | 'aws.cloudwatch.log_destinations': logDestinations, 97 | } : {}; 98 | }, 99 | }; 100 | -------------------------------------------------------------------------------- /src/helpers/events.js: -------------------------------------------------------------------------------- 1 | const consts = require('../consts.js'); 2 | 3 | /** 4 | * Checks if a URL is in the blacklist 5 | * @param {string} url The URL to check 6 | * @param {object} urlBlacklist Object of blacklist url objects (KEY=[url], VALUE=[condition]). 7 | * @param {string} path The Path to check (optional) 8 | * @returns {boolean} True if it is in the blacklist, False otherwise 9 | */ 10 | const isBlacklistURL = (url, urlBlacklist, path) => Object.keys(urlBlacklist).some((key) => { 11 | if (typeof urlBlacklist[key] === typeof (() => {})) { 12 | return urlBlacklist[key](url, key, path); 13 | } 14 | return url[urlBlacklist[key]](key); 15 | }); 16 | 17 | /** 18 | * Checks if a user agent header is in the blacklist 19 | * @param {string} headers The Headers to check. 20 | * @param {Array} userAgentsBlacklist Array of blacklist user agents. 21 | * @returns {boolean} True if it is in the blacklist, False otherwise 22 | */ 23 | const isBlacklistHeader = (headers, userAgentsBlacklist) => { 24 | if (headers) { 25 | return userAgentsBlacklist.includes(headers['user-agent']); 26 | } 27 | return false; 28 | }; 29 | 30 | /** 31 | * Checks if a key is in the STRONG_ID_KEYS array. 32 | * @param {string} key Event key. 33 | * @returns {boolean} True if is in the STRONG_ID_KEYS array, False otherwise. 34 | */ 35 | const isStrongId = key => consts.STRONG_ID_KEYS.includes(key.toLowerCase().replace(' ', '_')); 36 | 37 | module.exports = { 38 | isBlacklistURL, isBlacklistHeader, isStrongId, 39 | }; 40 | -------------------------------------------------------------------------------- /src/helpers/http.js: -------------------------------------------------------------------------------- 1 | const zlib = require('zlib'); 2 | const uuid4 = require('uuid4'); 3 | const uuidParse = require('uuid-parse'); 4 | const config = require('../config.js'); 5 | const eventInterface = require('../event.js'); 6 | const utils = require('../utils.js'); 7 | const { MAX_HTTP_VALUE_SIZE } = require('../consts.js'); 8 | 9 | const URL_BLACKLIST = { 10 | 'tc.epsagon.com': 'endsWith', 11 | 'oauth2.googleapis.com': 'endsWith', 12 | 'amazonaws.com': 13 | (url, pattern) => (url.endsWith(pattern) || url.endsWith(`${pattern}:443`)) && 14 | (url.indexOf('.execute-api.') === -1) && 15 | (url.indexOf('.es.') === -1) && 16 | (url.indexOf('.elb.') === -1) && 17 | (url.indexOf('.appsync-api.') === -1), 18 | 'blob.core.windows.net': 'endsWith', 19 | 'myqcloud.com': 'endsWith', 20 | 'documents.azure.com': 'endsWith', 21 | '127.0.0.1': (url, pattern, path) => (url === pattern) && path.startsWith('/2018-06-01/runtime/invocation/'), 22 | '169.254.169.254': 'startsWith', // EC2 document ip. Have better filtering in the future 23 | }; 24 | 25 | 26 | // Brotli decompression exists since Node v10 27 | const ENCODING_FUNCTIONS = { 28 | br: zlib.brotliDecompressSync, 29 | brotli: zlib.brotliDecompressSync, 30 | gzip: zlib.gunzipSync, 31 | deflate: zlib.deflateSync, 32 | }; 33 | 34 | const USER_AGENTS_BLACKLIST = ['openwhisk-client-js']; 35 | 36 | /** 37 | * Checks if a URL is in the user-defined blacklist. 38 | * @param {string} url The URL to check 39 | * @returns {boolean} True if it is in the user-defined blacklist, False otherwise. 40 | */ 41 | function isURLIgnoredByUser(url) { 42 | return config.getConfig().urlPatternsToIgnore.some(pattern => url.includes(pattern)); 43 | } 44 | 45 | 46 | /** 47 | * Set the duration of the event, and resolves the promise using the given function. 48 | * @param {object} httpEvent The current event 49 | * @param {Function} resolveFunction Function that will be used to resolve the promise 50 | * @param {integer} startTime The time the event started at 51 | */ 52 | function resolveHttpPromise(httpEvent, resolveFunction, startTime) { 53 | httpEvent.setDuration(utils.createDurationTimestamp(startTime)); 54 | resolveFunction(); 55 | } 56 | 57 | 58 | /** 59 | * Attempts to json parse the data and set it at key on the event's metadata. 60 | * @param {object} httpEvent The current event 61 | * @param {string} key name in metadata 62 | * @param {string} data data to jsonify 63 | * @param {string} encoding data encoding from the headers 64 | */ 65 | function setJsonPayload(httpEvent, key, data, encoding) { 66 | try { 67 | let decodedData = data; 68 | if (config.getConfig().decodeHTTP && ENCODING_FUNCTIONS[encoding]) { 69 | try { 70 | decodedData = ENCODING_FUNCTIONS[encoding](data); 71 | } catch (err) { 72 | utils.debugLog(`Could decode ${key} with ${encoding} in http`); 73 | } 74 | } 75 | const jsonData = decodedData; 76 | try { 77 | JSON.parse(jsonData); 78 | eventInterface.addToMetadata(httpEvent, {}, { 79 | [key]: jsonData.toString(), 80 | }); 81 | } catch (err) { 82 | utils.debugLog(`Could not parse JSON ${key} in http`); 83 | eventInterface.addToMetadata(httpEvent, {}, { 84 | [key]: decodedData.toString('utf-8'), 85 | }); 86 | } 87 | } catch (err) { 88 | utils.debugLog(`Could not decode data and parse JSON ${key} in http`); 89 | eventInterface.addToMetadata(httpEvent, {}, { 90 | [key]: data, 91 | }); 92 | } 93 | } 94 | 95 | 96 | /** 97 | * Return UUID in hex string. 98 | * @param {string} uuid uuid object. 99 | * @returns {string} UUID in hex. 100 | */ 101 | function UUIDToHex(uuid) { 102 | const uuidBuffer = Buffer.alloc(16); 103 | uuidParse.parse(uuid, uuidBuffer); 104 | return uuidBuffer.toString('hex'); 105 | } 106 | 107 | 108 | /** 109 | * Return an Epsagon trace ID to put in the request headers. 110 | * @returns {string} Epsagon trace id. 111 | */ 112 | function generateEpsagonTraceId() { 113 | const hexTraceId = UUIDToHex(uuid4()); 114 | const spanId = UUIDToHex(uuid4()).slice(16); 115 | const parentSpanId = UUIDToHex(uuid4()).slice(16); 116 | 117 | return `${hexTraceId}:${spanId}:${parentSpanId}:1`; 118 | } 119 | 120 | 121 | /** 122 | * Checks if API Gateway details appear in the headers, and update event accordingly 123 | * @param {object} headers data 124 | * @param {Event} httpEvent object 125 | */ 126 | function updateAPIGateway(headers, httpEvent) { 127 | if (headers && 'x-amzn-requestid' in headers) { 128 | // This is a request to AWS API Gateway 129 | httpEvent.getResource().setType('api_gateway'); 130 | eventInterface.addToMetadata(httpEvent, { 131 | request_trace_id: headers['x-amzn-requestid'], 132 | }); 133 | } 134 | } 135 | 136 | 137 | /** 138 | * Adding HTTP response chunks into the array, according to the constraints 139 | * @param {Object} chunk the part in String or Buffer 140 | * @param {Array} chunks array of chunks 141 | */ 142 | function addChunk(chunk, chunks) { 143 | if (chunk) { 144 | const totalSize = chunks.reduce((total, item) => item.length + total, 0); 145 | if (totalSize + chunk.length <= MAX_HTTP_VALUE_SIZE) { 146 | chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); 147 | } 148 | } 149 | } 150 | 151 | module.exports = { 152 | isURLIgnoredByUser, 153 | resolveHttpPromise, 154 | USER_AGENTS_BLACKLIST, 155 | URL_BLACKLIST, 156 | generateEpsagonTraceId, 157 | updateAPIGateway, 158 | setJsonPayload, 159 | addChunk, 160 | }; 161 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | // epsagon.d.ts 2 | declare module 'epsagon' { 3 | import 'aws-lambda' 4 | 5 | export function init(options: { 6 | token: string 7 | appName: string 8 | metadataOnly?: boolean 9 | useSSL?: boolean 10 | traceCollectorURL?: string 11 | isEpsagonDisabled?: boolean 12 | urlPatternsToIgnore?: string[] 13 | ignoredKeys?: (string | RegExp)[] 14 | ignoredDBTables?: (string | RegExp)[] 15 | labels?: string[][] 16 | sendOnlyErrors?: boolean 17 | sendTimeout?: number 18 | decodeHTTP?: boolean 19 | disableHttpResponseBodyCapture?: boolean 20 | loggingTracingEnabled?: boolean 21 | sendBatch?: boolean 22 | maxTraceWait?: number 23 | batchSize?: number 24 | maxBatchSizeBytes?: number 25 | }): void 26 | export function label(key: string, value: string | number | boolean): void 27 | export function unpatch(): void 28 | export function setError(error: Error): void 29 | export function setWarning(error: Error): void 30 | export function getTraceUrl(): string 31 | export function lambdaWrapper(f: T): T 32 | export function stepLambdaWrapper(f: T): T 33 | export function openWhiskWrapper(f: T): T 34 | export function nodeWrapper(f: T): T 35 | export function tencentFunctionWrapper(f: T): T 36 | export function googleCloudFunctionWrapper(f: T): T 37 | export function wrapBatchJob(): void 38 | export function enable(): void 39 | export function disable(): void 40 | } 41 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const lambdaWrapper = require('./wrappers/lambda.js'); 2 | const tencentFunctionWrapper = require('./wrappers/tencent.js'); 3 | const lambdaEnvWrapper = require('./wrappers/lambda_env'); 4 | const openWhiskWrapper = require('./wrappers/openwhisk'); 5 | const nodeWrapper = require('./wrappers/node.js'); 6 | const googleCloudFunctionWrapper = require('./wrappers/google_cloud_function.js'); 7 | const batchWrapper = require('./wrappers/batch.js'); 8 | const tracer = require('./tracer.js'); 9 | const config = require('./config.js'); 10 | const utils = require('./utils.js'); 11 | const eventInterface = require('./event.js'); 12 | const event = require('./proto/event_pb.js'); 13 | const httpHelpers = require('./helpers/http.js'); 14 | const tryRequire = require('./try_require.js'); 15 | const errorCode = require('./proto/error_code_pb.js'); 16 | const moduleUtils = require('./events/module_utils.js'); 17 | const sqsUtils = require('./resource_utils/sqs_utils.js'); 18 | const consts = require('./consts.js'); 19 | 20 | // Requiring patcher to instrument modules 21 | const patcher = require('./patcher.js'); // eslint-disable-line no-unused-vars 22 | 23 | module.exports = { 24 | lambdaWrapper: f => f, 25 | tencentFunctionWrapper: f => f, 26 | stepLambdaWrapper: f => f, 27 | openWhiskWrapper: f => f, 28 | googleCloudFunctionWrapper: f => f, 29 | nodeWrapper: f => f, 30 | wrapBatchJob: f => f, 31 | label: f => f, 32 | setError: f => f, 33 | setWarning: f => f, 34 | getTraceUrl: f => f, 35 | tracer, 36 | config, 37 | utils, 38 | eventInterface, 39 | event, 40 | tryRequire, 41 | errorCode, 42 | httpHelpers, 43 | consts, 44 | sqsUtils, 45 | }; 46 | 47 | if (!config.getConfig().isEpsagonDisabled) { 48 | module.exports.lambdaWrapper = lambdaWrapper.lambdaWrapper; 49 | module.exports.tencentFunctionWrapper = tencentFunctionWrapper.tencentFunctionWrapper; 50 | module.exports.stepLambdaWrapper = lambdaWrapper.stepLambdaWrapper; 51 | module.exports.nodeWrapper = nodeWrapper.nodeWrapper; 52 | module.exports.googleCloudFunctionWrapper = ( 53 | googleCloudFunctionWrapper.googleCloudFunctionWrapper 54 | ); 55 | module.exports.openWhiskWrapper = openWhiskWrapper.openWhiskWrapper; 56 | module.exports.wrapBatchJob = batchWrapper.wrapBatchJob; 57 | module.exports.label = tracer.label; 58 | module.exports.setError = tracer.setError; 59 | module.exports.setWarning = tracer.setWarning; 60 | module.exports.getTraceUrl = tracer.getTraceUrl; 61 | } 62 | 63 | module.exports.wrapper = lambdaEnvWrapper.wrapper; 64 | 65 | module.exports.init = tracer.initTrace; 66 | 67 | module.exports.disable = tracer.disable; 68 | module.exports.unpatch = moduleUtils.unpatchModules; 69 | 70 | module.exports.enable = tracer.enable; 71 | module.exports.moduleUtils = moduleUtils; 72 | -------------------------------------------------------------------------------- /src/patcher.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Patcher for all the libraries we are instrumenting 3 | * IMPORTANT: when requiring this module, all of the libraries will be automatically patched! 4 | */ 5 | const config = require('./config.js'); 6 | const utils = require('./utils.js'); 7 | const awsSDKPatcher = require('./events/aws_sdk.js'); 8 | const awsSDKv3Patcher = require('./events/aws_sdk_v3.js'); 9 | const consolePatcher = require('./events/console.js'); 10 | const daxPatcher = require('./events/amazon_dax_client.js'); 11 | const httpPatcher = require('./events/http.js'); 12 | const http2Patcher = require('./events/http2.js'); 13 | const pgPatcher = require('./events/pg.js'); 14 | const mysqlPatcher = require('./events/mysql.js'); 15 | const openWhiskPatcher = require('./events/openwhisk.js'); 16 | const googlePatcher = require('./events/google_cloud.js'); 17 | const redisPatcher = require('./events/redis.js'); 18 | const ioredisPatcher = require('./events/ioredis.js'); 19 | const memcachedPatcher = require('./events/memcached.js'); 20 | const mongoPatcher = require('./events/mongodb.js'); 21 | const dnsPatcher = require('./events/dns.js'); 22 | const natsPatcher = require('./events/nats.js'); 23 | const mqttPatcher = require('./events/mqtt.js'); 24 | const kafkajsPatcher = require('./events/kafkajs.js'); 25 | const kafkaNodePatcher = require('./events/kafka-node.js'); 26 | const bunyanPatcher = require('./events/bunyan.js'); 27 | const pinoPatcher = require('./events/pino.js'); 28 | const azureSdkPatcher = require('./events/azure_sdk.js'); 29 | const winstonCloudwatchPatcher = require('./events/winston_cloudwatch.js'); 30 | const winstonPatcher = require('./events/winston.js'); 31 | const amqplibPatcher = require('./events/amqplib.js'); 32 | const amqpPatcher = require('./events/amqp.js'); 33 | const ldapPatcher = require('./events/ldap.js'); 34 | const cassandraPatcher = require('./events/cassandra-driver.js'); 35 | const tencentCOSPatcher = require('./events/tencent-cos.js'); 36 | const neo4jPatcher = require('./events/neo4j.js'); 37 | 38 | const fs = require('./events/fs.js'); 39 | 40 | 41 | const LIBNAME_TO_PATCHER = { 42 | 'aws-sdk/global': awsSDKPatcher, 43 | 'aws-sdk/client-sns': awsSDKv3Patcher, 44 | 'aws-sdk/client-dynamodb': awsSDKv3Patcher, 45 | 'aws-sdk/client-sqs': awsSDKv3Patcher, 46 | 'azure-sdk': azureSdkPatcher, 47 | 'winston-cw': winstonCloudwatchPatcher, 48 | 'cos-nodejs-sdk-v5': tencentCOSPatcher, 49 | console: consolePatcher, 50 | http: httpPatcher, 51 | http2: http2Patcher, 52 | pg: pgPatcher, 53 | mysql: mysqlPatcher, 54 | redis: redisPatcher, 55 | ioredis: ioredisPatcher, 56 | memcached: memcachedPatcher, 57 | mongo: mongoPatcher, 58 | dax: daxPatcher, 59 | openwhisk: openWhiskPatcher, 60 | google: googlePatcher, 61 | dns: dnsPatcher, 62 | nats: natsPatcher, 63 | myqq: mqttPatcher, 64 | kafkajs: kafkajsPatcher, 65 | kafkanode: kafkaNodePatcher, 66 | bunyan: bunyanPatcher, 67 | pino: pinoPatcher, 68 | winston: winstonPatcher, 69 | amqplib: amqplibPatcher, 70 | amqp: amqpPatcher, 71 | ldap: ldapPatcher, 72 | cassandra: cassandraPatcher, 73 | neo4j: neo4jPatcher, 74 | fs, 75 | }; 76 | 77 | /** 78 | * Patches a module 79 | * @param {Object} patcher module 80 | */ 81 | function patch(patcher) { 82 | try { 83 | patcher.init(); 84 | } catch (error) { 85 | if ((process.env.EPSAGON_DEBUG || '').toUpperCase() === 'TRUE') { 86 | utils.debugLog('error initiating patch', error); 87 | } 88 | } 89 | } 90 | 91 | if (!config.getConfig().isEpsagonPatchDisabled) { 92 | if (!config.getConfig().patchWhitelist) { 93 | [ 94 | awsSDKPatcher, 95 | awsSDKv3Patcher, 96 | (config.getConfig().isConsolePatched ? consolePatcher : undefined), 97 | httpPatcher, 98 | http2Patcher, 99 | pgPatcher, 100 | mysqlPatcher, 101 | redisPatcher, 102 | ioredisPatcher, 103 | memcachedPatcher, 104 | mongoPatcher, 105 | daxPatcher, 106 | openWhiskPatcher, 107 | googlePatcher, 108 | dnsPatcher, 109 | natsPatcher, 110 | mqttPatcher, 111 | kafkajsPatcher, 112 | kafkaNodePatcher, 113 | bunyanPatcher, 114 | pinoPatcher, 115 | azureSdkPatcher, 116 | winstonCloudwatchPatcher, 117 | winstonPatcher, 118 | amqplibPatcher, 119 | amqpPatcher, 120 | ldapPatcher, 121 | cassandraPatcher, 122 | tencentCOSPatcher, 123 | neo4jPatcher, 124 | fs, 125 | ] 126 | .filter(p => p !== undefined) 127 | .forEach(patch); 128 | } else { 129 | config.getConfig().patchWhitelist.forEach( 130 | (lib) => { 131 | if (!(LIBNAME_TO_PATCHER[lib])) { 132 | utils.debugLog(`[PATCHER] Unable to find lib to patch: ${lib}`); 133 | } else { 134 | utils.debugLog(`[PATCHER] Whitelisting ${lib}`); 135 | patch(LIBNAME_TO_PATCHER[lib]); 136 | } 137 | } 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/proto/error_code_pb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * @enhanceable 4 | * @suppress {messageConventions} JS Compiler reports an error if a variable or 5 | * field starts with 'MSG_' and isn't a translatable message. 6 | * @public 7 | */ 8 | // GENERATED CODE -- DO NOT EDIT! 9 | 10 | var jspb = require('google-protobuf-minified'); 11 | var goog = jspb; 12 | var global = Function('return this')(); 13 | 14 | goog.exportSymbol('proto.ErrorCode', null, global); 15 | /** 16 | * @enum {number} 17 | */ 18 | proto.ErrorCode = { 19 | OK: 0, 20 | ERROR: 1, 21 | EXCEPTION: 2, 22 | TIMEOUT: 3 23 | }; 24 | 25 | goog.object.extend(exports, proto); 26 | -------------------------------------------------------------------------------- /src/proto/timestamp_pb.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview 3 | * @enhanceable 4 | * @suppress {messageConventions} JS Compiler reports an error if a variable or 5 | * field starts with 'MSG_' and isn't a translatable message. 6 | * @public 7 | */ 8 | // GENERATED CODE -- DO NOT EDIT! 9 | 10 | var jspb = require('google-protobuf-minified'); 11 | var goog = jspb; 12 | var global = Function('return this')(); 13 | 14 | goog.exportSymbol('proto.Timestamp', null, global); 15 | 16 | /** 17 | * Generated by JsPbCodeGenerator. 18 | * @param {Array=} opt_data Optional initial data array, typically from a 19 | * server response, or constructed directly in Javascript. The array is used 20 | * in place and becomes part of the constructed object. It is not cloned. 21 | * If no data is provided, the constructed object will be empty, but still 22 | * valid. 23 | * @extends {jspb.Message} 24 | * @constructor 25 | */ 26 | proto.Timestamp = function(opt_data) { 27 | jspb.Message.initialize(this, opt_data, 0, -1, null, null); 28 | }; 29 | goog.inherits(proto.Timestamp, jspb.Message); 30 | if (goog.DEBUG && !COMPILED) { 31 | proto.Timestamp.displayName = 'proto.Timestamp'; 32 | } 33 | 34 | 35 | if (jspb.Message.GENERATE_TO_OBJECT) { 36 | /** 37 | * Creates an object representation of this proto suitable for use in Soy templates. 38 | * Field names that are reserved in JavaScript and will be renamed to pb_name. 39 | * To access a reserved field use, foo.pb_, eg, foo.pb_default. 40 | * For the list of reserved names please see: 41 | * com.google.apps.jspb.JsClassTemplate.JS_RESERVED_WORDS. 42 | * @param {boolean=} opt_includeInstance Whether to include the JSPB instance 43 | * for transitional soy proto support: http://goto/soy-param-migration 44 | * @return {!Object} 45 | */ 46 | proto.Timestamp.prototype.toObject = function(opt_includeInstance) { 47 | return proto.Timestamp.toObject(opt_includeInstance, this); 48 | }; 49 | 50 | 51 | /** 52 | * Static version of the {@see toObject} method. 53 | * @param {boolean|undefined} includeInstance Whether to include the JSPB 54 | * instance for transitional soy proto support: 55 | * http://goto/soy-param-migration 56 | * @param {!proto.Timestamp} msg The msg instance to transform. 57 | * @return {!Object} 58 | * @suppress {unusedLocalVariables} f is only used for nested messages 59 | */ 60 | proto.Timestamp.toObject = function(includeInstance, msg) { 61 | var f, obj = { 62 | seconds: jspb.Message.getFieldWithDefault(msg, 1, 0), 63 | nanos: jspb.Message.getFieldWithDefault(msg, 2, 0) 64 | }; 65 | 66 | if (includeInstance) { 67 | obj.$jspbMessageInstance = msg; 68 | } 69 | return obj; 70 | }; 71 | } 72 | 73 | 74 | /** 75 | * Deserializes binary data (in protobuf wire format). 76 | * @param {jspb.ByteSource} bytes The bytes to deserialize. 77 | * @return {!proto.Timestamp} 78 | */ 79 | proto.Timestamp.deserializeBinary = function(bytes) { 80 | var reader = new jspb.BinaryReader(bytes); 81 | var msg = new proto.Timestamp; 82 | return proto.Timestamp.deserializeBinaryFromReader(msg, reader); 83 | }; 84 | 85 | 86 | /** 87 | * Deserializes binary data (in protobuf wire format) from the 88 | * given reader into the given message object. 89 | * @param {!proto.Timestamp} msg The message object to deserialize into. 90 | * @param {!jspb.BinaryReader} reader The BinaryReader to use. 91 | * @return {!proto.Timestamp} 92 | */ 93 | proto.Timestamp.deserializeBinaryFromReader = function(msg, reader) { 94 | while (reader.nextField()) { 95 | if (reader.isEndGroup()) { 96 | break; 97 | } 98 | var field = reader.getFieldNumber(); 99 | switch (field) { 100 | case 1: 101 | var value = /** @type {number} */ (reader.readInt64()); 102 | msg.setSeconds(value); 103 | break; 104 | case 2: 105 | var value = /** @type {number} */ (reader.readInt32()); 106 | msg.setNanos(value); 107 | break; 108 | default: 109 | reader.skipField(); 110 | break; 111 | } 112 | } 113 | return msg; 114 | }; 115 | 116 | 117 | /** 118 | * Serializes the message to binary data (in protobuf wire format). 119 | * @return {!Uint8Array} 120 | */ 121 | proto.Timestamp.prototype.serializeBinary = function() { 122 | var writer = new jspb.BinaryWriter(); 123 | proto.Timestamp.serializeBinaryToWriter(this, writer); 124 | return writer.getResultBuffer(); 125 | }; 126 | 127 | 128 | /** 129 | * Serializes the given message to binary data (in protobuf wire 130 | * format), writing to the given BinaryWriter. 131 | * @param {!proto.Timestamp} message 132 | * @param {!jspb.BinaryWriter} writer 133 | * @suppress {unusedLocalVariables} f is only used for nested messages 134 | */ 135 | proto.Timestamp.serializeBinaryToWriter = function(message, writer) { 136 | var f = undefined; 137 | f = message.getSeconds(); 138 | if (f !== 0) { 139 | writer.writeInt64( 140 | 1, 141 | f 142 | ); 143 | } 144 | f = message.getNanos(); 145 | if (f !== 0) { 146 | writer.writeInt32( 147 | 2, 148 | f 149 | ); 150 | } 151 | }; 152 | 153 | 154 | /** 155 | * optional int64 seconds = 1; 156 | * @return {number} 157 | */ 158 | proto.Timestamp.prototype.getSeconds = function() { 159 | return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 1, 0)); 160 | }; 161 | 162 | 163 | /** @param {number} value */ 164 | proto.Timestamp.prototype.setSeconds = function(value) { 165 | jspb.Message.setProto3IntField(this, 1, value); 166 | }; 167 | 168 | 169 | /** 170 | * optional int32 nanos = 2; 171 | * @return {number} 172 | */ 173 | proto.Timestamp.prototype.getNanos = function() { 174 | return /** @type {number} */ (jspb.Message.getFieldWithDefault(this, 2, 0)); 175 | }; 176 | 177 | 178 | /** @param {number} value */ 179 | proto.Timestamp.prototype.setNanos = function(value) { 180 | jspb.Message.setProto3IntField(this, 2, value); 181 | }; 182 | 183 | 184 | goog.object.extend(exports, proto); 185 | -------------------------------------------------------------------------------- /src/resource_utils/sqs_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview SQS utility functions 3 | */ 4 | 5 | /** 6 | * If exists, gets the SNS message that triggered the SQS, 7 | * and generates event data out of it. 8 | * @param {object} messages The SQS messages object 9 | * @returns {object} SNS event data json 10 | */ 11 | function getSNSTrigger(messages) { 12 | let foundSnsEvent = null; 13 | messages.some((message) => { 14 | try { 15 | let body = null; 16 | if ('Body' in message) { 17 | body = JSON.parse(message.Body); 18 | } else if ('body' in message) { 19 | body = JSON.parse(message.body); 20 | } else { 21 | return true; 22 | } 23 | 24 | if ('Type' in body && 25 | 'MessageId' in body && 26 | 'TopicArn' in body && 27 | 'Message' in body && 28 | 'Timestamp' in body) { 29 | foundSnsEvent = body; 30 | return true; 31 | } 32 | } catch (ex) { 33 | // Continue to the next message 34 | } 35 | 36 | return true; 37 | }); 38 | 39 | return foundSnsEvent; 40 | } 41 | 42 | module.exports.getSNSTrigger = getSNSTrigger; 43 | -------------------------------------------------------------------------------- /src/runners/aws_batch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Runner for AWS Batch environment 3 | */ 4 | const axios = require('axios-minified'); 5 | const serverlessEvent = require('../proto/event_pb.js'); 6 | const eventInterface = require('../event.js'); 7 | const errorCode = require('../proto/error_code_pb.js'); 8 | const tracer = require('../tracer.js'); 9 | const utils = require('../utils.js'); 10 | 11 | /** 12 | * Creates an Event representing the running function (runner) 13 | * @return {Object} The runner representing the lambda, and a promise 14 | * That resolved when all it's required fields are filled 15 | */ 16 | function createRunner() { 17 | const runnerResource = new serverlessEvent.Resource([ 18 | process.env.AWS_BATCH_JOB_ID, 19 | 'batch', 20 | 'invoke', 21 | {}, 22 | ]); 23 | 24 | const runner = new serverlessEvent.Event([ 25 | process.env.AWS_BATCH_JOB_ID, 26 | 0, 27 | null, 28 | 'runner', 29 | 0, 30 | errorCode.ErrorCode.OK, 31 | ]); 32 | 33 | runner.setResource(runnerResource); 34 | eventInterface.addToMetadata(runner, { 35 | 'Job ID': process.env.AWS_BATCH_JOB_ID, 36 | 'Job Queue Name': process.env.AWS_BATCH_JQ_NAME, 37 | 'Compute Environment Name': process.env.AWS_BATCH_CE_NAME, 38 | 'Job Attempt': process.env.AWS_BATCH_JOB_ATTEMPT, 39 | }, { 40 | Hostname: process.env.HOSTNAME, 41 | Home: process.env.HOME, 42 | Path: process.env.PATH, 43 | Arguments: JSON.stringify(process.argv), 44 | }); 45 | eventInterface.createTraceIdMetadata(runner); 46 | 47 | // Getting region 48 | const runnerPromise = axios.get( 49 | 'http://169.254.169.254/latest/dynamic/instance-identity/document', 50 | { timeout: 100 } 51 | ).then( 52 | (response) => { 53 | utils.debugLog(`Got Batch response ${response.data}`); 54 | try { 55 | const parsedBatchData = JSON.parse(response.data); 56 | eventInterface.addToMetadata(runner, { 57 | Region: parsedBatchData.region, 58 | }); 59 | } catch (err) { 60 | utils.debugLog(`Could not parse Batch env data ${err.toString()}`); 61 | } 62 | } 63 | ) 64 | .catch((err) => { 65 | tracer.addException(err); 66 | throw err; 67 | }); 68 | 69 | return { 70 | runner, 71 | runnerPromise, 72 | }; 73 | } 74 | 75 | module.exports.createRunner = createRunner; 76 | -------------------------------------------------------------------------------- /src/runners/aws_lambda.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview runners for the AWS Lambda environment 3 | */ 4 | 5 | const uuid4 = require('uuid4'); 6 | const consts = require('../consts.js'); 7 | const serverlessEvent = require('../proto/event_pb.js'); 8 | const eventInterface = require('../event.js'); 9 | const errorCode = require('../proto/error_code_pb.js'); 10 | 11 | /** 12 | * Creates an Event representing the running lambda (runner) 13 | * @param {object} originalContext The context the lambda was triggered with 14 | * @param {string} [resourceType='lambda'] The resource type to use for the runner 15 | * @return {proto.event_pb.Event} The runner representing the lambda 16 | */ 17 | function createRunner(originalContext, resourceType = 'lambda') { 18 | const runnerResource = new serverlessEvent.Resource([ 19 | originalContext.functionName, 20 | resourceType, 21 | 'invoke', 22 | {}, 23 | ]); 24 | 25 | const runner = new serverlessEvent.Event([ 26 | // Generating id in case of local invocation with Serverless framework 27 | (originalContext.awsRequestId === 'id') ? `local-${uuid4()}` : originalContext.awsRequestId, 28 | 0, 29 | null, 30 | 'runner', 31 | 0, 32 | errorCode.ErrorCode.OK, 33 | ]); 34 | 35 | runner.setResource(runnerResource); 36 | const arnSplit = (originalContext.invokedFunctionArn || '').split(':'); 37 | const AWSAccountNumber = (arnSplit.length > 4) ? arnSplit[4] : ''; 38 | 39 | // In case function is provisioned concurrency mode, so cold_start should be false 40 | if (process.env.AWS_LAMBDA_INITIALIZATION_TYPE === 'provisioned-concurrency') { 41 | consts.COLD_START = false; 42 | } 43 | 44 | eventInterface.addToMetadata(runner, { 45 | log_stream_name: `${originalContext.logStreamName}`, 46 | log_group_name: `${originalContext.logGroupName}`, 47 | function_version: `${originalContext.functionVersion}`, 48 | aws_account: AWSAccountNumber, 49 | cold_start: `${consts.COLD_START}`, 50 | memory: `${originalContext.memoryLimitInMB}`, 51 | region: consts.REGION, 52 | }); 53 | 54 | // Extract function alias if present. 55 | if (arnSplit.length === 8) { 56 | eventInterface.addToMetadata(runner, { 57 | function_alias: arnSplit[7], 58 | }); 59 | } 60 | 61 | consts.COLD_START = false; 62 | return runner; 63 | } 64 | 65 | module.exports.createRunner = createRunner; 66 | -------------------------------------------------------------------------------- /src/runners/tencent_function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview runners for the Tencent Serverless Cloud Function environment 3 | */ 4 | 5 | const consts = require('../consts.js'); 6 | const serverlessEvent = require('../proto/event_pb.js'); 7 | const eventInterface = require('../event.js'); 8 | const errorCode = require('../proto/error_code_pb.js'); 9 | 10 | /** 11 | * Creates an Event representing the running Serverless Cloud Function (runner) 12 | * @param {object} originalContext The context the Serverless Cloud Function was triggered with 13 | * @return {proto.event_pb.Event} The runner representing the Serverless Cloud Function 14 | */ 15 | function createRunner(originalContext) { 16 | const runnerResource = new serverlessEvent.Resource([ 17 | originalContext.function_name, 18 | 'tencent_function', 19 | 'invoke', 20 | {}, 21 | ]); 22 | 23 | const runner = new serverlessEvent.Event([ 24 | originalContext.request_id, 25 | 0, 26 | null, 27 | 'runner', 28 | 0, 29 | errorCode.ErrorCode.OK, 30 | ]); 31 | 32 | runner.setResource(runnerResource); 33 | 34 | eventInterface.addToMetadata(runner, { 35 | 'tencent.scf.version': originalContext.function_version, 36 | 'tencent.scf.memory': originalContext.memory_limit_in_mb, 37 | 'tencent.scf.cold_start': consts.COLD_START, 38 | 'tencent.namespace': originalContext.namespace, 39 | 'tencent.uin': originalContext.tencentcloud_uin, 40 | 'tencent.app_id': originalContext.tencentcloud_appid, 41 | 'tencent.region': originalContext.tencentcloud_region, 42 | }); 43 | 44 | consts.COLD_START = false; 45 | return runner; 46 | } 47 | 48 | module.exports.createRunner = createRunner; 49 | -------------------------------------------------------------------------------- /src/trace_object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview holds tracer singleton. 3 | */ 4 | const tracerModule = require('./tracer.js'); 5 | 6 | /** 7 | * The tracer singleton, used to manage the trace and send it at the end of the function invocation. 8 | * In a Lambda environment we use this singleton, while in other environment we use the one from 9 | * the context. 10 | */ 11 | let tracer = null; 12 | 13 | 14 | /** 15 | * The tracer singleton getter function 16 | * @returns {Object} tracer object 17 | */ 18 | module.exports.get = () => { 19 | if (!tracer || tracer.deleted) { 20 | tracer = tracerModule.createTracer(); 21 | } 22 | return tracer; 23 | }; 24 | -------------------------------------------------------------------------------- /src/trace_senders/http.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Send traces to the epsagon backend using http 3 | */ 4 | 5 | const axios = require('axios-minified'); 6 | const http = require('http'); 7 | const https = require('https'); 8 | const utils = require('../utils.js'); 9 | const config = require('../config.js'); 10 | 11 | /** 12 | * Session for the post requests to the collector 13 | */ 14 | const session = axios.create({ 15 | timeout: config.getConfig().sendTimeout, 16 | httpAgent: new http.Agent({ keepAlive: true }), 17 | httpsAgent: new https.Agent({ keepAlive: true }), 18 | }); 19 | 20 | module.exports.sendTrace = function sendTrace(traceObject) { 21 | utils.debugLog(`Posting trace to ${config.getConfig().traceCollectorURL}`); 22 | 23 | if ((process.env.EPSAGON_DEBUG || '').toUpperCase() === 'TRUE') { 24 | utils.debugLog(`trace: ${JSON.stringify(traceObject, null, 2)}`); 25 | } 26 | 27 | // based on https://github.com/axios/axios/issues/647#issuecomment-322209906 28 | // axios timeout is only after the connection is made, not the address resolution itself 29 | const cancelTokenSource = axios.CancelToken.source(); 30 | const handle = setTimeout(() => { 31 | cancelTokenSource.cancel('timeout sending trace'); 32 | }, config.getConfig().sendTimeout); 33 | 34 | return session.post( 35 | config.getConfig().traceCollectorURL, 36 | traceObject, 37 | { 38 | headers: { Authorization: `Bearer ${config.getConfig().token}` }, 39 | timeout: config.getConfig().sendTimeout, 40 | cancelToken: cancelTokenSource.token, 41 | } 42 | ).then((res) => { 43 | clearTimeout(handle); 44 | utils.debugLog('Trace posted!'); 45 | return res; 46 | }).catch((err) => { 47 | clearTimeout(handle); 48 | if (err.config && err.config.data) { 49 | utils.debugLog(`Error sending trace. Trace size: ${err.config.data.length}`); 50 | } else { 51 | utils.debugLog(`Error sending trace. Error: ${err}`); 52 | } 53 | utils.debugLog(`${err ? err.stack : err}`); 54 | return err; 55 | }); // Always resolve. 56 | }; 57 | -------------------------------------------------------------------------------- /src/trace_senders/logs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Send traces to the epsagon backend by printing it to the logs 3 | */ 4 | 5 | const { Buffer } = require('buffer'); 6 | const utils = require('../utils.js'); 7 | 8 | module.exports.sendTrace = function sendTrace(traceObject) { 9 | utils.debugLog('posting trace to logs'); 10 | utils.debugLog(`trace: ${JSON.stringify(traceObject, null, 2)}`); 11 | 12 | return new Promise((resolve) => { 13 | try { 14 | const encodedTrace = Buffer.from(JSON.stringify(traceObject)).toString('base64'); 15 | process.stdout.write(`EPSAGON_TRACE: ${encodedTrace}\n`); 16 | utils.debugLog('Trace posted!'); 17 | } catch (err) { 18 | utils.debugLog(`Error sending trace. Error: ${err}`); 19 | } finally { 20 | resolve(); 21 | } 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /src/try_require.js: -------------------------------------------------------------------------------- 1 | let lastError = null; 2 | 3 | const tryRequire = (id) => { 4 | let path; 5 | // for webpack we only support requiring external packages 6 | const currentRequire = ( 7 | typeof __webpack_require__ === 'function' ? // eslint-disable-line camelcase 8 | __non_webpack_require__ : require // eslint-disable-line no-undef, camelcase 9 | ); 10 | 11 | try { 12 | path = currentRequire.resolve(id); 13 | 14 | lastError = null; 15 | } catch (e) { 16 | lastError = e; 17 | } 18 | 19 | if (path) { 20 | try { 21 | return currentRequire(path); 22 | } catch (e) { 23 | lastError = e; 24 | } 25 | } 26 | 27 | return undefined; 28 | }; 29 | 30 | tryRequire.lastError = () => lastError; 31 | 32 | module.exports = tryRequire; 33 | -------------------------------------------------------------------------------- /src/wrappers/batch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Epsagon's batch wrapper, for tracing Batch Jobs 3 | */ 4 | const shimmer = require('shimmer'); 5 | const tracer = require('../tracer.js'); 6 | const traceObject = require('../trace_object.js'); 7 | const utils = require('../utils.js'); 8 | const eventInterface = require('../event.js'); 9 | const batchRunner = require('../runners/aws_batch.js'); 10 | 11 | /** 12 | * Epsagon's batch Job wrapper, sets tracing on a batch job 13 | */ 14 | module.exports.wrapBatchJob = function wrapBatchJob() { 15 | tracer.getTrace = traceObject.get; 16 | tracer.restart(); 17 | try { 18 | const { runner, runnerPromise } = batchRunner.createRunner(); 19 | tracer.addRunner(runner, runnerPromise); 20 | const startTime = Date.now(); 21 | 22 | const runnerSendUpdateHandler = (() => { 23 | runner.setDuration(utils.createDurationTimestamp(startTime)); 24 | }); 25 | 26 | runner.setStartTime(utils.createTimestampFromTime(startTime)); 27 | process.on('uncaughtException', (err) => { 28 | eventInterface.setException(runner, err); 29 | }); 30 | process.once('beforeExit', () => { 31 | tracer.sendTrace(runnerSendUpdateHandler); 32 | }); 33 | 34 | const processExitWrapper = ( 35 | wrappedFunction => function internalProcessExitWrapper(errorCode) { 36 | runnerSendUpdateHandler(); 37 | tracer.sendTraceSync(); 38 | wrappedFunction.apply(this, [errorCode]); 39 | } 40 | ); 41 | 42 | shimmer.wrap(process, 'exit', processExitWrapper); 43 | } catch (err) { 44 | utils.debugLog( 45 | 'failed to create Batch runner', 46 | err 47 | ); 48 | tracer.addException(err); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/wrappers/google_cloud_function.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Epsagon's Google Cloud Function wrapper. 3 | */ 4 | const tracer = require('../tracer.js'); 5 | const traceObject = require('../trace_object.js'); 6 | const utils = require('../utils.js'); 7 | const consts = require('../consts'); 8 | const eventInterface = require('../event.js'); 9 | 10 | 11 | /** 12 | * Creates an Event representing the running function (runner) 13 | * @param {express.Request} req incoming http request 14 | * @return {proto.event_pb.Event} The runner representing the gcp function 15 | */ 16 | function createRunner(req) { 17 | const { slsEvent, startTime } = eventInterface.initializeEvent( 18 | 'google_cloud_function', 19 | process.env.K_SERVICE, 20 | 'execute', 21 | 'runner' 22 | ); 23 | eventInterface.addToMetadata(slsEvent, { 24 | 'gcp.function.name': process.env.K_SERVICE, 25 | 'gcp.function.revision': process.env.K_REVISION, 26 | 'gcp.function.runtime': process.env.GAE_RUNTIME, 27 | 'gcp.function.execution_id': req.headers['function-execution-id'], 28 | 'gcp.function.cold_start': consts.COLD_START, 29 | }); 30 | eventInterface.createTraceIdMetadata(slsEvent); 31 | 32 | consts.COLD_START = false; 33 | return { slsEvent, startTime }; 34 | } 35 | 36 | 37 | /** 38 | * Creates an Event representing the trigger function 39 | * @param {express.Request} req incoming http request 40 | * @param {express.Response} res outgoing http response 41 | * @return {proto.event_pb.Event} The runner representing the trigger 42 | */ 43 | function createHTTPTrigger(req, res) { 44 | const { slsEvent } = eventInterface.initializeEvent( 45 | 'http', 46 | req.hostname, 47 | req.method, 48 | 'trigger' 49 | ); 50 | eventInterface.addToMetadata(slsEvent, { 51 | 'http.status_code': res.statusCode, 52 | 'http.request.path': req.path, 53 | }, { 54 | 'http.request.path_params': req.params, 55 | 'http.request.headers': req.headers, 56 | 'http.response.headers': res.getHeaders(), 57 | }); 58 | if (req.body) { 59 | eventInterface.addToMetadata(slsEvent, {}, { 60 | 'http.request.body': req.body, 61 | }); 62 | } 63 | if (req.query) { 64 | eventInterface.addToMetadata(slsEvent, {}, { 65 | 'http.request.query_params': req.query, 66 | }); 67 | } 68 | return slsEvent; 69 | } 70 | 71 | 72 | /** 73 | * Epsagon's node function wrapper, wrap a gcp function function with it to trace it 74 | * @param {function} functionToWrap The function to wrap and trace 75 | * @return {function} The original function, wrapped by our tracer 76 | */ 77 | module.exports.googleCloudFunctionWrapper = function googleCloudFunctionWrapper(functionToWrap) { 78 | tracer.getTrace = traceObject.get; 79 | return (req, res) => { 80 | tracer.restart(); 81 | let runner; 82 | let eventStartTime; 83 | 84 | try { 85 | const { slsEvent, startTime } = createRunner(req); 86 | runner = slsEvent; 87 | eventStartTime = startTime; 88 | tracer.addRunner(runner); 89 | } catch (err) { 90 | utils.debugLog(err); 91 | // If we failed, call the user's function anyway 92 | return functionToWrap(req, res); 93 | } 94 | 95 | res.on('finish', () => { 96 | runner.setDuration(utils.createDurationTimestamp(eventStartTime)); 97 | try { 98 | const trigger = createHTTPTrigger(req, res); 99 | trigger.setDuration(utils.createDurationTimestamp(eventStartTime)); 100 | tracer.addEvent(trigger); 101 | } catch (err) { 102 | utils.debugLog(`Error parsing trigger: ${err.stack}`); 103 | } 104 | tracer.sendTrace(() => {}); 105 | }); 106 | return functionToWrap(req, res); 107 | }; 108 | }; 109 | -------------------------------------------------------------------------------- /src/wrappers/lambda_env.js: -------------------------------------------------------------------------------- 1 | const tryRequire = require('../try_require.js'); 2 | const lambda = require('./lambda.js'); 3 | const config = require('../config.js'); 4 | 5 | /** 6 | * @return {Function} the user's handler, or an error handler if an 7 | * error occurred. 8 | */ 9 | function getUserHandler() { 10 | const createErrorHandler = err => ((event, context) => { 11 | context.fail(err); 12 | }); 13 | 14 | const createErrorHandlerWithMessage = (message, err) => ((event, context) => { 15 | console.log(message); // eslint-disable-line no-console 16 | context.fail(err); 17 | }); 18 | 19 | const handlerString = process.env.EPSAGON_HANDLER; 20 | if (!handlerString) { 21 | return createErrorHandler(new Error(`invalid EPSAGON_HANDLER ${handlerString}`)); 22 | } 23 | 24 | const appParts = handlerString.split('.'); 25 | if (appParts.length !== 2) { 26 | return createErrorHandler(new Error(`Bad handler ${handlerString}`)); 27 | } 28 | 29 | const modulePath = appParts[0]; 30 | const handlerName = appParts[1]; 31 | try { 32 | const lambdaTaskRoot = process.env.LAMBDA_TASK_ROOT; 33 | const moduleFullPath = `${lambdaTaskRoot}/${modulePath}`; 34 | const app = tryRequire(moduleFullPath); 35 | if (!app) { 36 | return createErrorHandler(tryRequire.lastError()); 37 | } 38 | const userHandler = app[handlerName]; 39 | 40 | if (!userHandler) { 41 | return createErrorHandler( 42 | new Error(`Handler '${handlerName}' missing on module '${modulePath}'`) 43 | ); 44 | } 45 | 46 | return userHandler; 47 | } catch (e) { 48 | if (e.code === 'MODULE_NOT_FOUND') { 49 | return createErrorHandlerWithMessage( 50 | `Unable to import module '${modulePath}'`, 51 | e 52 | ); 53 | } 54 | if (e instanceof SyntaxError) { 55 | return createErrorHandlerWithMessage( 56 | `Syntax error in module '${modulePath}'`, 57 | e 58 | ); 59 | } 60 | return createErrorHandlerWithMessage( 61 | 'module initialization error', 62 | e 63 | ); 64 | } 65 | } 66 | const userHandler = getUserHandler(); 67 | module.exports.wrapper = config.getConfig().isEpsagonDisabled ? userHandler : 68 | lambda.lambdaWrapper(userHandler); 69 | -------------------------------------------------------------------------------- /src/wrappers/node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Epsagon's node wrapper, for tracing node functions. 3 | */ 4 | const uuid4 = require('uuid4'); 5 | const tracer = require('../tracer.js'); 6 | const traceObject = require('../trace_object.js'); 7 | const utils = require('../utils.js'); 8 | const consts = require('../consts'); 9 | const serverlessEvent = require('../proto/event_pb.js'); 10 | const eventInterface = require('../event.js'); 11 | const errorCode = require('../proto/error_code_pb.js'); 12 | const { getConfig } = require('../config.js'); 13 | 14 | const FAILED_TO_SERIALIZE_MESSAGE = 'Unable to stringify response body as json'; 15 | 16 | /** 17 | * Creates an Event representing the running function (runner) 18 | * @param {object} functionToWrap The function that is wrapped 19 | * @param {Array} args The arguments passed to the function 20 | * @return {proto.event_pb.Event} The runner representing the lambda 21 | */ 22 | function createRunner(functionToWrap, args) { 23 | const runnerResource = new serverlessEvent.Resource([ 24 | functionToWrap.name || 'handler', 25 | 'node_function', 26 | 'invoke', 27 | {}, 28 | ]); 29 | 30 | const runner = new serverlessEvent.Event([ 31 | uuid4(), 32 | 0, 33 | null, 34 | 'runner', 35 | 0, 36 | errorCode.ErrorCode.OK, 37 | ]); 38 | 39 | runner.setResource(runnerResource); 40 | eventInterface.addToMetadata(runner, { 41 | args_length: args.length, 42 | }, { 43 | arguments: args, 44 | }); 45 | eventInterface.createTraceIdMetadata(runner); 46 | 47 | consts.COLD_START = false; 48 | return runner; 49 | } 50 | 51 | /** 52 | * Handle ECS step of AWS Step Functions. 53 | * Getting step id from epsagon steps dict environment variable. 54 | * If exists - Adding steps_dict object to event metadata. 55 | * optional - Step number can be set by the user, default is 0. 56 | * @param {*} runner Runner event. 57 | */ 58 | function handleEcsStepOfStepFunctions(runner) { 59 | if (process.env.EPSAGON_STEPS_ID) { 60 | eventInterface.addToMetadata(runner, { 61 | steps_dict: { 62 | id: process.env.EPSAGON_STEPS_ID, 63 | step_num: process.env.EPSAGON_STEPS_NUM || 0, 64 | }, 65 | }); 66 | } 67 | } 68 | 69 | /** 70 | * Epsagon's node function wrapper, wrap a lambda function with it to trace it 71 | * @param {function} functionToWrap The function to wrap and trace 72 | * @return {function} The original function, wrapped by our tracer 73 | */ 74 | module.exports.nodeWrapper = function nodeWrapper(functionToWrap) { 75 | tracer.getTrace = traceObject.get; 76 | return (...args) => { // eslint-disable-line consistent-return 77 | tracer.restart(); 78 | let runner; 79 | 80 | try { 81 | runner = createRunner(functionToWrap, args); 82 | } catch (err) { 83 | // If we failed, call the user's function anyway 84 | return functionToWrap(...args); 85 | } 86 | 87 | tracer.addRunner(runner); 88 | 89 | const startTime = Date.now(); 90 | const runnerSendUpdateHandler = (() => { 91 | runner.setDuration(utils.createDurationTimestamp(startTime)); 92 | }); 93 | 94 | try { 95 | runner.setStartTime(utils.createTimestampFromTime(startTime)); 96 | const result = functionToWrap(...args); 97 | handleEcsStepOfStepFunctions(runner); 98 | const promiseResult = Promise.resolve(result).then((resolvedResult) => { 99 | if (!getConfig().metadataOnly) { 100 | let jsonResult; 101 | try { 102 | jsonResult = JSON.stringify( 103 | typeof resolvedResult === 'undefined' ? null : resolvedResult 104 | ); 105 | } catch (err) { 106 | jsonResult = `${FAILED_TO_SERIALIZE_MESSAGE}: ${err.message}`; 107 | } 108 | eventInterface.addToMetadata( 109 | runner, 110 | { 111 | return_value: jsonResult.substring(0, consts.MAX_VALUE_CHARS), 112 | } 113 | ); 114 | } 115 | tracer.sendTrace(runnerSendUpdateHandler); 116 | }).catch((err) => { 117 | eventInterface.setException(runner, err); 118 | runnerSendUpdateHandler(); // Doing it here since the send is synchronous on error 119 | tracer.sendTraceSync().then(() => { 120 | throw err; 121 | }); 122 | }); 123 | if (result && typeof result.then === 'function') { 124 | return promiseResult; 125 | } 126 | return result; 127 | } catch (err) { 128 | eventInterface.setException(runner, err); 129 | runnerSendUpdateHandler(); // Doing it here since the send is synchronous on error 130 | tracer.sendTraceSync(); // best effort 131 | throw err; 132 | } 133 | }; 134 | }; 135 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true, 4 | }, 5 | "plugins": [ 6 | "mocha", 7 | "chai-friendly" 8 | ], 9 | "rules": { 10 | "indent": ["error", 4], 11 | "function-paren-newline": ["error", "consistent"], 12 | "mocha/no-exclusive-tests": "error", 13 | "no-unused-expressions": 0, 14 | "chai-friendly/no-unused-expressions": 2, 15 | "require-jsdoc": 0, 16 | "valid-jsdoc": 0, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/acceptance/acceptance.js: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 expandtab 2 | /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ 3 | const chai = require('chai'); 4 | const chaiAsPromised = require('chai-as-promised'); 5 | const { expect } = require('chai'); 6 | const Lambda = require('aws-sdk/clients/lambda'); 7 | 8 | chai.use(chaiAsPromised); 9 | 10 | const SERVICE_PREFIX = `acceptance-node-${process.env.TRAVIS_BUILD_NUMBER}-${process.env.RUNTIME_NAME}-`; 11 | const RUNTIME = process.env.RUNTIME || 'nodejs8.10'; 12 | 13 | /** 14 | * invokes a lambda 15 | * @param {string} name 16 | * @param {object} payload 17 | * @returns {hash} The functions output 18 | */ 19 | function invoke(name, payload) { 20 | const lambda = new Lambda({ region: 'us-east-2' }); 21 | const params = { 22 | FunctionName: SERVICE_PREFIX + name, 23 | Payload: JSON.stringify(payload), 24 | }; 25 | return lambda.invoke(params).promise(); 26 | } 27 | 28 | /** 29 | * testSameOutput tests that the output of both normal and instrumented lambdas is the same 30 | * instrumented lambdas are identified by having a '_e' suffix to the name 31 | * 32 | * @param {string} lambdaName 33 | * @param {object} input 34 | * @returns {undefined} 35 | */ 36 | async function testSameOutput(lambdaName, input) { 37 | const responseOriginal = await invoke(lambdaName, input); 38 | const responseInstrumented = await invoke(`${lambdaName}_e`, input); 39 | expect(responseOriginal).to.eql(responseInstrumented); 40 | } 41 | 42 | /** 43 | * basicTestNoInput - invokes a lambda and expects to get the input back 44 | * 45 | * @param {string} lambdaName 46 | * @param {object} input 47 | * @returns {undefined} 48 | */ 49 | async function basicTestNoInput(lambdaName, input) { 50 | const response = await invoke(lambdaName, input); 51 | expect(response.StatusCode).to.equal(200); 52 | const content = JSON.parse(response.Payload); 53 | expect(content.statusCode).to.equal(200); 54 | const body = JSON.parse(content.body); 55 | expect(body.message).to.eql('It Worked'); 56 | } 57 | 58 | /** 59 | * basicTest - invokes a lambda and expects to get the input back 60 | * 61 | * @param {string} lambdaName 62 | * @param {object} input 63 | * @returns {undefined} 64 | */ 65 | async function basicTest(lambdaName, input) { 66 | const response = await invoke(lambdaName, input); 67 | expect(response.StatusCode).to.equal(200); 68 | const content = JSON.parse(response.Payload); 69 | expect(content.statusCode).to.equal(200); 70 | const body = JSON.parse(content.body); 71 | expect(body.input).to.eql(input); 72 | } 73 | 74 | /** 75 | * basicNoReturn 76 | * 77 | * @param {string} lambdaName 78 | * @param {object} input 79 | * @returns {undefined} 80 | */ 81 | async function basicNoReturn(lambdaName, input) { 82 | const response = await invoke(lambdaName, input); 83 | expect(response.StatusCode).to.equal(200); 84 | expect(response.Payload).to.equal('null'); 85 | } 86 | 87 | /** 88 | * failTest 89 | * 90 | * @param {string} lambdaName 91 | * @param {object} input 92 | * @returns {undefined} 93 | */ 94 | async function failTest(lambdaName, input) { 95 | const response = await invoke(lambdaName, input); 96 | expect(response.StatusCode).to.equal(200); 97 | const content = JSON.parse(response.Payload); 98 | expect(content.errorMessage).to.equal(input); 99 | } 100 | 101 | describe('Lambda Wrapper', () => { 102 | const sanityTests = [ 103 | '', 104 | '{afwe', 105 | [], 106 | [1, 2, 3], 107 | {}, 108 | { test: 'test' }, 109 | { test: 'test', more: [1, 2, '3'] }, 110 | ]; 111 | 112 | sanityTests.forEach((input) => { 113 | it('works with sanity example', async () => { 114 | await basicTest('sanity', input); 115 | }); 116 | it('works with epsagon labels', async () => { 117 | await basicTest('labels', input); 118 | }); 119 | it('failsafe: event occurs without tracer initialized', async () => { 120 | await basicTest('failsafe_no_tracer_init', input); 121 | }); 122 | }); 123 | 124 | const syncOptions = ['sync']; 125 | const paramOptions = ['no', '1', '2', '3']; 126 | const returnOptions = [ 127 | 'simple_return', 'no_return', 128 | 'succeed', 'fail', 'done', 129 | 'callback', 'callback_error', 'callback_then_fail']; 130 | if (RUNTIME === 'nodejs8.10') { 131 | returnOptions.push('promise'); 132 | } 133 | const testMatrix = { 134 | sync: { 135 | no: { 136 | promise: basicTestNoInput, 137 | simple_return: basicNoReturn, 138 | no_return: basicNoReturn, 139 | }, 140 | 1: { 141 | promise: basicTest, 142 | simple_return: basicNoReturn, 143 | no_return: basicNoReturn, 144 | }, 145 | 2: { 146 | promise: basicTest, 147 | simple_return: basicNoReturn, 148 | no_return: basicNoReturn, 149 | succeed: basicTest, 150 | done: basicTest, 151 | fail: failTest, 152 | }, 153 | 3: { 154 | promise: basicTest, 155 | simple_return: basicNoReturn, 156 | no_return: basicNoReturn, 157 | succeed: basicTest, 158 | done: basicTest, 159 | fail: failTest, 160 | callback: basicTest, 161 | callback_error: failTest, 162 | // callback_then_fail: basicTest, 163 | }, 164 | }, 165 | async: { 166 | no: { }, 167 | 1: { }, 168 | 2: { }, 169 | 3: { }, 170 | }, 171 | }; 172 | 173 | syncOptions.forEach((syncOpt) => { 174 | paramOptions.forEach((paramOpt) => { 175 | returnOptions.forEach((returnOpt) => { 176 | const funcName = `${syncOpt}_${paramOpt}_param_${returnOpt}`; 177 | if (testMatrix[syncOpt][paramOpt][returnOpt]) { 178 | it(`behaves correctly on ${funcName}`, async () => { 179 | await testSameOutput(funcName, 'hello world'); 180 | await testMatrix[syncOpt][paramOpt][returnOpt]( 181 | `${funcName}_e`, 'hello world' 182 | ); 183 | }); 184 | } 185 | }); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/acceptance/lambda-handlers/.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 | -------------------------------------------------------------------------------- /test/acceptance/lambda-handlers/handler_tracer_test.js: -------------------------------------------------------------------------------- 1 | // vim: ts=4 sw=4 expandtab 2 | /* eslint no-unused-vars: ["error", { "argsIgnorePattern": "^_" }] */ 3 | const epsagon = require('epsagon'); 4 | const http = require('http'); 5 | 6 | epsagon.init({ 7 | token: 'my-secret-token', 8 | appName: 'my-app-name', 9 | metadataOnly: false, 10 | }); 11 | 12 | // Calling event before tracer initialized 13 | http.get('http://dummy.restapiexample.com/api/v1/employees', (resp) => { 14 | resp.on('end', () => { 15 | console.log('end'); 16 | }); 17 | }).on('error', (err) => { 18 | console.log(`Error: ${err.message}`); 19 | }); 20 | 21 | module.exports.failsafe_no_tracer_init = epsagon.lambdaWrapper((event, context, callback) => { 22 | const response = { 23 | statusCode: 200, 24 | body: JSON.stringify({ 25 | message: 'It Worked!', 26 | input: event, 27 | }), 28 | }; 29 | 30 | callback(null, response); 31 | }); 32 | -------------------------------------------------------------------------------- /test/acceptance/lambda-handlers/serverless.yml: -------------------------------------------------------------------------------- 1 | service: acceptance-node 2 | 3 | provider: 4 | name: aws 5 | runtime: ${opt:runtime} 6 | region: ${opt:region, 'us-east-2'} 7 | role: acceptanceRole 8 | stage: ${self:custom.buildNumber}-${opt:runtimeName} 9 | environment: 10 | STAGE: dev 11 | EPSAGON_DEBUG: "TRUE" 12 | package: 13 | exclude: 14 | - './**' 15 | - 'node_modules/**' 16 | 17 | custom: 18 | buildNumber: ${opt:buildNumber} 19 | 20 | functions: 21 | sanity: 22 | handler: handler.sanity 23 | labels: 24 | handler: handler.labels 25 | 26 | failsafe_no_tracer_init: 27 | handler: handler_tracer_test.failsafe_no_tracer_init 28 | 29 | sync_no_param_promise: 30 | handler: handler.sync_no_param_promise 31 | sync_no_param_promise_e: 32 | handler: handler.sync_no_param_promise_e 33 | sync_no_param_simple_return: 34 | handler: handler.sync_no_param_simple_return 35 | sync_no_param_simple_return_e: 36 | handler: handler.sync_no_param_simple_return_e 37 | sync_no_param_no_return: 38 | handler: handler.sync_no_param_no_return 39 | sync_no_param_no_return_e: 40 | handler: handler.sync_no_param_no_return_e 41 | 42 | sync_1_param_promise: 43 | handler: handler.sync_1_param_promise 44 | sync_1_param_promise_e: 45 | handler: handler.sync_1_param_promise_e 46 | sync_1_param_simple_return: 47 | handler: handler.sync_1_param_simple_return 48 | sync_1_param_simple_return_e: 49 | handler: handler.sync_1_param_simple_return_e 50 | sync_1_param_no_return: 51 | handler: handler.sync_1_param_no_return 52 | sync_1_param_no_return_e: 53 | handler: handler.sync_1_param_no_return_e 54 | 55 | sync_2_param_succeed: 56 | handler: handler.sync_2_param_succeed 57 | sync_2_param_succeed_e: 58 | handler: handler.sync_2_param_succeed_e 59 | sync_2_param_fail: 60 | handler: handler.sync_2_param_fail 61 | sync_2_param_fail_e: 62 | handler: handler.sync_2_param_fail_e 63 | sync_2_param_done: 64 | handler: handler.sync_2_param_done 65 | sync_2_param_done_e: 66 | handler: handler.sync_2_param_done_e 67 | sync_2_param_promise: 68 | handler: handler.sync_2_param_promise 69 | sync_2_param_promise_e: 70 | handler: handler.sync_2_param_promise_e 71 | sync_2_param_simple_return: 72 | handler: handler.sync_2_param_simple_return 73 | sync_2_param_simple_return_e: 74 | handler: handler.sync_2_param_simple_return_e 75 | sync_2_param_no_return: 76 | handler: handler.sync_2_param_no_return 77 | sync_2_param_no_return_e: 78 | handler: handler.sync_2_param_no_return_e 79 | 80 | sync_3_param_succeed: 81 | handler: handler.sync_3_param_succeed 82 | sync_3_param_succeed_e: 83 | handler: handler.sync_3_param_succeed_e 84 | sync_3_param_fail: 85 | handler: handler.sync_3_param_fail 86 | sync_3_param_fail_e: 87 | handler: handler.sync_3_param_fail_e 88 | sync_3_param_done: 89 | handler: handler.sync_3_param_done 90 | sync_3_param_done_e: 91 | handler: handler.sync_3_param_done_e 92 | sync_3_param_promise: 93 | handler: handler.sync_3_param_promise 94 | sync_3_param_promise_e: 95 | handler: handler.sync_3_param_promise_e 96 | sync_3_param_simple_return: 97 | handler: handler.sync_3_param_simple_return 98 | sync_3_param_simple_return_e: 99 | handler: handler.sync_3_param_simple_return_e 100 | sync_3_param_no_return: 101 | handler: handler.sync_3_param_no_return 102 | sync_3_param_no_return_e: 103 | handler: handler.sync_3_param_no_return_e 104 | sync_3_param_callback: 105 | handler: handler.sync_3_param_callback 106 | sync_3_param_callback_e: 107 | handler: handler.sync_3_param_callback_e 108 | sync_3_param_callback_error: 109 | handler: handler.sync_3_param_callback_error 110 | sync_3_param_callback_error_e: 111 | handler: handler.sync_3_param_callback_error_e 112 | sync_3_param_callback_then_fail: 113 | handler: handler.sync_3_param_callback_then_fail 114 | sync_3_param_callback_then_fail_e: 115 | handler: handler.sync_3_param_callback_then_fail_e 116 | 117 | resources: 118 | Resources: 119 | acceptanceRole: 120 | Type: AWS::IAM::Role 121 | Properties: 122 | RoleName: acceptanceRole-${self:custom.buildNumber}-${opt:runtimeName} 123 | AssumeRolePolicyDocument: 124 | Version: '2012-10-17' 125 | Statement: 126 | - Effect: Allow 127 | Principal: 128 | Service: 129 | - lambda.amazonaws.com 130 | Action: sts:AssumeRole 131 | Policies: 132 | - PolicyName: acceptancePolicyName-${self:custom.buildNumber}-${opt:runtimeName} 133 | PolicyDocument: 134 | Version: '2012-10-17' 135 | Statement: 136 | - Effect: "Allow" 137 | Action: 138 | - "s3:PutObject" 139 | Resource: 140 | Fn::Join: 141 | - "" 142 | - - "arn:aws:s3:::" 143 | - "Ref" : "ServerlessDeploymentBucket" 144 | -------------------------------------------------------------------------------- /test/acceptance/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function cleanup() { 4 | echo 'cleaning up' 5 | cd /tmp/acceptance/lambda-handlers/ 6 | serverless remove --runtime ${runtime} --runtimeName ${runtimeName} --buildNumber ${build_num} 7 | cd $INITIAL_PWD 8 | rm -rf /tmp/acceptance 9 | } 10 | 11 | trap clenaup 1 2 15 12 | 13 | function run_acceptance_test() { 14 | build_num=$1 15 | runtime=$2 16 | runtimeName=$3 17 | echo "deploying of ${runtime} [build: ${build_num}]" 18 | cp -r test/acceptance/ /tmp/acceptance/ 19 | cd /tmp/acceptance/lambda-handlers/ 20 | npm install $INITIAL_PWD 21 | serverless deploy --runtime ${runtime} --runtimeName ${runtimeName} --buildNumber ${build_num} || { echo "deployment of ${runtime} [build: ${build_num}] failed" ; result=1; } 22 | cd - 23 | TRAVIS_BUILD_NUMBER=${build_num} RUNTIME=${runtime} RUNTIME_NAME=${runtimeName} mocha -t 30000 test/acceptance/acceptance.js || { echo "tests ${runtime} [build: ${build_num}] failed" ; result=1; } 24 | cleanup 25 | } 26 | 27 | INITIAL_PWD=`pwd` 28 | build_num=$1 29 | result=0 30 | 31 | run_acceptance_test ${build_num} nodejs14.x njs14 32 | 33 | exit ${result} 34 | -------------------------------------------------------------------------------- /test/unit_tests/consts.js: -------------------------------------------------------------------------------- 1 | const consts = require('../../src/consts.js'); 2 | 3 | 4 | module.exports.REGION = consts.REGION; 5 | module.exports.SNS_NON_EXISTING_ARN = 'arn:aws:sns:us-east-1:100010001000:non-exist-topic1'; 6 | module.exports.MESSAGE = 'MESSAGE_TEXT'; 7 | module.exports.SNS_APP_NAME = 'aws-sdk-v3-sns-test'; 8 | -------------------------------------------------------------------------------- /test/unit_tests/events/google_cloud.test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const { getMessageData, handlePublishMethod, getMessagesFromResponse } = require('../../../src/events/google_cloud'); 3 | 4 | describe('Google cloud events tests', () => { 5 | it('Getting message data from array should return a js object on a valid request', () => { 6 | // Arrange 7 | const expectedOutput = { a: 'test' }; 8 | const mockReqOptsMessages = [{ data: JSON.stringify(expectedOutput) }]; 9 | // Act 10 | const messageDataResponse = getMessageData(mockReqOptsMessages, 0); 11 | // Assert 12 | expect(messageDataResponse).to.deep.equal(expectedOutput); 13 | }); 14 | 15 | it('Getting message data from array should return null when req param is not an array', () => { 16 | [undefined, [], [{}], null, [{ data: 'test' }]].forEach((input) => { 17 | // Arrange 18 | const expectedOutput = null; 19 | const mockReqOptsMessages = input; 20 | // Act 21 | const messageDataResponse = getMessageData(mockReqOptsMessages, 0); 22 | // Assert 23 | expect(messageDataResponse).to.equal(expectedOutput); 24 | }); 25 | }); 26 | 27 | it('Handle publish method should return array of messasges and array of ids on valid input', () => { 28 | // Arrange 29 | const mockMessages = { messageIds: ['1027170925339701'] }; 30 | const mockConfig = { 31 | reqOpts: { 32 | messages: [{ 33 | attributes: {}, 34 | data: Buffer.from( 35 | // eslint-disable-next-line max-len 36 | [123, 34, 109, 101, 115, 115, 97, 103, 101, 34, 58, 34, 116, 101, 115, 116, 34, 125] 37 | ), 38 | }], 39 | }, 40 | }; 41 | const expectedOutput = { messages: [{ id: '1027170925339701', message: 'test' }], messageIdsArray: ['1027170925339701'] }; 42 | // Act 43 | const messageDataResponse = handlePublishMethod(mockMessages, mockConfig); 44 | // Assert 45 | expect(messageDataResponse).to.deep.equal(expectedOutput); 46 | }); 47 | it('Handle publish method should return empty object when messages is not array', () => { 48 | [undefined, null, {}].forEach( 49 | (input) => { 50 | // Arrange 51 | const expectedOutput = {}; 52 | const mockMessages = input; 53 | // Act 54 | const messageDataResponse = handlePublishMethod(mockMessages, 'not-relveant-for-test'); 55 | // Assert 56 | expect(messageDataResponse).to.deep.equal(expectedOutput); 57 | } 58 | ); 59 | }); 60 | it('Getting messages from pull response should return array of messages and array of ids when messages arg is valid', () => { 61 | // Arrange 62 | const mockRes = { 63 | receivedMessages: [{ 64 | ackId: 'ackId', 65 | message: { 66 | data: Buffer.from( 67 | // eslint-disable-next-line max-len 68 | [123, 34, 109, 101, 115, 115, 97, 103, 101, 34, 58, 34, 116, 101, 115, 116, 34, 125] 69 | ), 70 | messageId: '1027170925339701', 71 | }, 72 | }, 73 | { 74 | ackId: 'ackId', 75 | message: { 76 | data: null, 77 | messageId: '1027170925339702', 78 | }, 79 | }], 80 | }; 81 | 82 | 83 | const expectedOutput = { 84 | messages: [{ message: 'test', messageId: '1027170925339701' }, { messageId: '1027170925339702' }], 85 | messageIdsArray: ['1027170925339701', '1027170925339702'], 86 | }; 87 | // Act 88 | const messageDataResponse = getMessagesFromResponse(mockRes); 89 | // Assert 90 | expect(messageDataResponse).to.deep.equal(expectedOutput); 91 | }); 92 | it('Getting messages from pull response should return empty object when res is not valid', () => { 93 | [undefined, [], [{}], null, { receivedMessages: null }].forEach( 94 | (input) => { 95 | // Arrange 96 | const expectedOutput = {}; 97 | const mockReqOptsMessages = input; 98 | // Act 99 | const messageDataResponse = getMessagesFromResponse(mockReqOptsMessages); 100 | // Assert 101 | expect(messageDataResponse).to.deep.equal(expectedOutput); 102 | } 103 | ); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /test/unit_tests/events/ldap-server-mock/ldap-server-mock-conf.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "port": 4389, 4 | "userLoginAttribute": "cn", 5 | "searchBase": "ou=users,dc=myorg,dc=com", 6 | "searchFilter": "(&(objectClass=posixAccount)(!(shadowExpire=0))(uid={{username}}))" 7 | } -------------------------------------------------------------------------------- /test/unit_tests/events/ldap-server-mock/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "dn": "ou=users,dc=myorg,dc=com", 4 | "cn": "user", 5 | "attribute1": "value1", 6 | "attribute2": "value2" 7 | } 8 | ] -------------------------------------------------------------------------------- /test/unit_tests/events/ldapjs.js: -------------------------------------------------------------------------------- 1 | const ldap = require('ldapjs'); 2 | const epsagon = require('../../../src/index'); 3 | 4 | // Test for ldap.js event without assersion, ldap server can be launched with: 5 | // node node_modules/ldap-server-mock/server.js 6 | // --conf=./test/unit_tests/events/ldap-server-mock/ldap-server-mock-conf.json 7 | // --database=./test/unit_tests/events/ldap-server-mock/users.json 8 | describe('ldap.js client events tests', () => { 9 | const client = ldap.createClient({ 10 | url: 'ldap://localhost:4389', 11 | }); 12 | before((done) => { 13 | epsagon.init({ 14 | token: process.env.EPSAGON_TOKEN, 15 | appName: 'itay-ldap-test', 16 | metadataOnly: false, 17 | }); 18 | done(); 19 | }); 20 | 21 | after((done) => { 22 | done(); 23 | }); 24 | 25 | describe('tests without cache', () => { 26 | it('bind', (done) => { 27 | function testFunction() { 28 | client.bind('ou=users,dc=myorg,dc=com', 'secret', () => {}); 29 | } 30 | 31 | const wrappedTestFunction = epsagon.nodeWrapper(testFunction); 32 | wrappedTestFunction(); 33 | client.destroy(); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/unit_tests/events/simple-oauth2.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const semver = require('semver'); 3 | const app = require('./oauth2-server-mock/server'); 4 | const epsagon = require('../../../src/index'); 5 | 6 | 7 | const PORT = 8083; 8 | 9 | 10 | describe('simple-oauth2 tests', () => { 11 | it('sanity', async () => { 12 | if (semver.lt(process.version, '12.0.0')) return; 13 | const SimpleOAuth2 = require('simple-oauth2'); // eslint-disable-line global-require 14 | epsagon.init(); 15 | const server = app.app.listen(PORT); 16 | const oauth2Client = new SimpleOAuth2.AuthorizationCode({ 17 | client: { 18 | id: 'dummy-client-id', 19 | secret: 'dummy-client-secret', 20 | }, 21 | auth: { 22 | tokenHost: 'http://localhost:8083', 23 | tokenPath: '/oauth2/v4/token', 24 | authorizePath: '/o/oauth2/v2/auth', 25 | }, 26 | }); 27 | let accessToken; 28 | try { 29 | accessToken = await oauth2Client.getToken({ 30 | scope: '', 31 | client_id: 'dummy-client-id', 32 | client_secret: 'dummy-client-secret', 33 | code: 'test', 34 | }); 35 | } catch (err) { 36 | console.log(err); // eslint-disable-line no-console 37 | } finally { 38 | server.close(); 39 | expect(accessToken.token.access_token).to.equal('test'); 40 | } 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/unit_tests/test_trace_queue.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const chai = require('chai'); 3 | const chaiAsPromised = require('chai-as-promised'); 4 | const { expect } = require('chai'); 5 | const TraceQueue = require('../../src/trace_queue.js'); 6 | // const config = require('../../src/config.js'); 7 | // const consts = require('../../src/consts.js'); 8 | 9 | chai.use(chaiAsPromised); 10 | 11 | const traceQueue = TraceQueue.getInstance(); 12 | 13 | describe('trace queue tests', () => { 14 | beforeEach(() => { 15 | traceQueue.initQueue(); 16 | traceQueue.batchSender = function batchSender(batch) { 17 | console.log(`Sending batch: ${batch.map(trace => trace)}`); 18 | return batch; 19 | }; 20 | }); 21 | 22 | it('push single trace', () => { 23 | traceQueue.batchSize = 5; 24 | const traces = ['trace_1']; 25 | traceQueue.push(traces[0]); 26 | expect(traceQueue.currentSize).to.equal(1); 27 | }); 28 | it('push traces without reaching maximum', () => { 29 | traceQueue.batchSize = 5; 30 | const traces = ['trace_2', 'trace_3', 'trace_4', 'trace_5']; 31 | traceQueue.push(traces[0]); 32 | traceQueue.push(traces[1]); 33 | traceQueue.push(traces[2]); 34 | traceQueue.push(traces[3]); 35 | expect(traceQueue.currentSize).to.equal(4); 36 | expect(traceQueue.queue.map(trace => trace.json)).to.eql(traces); 37 | }); 38 | 39 | it('push traces with reaching batch size', () => { 40 | traceQueue.batchSize = 5; 41 | const traces = ['trace_1', 'trace_2', 'trace_3', 'trace_4', 'trace_5']; 42 | traceQueue.push(traces[0]); 43 | traceQueue.push(traces[1]); 44 | traceQueue.push(traces[2]); 45 | traceQueue.push(traces[3]); 46 | traceQueue.push(traces[4]); 47 | expect(traceQueue.currentSize).to.equal(0); 48 | }); 49 | it('release batch on reaching batch size', (done) => { 50 | traceQueue.batchSize = 5; 51 | // console.log(traceQueue.maxBatchSizeBytes); 52 | console.log(traceQueue.maxQueueSizeBytes); 53 | const traces = ['trace_1', 'trace_2', 'trace_3', 'trace_4', 'trace_5']; 54 | traceQueue.on('batchReleased', (batch) => { 55 | expect(traceQueue.currentSize).to.equal(0); 56 | expect(batch.map(trace => trace.json)).to.eql(traces); 57 | done(); 58 | }); 59 | 60 | traceQueue.push(traces[0]); 61 | traceQueue.push(traces[1]); 62 | traceQueue.push(traces[2]); 63 | traceQueue.push(traces[3]); 64 | traceQueue.push(traces[4]); 65 | }); 66 | it('sending batch on reaching batch size', (done) => { 67 | traceQueue.batchSize = 5; 68 | const traces = ['trace_1', 'trace_2', 'trace_3', 'trace_4', 'trace_5']; 69 | traceQueue.on('batchReleased', (batch) => { 70 | expect(traceQueue.currentSize).to.equal(0); 71 | expect(batch.map(trace => trace.json)).to.eql(traces); 72 | done(); 73 | }); 74 | traceQueue.push(traces[0]); 75 | traceQueue.push(traces[1]); 76 | traceQueue.push(traces[2]); 77 | traceQueue.push(traces[3]); 78 | traceQueue.push(traces[4]); 79 | }); 80 | it('push traces while not reaching byte size limit', () => { 81 | traceQueue.batchSize = 5; 82 | traceQueue.maxBatchSizeBytes = 32; 83 | const traces = ['trace_1', 'trace_2', 'trace_3', 'trace_4', 'trace_5']; 84 | traceQueue.push(traces[0]); 85 | expect(traceQueue.currentSize).to.equal(1); 86 | expect(traceQueue.queue[0].json).to.eql('trace_1'); 87 | }); 88 | it('push traces while reaching more than byte size limit', () => { 89 | traceQueue.batchSize = 5; 90 | traceQueue.maxBatchSizeBytes = 32; 91 | const traces = ['trace_1', 'trace_2', 'trace_3', 'trace_4', 'trace_5', 'trace_6']; 92 | traceQueue.push(traces[0]); 93 | traceQueue.push(traces[1]); 94 | traceQueue.push(traces[2]); 95 | traceQueue.push(traces[3]); 96 | traceQueue.push(traces[4]); 97 | traceQueue.push(traces[5]); 98 | expect(traceQueue.currentSize).to.equal(3); 99 | expect(traceQueue.queue.map(trace => trace.json)).to.eql(['trace_4', 'trace_5', 'trace_6']); 100 | }); 101 | it('push traces while reaching no more than byte size limit', () => { 102 | traceQueue.batchSize = 5; 103 | traceQueue.maxBatchSizeBytes = 20; 104 | const traces = ['trace_1', 'trace_2', 'trace_3']; 105 | traceQueue.push(traces[0]); 106 | traceQueue.push(traces[1]); 107 | traceQueue.push(traces[2]); 108 | expect(traceQueue.currentSize).to.equal(1); 109 | expect(traceQueue.queue[0].json).to.eql('trace_3'); 110 | }); 111 | it('push big trace larger than batch size limit', () => { 112 | traceQueue.batchSize = 5; 113 | traceQueue.maxBatchSizeBytes = 5; 114 | const traces = ['trace_1']; 115 | traceQueue.push(traces[0]); 116 | expect(traceQueue.currentSize).to.equal(0); 117 | }); 118 | it('push big trace larger than queue size limit', () => { 119 | traceQueue.batchSize = 5; 120 | traceQueue.maxQueueSizeBytes = 8; 121 | traceQueue.maxBatchSizeBytes = 100; 122 | 123 | console.log(traceQueue.queue); 124 | const traces = ['trace_1', 'trace_2']; 125 | traceQueue.push(traces[0]); 126 | traceQueue.push(traces[1]); 127 | expect(traceQueue.currentSize).to.equal(1); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /test/unit_tests/wrappers/test_batch.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const sinon = require('sinon'); 3 | const tracer = require('../../../src/tracer.js'); 4 | const eventInterface = require('../../../src/event.js'); 5 | const batchWrapper = require('../../../src/wrappers/batch.js'); 6 | const batchRunner = require('../../../src/runners/aws_batch.js'); 7 | const serverlessEvent = require('../../../src/proto/event_pb.js'); 8 | 9 | describe('batchWrapper tests', () => { 10 | beforeEach(() => { 11 | this.restartStub = sinon.stub( 12 | tracer, 13 | 'restart' 14 | ); 15 | 16 | this.addEventStub = sinon.stub( 17 | tracer, 18 | 'addEvent' 19 | ); 20 | 21 | this.addExceptionStub = sinon.stub( 22 | tracer, 23 | 'addException' 24 | ); 25 | 26 | this.sendTraceStub = sinon.stub( 27 | tracer, 28 | 'sendTrace' 29 | ).returns(Promise.resolve('success')); 30 | 31 | this.sendTraceSyncStub = sinon.stub( 32 | tracer, 33 | 'sendTraceSync' 34 | ).returns(Promise.resolve('success')); 35 | 36 | this.setExceptionStub = sinon.stub( 37 | eventInterface, 38 | 'setException' 39 | ); 40 | 41 | this.processOnStub = sinon.stub( 42 | process, 43 | 'on' 44 | ); 45 | 46 | this.createRunnerStub = sinon.stub( 47 | batchRunner, 48 | 'createRunner' 49 | ); 50 | 51 | const resource = new serverlessEvent.Resource(); 52 | const event = new serverlessEvent.Event(); 53 | event.setResource(resource); 54 | this.createRunnerStub.returns({ 55 | runner: event, 56 | runnerPromise: Promise.resolve(1), 57 | }); 58 | }); 59 | 60 | afterEach(() => { 61 | this.addEventStub.restore(); 62 | this.addExceptionStub.restore(); 63 | this.sendTraceStub.restore(); 64 | this.sendTraceSyncStub.restore(); 65 | this.restartStub.restore(); 66 | this.setExceptionStub.restore(); 67 | this.processOnStub.restore(); 68 | this.createRunnerStub.restore(); 69 | }); 70 | 71 | it('wrapBatchJob: sanity', () => { 72 | batchWrapper.wrapBatchJob(); 73 | expect(this.restartStub.callCount).to.equal(1); 74 | expect(this.addEventStub.callCount).to.equal(0); 75 | expect(this.addExceptionStub.called).to.be.false; 76 | expect(this.sendTraceStub.callCount).to.equal(0); 77 | expect(this.sendTraceSyncStub.callCount).to.equal(0); 78 | expect(this.processOnStub.callCount).to.equal(2); 79 | this.processOnStub.getCall(1).args[1](); 80 | expect(this.sendTraceStub.callCount).to.equal(1); 81 | }); 82 | 83 | it('wrapBatchJob: raised Error', () => { 84 | batchWrapper.wrapBatchJob(); 85 | expect(this.restartStub.callCount).to.equal(1); 86 | expect(this.addEventStub.callCount).to.equal(0); 87 | expect(this.addExceptionStub.called).to.be.false; 88 | expect(this.sendTraceStub.callCount).to.equal(0); 89 | expect(this.sendTraceSyncStub.callCount).to.equal(0); 90 | expect(this.processOnStub.callCount).to.equal(2); 91 | // simulate raising an error 92 | const err = new Error('err'); 93 | this.processOnStub.getCall(0).args[1](err); 94 | expect(this.setExceptionStub.callCount).to.equal(1); 95 | this.processOnStub.getCall(1).args[1](); 96 | expect(this.sendTraceStub.callCount).to.equal(1); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/unit_tests/wrappers/test_node.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const sinon = require('sinon'); 3 | const tracer = require('../../../src/tracer.js'); 4 | const eventInterface = require('../../../src/event.js'); 5 | const consts = require('../../../src/consts.js'); 6 | const nodeWrapper = require('../../../src/wrappers/node.js'); 7 | const errorCode = require('../../../src/proto/error_code_pb.js'); 8 | 9 | describe('nodeWrapper tests', () => { 10 | beforeEach(() => { 11 | this.restartStub = sinon.stub( 12 | tracer, 13 | 'restart' 14 | ); 15 | 16 | this.addRunnerStub = sinon.stub( 17 | tracer, 18 | 'addRunner' 19 | ); 20 | 21 | this.addExceptionStub = sinon.stub( 22 | tracer, 23 | 'addException' 24 | ); 25 | 26 | this.sendTraceStub = sinon.stub( 27 | tracer, 28 | 'sendTrace' 29 | ); 30 | 31 | this.sendTraceSyncStub = sinon.stub( 32 | tracer, 33 | 'sendTraceSync' 34 | ); 35 | 36 | this.setExceptionStub = sinon.stub( 37 | eventInterface, 38 | 'setException' 39 | ); 40 | 41 | this.stubFunction = sinon.stub(); 42 | this.wrappedStub = nodeWrapper.nodeWrapper(this.stubFunction); 43 | consts.COLD_START = true; 44 | }); 45 | 46 | afterEach(() => { 47 | this.addRunnerStub.restore(); 48 | this.addExceptionStub.restore(); 49 | this.sendTraceStub.restore(); 50 | this.sendTraceSyncStub.restore(); 51 | this.restartStub.restore(); 52 | this.setExceptionStub.restore(); 53 | }); 54 | 55 | it('nodeWrapper: return a function', () => { 56 | expect(this.wrappedStub).to.be.a('function'); 57 | }); 58 | 59 | it('nodeWrapper: sanity', (done) => { 60 | this.wrappedStub(1, 2, 3); 61 | setTimeout(() => { 62 | expect(this.restartStub.callCount).to.equal(1); 63 | expect(this.addRunnerStub.callCount).to.equal(1); 64 | expect(this.addExceptionStub.called).to.be.false; 65 | expect(this.sendTraceStub.callCount).to.equal(1); 66 | expect(this.stubFunction.callCount).to.equal(1); 67 | expect(this.setExceptionStub.called).to.be.false; 68 | done(); 69 | }, 1); 70 | }); 71 | 72 | it('nodeWrapper: create correct runner event', () => { 73 | const wrappedFunction = function name() {}; 74 | this.wrappedStub = nodeWrapper.nodeWrapper(wrappedFunction); 75 | this.wrappedStub(1, 2, 3); 76 | expect(this.addRunnerStub.callCount).to.equal(1); 77 | const runnerEvent = this.addRunnerStub.getCall(0).args[0]; 78 | expect(runnerEvent.getId()).to.be.a('string'); 79 | expect(runnerEvent.getStartTime()).to.be.a('number'); 80 | expect(runnerEvent.getDuration()).to.be.a('number'); 81 | expect(runnerEvent.getOrigin()).to.equal('runner'); 82 | expect(runnerEvent.getErrorCode()).to.equal(errorCode.ErrorCode.OK); 83 | expect(runnerEvent.getException()).to.be.undefined; 84 | const resource = runnerEvent.getResource(); 85 | expect(resource.getName()).to.equal('name'); 86 | expect(resource.getType()).to.equal('node_function'); 87 | expect(resource.getOperation()).to.equal('invoke'); 88 | expect(resource.getMetadataMap().get('args_length')).to.equal(3); 89 | }); 90 | 91 | it('nodeWrapper: wrapped function throws error', () => { 92 | this.stubFunction.throws(); 93 | expect(() => this.wrappedStub(1, 2, 3)).to.throw(); 94 | expect(this.restartStub.callCount).to.equal(1); 95 | expect(this.addRunnerStub.callCount).to.equal(1); 96 | expect(this.addExceptionStub.called).to.be.false; 97 | expect(this.sendTraceStub.called).to.be.false; 98 | expect(this.sendTraceSyncStub.callCount).to.equal(1); 99 | expect(this.stubFunction.callCount).to.equal(1); 100 | expect(this.setExceptionStub.callCount).to.equal(1); 101 | }); 102 | 103 | it('nodeWrapper: update COLD_START value', () => { 104 | consts.COLD_START = true; 105 | this.wrappedStub(1, 2, 3); 106 | expect(consts.COLD_START).to.be.false; 107 | }); 108 | 109 | it('nodeWrapper: COLD_START value should be false after more then 1 call', () => { 110 | consts.COLD_START = true; 111 | 112 | this.wrappedStub(1, 2, 3); 113 | expect(consts.COLD_START).to.be.false; 114 | 115 | this.wrappedStub(1, 2, 3); 116 | expect(consts.COLD_START).to.be.false; 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /test/unit_tests/wrappers/test_openwhisk.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | const request = require('request-promise-native'); 3 | const { expect } = require('chai'); 4 | const epsagon = require('../../../src/index'); 5 | 6 | const owsimple = () => ({ 7 | hello: 'world', 8 | }); 9 | 10 | const owpromise = () => Promise.resolve({ 11 | hello: 'world', 12 | }); 13 | 14 | const owrequest = () => request.get('http://www.example.com').then(response => ({ 15 | body: response, 16 | })); 17 | 18 | describe('openwhiskWrapper tests', () => { 19 | it('openwhiskWrapper: return a function', () => { 20 | const wrapped = epsagon.openWhiskWrapper(owsimple); 21 | expect(wrapped).to.be.a('function'); 22 | }); 23 | 24 | it('openwhiskWrapper: guards against rewrapping', () => { 25 | const wrapped = epsagon.openWhiskWrapper(owsimple); 26 | const wrappedTwice = epsagon.openWhiskWrapper(wrapped); 27 | expect(wrapped).to.be.equal(wrappedTwice); 28 | }); 29 | 30 | it('openwhiskWrapper: wrapped function returns values', () => { 31 | const wrapped = epsagon.openWhiskWrapper(owsimple); 32 | const retval = wrapped(); 33 | expect(retval).to.deep.equal({ 34 | hello: 'world', 35 | }); 36 | }); 37 | 38 | it('openwhiskWrapper: wrapped function returns promises', async () => { 39 | const wrapped = epsagon.openWhiskWrapper(owpromise); 40 | const retval = await wrapped(); 41 | expect(retval).to.deep.equal({ 42 | hello: 'world', 43 | }); 44 | }); 45 | 46 | it('openwhiskWrapper: wrapped function returns async values', async () => { 47 | const wrapped = epsagon.openWhiskWrapper(owrequest); 48 | const retval = await wrapped(); 49 | expect(retval.body).to.contain('Example Domain'); 50 | }); 51 | 52 | it('openwhiskWrapper: can pass token into wrapper function', async () => { 53 | const wrapped = epsagon.openWhiskWrapper(owrequest, { token: 'foobar' }); 54 | 55 | let foundtoken; 56 | const oldinit = epsagon.tracer.initTrace; 57 | 58 | epsagon.tracer.initTrace = function (options) { 59 | foundtoken = options.token; 60 | oldinit(options); 61 | }; 62 | 63 | const retval = await wrapped(); 64 | expect(retval.body).to.contain('Example Domain'); 65 | 66 | epsagon.tracer.initTrace = oldinit; 67 | expect(foundtoken).to.equal('foobar'); 68 | }); 69 | 70 | it('openwhiskWrapper: can indirectly pass token into wrapper function', async () => { 71 | const wrapped = epsagon.openWhiskWrapper(owrequest, { token_param: 'EPSAGON_TOKEN' }); 72 | 73 | let foundtoken; 74 | const oldinit = epsagon.tracer.initTrace; 75 | 76 | epsagon.tracer.initTrace = function (options) { 77 | foundtoken = options.token; 78 | oldinit(options); 79 | }; 80 | 81 | const retval = await wrapped({ EPSAGON_TOKEN: 'barbaz' }); 82 | expect(retval.body).to.contain('Example Domain'); 83 | 84 | epsagon.tracer.initTrace = oldinit; 85 | expect(foundtoken).to.equal('barbaz'); 86 | }); 87 | 88 | it('openwhiskWrapper: wrapper will not fail when called without params', async () => { 89 | const wrapped = epsagon.openWhiskWrapper(owrequest, { token_param: 'EPSAGON_TOKEN' }); 90 | 91 | let foundtoken; 92 | const oldinit = epsagon.tracer.initTrace; 93 | 94 | epsagon.tracer.initTrace = function (options) { 95 | foundtoken = options.token; 96 | oldinit(options); 97 | }; 98 | 99 | const retval = await wrapped(); 100 | expect(retval.body).to.contain('Example Domain'); 101 | 102 | epsagon.tracer.initTrace = oldinit; 103 | expect(foundtoken).to.be.undefined; 104 | }); 105 | 106 | it('openwhiskWrapper: hard coded token overrides variable token', async () => { 107 | const wrapped = epsagon.openWhiskWrapper(owrequest, { token_param: 'EPSAGON_TOKEN', token: 'fooboo' }); 108 | 109 | let foundtoken; 110 | const oldinit = epsagon.tracer.initTrace; 111 | 112 | epsagon.tracer.initTrace = function (options) { 113 | foundtoken = options.token; 114 | oldinit(options); 115 | }; 116 | 117 | const retval = await wrapped({ EPSAGON_TOKEN: 'barbaz' }); 118 | expect(retval.body).to.contain('Example Domain'); 119 | 120 | epsagon.tracer.initTrace = oldinit; 121 | expect(foundtoken).to.equal('fooboo'); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /test/unit_tests/wrappers/test_openwhisk_traces.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const sinon = require('sinon'); 3 | const crypto = require('crypto'); 4 | const tracer = require('../../../src/tracer.js'); 5 | const eventInterface = require('../../../src/event.js'); 6 | const consts = require('../../../src/consts.js'); 7 | const openwhiskWrapper = require('../../../src/wrappers/openwhisk.js'); 8 | const config = require('../../../src/config.js'); 9 | 10 | const DEFAULT_TIMEOUT = 10000; 11 | const RETURN_VALUE = { 12 | status: 200, 13 | body: 'ok', 14 | }; 15 | 16 | function setupProcessEnv() { 17 | process.env['__OW_ACTIVATION_ID'] = crypto.randomBytes(16).toString('hex'); // eslint-disable-line dot-notation 18 | process.env['__OW_TRANSACTION_ID'] = crypto.randomBytes(16).toString('hex'); // eslint-disable-line dot-notation 19 | process.env['__OW_API_HOST'] = 'runtime.example.com'; // eslint-disable-line dot-notation 20 | process.env['__OW_NAMESPACE'] = 'test-namespace'; // eslint-disable-line dot-notation 21 | process.env['__OW_ACTION_NAME'] = 'test-action-name'; // eslint-disable-line dot-notation 22 | } 23 | 24 | describe('openwhiskWrapper tracer tests', () => { 25 | beforeEach(() => { 26 | config.setConfig({ metadataOnly: false }); 27 | setupProcessEnv(); 28 | 29 | this.restartStub = sinon.stub( 30 | tracer, 31 | 'restart' 32 | ); 33 | 34 | this.addEventStub = sinon.stub( 35 | tracer, 36 | 'addEvent' 37 | ); 38 | 39 | this.addExceptionStub = sinon.stub( 40 | tracer, 41 | 'addException' 42 | ); 43 | 44 | this.postTraceStub = sinon.stub( 45 | tracer, 46 | 'postTrace' 47 | ).returns(Promise.resolve('success')); 48 | 49 | this.setExceptionStub = sinon.stub( 50 | eventInterface, 51 | 'setException' 52 | ); 53 | 54 | this.markAsTimeoutStub = sinon.stub( 55 | eventInterface, 56 | 'markAsTimeout' 57 | ); 58 | this.stubFunction = sinon.stub().returns(RETURN_VALUE); 59 | this.wrappedStub = openwhiskWrapper.openWhiskWrapper(this.stubFunction, { 60 | token: 'foo', 61 | }); 62 | 63 | this.context = { 64 | getRemainingTimeInMillis() { return DEFAULT_TIMEOUT; }, 65 | callbackWaitsForEmptyEventLoop: true, 66 | fail() {}, 67 | succeed() {}, 68 | done() {}, 69 | }; 70 | consts.COLD_START = true; 71 | }); 72 | 73 | afterEach(() => { 74 | this.addEventStub.restore(); 75 | this.addExceptionStub.restore(); 76 | this.postTraceStub.restore(); 77 | this.restartStub.restore(); 78 | this.setExceptionStub.restore(); 79 | this.markAsTimeoutStub.restore(); 80 | }); 81 | 82 | it('openwhiskWrapper: sanity', async () => { 83 | await this.wrappedStub({}); 84 | expect(this.restartStub.callCount).to.equal(1); 85 | expect(this.addExceptionStub.called).to.be.false; 86 | expect(this.postTraceStub.callCount).to.equal(1); 87 | expect(this.setExceptionStub.called).to.be.false; 88 | expect(this.stubFunction.callCount).to.equal(1); 89 | }); 90 | 91 | it('openwhiskWrapper: send 2 traces for 2 actions.', async () => { 92 | await this.wrappedStub({}); 93 | expect(this.stubFunction.callCount).to.equal(1); 94 | 95 | this.stubFunction = sinon.stub().returns(RETURN_VALUE); 96 | this.wrappedStub = openwhiskWrapper.openWhiskWrapper(this.stubFunction, { 97 | token: 'foo', 98 | }); 99 | setupProcessEnv(); 100 | await this.wrappedStub({}); 101 | expect(this.stubFunction.callCount).to.equal(1); 102 | expect(this.restartStub.callCount).to.equal(2); 103 | expect(this.addExceptionStub.called).to.be.false; 104 | expect(this.postTraceStub.callCount).to.equal(2); 105 | expect(this.setExceptionStub.called).to.be.false; 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /trace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/epsagon/epsagon-node/67026dc2e6a333853238b1dcc3ff532023ed53ba/trace.png --------------------------------------------------------------------------------