diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ce45b111 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.log +docs diff --git a/README.md b/README.md index f0ce0dbc..bc603416 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,75 @@ -# aws-xray-sdk-node -The official AWS X-Ray SDK for Node.js. +# AWS X-Ray SDK for Node.js + +![Screenshot of the AWS X-Ray console](/images/example_servicemap.png?raw=true) + +## Installing + +The AWS X-Ray SDK for Node.js is compatible with Node.js version 0.8 and later. + +The SDK is available from NPM. For local development, install the SDK in your project directory with npm. + +``` +npm install aws-xray-sdk +``` + +Use the --save option to save the SDK as a dependency in your application's package.json. + +``` +npm install aws-xray-sdk --save +``` + +## Getting Help + +Use the following community resources for getting help with the SDK. We use the GitHub +issues for tracking bugs and feature requests. + +* Ask a question in the [AWS X-Ray Forum](https://forums.aws.amazon.com/forum.jspa?forumID=241&start=0). +* Open a support ticket with [AWS Support](http://docs.aws.amazon.com/awssupport/latest/user/getting-started.html). +* If you think you may have found a bug, open an [issue](/~https://github.com/aws/aws-xray-sdk-node/issues/new). + +## Opening Issues + +If you encounter a bug with the AWS X-Ray SDK for Node.js, we want to hear about +it. Before opening a new issue, search the [existing issues](/~https://github.com/aws/aws-xray-sdk-node/issues) +to see if others are also experiencing the issue. Include the version of the AWS X-Ray +SDK for Node.js, Node.js runtime, and other dependencies if applicable. In addition, +include the repro case when appropriate. + +The GitHub issues are intended for bug reports and feature requests. For help and +questions about using the AWS X-Ray SDK for Node.js, use the resources listed +in the [Getting Help](/~https://github.com/aws/aws-xray-sdk-node#getting-help) section. Keeping the list of open issues lean helps us respond in a timely manner. + +## Documentation + +The [developer guide](https://docs.aws.amazon.com/xray/latest/devguide) provides in-depth +guidance about using the AWS X-Ray service. +The [API Reference](http://docs.aws.amazon.com/xray-sdk-for-nodejs/latest/reference/) +provides guidance for using the SDK and module-level documentation. + +## Contributing + +This monorepo hosts the following npm packages for the SDK: +- [aws-xray-sdk](https://www.npmjs.com/package/aws-xray-sdk) +- [aws-xray-sdk-core](https://www.npmjs.com/package/aws-xray-sdk-core) +- [aws-xray-sdk-express](https://www.npmjs.com/package/aws-xray-sdk-express) +- [aws-xray-sdk-mysql](https://www.npmjs.com/package/aws-xray-sdk-mysql) +- [aws-xray-sdk-postgres](https://www.npmjs.com/package/aws-xray-sdk-postgres) +- [aws-xray-sdk-restify](https://www.npmjs.com/package/aws-xray-sdk-restify) + +This repo uses [Lerna](https://lernajs.io) to manage multiple packages. To install Lerna: +``` +npm install lerna +``` +To install devDependencies and peerDependencies for all packages: +``` +lerna bootstrap --hoist +``` +To run tests for all packages: +``` +lerna run test +``` +or go to each package and run `npm test` as usual. + +## License + +The AWS X-Ray SDK for Node.js is licensed under the Apache 2.0 License. See LICENSE and NOTICE.txt for more information. diff --git a/images/example_servicemap.png b/images/example_servicemap.png new file mode 100644 index 00000000..3b44ecea Binary files /dev/null and b/images/example_servicemap.png differ diff --git a/lerna.json b/lerna.json new file mode 100644 index 00000000..993ace3b --- /dev/null +++ b/lerna.json @@ -0,0 +1,10 @@ +{ + "lerna": "2.4.0", + "packages": [ + "packages/*" + ], + "version": "independent", + "npmClientArgs": [ + "--no-package-lock" + ] +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..c8ce2438 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "aws-xray-sdk-node", + "private": true, + "license": "Apache-2.0", + "devDependencies": { + "aws-xray-sdk-core": "^1.1.4", + "chai": "^3.5.0", + "eslint": "^3.10.2", + "grunt": "^1.0.1", + "grunt-contrib-clean": "^1.0.0", + "grunt-jsdoc": "^2.1.0", + "lerna": "^2.4.0", + "mocha": "^3.0.2", + "nock": "^8.0.0", + "sinon": "^1.17.5", + "sinon-chai": "^2.8.0" + }, + "engines": { + "node": ">= 4.x <= 9.x", + "npm": ">= 2.x <= 5.x" + } +} diff --git a/packages/core/.eslintrc.json b/packages/core/.eslintrc.json new file mode 100644 index 00000000..a3e0b54b --- /dev/null +++ b/packages/core/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "eol-last": [ + "error", + "always" + ] + } +} diff --git a/packages/core/.npmignore b/packages/core/.npmignore new file mode 100644 index 00000000..a17aa44b --- /dev/null +++ b/packages/core/.npmignore @@ -0,0 +1,11 @@ +.npmignore +node_modules +npm-debug.log +docs +AWSXRay.log +Config +fat_sdk +core +express +mysql +postgres diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md new file mode 100644 index 00000000..08a3cf34 --- /dev/null +++ b/packages/core/CHANGELOG.md @@ -0,0 +1,37 @@ +# Changelog for AWS X-Ray Core SDK for JavaScript + + + +## 1.1.5 +* The X-Ray SDK for Node.js is now an open source project. You can follow the project and submit issues and pull requests on [GitHub](/~https://github.com/aws/aws-xray-sdk-node). + +## 1.14 +* bugfix: Fixing issue where an unexpected segment on the CLS context would fail in Lambda +## 1.1.3 +* bugfix: Resolving Lambda segment information on `getSegment` rather than on `addSubsegment`. + +## 1.1.2 +* feature: Reintroduced global HTTP/S patcher. See the documentation for 'captureHTTPsGlobal' for details. +* feature: Added AWSXray.appendAWSWhitelist function to append to the current whitelist loaded. +* bugfix: Fixed compatibility issues with webpack in regard to custom sampling rules and AWS whitelists. +* bugfix: Fixed issue where partial subsegment streaming would throw an error on 'undefined'. +* bugfix: Fixed issue where AWS call response descriptors were attempting to be read on an error. + +## 1.1.1 +* feature: Added debug logs for sampling rates and matches. +* feature: Added patcher for the http.get helper function as a part of the captureHTTPs function. +* bugfix: Fixed issue where default fixed target/rate set to zero in sampling rate file would erroneously throw an error. +* bugfix: Fixed issue where url capturing for incoming requests was coded to an Express-only property. +* bugfix: Fixed issue with S3 calls where only x-amz-id-2 was captured as request ID. Added 'id_2' property to properly capture S3 request ID pairs. +* bugfix: Fixed issue where capturing a count of parameters on a parameter of an AWS call when the parameter wasn't defined would capture 'undefined'. +* bugfix: Fixed compatibility issue with webpack. +* bugfix: Fixed issue where the open socket to send segments would cause Node process to hang on attempted graceful shutdown. + +## 1.1.0 +* **BREAKING** change: Segment.addSDKVersions() reworked into setSDKData(). +* **BREAKING** change: Segment.addServiceVersions() reworked into setServiceData(). +* change: Capturing AWS and HTTP calls in manual mode now uses a param 'XRaySegment' rather than 'Segment'. Backwards compatible, see usage examples in README.md. +* feature: Added support for capturing AWS Lambda function invocations. +* feature: Added additional data captured from NPM and process.env to segments. +* feature: Added custom namespaces for metadata. Usage: segment.addMetadata(, , ). +* bugfix: Fixed issue where AWS call capturing marked exceptions due to throttling as 'error'. Now marked as 'throttled'. diff --git a/packages/core/Gruntfile.js b/packages/core/Gruntfile.js new file mode 100644 index 00000000..ea05919e --- /dev/null +++ b/packages/core/Gruntfile.js @@ -0,0 +1,20 @@ +module.exports = function(grunt) { + // Project configuration. + grunt.initConfig({ + jsdoc: { + dist: { + src: ['lib/**/*.js', 'README.md'], + dest: 'docs' + } + }, + clean: { + folder: ['docs'] + } + }); + + // Register jsdoc as a grunt task + grunt.loadNpmTasks('grunt-jsdoc'); + grunt.loadNpmTasks('grunt-contrib-clean'); + + grunt.registerTask('docs', ['clean', 'jsdoc']); +}; diff --git a/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/packages/core/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/core/NOTICE.txt b/packages/core/NOTICE.txt new file mode 100644 index 00000000..acbb7e19 --- /dev/null +++ b/packages/core/NOTICE.txt @@ -0,0 +1,5 @@ +AWS X-Ray SDK Core for JavaScript +Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..9efeac7f --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,581 @@ + +## Requirements + +AWS SDK v2.7.15 or greater if using `captureAWS` or `captureAWSClient` + +## AWS X-Ray + +The AWS X-Ray SDK (the SDK) automatically records information for incoming and outgoing +requests and responses (via middleware). It also automatically records local data +such as function calls, time, variables (via metadata and annotations), and Amazon +EC2 instance data (via plugins). Currently, only Express +applications are supported for automatic capturing. See the [aws-xray-sdk-express] +(/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/express) package for additional information. + +The SDK exposes the Segment and Subsegment objects so you can create your own capturing +mechanisms, but a few are supplied. +These keep the current subsegment up to date in automatic mode, or propagate the current subsegment in manual mode. + +`AWSXRay.captureFunc` - Takes a function that takes a single subsegment argument. This creates a new nested subsegment and exposes it. The segment +closes automatically when the function finishes executing. This does not correctly +time functions with asynchronous calls. Instead, use +captureAsyncFunc. + +`AWSXRay.captureAsyncFunc` - Takes a function that takes a single subsegment argument. This creates a new nested subsegment and exposes it. The segment +must be closed using subsegment.close() or subsegment.close(error) when the asynchronous function completes. + +`AWSXRay.captureCallbackFunc` - Takes a function to be used as a callback. Useful +for capturing callback information and directly associating it to the call +that generated it. This creates a new nested subsegment and exposes it by appending it onto the arguments used to call the callback. For this reason, +always call your captured callbacks with the full parameter list. The subsegment closes +automatically when the function finishes executing. + +## Setup + +### Automatic and manual mode + +The AWS X-Ray SDK has two modes: `manual` and `automatic`. +By default, the SDK is in automatic mode. You can flip the mode of the SDK using the following: + + AWSXRay.enableManualMode(); + + AWSXRay.enableAutomaticMode(); + +#### Automatic mode + +Automatic mode is for use with the `aws-xray-sdk-express` module to support Express +applications, but can be used outside of Express applications. +The `aws-xray-sdk-express` module captures incoming request/response information via middleware and creates the base segment object automatically. +If your application isn't using the Express middleware, you have to create a +new segment, and set this on the SDK when in automatic mode. + + var segment = new AWSXRay.Segment(name, [optional root ID], [optional parent ID]); + AWSXRay.setSegment(segment); + +For more information about developing your own middleware or using automatic mode without middleware, see the `developing custom solutions +using automatic mode` section below. + +Automatic mode uses the Continuation Local Storage package and automatically tracks +the current segment or subsegment when using the built-in capture functions or any +of the aws-xray-sdk modules. Using the built-in capture functions or other aws-xray-sdk modules automatically creates +new subsegments to capture additional data and update the current segment or subsegment on that context. + +You can retrieve the current segment or subsegment at any time using +the following: + + var segment = AWSXRay.getSegment(); + +#### Manual mode + +Manual mode requires that you pass around the segment reference. See the examples +below for the different usages. + +### Environment variables + +**Environment variables always override values set in code.** + + AWS_XRAY_DEBUG_MODE Enables logging to console output. Logging to a file is no longer built in. See 'configure logging' below. + AWS_XRAY_TRACING_NAME For overriding the default segment name to use + with the middleware. See 'dynamic and fixed naming modes'. + AWS_XRAY_DAEMON_ADDRESS For setting the daemon address and port. Expects 'x.x.x.x', 'hostname', ':yyyy', 'x.x.x.x:yyyy' or 'hostname:yyyy' IPv4 formats. + AWS_XRAY_CONTEXT_MISSING For setting the SDK behavior when trace context is missing. Valid values are 'RUNTIME_ERROR' or 'LOG_ERROR'. The SDK's default behavior is 'RUNTIME_ERROR'. + +### Daemon configuration + +By default, the SDK expects the daemon to be at 127.0.0.1 (localhost) on port 2000. You can override the address, port, or both. +You can change this via the environment variables listed above, or through +code. The same format applies to both. + + AWSXRay.setDaemonAddress('hostname:8000'); + AWSXRay.setDaemonAddress('186.34.0.23:8082'); + AWSXRay.setDaemonAddress(':8082'); + AWSXRay.setDaemonAddress('186.34.0.23'); + +### Logging configuration + +Default logging to a file has been removed. To set up file logging, configure a logger +that responds to debug, info, warn, and error functions. +To log information about configuration, be sure you set the logger before other configuration +options. + + AWSXRay.setLogger(logger); + +### Sampling configuration + +When using our supported AWS X-Ray-enabled frameworks, you can configure the rates +at which the SDK samples requests to capture. + +A sampling rule defines the rate at which requests are sampled for a particular endpoint, HTTP method, and URL of the incoming request. +In this way, you can change the behavior of sampling using `http_method`, `service_name`, +`url_path` attributes to specify the route, and then use +`fixed_target` and rate to determine sampling rates. + +Fixed target refers to the maximum number of requests to sample per second. When this +threshold is reached, the sampling decision uses the specified percentage (rate) to sample on. + +The SDK comes with a default sampling file at `/lib/resources/sampling_rules.js`. +You can choose to override this by providing a custom sampling file. + + AWSXRay.middleware.setSamplingRules(); + AWSXRay.middleware.setSamplingRules(); + +A sampling file must have a "default" defined. The default matches all routes as a fallback, if none of the rules match. + + { + "rules": [], + "default": { + "fixed_target": 10, + "rate": 0.05 + }, + "version": 1 + } + +Order of priority is determined by the spot in the rules array, top being highest priority. The default is always checked last. +Service name, URL path, and HTTP method patterns are case insensitive, and use a string with wild cards as the pattern format. +A `*` represents any number of characters, while `?` represents a single character. A description is optional. + + { + "rules": [ + { + "description": "Sign-in request", + "http_method": "GET", + "service_name": "*.foo.com", + "url_path": "/signin/*", + "fixed_target": 10, + "rate": 0.05 + } + ], + "default": { + "fixed_target": 10, + "rate": 0.05 + }, + "version": 1 + } + +### AWS SDK whitelist configuration + +The AWS X-Ray SDK automatically captures data from AWS SDK calls, including service, +operation, start time, end time, and any errors returned. +However, some service operations are whitelisted to capture extra parameters on the request and response. +These are pulled in via a default whitelisting file in the SDK in the `aws-xray-sdk-core` package under `lib/resources/aws_whitelist.json`. +Each service is whitelisted by the AWS SDK's `service identifier` and `operation` properties. + + request_parameters are properties to capture in the request + request_descriptors are objects to capture, or to process and capture in the request (get_keys, get_count) + response_parameters are properties to capture in the response data + response_descriptors are objects to capture, or to process and capture in the response data (get_keys, get_count) + +This is an example document that whitelists X-Ray to capture the `Bucket` and `key` request parameters on an s3.getObject call. + + { + "services": { + "s3": { + "operations": { + "getObject": { + "request_parameters": [ + "Bucket", + "Key" + ] + } + } + } + } + } + +You can set a custom AWS whitelist using the following: + + AWSXRay.setAWSWhitelist(); //Replaces the default whitelist with the given custom one + AWSXRay.setAWSWhitelist(); + + AWSXRay.appendAWSWhitelist(); //Appends to the current whitelist + AWSXRay.appendAWSWhitelist(); + +### Dynamic and fixed naming modes + +The SDK requires that a default segment name is set when using middleware. If it +isn't set, an error is thrown. You can override this value via the `AWS_XRAY_TRACING_NAME` +environment variable. + + app.use(AWSXRay.express.openSegment('defaultName')); + +The SDK defaults to a fixed naming mode. This means that each time a new segment is created for an incoming request, +the name of that segment is set to the default name. + +In dynamic mode, the segment name can vary between the host header of the request or the default name. + + AWSXRay.middleware.enableDynamicNaming(); + +If no pattern is provided, the host header is used as the segment name. If no host header is present, the default is used. +This is equivalent to using the pattern `*`. + +If a pattern is provided, in the form of a string with wild cards (ex: `*.*.us-east-?.elasticbeanstalk.com`), +the host header of the request is checked against it. +A `*` represents any number of characters, while `?` represents a single character. +If the host header is present and matches this pattern, it's used as the segment name. +Otherwise, the default name is used. + +### Partial subsegment streaming and the streaming threshold + +By default, the SDK is configured to have a threshold of 100 subsegments per segment. +This is because the UDP packet maximum size is ~65 kb, and +larger segments might trigger the 'Segment too large to send' error. + +To remedy this, the SDK automatically sends the completed subsegments to the daemon +when the threshold is breached. +Additionally, subsegments that complete when over the threshold automatically send +themselves. If a subsegment is sent out of band, it +is pruned from the segment object. The full segment is reconstructed on the service +side. You can change the threshold as needed. + + AWSXRay.setStreamingThreshold(10); + +Subsegments can be marked as `in_progress` when sent to the daemon. The SDK is telling +the service to anticipate the asynchronous subsegment +to be received out of band when it has completed. When received, the in_progress subsegment +is discarded in favor of the completed subsegment. + +### Developing custom solutions using automatic mode + +Automatic mode is for use with the aws-xray-sdk-express module to support Express +applications, however it can be used outside of Express applictions. +If your application isn't using the Express middleware, you have to create the +new segment and set this on the SDK. +You need to create a new level of CLS, and you can do so by using the CLS namespace object. We expose this via the following. + + AWSXRay.getNamespace(); + +CLS provides several methods of setting the context. Here is an example usage. + + var segment = new AWSXRay.Segment(name, [optional root ID], [optional parent ID]); + var ns = AWSXRay.getNamespace(); + + ns.run(function () { + AWSXRay.setSegment(segment); + .... + }); + +If you are using a different web framework and want to set up automatic capturing, +the X-Ray SDK provides helper functions under `AWSXRay.middleware`. +See the [aws-xray-sdk-express](/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/express) module for more information. + +For additional information about and examples for using the CLS namespace to create +a new context, see: /~https://github.com/othiym23/node-continuation-local-storage. + +## Example code + +### Version capturing + + Use the 'npm start' script to enable. + +### Capture all incoming HTTP requests to '/' + + var app = express(); + + //... + + var AWSXRay = require('aws-xray-sdk'); + + app.use(AWSXRay.express.openSegment('defaultName')); //required at the start of your routes + + app.get('/', function (req, res) { + res.render('index'); + }); + + app.use(AWSXRay.express.closeSegment()); //Required at the end of your routes / first in error handling routes + +### Capture all outgoing AWS requests + + var AWS = captureAWS(require('aws-sdk')); + + //Create new clients as usual + //Be sure any outgoing calls that are dependent on another async + //function are wrapped with captureAsyncFunc, or duplicate segments might + leak + //See usages for clients in manual and automatic modes + +### Configure AWSXRay to automatically capture EC2 instance data + + var AWSXRay = require('aws-xray-sdk'); + AWSXRay.config([AWSXRay.plugins.EC2Plugin]); + +### Add annotations + + var key = 'hello'; + var value = 'there'; // must be string, boolean or finite number + + subsegment.addAnnotation(key, value); + +### Add metadata + + var key = 'hello'; + var value = 'there'; + + subsegment.addMetadata(key, value); + subsegment.addMetadata(key, value, 'greeting'); //custom namespace + +### Create new subsegment + + var newSubseg = subsegment.addNewSubsegment(name); + + // Or + + var subsegment = new Subsegment(name); + +## Automatic mode examples + +Automatic mode is for use with the aws-xray-sdk-express module to support Express +applications, however it can be used outside of Express applications. +If the Express middleware isn't being used, you have to create a root segment and +set on the SDK using the following. + + var segment = new AWSXRay.Segment(name, [optional root ID], [optional parent ID]); + AWSXRay.setSegment(segment); + +Only then will the segment be available for use in automatic mode and be able to be picked up by the capture functions and other aws-xray-sdk modules. + +### Capture all incoming HTTP requests to '/' + + var app = express(); + + //... + + var AWSXRay = require('aws-xray-sdk'); + + app.use(AWSXRay.express.openSegment('defaultName')); + + app.get('/', function (req, res) { + res.render('index'); + }); + + app.use(AWSXRay.express.closeSegment()); + +### Capture through function calls + + var AWSXRay = require('aws-xray-sdk'); + + app.use(AWSXRay.express.openSegment('defaultName')); + + //... + + //The root segment is created by the Express middleware + //This creates 5 nested subsegments on the root segment + //and captures timing data individually for each subsegment + + app.get('/', function (req, res) { + captureFunc('1', function(subsegment1) { + //Exposing the subsegment in the function is optional, and is listed here + as an example + //You can also use + //var subsegment1 = AWSXRay.getSegment(); + + captureFunc('2', function(subsegment2) { + captureFunc('3', function(subsegment3) { + captureFunc('4', function(subsegment4) { + captureFunc('5', function() { + //exposing the subsegment is optional + res.render('index'); + }); + }); + }); + }); + }); + }); + + app.use(AWSXRay.express.closeSegment()); + +### Capture through async function calls + + var AWSXRay = require('aws-xray-sdk'); + + //... + + app.use(AWSXRay.express.openSegment('defaultName')); + + app.get('/', function (req, res) { + var host = 'samplego-env.us-east-1.elasticbeanstalk.com'; + + AWSXRay.captureAsyncFunc('send', function(subsegment) { + //'subsegment' here is the newly created and exposed subsegment for the async + //request, and must be closed manually (this ensures timing data is correct) + + sendRequest(host, function() { + console.log("rendering!"); + res.render('index'); + subsegment.close(); + }); + }); + }); + + app.use(AWSXRay.express.closeSegment()); + + function sendRequest(host, cb) { + var options = { + host: host, + path: '/', + }; + + var callback = function(response) { + var str = ''; + + response.on('data', function (chunk) { + str += chunk; + }); + + response.on('end', function () { + cb(); + }); + } + + http.request(options, callback).end(); + }; + +### Capture outgoing AWS requests on a single client + + var s3 = AWSXRay.captureAWSClient(new AWS.S3()); + + //Use client as usual + //Be sure any outgoing calls that are dependent on another async + //function are wrapped with captureAsyncFunc, or duplicate segments might leak + +### Capture outgoing AWS requests on every AWS SDK client + + var aws = AWSXRay.captureAWS(require('aws-sdk')); + + //Create new clients as usual + //Be sure any outgoing calls that are dependent on another async + //function are wrapped with captureAsyncFunc, or duplicate segments might leak + +### Capture all outgoing HTTP/S requests + + var tracedHttp = AWSXRay.captureHTTPs(require('http')); //returns a copy of the http module that is patched, can patch https as well. + + var options = { + ... + } + + tracedHttp.request(options, callback).end(); + + //Create new requests as usual + //Be sure any outgoing calls that are dependent on another async + //function are wrapped with captureAsyncFunc, or duplicate segments might leak + +## Manual mode examples + +Enable manual mode: + + AWSXRay.enableManualMode(); + +### Capture through function calls + + var AWSXRay = require('aws-xray-sdk'); + + app.use(AWSXRay.express.openSegment('defaultName')); + + //... + + //The root segment is created by the Express middleware + //This creates 5 nested subsegments on the root segment + //and captures timing data individually for each subsegment + + app.get('/', function (req, res) { + var segment = req.segment; + + captureFunc('1', function(subsegment1) { + captureFunc('2', function(subsegment2) { + captureFunc('3', function(subsegment3) { + captureFunc('4', function(subsegment4) { + captureFunc('5', function() { + //subsegment need not be exposed here since we're not doing anything with it + + res.render('index'); + }, subsegment4); + }, subsegment3); + }, subsegment2); + }, subsegment1); + }, segment); + }); + + app.use(AWSXRay.express.closeSegment()); + +### Capture through async function calls + + var AWSXRay = require('aws-xray-sdk'); + + AWSXRay.enableManualMode(); + + app.use(AWSXRay.express.openSegment('defaultName')); + + app.get('/', function (req, res) { + var segment = req.segment; + var host = 'samplego-env.us-east-1.elasticbeanstalk.com'; + + AWSXRay.captureAsyncFunc('send', function(subsegment) { + sendRequest(host, function() { + console.log("rendering!"); + res.render('index'); + subsegment.close(); + }, subsegment); + }, segment); + }); + + app.use(AWSXRay.express.closeSegment()); + + function sendRequest(host, cb, subsegment) { + var options = { + host: host, + path: '/', + XRaySegment: subsegment //required 'XRaySegment' param + }; + + var callback = function(response) { + var str = ''; + + //The whole response has been received, so we just print it out here + //Another chunk of data has been received, so append it to `str` + response.on('data', function (chunk) { + str += chunk; + }); + + response.on('end', function () { + cb(); + }); + } + + http.request(options, callback).end(); + }; + +### Capture outgoing AWS requests on a single client + + var s3 = AWSXRay.captureAWSClient(new AWS.S3()); + var params = { + Bucket: bucketName, + Key: keyName, + Body: 'Hello!', + XRaySegment: subsegment //required 'XRaySegment' param + }; + + s3.putObject(params, function(err, data) { + ... + }); + +### Capture all outgoing AWS requests + + var AWS = captureAWS(require('aws-sdk')); + + //Create new clients as usual + //Be sure any outgoing calls that are dependent on another async + //function are wrapped, or duplicate segments might leak + +### Capture all outgoing HTTP/S requests + + var tracedHttp = AWSXRay.captureHTTPs(require('http')); //returns a copy of the http module that is patched, can patch https as well. + + ... + + //Include sub/segment reference in options as 'XRaySegment' + var options = { + ... + XRaySegment: subsegment //required 'XRaySegment' param + } + + tracedHttp.request(options, callback).end(); diff --git a/packages/core/lib/aws-xray.js b/packages/core/lib/aws-xray.js new file mode 100644 index 00000000..051aa482 --- /dev/null +++ b/packages/core/lib/aws-xray.js @@ -0,0 +1,357 @@ +var contextUtils = require('./context_utils'); +var logging = require('./logger'); +var segmentUtils = require('./segments/segment_utils'); +var utils = require('./utils'); +var LambdaEnv = require('./env/aws_lambda'); + +var UNKNOWN = 'unknown'; + +var pkginfo = module.filename ? require('pkginfo') : function() {}; +pkginfo(module); + +/** + * A module representing the AWSXRay SDK. + * @namespace AWSXRay + */ + +var AWSXRay = { + + /** + * @memberof AWSXRay + * @type {object} + * @namespace AWSXRay.plugins + */ + + plugins: { + + /** + * Exposes the AWS EC2 plugin. + * @memberof AWSXRay.plugins + */ + + EC2Plugin: require('./segments/plugins/ec2_plugin'), + + /** + * Exposes the AWS ECS plugin. + * @memberof AWSXRay.plugins + */ + + ECSPlugin: require('./segments/plugins/ecs_plugin'), + + /** + * Exposes the AWS Elastic Beanstalk plugin. + * @memberof AWSXRay.plugins + */ + + ElasticBeanstalkPlugin: require('./segments/plugins/elastic_beanstalk_plugin'), + }, + + /** + * Enables use of plugins to capture additional data for segments. + * @param {Array} plugins - A configurable subset of AWSXRay.plugins. + * @memberof AWSXRay + * @see AWSXRay.plugins + */ + + config: function(plugins) { + var pluginData = {}; + plugins.forEach(function(plugin) { + plugin.getData(function(data) { + if (data) { + for (var attribute in data) { pluginData[attribute] = data[attribute]; } + } + }); + segmentUtils.setOrigin(plugin.originName); + segmentUtils.setPluginData(pluginData); + }); + }, + + /** + * Overrides the default whitelisting file to specify what params to capture on each AWS Service call. + * If a service or API is not listed, no additional data is captured. + * The base whitelisting file can be found at /lib/resources/aws_whitelist.json + * @param {string|Object} source - The path to the custom whitelist file, or a whitelist source JSON object. + * @memberof AWSXRay + */ + + setAWSWhitelist: require('./segments/attributes/aws').setAWSWhitelist, + + /** + * Appends to the current whitelisting file. + * In the case of a duplicate service API listed, the new source will override the previous values. + * @param {string|Object} source - The path to the custom whitelist file, or a whitelist source JSON object. + * @memberof AWSXRay + */ + + appendAWSWhitelist: require('./segments/attributes/aws').appendAWSWhitelist, + + /** + * Overrides the default streaming threshold (100). + * The threshold represents the maximum number of subsegments on a single segment before + * the SDK begins to send the completed subsegments out of band of the main segment. + * Reduce this threshold if you see the 'Segment too large to send' error. + * @param {number} threshold - The new threshold to use. + * @memberof AWSXRay + */ + + setStreamingThreshold: segmentUtils.setStreamingThreshold, + + /** + * Set your own logger for the SDK. + * @param {Object} logger - A logger which responds to debug/info/warn/error calls. + * @memberof AWSXRay + */ + + setLogger: logging.setLogger, + + /** + * Gets the set logger for the SDK. + * @memberof AWSXRay + */ + + getLogger: logging.getLogger, + + /** + * Configures the address and port the daemon is expected to be on. + * @param {string} address - Address of the daemon the segments should be sent to. Expects 'x.x.x.x', ':yyyy' or 'x.x.x.x:yyyy' IPv4 formats. + * @module SegmentEmitter + * @memberof AWSXRay + * @function + * @see module:SegmentEmitter.setDaemonAddress + */ + + setDaemonAddress: require('./segment_emitter').setDaemonAddress, + + /** + * @param {string} name - The name of the new subsegment. + * @param {function} fcn - The function conext to wrap. + * @param {Segment|Subsegment} [parent] - The parent for the new subsegment, for manual mode. + * @memberof AWSXRay + * @function + * @see module:capture.captureFunc + */ + + captureFunc: require('./capture').captureFunc, + + /** + * @param {string} name - The name of the new subsegment. + * @param {function} fcn - The function conext to wrap. + * @param {Segment|Subsegment} [parent] - The parent for the new subsegment, for manual mode. + * @memberof AWSXRay + * @function + * @see module:capture.captureAsyncFunc + */ + + captureAsyncFunc: require('./capture').captureAsyncFunc, + + /** + * @param {string} name - The name of the new subsegment. + * @param {function} fcn - The function conext to wrap. + * @param {Segment|Subsegment} [parent] - The parent for the new subsegment, for manual mode. + * @memberof AWSXRay + * @function + * @see module:capture.captureCallbackFunc + */ + + captureCallbackFunc: require('./capture').captureCallbackFunc, + + /** + * @param {AWS} awssdk - The Javascript AWS SDK. + * @memberof AWSXRay + * @function + * @see module:aws_p.captureAWS + */ + + captureAWS: require('./patchers/aws_p').captureAWS, + + /** + * @param {AWS.Service} service - An instance of a AWS service to wrap. + * @memberof AWSXRay + * @function + * @see module:aws_p.captureAWSClient + */ + + captureAWSClient: require('./patchers/aws_p').captureAWSClient, + + /** + * @param {http|https} module - The built in Node.js HTTP or HTTPS module. + * @memberof AWSXRay + * @function + * @returns {http|https} + * @see module:http_p.captureHTTPs + */ + + captureHTTPs: require('./patchers/http_p').captureHTTPs, + + /** + * @param {http|https} module - The built in Node.js HTTP or HTTPS module. + * @memberof AWSXRay + * @function + * @see module:http_p.captureHTTPsGlobal + */ + + captureHTTPsGlobal: require('./patchers/http_p').captureHTTPsGlobal, + + /** + * Exposes various helper methods. + * @memberof AWSXRay + * @function + * @see module:utils + */ + + utils: utils, + + /** + * @memberof AWSXRay + * @type {object} + * @namespace AWSXRay.database + */ + + database: { + + /** + * Exposes the SqlData class. + * @memberof AWSXRay.database + * @see SqlData + */ + + SqlData: require('./database/sql_data'), + }, + + /** + * Exposes the Middleware Utils class. + * @memberof AWSXRay + * @function + * @see module:mw_utils + */ + + middleware: require('./middleware/mw_utils'), + + /** + * Gets the current namespace of the context. + * Used for supporting functions that can be used in automatic mode. + * @memberof AWSXRay + * @function + * @returns {Segment|Subsegment} + * @see module:context_utils.getNamespace + */ + + getNamespace: contextUtils.getNamespace, + + /** + * Resolves the current segment or subsegment, checks manual and automatic modes. + * Used for supporting functions that can be used in both manual and automatic modes. + * @memberof AWSXRay + * @function + * @returns {Segment|Subsegment} + * @see module:context_utils.resolveSegment + */ + + resolveSegment: contextUtils.resolveSegment, + + /** + * Returns the current segment or subsegment. For use with automatic mode only. + * @memberof AWSXRay + * @function + * @returns {Segment|Subsegment} + * @see module:context_utils.getSegment + */ + + getSegment: contextUtils.getSegment, + + /** + * Sets the current segment or subsegment. For use with automatic mode only. + * @memberof AWSXRay + * @function + * @see module:context_utils.setSegment + */ + + setSegment: contextUtils.setSegment, + + /** + * Returns true if automatic mode is enabled, otherwise false. + * @memberof AWSXRay + * @function + * @see module:context_utils.isAutomaticMode + */ + + isAutomaticMode: contextUtils.isAutomaticMode, + + /** + * Enables automatic mode. Automatic mode uses 'continuation-local-storage'. + * @see /~https://github.com/othiym23/node-continuation-local-storage + * @memberof AWSXRay + * @function + * @see module:context_utils.enableAutomaticMode + */ + + enableAutomaticMode: contextUtils.enableAutomaticMode, + + /** + * Disables automatic mode. Current segment or subsegment must be passed manually + * via the parent optional on captureFunc, captureAsyncFunc etc. + * @memberof AWSXRay + * @function + * @see module:context_utils.enableManualMode + */ + + enableManualMode: contextUtils.enableManualMode, + + /** + * Sets the context missing strategy. + * @param {Object} strategy - The strategy to set. This object's contextMissing function will be called whenever trace context is not found. + */ + + setContextMissingStrategy: contextUtils.setContextMissingStrategy, + + + /** + * Exposes the segment class. + * @memberof AWSXRay + * @function + */ + + Segment: require('./segments/segment'), + + /** + * Exposes the subsegment class. + * @memberof AWSXRay + * @see Subsegment + */ + + Subsegment: require('./segments/attributes/subsegment'), + + SegmentUtils: segmentUtils +}; + +/** + * Exposes the IncomingRequestData, to capture incoming request data. + * For use with middleware. + * @memberof AWSXRay.middleware + * @see IncomingRequestData + */ + +AWSXRay.middleware.IncomingRequestData = require('./middleware/incoming_request_data'), + +(function() { + var data = { + runtime: (process.release && process.release.name) ? process.release.name : UNKNOWN, + runtime_version: process.version, + version: process.env.npm_package_version || UNKNOWN, + name: process.env.npm_package_name || UNKNOWN + }; + + var sdkData = { + sdk: 'X-Ray for Node.js', + sdk_version: (module.exports && module.exports.version) ? module.exports.version : UNKNOWN, + package: (module.exports && module.exports.name) ? module.exports.name : UNKNOWN, + }; + + segmentUtils.setSDKData(sdkData); + segmentUtils.setServiceData(data); + + if (process.env.LAMBDA_TASK_ROOT) + LambdaEnv.init(); +})(); + +module.exports = AWSXRay; diff --git a/packages/core/lib/capture.js b/packages/core/lib/capture.js new file mode 100644 index 00000000..9c47f7b5 --- /dev/null +++ b/packages/core/lib/capture.js @@ -0,0 +1,145 @@ +/** + * Capture module. + * @module capture + */ + +var contextUtils = require('./context_utils'); + +var logger = require('./logger'); + +/** + * Wrap to automatically capture information for the segment. + * @param {string} name - The name of the new subsegment. + * @param {function} fcn - The function context to wrap. Can take a single 'subsegment' argument. + * @param {Segment|Subsegment} [parent] - The parent for the new subsegment, for manual mode. + * @alias module:capture.captureFunc + */ + +var captureFunc = function captureFunc(name, fcn, parent) { + validate(name, fcn); + + var current, executeFcn; + + var parentSeg = contextUtils.resolveSegment(parent); + + if (!parentSeg) { + logger.getLogger().warn('Failed to capture function.'); + return fcn(); + } + + current = parentSeg.addNewSubsegment(name); + executeFcn = captureFcn(fcn, current); + + try { + executeFcn(current); + current.close(); + } catch (e) { + current.close(e); + throw(e); + } +}; + +/** + * Wrap to automatically capture information for the sub/segment. You must close the segment + * manually from within the function. + * @param {string} name - The name of the new subsegment. + * @param {function} fcn - The function context to wrap. Must take a single 'subsegment' argument and call 'subsegment.close([optional error])' when the async function completes. + * @param {Segment|Subsegment} [parent] - The parent for the new subsegment, for manual mode. + * @alias module:capture.captureAsyncFunc + */ + +var captureAsyncFunc = function captureAsyncFunc(name, fcn, parent) { + validate(name, fcn); + + var current, executeFcn; + var parentSeg = contextUtils.resolveSegment(parent); + + if (!parentSeg) { + logger.getLogger().warn('Failed to capture async function.'); + return fcn(); + } + + current = parentSeg.addNewSubsegment(name); + executeFcn = captureFcn(fcn, current); + + try { + executeFcn(current); + } catch (e) { + current.close(e); + throw(e); + } +}; + +/** + * Wrap to automatically capture information for the sub/segment. This wraps the callback and returns a function. + * when executed, all arguments are passed through accordingly. An additional argument is appended to gain access to the newly created subsegment. + * For this reason, always call the captured callback with the full list of arguments. + * @param {string} name - The name of the new subsegment. + * @param {function} fcn - The function context to wrap. Can take a single 'subsegment' argument. + * @param {Segment|Subsegment} [parent] - The parent for the new subsegment, for manual mode. + * @alias module:capture.captureCallbackFunc + */ + +var captureCallbackFunc = function captureCallbackFunc(name, fcn, parent) { + validate(name, fcn); + + var base = contextUtils.resolveSegment(parent); + + if (!base) { + logger.getLogger().warn('Failed to capture callback function.'); + return fcn; + } + + base.incrementCounter(); + + return function() { + var parentSeg = contextUtils.resolveSegment(parent); + var args = Array.prototype.slice.call(arguments); + + captureFunc(name, fcn.bind.apply(fcn, [null].concat(args)), parentSeg); + + base.decrementCounter(); + }.bind(this); +}; + +function captureFcn(fcn, current) { + var executeFcn; + + if (contextUtils.isAutomaticMode()) { + var session = contextUtils.getNamespace(); + + var contextFcn = function() { + var value; + + session.run(function() { + contextUtils.setSegment(current); + value = fcn(current); + }); + return value; + }; + + executeFcn = contextFcn; + } else { + executeFcn = fcn; + } + + return executeFcn; +} + +function validate(name, fcn) { + var error; + + if (!name || typeof name !== 'string') { + error = 'Param "name" must be a non-empty string.'; + logger.getLogger().error(error); + throw new Error(error); + } else if (typeof fcn !== 'function') { + error = 'Param "fcn" must be a function.'; + logger.getLogger().error(error); + throw new Error(error); + } +} + +module.exports.captureFunc = captureFunc; +module.exports.captureAsyncFunc = captureAsyncFunc; +module.exports.captureCallbackFunc = captureCallbackFunc; diff --git a/packages/core/lib/context_utils.js b/packages/core/lib/context_utils.js new file mode 100644 index 00000000..90869c4c --- /dev/null +++ b/packages/core/lib/context_utils.js @@ -0,0 +1,207 @@ +/** + * @module context_utils + */ + +var cls = require('continuation-local-storage'); + +var logger = require('./logger'); +var Segment = require('./segments/segment'); +var Subsegment = require('./segments/attributes/subsegment'); + +var cls_mode = true; +var NAMESPACE ='AWSXRay'; +var SEGMENT = 'segment'; + +var contextOverride = false; + +var contextUtils = { + CONTEXT_MISSING_STRATEGY: { + RUNTIME_ERROR: { + contextMissing: function contextMissingRuntimeError(message) { + throw new Error(message); + } + }, + LOG_ERROR: { + contextMissing: function contextMissingLogError(message) { + var err = new Error(message); + logger.getLogger().error(err.stack); + } + } + }, + + contextMissingStrategy: {}, + + /** + * Resolves the segment or subsegment given manual mode and params on the call required. + * @param [Segment|Subsegment] segment - The segment manually provided via params.XraySegment, if provided. + * @returns {Segment|Subsegment} + * @alias module:context_utils.resolveManualSegmentParams + */ + + resolveManualSegmentParams: function resolveManualSegmentParams(params) { + if (params && !contextUtils.isAutomaticMode()) { + var xraySegment = params.XRaySegment || params.XraySegment; + var segment = params.Segment; + var found = null; + + if (xraySegment && (xraySegment instanceof Segment || xraySegment instanceof Subsegment)) { + found = xraySegment; + delete params.XRaySegment; + delete params.XraySegment; + } else if (segment && (segment instanceof Segment || segment instanceof Subsegment)) { + found = segment; + delete params.Segment; + } + + return found; + } + }, + + getNamespace: function getNamespace() { + return cls.getNamespace(NAMESPACE); + }, + + /** + * Resolves the segment or subsegment given manual or automatic mode. + * @param [Segment|Subsegment] segment - The segment manually provided, if provided. + * @returns {Segment|Subsegment} + * @alias module:context_utils.resolveSegment + */ + + resolveSegment: function resolveSegment(segment) { + if (cls_mode) { + return this.getSegment(); + } else if (segment && !cls_mode) { + return segment; + } else if (!segment && !cls_mode) { + contextUtils.contextMissingStrategy.contextMissing('No sub/segment specified. A sub/segment must be provided for manual mode.'); + } + }, + + /** + * Returns the current segment or subsegment. For use with in automatic mode only. + * @returns {Segment|Subsegment} + * @alias module:context_utils.getSegment + */ + + getSegment: function getSegment() { + if (cls_mode) { + var segment = cls.getNamespace(NAMESPACE).get(SEGMENT); + + if (!segment) { + contextUtils.contextMissingStrategy.contextMissing('Failed to get the current sub/segment from the context.'); + } else if (segment instanceof Segment && process.env.LAMBDA_TASK_ROOT && segment.facade == true) { + segment.resolveLambdaTraceData(); + } + + return segment; + } else { + contextUtils.contextMissingStrategy.contextMissing('Cannot get sub/segment from context. Not supported in manual mode.'); + } + }, + + /** + * Sets the current segment or subsegment. For use with in automatic mode only. + * @param [Segment|Subsegment] segment - The sub/segment to set. + * @returns {Segment|Subsegment} + * @alias module:context_utils.setSegment + */ + + setSegment: function setSegment(segment) { + if (cls_mode) { + if (!cls.getNamespace(NAMESPACE).set(SEGMENT, segment)) + logger.getLogger().warn('Failed to set the current sub/segment on the context.'); + } else { + contextUtils.contextMissingStrategy.contextMissing('Cannot set sub/segment on context. Not supported in manual mode.'); + } + }, + + /** + * Returns true if in automatic mode, otherwise false. + * @returns {Segment|Subsegment} + * @alias module:context_utils.isAutomaticMode + */ + + isAutomaticMode: function isAutomaticMode() { + return cls_mode; + }, + + /** + * Enables automatic mode. Automatic mode uses 'continuation-local-storage'. + * @see /~https://github.com/othiym23/node-continuation-local-storage + * @alias module:context_utils.enableAutomaticMode + */ + + enableAutomaticMode: function enableAutomaticMode() { + cls_mode = true; + cls.createNamespace(NAMESPACE); + + logger.getLogger().debug('Overriding AWS X-Ray SDK mode. Set to automatic mode.'); + }, + + /** + * Disables automatic mode. Current segment or subsegment then must be passed manually + * via the parent optional on captureFunc, captureAsyncFunc etc. + * @alias module:context_utils.enableManualMode + */ + + enableManualMode: function enableManualMode() { + cls_mode = false; + + if (cls.getNamespace(NAMESPACE)) + cls.destroyNamespace(NAMESPACE); + + logger.getLogger().debug('Overriding AWS X-Ray SDK mode. Set to manual mode.'); + }, + + /** + * Sets the context missing strategy if no context missing strategy is set using the environment variable with + * key AWS_XRAY_CONTEXT_MISSING. The context missing strategy's contextMissing function will be called whenever + * trace context is not found. + * @param {string|function} strategy - The strategy to set. Valid string values are 'LOG_ERROR' and 'RUNTIME_ERROR'. + * Alternatively, a custom function can be supplied, which takes a error message string. + */ + + setContextMissingStrategy: function setContextMissingStrategy(strategy) { + if (!contextOverride) { + if (typeof strategy === 'string') { + var lookupStrategy = contextUtils.CONTEXT_MISSING_STRATEGY[strategy.toUpperCase()]; + + if (lookupStrategy) { + contextUtils.contextMissingStrategy.contextMissing = lookupStrategy.contextMissing; + + if (process.env.AWS_XRAY_CONTEXT_MISSING) + logger.getLogger().info('AWS_XRAY_CONTEXT_MISSING is set. Configured context missing strategy to ' + + process.env.AWS_XRAY_CONTEXT_MISSING + '.'); + else + logger.getLogger().info('Configured context missing strategy to: ' + strategy); + } else { + throw new Error('Invalid context missing strategy: ' + strategy + '. Valid values are ' + + Object.keys(contextUtils.CONTEXT_MISSING_STRATEGY) + '.'); + } + } else if (typeof strategy === 'function') { + contextUtils.contextMissingStrategy.contextMissing = strategy; + logger.getLogger().info('Configured custom context missing strategy to function: ' + strategy.name); + } else { + throw new Error('Context missing strategy must be either a string or a custom function.'); + } + + } else { + logger.getLogger().warn('Ignoring call to setContextMissingStrategy as AWS_XRAY_CONTEXT_MISSING is set. ' + + 'The current context missing strategy will not be changed.'); + } + } +}; + +cls.createNamespace(NAMESPACE); +logger.getLogger().debug('Starting the AWS X-Ray SDK in automatic mode (default).'); + +if (process.env.AWS_XRAY_CONTEXT_MISSING) { + contextUtils.setContextMissingStrategy(process.env.AWS_XRAY_CONTEXT_MISSING); + contextOverride = true; +} else { + contextUtils.contextMissingStrategy.contextMissing = contextUtils.CONTEXT_MISSING_STRATEGY.RUNTIME_ERROR.contextMissing; + logger.getLogger().debug('Using default context missing strategy: RUNTIME_ERROR'); +} + +module.exports = contextUtils; diff --git a/packages/core/lib/database/sql_data.js b/packages/core/lib/database/sql_data.js new file mode 100644 index 00000000..bbfb11ec --- /dev/null +++ b/packages/core/lib/database/sql_data.js @@ -0,0 +1,28 @@ +/** + * Represents a SQL database call. + * @constructor + * @param {string} databaseVer - The version on the database (user supplied). + * @param {string} driverVer - The version on the database driver (user supplied). + * @param {string} user - The user associated to the database call. + * @param {string} queryType - The SQL query type. + */ + +function SqlData(databaseVer, driverVer, user, url, queryType) { + this.init(databaseVer, driverVer, user, url, queryType); +} + +SqlData.prototype.init = function init(databaseVer, driverVer, user, url, queryType) { + if (databaseVer) + this.database_version = databaseVer; + + if (driverVer) + this.driver_version = driverVer; + + if (queryType) + this.preparation = queryType; + + this.url = url; + this.user = user; +}; + +module.exports = SqlData; diff --git a/packages/core/lib/env/aws_lambda.js b/packages/core/lib/env/aws_lambda.js new file mode 100644 index 00000000..34fc753f --- /dev/null +++ b/packages/core/lib/env/aws_lambda.js @@ -0,0 +1,103 @@ +var fs = require('fs'); + +var contextUtils = require('../context_utils'); +var LambdaUtils = require('../utils').LambdaUtils; +var Segment = require('../segments/segment'); +var SegmentEmitter = require('../segment_emitter'); +var SegmentUtils = require('../segments/segment_utils'); + +var logger = require('../logger'); + +/** +* Used to initialize segments on AWS Lambda with extra data from the context. +*/ + + +/** + * @namespace + * @ignore + */ +var xAmznTraceIdPrev = null; + +module.exports.init = function init() { + contextUtils.enableManualMode = function() { + logger.getLogger().warn('AWS Lambda does not support AWS X-Ray manual mode.'); + }; + + fs.mkdir('/tmp/', function() { + fs.mkdir('/tmp/.aws-xray/', function() { + var filename = '/tmp/.aws-xray/initialized'; + fs.closeSync(fs.openSync(filename, 'a')); + var now = new Date(); + fs.utimesSync(filename, now, now); + }); + }); + + SegmentEmitter.disableReusableSocket(); + SegmentUtils.setStreamingThreshold(0); + + var namespace = contextUtils.getNamespace(); + namespace.enter(namespace.createContext()); + contextUtils.setSegment(facadeSegment()); +}; + +var facadeSegment = function facadeSegment() { + var segment = new Segment('facade'); + var whitelistFcn = ['addNewSubsegment', 'addSubsegment', 'removeSubsegment', 'toString']; + var silentFcn = ['incrementCounter', 'decrementCounter', 'isClosed', 'close', 'format', 'flush']; + var xAmznTraceId = process.env._X_AMZN_TRACE_ID; + + for (var key in segment) { + if (typeof segment[key] === 'function' && whitelistFcn.indexOf(key) === -1) { + if (silentFcn.indexOf(key) === -1) { + segment[key] = (function() { + var func = key; + return function facade() { + logger.getLogger().warn('Function "' + func + '" cannot be called on an AWS Lambda segment. Please use a subsegment to record data.'); + return; + }; + })(); + } else { + segment[key] = function facade() { return; }; + } + } + } + + segment.trace_id = null; + segment.isClosed = function() { return true; }; + segment.in_progress = false; + segment.counter = 1; + segment.notTraced = true; + segment.facade = true; + + segment.reset = function reset() { + this.trace_id = null; + this.id = null; + delete this.subsegments; + this.notTraced = true; + }; + + segment.resolveLambdaTraceData = function resolveLambdaTraceData() { + var xAmznLambda = process.env._X_AMZN_TRACE_ID; + + if (xAmznLambda) { + if (xAmznLambda != xAmznTraceIdPrev) { + this.reset(); + + if (LambdaUtils.populateTraceData(segment, xAmznLambda)) + xAmznTraceIdPrev = xAmznLambda; + } + } + else { + this.reset(); + contextUtils.contextMissingStrategy.contextMissing('Missing AWS Lambda trace data for X-Ray. Expected _X_AMZN_TRACE_ID to be set.'); + } + }; + + if (LambdaUtils.validTraceData(xAmznTraceId)) { + if (LambdaUtils.populateTraceData(segment, xAmznTraceId)) + xAmznTraceIdPrev = xAmznTraceId; + } + + return segment; +}; diff --git a/packages/core/lib/index.js b/packages/core/lib/index.js new file mode 100644 index 00000000..15cf8374 --- /dev/null +++ b/packages/core/lib/index.js @@ -0,0 +1,2 @@ +// Convenience file to require the SDK from the root of the repository +module.exports = require('./aws-xray'); diff --git a/packages/core/lib/logger.js b/packages/core/lib/logger.js new file mode 100644 index 00000000..a181b1a9 --- /dev/null +++ b/packages/core/lib/logger.js @@ -0,0 +1,94 @@ +var winston = require('winston'); +var moment = require('moment'); + +var logger; + +if (process.env.AWS_XRAY_DEBUG_MODE) { + logger = new (winston.Logger)({ + transports: [ + new (winston.transports.Console)({ + formatter: outputFormatter, + level: 'debug', + timestamp: timestampFormatter + }) + ] + }); +} else { + logger = new (winston.Logger)({}); +} + +/* eslint-disable no-console */ +if (process.env.LAMBDA_TASK_ROOT) { + logger.error = function(string) { console.error(string); }; + logger.info = function(string) { console.info(string); }; + logger.warn = function(string) { console.warn(string); }; +} +/* eslint-enable no-console */ + +function timestampFormatter() { + return moment().format('YYYY-MM-DD HH:mm:ss.SSSS Z'); +} + +function outputFormatter(options) { + return options.timestamp() +' [' + options.level.toUpperCase() + '] '+ + (options.message !== undefined ? options.message : '') + + (options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' ); +} + +/** + * Polyfill for Object.keys + * @see: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/keys + */ + +if (!Object.keys) { + Object.keys = (function() { + 'use strict'; + var hasOwnProperty = Object.prototype.hasOwnProperty, + hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'), + dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' + ], + dontEnumsLength = dontEnums.length; + + return function(obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = [], prop, i; + + for (prop in obj) { + if (hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + }()); +} + +var logging = { + setLogger: function setLogger(logObj) { + logger = logObj; + }, + + getLogger: function getLogger() { + return logger; + } +}; + +module.exports = logging; diff --git a/packages/core/lib/middleware/incoming_request_data.js b/packages/core/lib/middleware/incoming_request_data.js new file mode 100644 index 00000000..c0e68c5c --- /dev/null +++ b/packages/core/lib/middleware/incoming_request_data.js @@ -0,0 +1,60 @@ + +/** + * Represents an incoming HTTP/HTTPS call. + * @constructor + * @param {http.IncomingMessage|https.IncomingMessage} req - The request object from the HTTP/HTTPS call. + */ + +function IncomingRequestData(req) { + this.init(req); +} + +IncomingRequestData.prototype.init = function init(req) { + var forwarded = !!req.headers['x-forwarded-for']; + var url; + + if (req.connection) + url = ((req.connection.secure || req.connection.encrypted) ? 'https://' : 'http://') + + ((req.headers['host'] || '') + (req.url || '')); + + this.request = { + method: req.method || '', + user_agent: req.headers['user-agent'] || '', + client_ip: getClientIp(req) || '', + url: url || '', + }; + + if (forwarded) + this.request.x_forwarded_for = forwarded; +}; + +var getClientIp = function getClientIp(req) { + var clientIp; + + if (req.headers['x-forwarded-for']) + clientIp = (req.headers['x-forwarded-for'] || '').split(',')[0]; + else if (req.connection && req.connection.remoteAddress) + clientIp = req.connection.remoteAddress; + else if (req.socket && req.socket.remoteAddress) + clientIp = req.socket.remoteAddress; + else if (req.connection && req.connection.socket && req.connection.socket.remoteAddress) + clientIp = req.connection.socket.remoteAddress; + + return clientIp; +}; + +/** + * Closes the local and automatically captures the response data. + * @param {http.ServerResponse|https.ServerResponse} res - The response object from the HTTP/HTTPS call. + */ + +IncomingRequestData.prototype.close = function close(res) { + this.response = { + status: res.statusCode || '' + }; + + if (res.headers && res.headers['content-length']) + this.response.content_length = res.headers['content-length']; +}; + +module.exports = IncomingRequestData; diff --git a/packages/core/lib/middleware/mw_utils.js b/packages/core/lib/middleware/mw_utils.js new file mode 100644 index 00000000..5060e759 --- /dev/null +++ b/packages/core/lib/middleware/mw_utils.js @@ -0,0 +1,126 @@ +/** + * Middleware Utils module. + * + * Exposes various configuration and helper methods to be used by the middleware. + * @module mw_utils + */ + +var SamplingRules = require('./sampling/sampling_rules'); + +var wildcardMatch = require('../utils').wildcardMatch; +var processTraceData = require('../utils').processTraceData; + +//headers are case-insensitive +var XRAY_HEADER = 'x-amzn-trace-id'; +var overrideFlag = !!process.env.AWS_XRAY_TRACING_NAME; + +var utils = { + defaultName: process.env.AWS_XRAY_TRACING_NAME, + dynamicNaming: false, + hostPattern: null, + sampler: new SamplingRules(), + + /** + * Enables dynamic naming for segments via the middleware. Use 'AWSXRay.middleware.enableDynamicNaming()'. + * @param {string} [hostPattern] - The pattern to match the host header. See the README on dynamic and fixed naming modes. + * @alias module:mw_utils.enableDynamicNaming + */ + + enableDynamicNaming: function(hostPattern) { + this.dynamicNaming = true; + + if (hostPattern && typeof hostPattern !== 'string') + throw new Error('Host pattern must be a string.'); + + this.hostPattern = hostPattern || null; + }, + + /** + * Splits out the 'x-amzn-trace-id' header params from the incoming request. Used by the middleware. + * @param {http.IncomingMessage|https.IncomingMessage} req - The request object from the incoming call. + * @returns {object} + * @alias module:mw_utils.processHeaders + */ + + processHeaders: function processHeaders(req) { + var amznTraceHeader = {}; + + if (req && req.headers && req.headers[XRAY_HEADER]) { + amznTraceHeader = processTraceData(req.headers[XRAY_HEADER]); + } + + return amznTraceHeader; + }, + + /** + * Resolves the name of the segment as determined by fixed or dynamic mode options. Used by the middleware. + * @param {string} hostHeader - The string from the request.headers.host property. + * @returns {string} + * @alias module:mw_utils.resolveName + */ + + resolveName: function resolveName(hostHeader) { + var name; + + if (this.dynamicNaming && hostHeader) + name = this.hostPattern ? (wildcardMatch(this.hostPattern, hostHeader) ? hostHeader : this.defaultName) : hostHeader; + else + name = this.defaultName; + + return name; + }, + + /** + * Resolves the sampling decision as determined by the values given and options set. Used by the middleware. + * @param {object} amznTraceHeader - The object as returned by the processHeaders function. + * @param {Segment} segment - The string from the request.headers.host property. + * @param {http.ServerResponse|https.ServerResponse} res - The response object from the incoming call. + * @returns {boolean} + * @alias module:mw_utils.resolveSampling + */ + + resolveSampling: function resolveSampling(amznTraceHeader, segment, res) { + var isSampled; + + if (amznTraceHeader.Sampled === '1') + isSampled = true; + else if (amznTraceHeader.Sampled === '0') + isSampled = false; + else + isSampled = this.sampler.shouldSample(res.req.headers.host, res.req.method, res.req.url); + + if (amznTraceHeader.Sampled === '?') + res.header[XRAY_HEADER] = 'Root=' + amznTraceHeader.Root + ';Sampled=' + (isSampled ? '1' : '0'); + + if (!isSampled) + segment.notTraced = true; + }, + + /** + * Sets the default name of created segments. Used with the middleware. + * Can be overridden by the AWS_XRAY_TRACING_NAME environment variable. + * @param {string} name - The default name for segments created in the middleware. + * @alias module:mw_utils.setDefaultName + */ + + setDefaultName: function setDefaultName(name) { + if (!overrideFlag) + this.defaultName = name; + }, + + /** + * Overrides the default sampling rules file to specify at what rate to sample at for specific routes. + * The base sampling rules file can be found at /lib/resources/default_sampling_rules.json + * @param {string|Object} source - The path to the custom sampling rules file, or the source JSON object. + * @memberof AWSXRay + */ + + setSamplingRules: function setSamplingRules(source) { + if (!source || source instanceof String || !(typeof source === 'string' || (source instanceof Object))) + throw new Error('Please specify a path to the local sampling rules file, or supply an object containing the rules.'); + + this.sampler = new SamplingRules(source); + } +}; + +module.exports = utils; diff --git a/packages/core/lib/middleware/sampling/sampler.js b/packages/core/lib/middleware/sampling/sampler.js new file mode 100644 index 00000000..de9a8584 --- /dev/null +++ b/packages/core/lib/middleware/sampling/sampler.js @@ -0,0 +1,43 @@ + +/** + * Represents a Sampler object that keeps track of the number of traces per second sampled and the fallback rate for a given sampling rule. It also decides + * if a given trace should be sampled or not given the configuration options. + * @constructor + * @param {number} fixedTarget - An integer value to specify the maximum number of traces per second to sample. + * @param {number} fallbackRate - A value between 0 and 1 indicating the sampling rate after the maximum traces per second has been hit. + */ + +function Sampler (fixedTarget, fallbackRate) { + this.init(fixedTarget, fallbackRate); +} + +Sampler.prototype.init = function init(fixedTarget, fallbackRate) { + this.usedThisSecond = 0; + + if (typeof fixedTarget === 'number' && fixedTarget % 1 === 0 && fixedTarget >= 0) + this.fixedTarget = fixedTarget; + else + throw new Error('Error in sampling file. Rule attribute "fixed_target" must be a non-negative integer.'); + + if (typeof fallbackRate === 'number' && fallbackRate >= 0 && fallbackRate <= 1) + this.fallbackRate = fallbackRate; + else + throw new Error('Error in sampling file. Rule attribute "rate" must be a number between 0 and 1 inclusive.'); +}; + +Sampler.prototype.isSampled = function isSampled() { + var now = Math.round(new Date().getTime() / 1000); + + if (now !== this.thisSecond) { + this.usedThisSecond = 0; + this.thisSecond = now; + } + + if (this.usedThisSecond >= this.fixedTarget) + return Math.random() < this.fallbackRate; + + this.usedThisSecond++; + return true; +}; + +module.exports = Sampler; diff --git a/packages/core/lib/middleware/sampling/sampling_rules.js b/packages/core/lib/middleware/sampling/sampling_rules.js new file mode 100644 index 00000000..8416b93a --- /dev/null +++ b/packages/core/lib/middleware/sampling/sampling_rules.js @@ -0,0 +1,130 @@ +var fs = require('fs'); + +var Sampler = require('./sampler'); +var Utils = require('../../utils'); + +var defaultRules = require('../../resources/default_sampling_rules.json'); +var logger = require('../../logger'); + +/** + * Represents a set of matchers and rules in regards to sampling rates. + * @constructor + * @param {string|Object} [source] - The path to the custom sampling rules file, or the source JSON object. If none is provided, the default file will be used. + */ + +function SamplingRules(source) { + this.init(source); +} + +SamplingRules.prototype.init = function init(source) { + if (source) { + if (typeof source === 'string') { + logger.getLogger().info('Using custom sampling rules file: ' + source); + this.rules = loadRulesConfig(JSON.parse(fs.readFileSync(source, 'utf8'))); + } else { + logger.getLogger().info('Using custom sampling rules source.'); + this.rules = loadRulesConfig(source); + } + } else + this.rules = parseRulesConfig(defaultRules); +}; + +SamplingRules.prototype.shouldSample = function shouldSample(serviceName, httpMethod, urlPath) { + var formatted = '{ http_method: ' + httpMethod + ', service_name: ' + serviceName + ', url_path: ' + urlPath + ' }'; + var matched; + + this.rules.some(function(rule) { + if (rule.default || (Utils.wildcardMatch(rule.service_name, serviceName) + && Utils.wildcardMatch(rule.http_method, httpMethod) && Utils.wildcardMatch(rule.url_path, urlPath))) { + + matched = rule.sampler; + + logger.getLogger().debug('Sampling rule match found for ' + formatted + '. Matched ' + (rule.default ? + 'default' : '{ http_method: ' + rule.http_method + ', service_name: ' + rule.service_name + ', url_path: ' + + rule.url_path + ' }') + '. Using fixed_target: ' + matched.fixedTarget + ' and rate: ' + matched.fallbackRate + '.'); + + return true; + } + }); + + if (matched) { + return matched.isSampled(); + } else { + logger.getLogger().debug('No sampling rule matched for ' + formatted); + return false; + } +}; + +function loadRulesConfig(config) { + if (!config.version) + throw new Error('Error in sampling file. Missing "version" attribute.'); + + if (config.version === 1) + return parseRulesConfig(config); + else + throw new Error('Error in sampling file. Unknown version "' + config.version + '".'); +} + +function parseRulesConfig(config) { + var defaultRule; + var rules = []; + + if (config.default) { + var missing = []; + + for (var key in config.default) { + if (key !== 'fixed_target' && key !== 'rate') { + throw new Error('Error in sampling file. Invalid attribute for default: ' + key + + '. Valid attributes for default are "fixed_target" and "rate".'); + } else if (typeof config.default[key] !== 'number') { + throw new Error('Error in sampling file. Default ' + key + ' must be a number.'); + } + } + + if (typeof config.default.fixed_target === 'undefined') + missing.push('fixed_target'); + + if (typeof config.default.rate === 'undefined') + missing.push('rate'); + + if (missing.length !== 0) + throw new Error('Error in sampling file. Missing required attributes for default: ' + missing + '.'); + + defaultRule = { default: true, sampler: new Sampler(config.default.fixed_target, config.default.rate) }; + } else { + throw new Error('Error in sampling file. Expecting "default" object to be defined with attributes "fixed_target" and "rate".'); + } + + if (Array.isArray(config.rules)) { + config.rules.forEach(function(rawRule) { + var params = {}; + var required = { service_name: 1, http_method: 1, url_path: 1, fixed_target: 1, rate: 1 }; + + for(var key in rawRule) { + var value = rawRule[key]; + + if (!required[key] && key != 'description') + throw new Error('Error in sampling file. Rule ' + JSON.stringify(rawRule) + ' has invalid attribute: ' + key + '.'); + else if (key != 'description' && !value && value !== 0) + throw new Error('Error in sampling file. Rule ' + JSON.stringify(rawRule) + ' attribute "' + key + '" has invalid value: ' + value + '.'); + else { + params[key] = value; + delete required[key]; + } + } + + if (Object.keys(required).length !== 0 && required.constructor === Object) + throw new Error('Error in sampling file. Rule ' + JSON.stringify(rawRule) + ' is missing required attributes: ' + Object.keys(required) + '.'); + + var rule = params; + rule.sampler = new Sampler(rawRule.fixed_target, rawRule.rate); + rules.push(rule); + }); + } + + rules.push(defaultRule); + + return rules; +} + +module.exports = SamplingRules; diff --git a/packages/core/lib/patchers/aws_p.js b/packages/core/lib/patchers/aws_p.js new file mode 100644 index 00000000..6278cb63 --- /dev/null +++ b/packages/core/lib/patchers/aws_p.js @@ -0,0 +1,151 @@ +/** + * Capture module. + * @module aws_p + */ + +var semver = require('semver'); + +var Aws = require('../segments/attributes/aws'); +var contextUtils = require('../context_utils'); +var Utils = require('../utils'); + +var logger = require('../logger'); + +var minVersion = '2.7.15'; + +var throttledErrorDefault = function throttledErrorDefault() { + return false; // If the customer doesn't provide an aws-sdk with a throttled error function, we can't make assumptions. +}; + +/** + * Configures the AWS SDK to automatically capture information for the segment. + * All created clients will automatically be captured. See 'captureAWSClient' + * for additional details. + * @param {AWS} awssdk - The Javascript AWS SDK. + * @alias module:aws_p.captureAWS + * @returns {AWS} + * @see /~https://github.com/aws/aws-sdk-js + */ + +var captureAWS = function captureAWS(awssdk) { + if (!semver.gte(awssdk.VERSION, minVersion)) + throw new Error ('AWS SDK version ' + minVersion + ' or greater required.'); + + for (var prop in awssdk) { + if (awssdk[prop].serviceIdentifier) { + var Service = awssdk[prop]; + Service.prototype.customizeRequests(captureAWSRequest); + } + } + + return awssdk; +}; + +/** + * Configures any AWS Client instance to automatically capture information for the segment. + * For manual mode, a param with key called 'Segment' is required as a part of the AWS + * call paramaters, and must reference a Segment or Subsegment object. + * @param {AWS.Service} service - An instance of a AWS service to wrap. + * @alias module:aws_p.captureAWSClient + * @returns {AWS.Service} + * @see /~https://github.com/aws/aws-sdk-js + */ + +var captureAWSClient = function captureAWSClient(service) { + service.customizeRequests(captureAWSRequest); + return service; +}; + +function captureAWSRequest(req) { + var parent = contextUtils.resolveSegment(contextUtils.resolveManualSegmentParams(req.params)); + + if (!parent) { + var output = this.serviceIdentifier + '.' + req.operation; + + if (!contextUtils.isAutomaticMode()) { + logger.getLogger().info('Call ' + output + ' requires a segment object' + + ' on the request params as "XRaySegment" for tracing in manual mode. Ignoring.'); + } else { + logger.getLogger().info('Call ' + output + + ' is missing the sub/segment context for automatic mode. Ignoring.'); + } + return req; + } + + var throttledError = this.throttledError || throttledErrorDefault; + + var stack = (new Error()).stack; + var subsegment = parent.addNewSubsegment(this.serviceIdentifier); + var traceId = parent.segment ? parent.segment.trace_id : parent.trace_id; + + req.on('build', function(req) { + req.httpRequest.headers['X-Amzn-Trace-Id'] = 'Root=' + traceId + ';Parent=' + subsegment.id + + ';Sampled=' + (subsegment.segment.notTraced ? '0' : '1'); + }).on('complete', function(res) { + subsegment.addAttribute('namespace', 'aws'); + subsegment.addAttribute('aws', new Aws(res, subsegment.name)); + + var httpRes = res.httpResponse; + + if (httpRes) { + subsegment.addAttribute('http', new HttpResponse(httpRes)); + + if (httpRes.statusCode === 429 || (res.error && throttledError(res.error))) + subsegment.addThrottleFlag(); + } + + if (res.error) { + var err = { message: res.error.message, name: res.error.code, stack: stack }; + + if (httpRes && httpRes.statusCode) { + if (Utils.getCauseTypeFromHttpStatus(httpRes.statusCode) == 'error') { + subsegment.addErrorFlag(); + } + subsegment.close(err, true); + } + else + subsegment.close(err); + } else { + if (httpRes && httpRes.statusCode) { + var cause = Utils.getCauseTypeFromHttpStatus(httpRes.statusCode); + + if (cause) + subsegment[cause] = true; + } + subsegment.close(); + } + }); + + if (!req.__send) { + req.__send = req.send; + + req.send = function(callback) { + if (contextUtils.isAutomaticMode()) { + var session = contextUtils.getNamespace(); + + session.run(function() { + contextUtils.setSegment(subsegment); + req.__send(callback); + }); + } else { + req.__send(callback); + } + }; + } +} + +function HttpResponse(res) { + this.init(res); +} + +HttpResponse.prototype.init = function init(res) { + this.response = { + status: res.statusCode || '', + }; + + if (res.headers && res.headers['content-length']) + this.response.content_length = res.headers['content-length']; +}; + +module.exports.captureAWSClient = captureAWSClient; +module.exports.captureAWS = captureAWS; diff --git a/packages/core/lib/patchers/call_capturer.js b/packages/core/lib/patchers/call_capturer.js new file mode 100644 index 00000000..127e5278 --- /dev/null +++ b/packages/core/lib/patchers/call_capturer.js @@ -0,0 +1,128 @@ +var fs = require('fs'); + +var logger = require('../logger'); +var whitelist = require('../resources/aws_whitelist.json'); + +var paramTypes = { + REQ_DESC: 'request_descriptors', + REQ_PARAMS: 'request_parameters', + RES_DESC: 'response_descriptors', + RES_PARAMS: 'response_parameters' +}; + +/** + * Represents a set of AWS services, operations and keys or params to capture. + * @constructor + * @param {string|Object} [source] - The location or source JSON object of the custom AWS whitelist file. If none is provided, the default file will be used. + */ + +function CallCapturer (source) { + this.init(source); +} + +CallCapturer.prototype.init = function init(source) { + if (source) { + if (typeof source === 'string') { + logger.getLogger().info('Using custom AWS whitelist file: ' + source); + this.services = loadWhitelist(JSON.parse(fs.readFileSync(source, 'utf8'))); + } else { + logger.getLogger().info('Using custom AWS whitelist source.'); + this.services = loadWhitelist(source); + } + } else + this.services = whitelist.services; +}; + +CallCapturer.prototype.append = function append(source) { + var newServices = {}; + + if (typeof source === 'string') { + logger.getLogger().info('Appending AWS whitelist with custom file: ' + source); + newServices = loadWhitelist(require(source)); + } else { + logger.getLogger().info('Appending AWS whitelist with a custom source.'); + newServices = loadWhitelist(source); + } + + for (var attribute in newServices) { this.services[attribute] = newServices[attribute]; } +}; + +CallCapturer.prototype.capture = function capture(serviceName, response) { + var operation = response.request.operation; + var call = this.services[serviceName] !== undefined ? this.services[serviceName].operations[operation] : null; + + if (call === null) { + logger.getLogger().debug('Call "' + serviceName + '.' + operation + '" is not whitelisted for additional data capturing. Ignoring.'); + return; + } + + var dataCaptured = {}; + + for (var paramType in call) { + var params = call[paramType]; + + if (paramType === paramTypes.REQ_PARAMS) { + captureCallParams(params, response.request.params, dataCaptured); + } else if (paramType === paramTypes.REQ_DESC) { + captureDescriptors(params, response.request.params, dataCaptured); + } else if (paramType === paramTypes.RES_PARAMS) { + if (response.data) { captureCallParams(params, response.data, dataCaptured); } + } else if (paramType === paramTypes.RES_DESC) { + if (response.data) { captureDescriptors(params, response.data, dataCaptured); } + } else { + logger.getLogger().error('Unknown parameter type "' + paramType + '". Must be "request_descriptors", "response_descriptors", ' + + '"request_parameters" or "response_parameters".'); + } + } + + return dataCaptured; +}; + +function captureCallParams(params, call, data) { + params.forEach(function(param) { + if (typeof call[param] !== 'undefined') { + var formatted = toSnakeCase(param); + this[formatted] = call[param]; + } + }, data); +} + +function captureDescriptors(descriptors, params, data) { + for (var paramName in descriptors) { + var attributes = descriptors[paramName]; + + if (typeof params[paramName] !== 'undefined') { + var paramData; + + if (attributes.list && attributes.get_count) + paramData = params[paramName] ? params[paramName].length : 0; + else + paramData = attributes.get_keys === true ? Object.keys(params[paramName]) : params[paramName]; + + if (typeof attributes.rename_to === 'string') { + data[attributes.rename_to] = paramData; + } else { + var formatted = toSnakeCase(paramName); + data[formatted] = paramData; + } + } + } +} + +function toSnakeCase(param) { + if (param === 'IPAddress') + return 'ip_address'; + else + return param.split(/(?=[A-Z])/).join('_').toLowerCase(); +} + +function loadWhitelist(source) { + var doc = source; + + if (doc.services === undefined) + throw new Error('Document formatting is incorrect. Expecting "services" param.'); + + return doc.services; +} + +module.exports = CallCapturer; diff --git a/packages/core/lib/patchers/http_p.js b/packages/core/lib/patchers/http_p.js new file mode 100644 index 00000000..55221acd --- /dev/null +++ b/packages/core/lib/patchers/http_p.js @@ -0,0 +1,153 @@ +/** + * @module http_p + */ + +/** + * This module patches the HTTP and HTTPS node built-in libraries and returns a copy of the module with tracing enabled. + */ + +var _ = require('underscore'); + +var contextUtils = require('../context_utils'); +var Utils = require('../utils'); + +var logger = require('../logger'); + +/** + * Wraps the http/https.request() and .get() calls to automatically capture information for the segment. + * This patches the built-in HTTP and HTTPS modules globally. If using a 3rd party HTTP library, + * it should still use HTTP under the hood. Be sure to patch globally before requiring the 3rd party library. + * 3rd party library compatibility is best effort. Some incompatibility issues may arise. + * @param {http|https} module - The built in Node.js HTTP or HTTPS module. + * @param {boolean} downstreamXRayEnabled - when true, adds a "traced:true" property to the subsegment + * so the AWS X-Ray service expects a corresponding segment from the downstream service. + * @alias module:http_p.captureHTTPsGlobal + */ + +var captureHTTPsGlobal = function captureHTTPsGlobal(module, downstreamXRayEnabled) { + if (!module.__request) + enableCapture(module, downstreamXRayEnabled); +}; + +/** + * Wraps the http/https.request() and .get() calls to automatically capture information for the segment. + * Returns an instance of the HTTP or HTTPS module that is patched. + * @param {http|https} module - The built in Node.js HTTP or HTTPS module. + * @param {boolean} downstreamXRayEnabled - when true, adds a "traced:true" property to the subsegment + * so the AWS X-Ray service expects a corresponding segment from the downstream service. + * @alias module:http_p.captureHTTPs + * @returns {http|https} + */ + +var captureHTTPs = function captureHTTPs(module, downstreamXRayEnabled) { + if (module.__request) + return module; + + var tracedModule = {}; + + Object.keys(module).forEach(function(val) { + tracedModule[val] = module[val]; + }); + + enableCapture(tracedModule, downstreamXRayEnabled); + return tracedModule; +}; + +function enableCapture(module, downstreamXRayEnabled) { + function captureOutgoingHTTPs(baseFunc, options, callback) { + if (!options || (options.headers && (options.headers['X-Amzn-Trace-Id']))) { + return baseFunc(options, callback); + } + + var parent = contextUtils.resolveSegment(contextUtils.resolveManualSegmentParams(options)); + var hostname = options.hostname || options.host || 'Unknown host'; + + if (!parent) { + var output = '[ host: ' + hostname; + output = options.method ? (output + ', method: ' + options.method) : output; + output += ', path: ' + options.path + ' ]'; + + if (!contextUtils.isAutomaticMode()) { + logger.getLogger().info('Options for request ' + output + + ' requires a segment object on the options params as "XRaySegment" for tracing in manual mode. Ignoring.'); + } else { + logger.getLogger().info('Options for request ' + output + + ' is missing the sub/segment context for automatic mode. Ignoring.'); + } + + return baseFunc(options, callback); + } + + var subsegment = parent.addNewSubsegment(hostname); + var root = parent.segment ? parent.segment : parent; + subsegment.namespace = 'remote'; + + if (!options.headers) + options.headers = {}; + + options.headers['X-Amzn-Trace-Id'] = 'Root=' + root.trace_id + ';Parent=' + subsegment.id + + ';Sampled=' + (!root.notTraced ? '1' : '0'); + + var errorCapturer = function errorCapturer(e) { + if (subsegment.http && subsegment.http.response) { + if (Utils.getCauseTypeFromHttpStatus(subsegment.http.response.status) === 'error') { + subsegment.addErrorFlag(); + } + subsegment.close(e, true); + } else { + var madeItToDownstream = (e.code !== 'ECONNREFUSED'); + + subsegment.addRemoteRequestData(this, null, madeItToDownstream && downstreamXRayEnabled); + subsegment.close(e); + } + + if (this._events && this._events.error && this._events.error.length === 1) { + this.removeListener('error', errorCapturer); + this.emit('error', e); + } + }; + + var req = baseFunc(_.omit(options, 'Segment'), function(res) { + res.on('end', function() { + if (res.statusCode === 429) + subsegment.addThrottleFlag(); + + var cause = Utils.getCauseTypeFromHttpStatus(res.statusCode); + + if (cause) + subsegment[cause] = true; + + subsegment.addRemoteRequestData(res.req, res, !!downstreamXRayEnabled); + subsegment.close(); + }); + + if (typeof callback === 'function') { + if (contextUtils.isAutomaticMode()) { + var session = contextUtils.getNamespace(); + + session.run(function() { + contextUtils.setSegment(subsegment); + callback(res); + }); + } else { + callback(res); + } + } + }).on('error', errorCapturer); + + return req; + } + + module.__request = module.request; + module.request = function captureHTTPsRequest(options, callback) { + return captureOutgoingHTTPs(module.__request, options, callback); + }; + + module.__get = module.get; + module.get = function captureHTTPsGet(options, callback) { + return captureOutgoingHTTPs(module.__get, options, callback); + }; +} + +module.exports.captureHTTPsGlobal = captureHTTPsGlobal; +module.exports.captureHTTPs = captureHTTPs; diff --git a/packages/core/lib/resources/aws_whitelist.json b/packages/core/lib/resources/aws_whitelist.json new file mode 100644 index 00000000..f270b652 --- /dev/null +++ b/packages/core/lib/resources/aws_whitelist.json @@ -0,0 +1,335 @@ +{ + "services": { + "dynamodb": { + "operations": { + "batchGetItem": { + "request_descriptors": { + "RequestItems": { + "get_keys": true, + "rename_to": "table_names" + } + }, + "response_parameters": [ + "ConsumedCapacity" + ] + }, + "batchWriteItem": { + "request_descriptors": { + "RequestItems": { + "get_keys": true, + "rename_to": "table_names" + } + }, + "response_parameters": [ + "ConsumedCapacity", + "ItemCollectionMetrics" + ] + }, + "createTable": { + "request_parameters": [ + "GlobalSecondaryIndexes", + "LocalSecondaryIndexes", + "ProvisionedThroughput", + "TableName" + ] + }, + "deleteItem": { + "request_parameters": [ + "TableName" + ], + "response_parameters": [ + "ConsumedCapacity", + "ItemCollectionMetrics" + ] + }, + "deleteTable": { + "request_parameters": [ + "TableName" + ] + }, + "describeTable": { + "request_parameters": [ + "TableName" + ] + }, + "getItem": { + "request_parameters": [ + "ConsistentRead", + "ProjectionExpression", + "TableName" + ], + "response_parameters": [ + "ConsumedCapacity" + ] + }, + "listTables": { + "request_parameters": [ + "ExclusiveStartTableName", + "Limit" + ], + "response_descriptors": { + "TableNames": { + "list": true, + "get_count": true, + "rename_to": "table_count" + } + } + }, + "putItem": { + "request_parameters": [ + "TableName" + ], + "response_parameters": [ + "ConsumedCapacity", + "ItemCollectionMetrics" + ] + }, + "query": { + "request_parameters": [ + "AttributesToGet", + "ConsistentRead", + "IndexName", + "Limit", + "ProjectionExpression", + "ScanIndexForward", + "Select", + "TableName" + ], + "response_parameters": [ + "ConsumedCapacity" + ] + }, + "scan": { + "request_parameters": [ + "AttributesToGet", + "ConsistentRead", + "IndexName", + "Limit", + "ProjectionExpression", + "Segment", + "Select", + "TableName", + "TotalSegments" + ], + "response_parameters": [ + "ConsumedCapacity", + "Count", + "ScannedCount" + ] + }, + "updateItem": { + "request_parameters": [ + "TableName" + ], + "response_parameters": [ + "ConsumedCapacity", + "ItemCollectionMetrics" + ] + }, + "updateTable": { + "request_parameters": [ + "AttributeDefinitions", + "GlobalSecondaryIndexUpdates", + "ProvisionedThroughput", + "TableName" + ] + } + } + }, + "sqs": { + "operations": { + "addPermission": { + "request_parameters": [ + "Label", + "QueueUrl" + ] + }, + "changeMessageVisibility": { + "request_parameters": [ + "QueueUrl", + "VisibilityTimeout" + ] + }, + "changeMessageVisibilityBatch": { + "request_parameters": [ + "QueueUrl" + ], + "response_parameters": [ + "Failed" + ] + }, + "createQueue": { + "request_parameters": [ + "Attributes", + "QueueName" + ] + }, + "deleteMessage": { + "request_parameters": [ + "QueueUrl" + ] + }, + "deleteMessageBatch": { + "request_parameters": [ + "QueueUrl" + ], + "response_parameters": [ + "Failed" + ] + }, + "deleteQueue": { + "request_parameters": [ + "QueueUrl" + ] + }, + "getQueueAttributes": { + "request_parameters": [ + "QueueUrl" + ], + "response_parameters": [ + "Attributes" + ] + }, + "getQueueUrl": { + "request_parameters": [ + "QueueName", + "QueueOwnerAWSAccountId" + ], + "response_parameters": [ + "QueueUrl" + ] + }, + "listDeadLetterSourceQueues": { + "request_parameters": [ + "QueueUrl" + ], + "response_parameters": [ + "QueueUrls" + ] + }, + "listQueues": { + "request_parameters": [ + "QueueNamePrefix" + ], + "response_descriptors": { + "QueueUrls": { + "list": true, + "get_count": true, + "rename_to": "queue_count" + } + } + }, + "purgeQueue": { + "request_parameters": [ + "QueueUrl" + ] + }, + "receiveMessage": { + "request_parameters": [ + "AttributeNames", + "MaxNumberOfMessages", + "MessageAttributeNames", + "QueueUrl", + "VisibilityTimeout", + "WaitTimeSeconds" + ], + "response_descriptors": { + "Messages": { + "list": true, + "get_count": true, + "rename_to": "message_count" + } + } + }, + "removePermission": { + "request_parameters": [ + "QueueUrl" + ] + }, + "sendMessage": { + "request_parameters": [ + "DelaySeconds", + "QueueUrl" + ], + "request_descriptors": { + "MessageAttributes": { + "get_keys": true, + "rename_to": "message_attribute_names" + } + }, + "response_parameters": [ + "MessageId" + ] + }, + "sendMessageBatch": { + "request_parameters": [ + "QueueUrl" + ], + "request_descriptors": { + "Entries": { + "list": true, + "get_count": true, + "rename_to": "message_count" + } + }, + "response_descriptors": { + "Failed": { + "list": true, + "get_count": true, + "rename_to": "failed_count" + }, + "Successful": { + "list": true, + "get_count": true, + "rename_to": "successful_count" + } + } + }, + "setQueueAttributes": { + "request_parameters": [ + "QueueUrl" + ], + "request_descriptors": { + "Attributes": { + "get_keys": true, + "rename_to": "attribute_names" + } + } + } + } + }, + "sns": { + "operations": { + "publish": { + "request_parameters": [ + "TopicArn" + ] + } + } + }, + "lambda": { + "operations": { + "invoke": { + "request_parameters": [ + "FunctionName", + "InvocationType", + "LogType", + "Qualifier" + ], + "response_parameters": [ + "FunctionError", + "StatusCode" + ] + }, + "invokeAsync": { + "request_parameters": [ + "FunctionName" + ], + "response_parameters": [ + "Status" + ] + } + } + } + } +} diff --git a/packages/core/lib/resources/default_sampling_rules.json b/packages/core/lib/resources/default_sampling_rules.json new file mode 100644 index 00000000..3cfc2c85 --- /dev/null +++ b/packages/core/lib/resources/default_sampling_rules.json @@ -0,0 +1,7 @@ +{ + "default": { + "fixed_target": 1, + "rate": 0.05 + }, + "version": 1 +} diff --git a/packages/core/lib/segment_emitter.js b/packages/core/lib/segment_emitter.js new file mode 100644 index 00000000..1e7bd8ff --- /dev/null +++ b/packages/core/lib/segment_emitter.js @@ -0,0 +1,108 @@ +var dgram = require('dgram'); + +var logger = require('./logger'); + +var DEFAULT_ADDRESS = '127.0.0.1'; +var DEFAULT_PORT = 2000; +var PROTOCOL_HEADER = '{"format": "json", "version": 1}'; +var PROTOCOL_DELIMITER = '\n'; + +/** + * Segment emitter module. + * @module SegmentEmitter + */ + +var SegmentEmitter = { + socket: dgram.createSocket('udp4'), + daemonAddress: DEFAULT_ADDRESS, + daemonPort: DEFAULT_PORT, + + /** + * Returns the formatted segment JSON string. + */ + + format: function format(segment) { + return PROTOCOL_HEADER + PROTOCOL_DELIMITER + segment.toString(); + }, + + /** + * Creates a UDP socket connection and send the formatted segment. + * @param {Segment} segment - The segment to send to the daemon. + */ + + send: function send(segment) { + var socket = this.socket; + var client = socket || dgram.createSocket('udp4'); + var formatted = segment.format(); + var data = PROTOCOL_HEADER + PROTOCOL_DELIMITER + formatted; + var message = new Buffer(data); + + var short = '{"trace_id:"' + segment.trace_id + '","id":"' + segment.id + '"}'; + var type = segment.type === 'subsegment' ? 'Subsegment' : 'Segment'; + + client.send(message, 0, message.length, this.daemonPort, this.daemonAddress, function(err) { + if (err) { + if (err.code === 'EMSGSIZE') + logger.getLogger().error(type + ' too large to send: ' + short + ' (' + message.length + ' bytes).'); + else + logger.getLogger().error('Error occured sending segment: ', err); + } else { + logger.getLogger().debug(type + ' sent: {"trace_id:"' + segment.trace_id + '","id":"' + segment.id + '"}'); + logger.getLogger().debug('UDP message sent: ' + segment); + } + + if (!socket) + client.close(); + }); + }, + + /** + * Configures the address and/or port the daemon is expected to be on. + * @param {string} address - Address of the daemon the segments should be sent to. Should be formatted as an IPv4 address. + * @module SegmentEmitter + * @function setDaemonAddress + */ + + setDaemonAddress: function setDaemonAddress(address) { + if (!process.env.AWS_XRAY_DAEMON_ADDRESS) { + processAddress(address); + logger.getLogger().info('Configured daemon address to ' + SegmentEmitter.daemonAddress + ':' + SegmentEmitter.daemonPort + '.'); + } else { + logger.getLogger().warn('Ignoring call to setDaemonAddress as AWS_XRAY_DAEMON_ADDRESS is set. '+ + 'The current daemon address will not be changed.'); + } + }, + + /** + * Forces the segment emitter to create a new socket on send, and close it on complete. + * @module SegmentEmitter + * @function disableReusableSocket + */ + + disableReusableSocket: function() { + delete this.socket; + } +}; + +var processAddress = function processAddress(rawAddress) { + if (rawAddress.indexOf(':') === -1) { + SegmentEmitter.daemonAddress = rawAddress; + } else { + var splitAddress = rawAddress.split(':'); + SegmentEmitter.daemonPort = parseInt(splitAddress[1]); + + if (splitAddress[0]) + SegmentEmitter.daemonAddress = splitAddress[0]; + } +}; + +if (process.env.AWS_XRAY_DAEMON_ADDRESS) { + processAddress(process.env.AWS_XRAY_DAEMON_ADDRESS); + logger.getLogger().info('AWS_XRAY_DAEMON_ADDRESS is set. Configured daemon address to ' + SegmentEmitter.daemonAddress + + ':' + SegmentEmitter.daemonPort + '.'); +} + +if (SegmentEmitter.socket && (typeof SegmentEmitter.socket.unref === 'function')) + SegmentEmitter.socket.unref(); + +module.exports = SegmentEmitter; diff --git a/packages/core/lib/segments/attributes/aws.js b/packages/core/lib/segments/attributes/aws.js new file mode 100644 index 00000000..d85f0fc6 --- /dev/null +++ b/packages/core/lib/segments/attributes/aws.js @@ -0,0 +1,71 @@ +var CallCapturer = require('../../patchers/call_capturer.js'); + +var capturer = new CallCapturer(); + +/** + * Represents a AWS client call. Automatically captures data from the supplied response object, + * Data captured depends on the whitelisting file supplied. + * The base whitelisting file can be found at /lib/resources/aws_whitelist.json. + * @constructor + * @param {AWS.Response} res - The response object from the AWS call. + * @param {string} serviceName - The service name of the AWS client. + * @see /~https://github.com/aws/aws-sdk-js/blob/master/lib/response.js + */ + +function Aws(res, serviceName) { + this.init(res, serviceName); +} + +Aws.prototype.init = function init(res, serviceName) { + //TODO: account ID + this.operation = formatOperation(res.request.operation) || ''; + this.region = res.request.httpRequest.region || ''; + this.request_id = res.requestId || ''; + this.retries = res.retryCount || 0; + + if (res.extendedRequestId && serviceName === 's3') + this.id_2 = res.extendedRequestId; + + this.addData(capturer.capture(serviceName, res)); +}; + +Aws.prototype.addData = function addData(data) { + for (var attribute in data) { this[attribute] = data[attribute]; } +}; + +/** + * Overrides the default whitelisting file to specify what params to capture on each AWS Service call. + * @param {string|Object} source - The path to the custom whitelist file, or a whitelist source JSON object. + * @exports setAWSWhitelist + */ + +var setAWSWhitelist = function setAWSWhitelist(source) { + if (!source || source instanceof String || !(typeof source === 'string' || (source instanceof Object))) + throw new Error('Please specify a path to the local whitelist file, or supply a whitelist source object.'); + + capturer = new CallCapturer(source); +}; + +/** + * Appends to the default whitelisting file to specify what params to capture on each AWS Service call. + * @param {string|Object} source - The path to the custom whitelist file, or a whitelist source JSON object. + * @exports appendAWSWhitelist + */ + +var appendAWSWhitelist = function appendAWSWhitelist(source) { + if (!source || source instanceof String || !(typeof source === 'string' || (source instanceof Object))) + throw new Error('Please specify a path to the local whitelist file, or supply a whitelist source object.'); + + capturer.append(source); +}; + +function formatOperation(operation) { + if (!operation) + return; + + return operation.charAt(0).toUpperCase() + operation.slice(1); +} + +module.exports = Aws; +module.exports.appendAWSWhitelist = appendAWSWhitelist; +module.exports.setAWSWhitelist = setAWSWhitelist; diff --git a/packages/core/lib/segments/attributes/captured_exception.js b/packages/core/lib/segments/attributes/captured_exception.js new file mode 100644 index 00000000..1bb6ed80 --- /dev/null +++ b/packages/core/lib/segments/attributes/captured_exception.js @@ -0,0 +1,45 @@ +var _ = require('underscore'); + +/** + * Represents a captured exception. + * @constructor + * @param {Exception} err - The exception to capture. + * @param {boolean} [remote] - Flag for whether the error was remote. + */ + +function CapturedException(err, remote) { + this.init(err, remote); +} + +CapturedException.prototype.init = function init(err, remote) { + var e = (typeof err === 'string' || err instanceof String) ? { message: err, name: '' } : err; + + this.message = e.message; + this.type = e.name; + this.stack = []; + this.remote = !!remote; + + if (e.stack) { + var stack = e.stack.split('\n'); + stack.shift(); + + _.each(stack, function(stackline) { + var line = stackline.trim().replace(/\(|\)/g, ''); + line = line.substring(line.indexOf(' ') + 1); + + var label = line.lastIndexOf(' ') >= 0 ? line.slice(0, line.lastIndexOf(' ')) : null; + var path = _.isEmpty(label) ? line : line.slice(line.lastIndexOf(' ') + 1); + path = path.split(':'); + + var entry = { + path: path[0], + line: parseInt(path[1]), + label: label || 'anonymous' + }; + + this.stack.push(entry); + }, this); + } +}; + +module.exports = CapturedException; diff --git a/packages/core/lib/segments/attributes/remote_request_data.js b/packages/core/lib/segments/attributes/remote_request_data.js new file mode 100644 index 00000000..507b6e76 --- /dev/null +++ b/packages/core/lib/segments/attributes/remote_request_data.js @@ -0,0 +1,31 @@ +/** + * Represents an outgoing HTTP/HTTPS call. + * @constructor + * @param {http.ClientRequest|https.ClientRequest} req - The request object from the HTTP/HTTPS call. + * @param {http.IncomingMessage|https.IncomingMessage} res - The response object from the HTTP/HTTPS call. + * @param {boolean} downstreamXRayEnabled - when true, adds a "traced": true hint to generated subsegments such that the AWS X-Ray service expects a corresponding segment from the downstream service. + */ + +function RemoteRequestData(req, res, downstreamXRayEnabled) { + this.init(req, res, downstreamXRayEnabled); +} + +RemoteRequestData.prototype.init = function init(req, res, downstreamXRayEnabled) { + this.request = { + url: (req.agent.protocol + '//' + req.getHeader('host') + req.path) || '', + method: req.method || '', + }; + + if (downstreamXRayEnabled) { + this.request.traced = true; + } + + if (res) { + this.response = { + status: res.statusCode || '', + content_length: (res.headers && res.headers['content-length']) ? res.headers['content-length'] : 0 + }; + } +}; + +module.exports = RemoteRequestData; diff --git a/packages/core/lib/segments/attributes/subsegment.js b/packages/core/lib/segments/attributes/subsegment.js new file mode 100644 index 00000000..6ddbd50a --- /dev/null +++ b/packages/core/lib/segments/attributes/subsegment.js @@ -0,0 +1,383 @@ +var _ = require('underscore'); +var crypto = require('crypto'); + +var CapturedException = require('./captured_exception'); +var RemoteRequestData = require('./remote_request_data'); +var SegmentEmitter = require('../../segment_emitter'); +var SegmentUtils = require('../segment_utils'); + +var logger = require('../../logger'); + +/** + * Represents a subsegment. + * @constructor + * @param {string} name - The name of the subsegment. + */ + +function Subsegment(name) { + this.init(name); +} + +Subsegment.prototype.init = function init(name) { + if (typeof name != 'string') + throw new Error('Subsegment name must be of type string.'); + + this.id = crypto.randomBytes(8).toString('hex'); + this.name = name; + this.start_time = SegmentUtils.getCurrentTime(); + this.in_progress = true; + this.counter = 0; +}; + +/** + * Nests a new subsegment to the array of subsegments. + * @param {string} name - The name of the new subsegment to append. + * @returns {Subsegment} - The newly created subsegment. + */ + +Subsegment.prototype.addNewSubsegment = function addNewSubsegment(name) { + var subsegment = new Subsegment(name); + this.addSubsegment(subsegment); + return subsegment; +}; + +/** + * Adds a subsegment to the array of subsegments. + * @param {Subsegment} subsegment - The subsegment to append. + */ + +Subsegment.prototype.addSubsegment = function(subsegment) { + if (!(subsegment instanceof Subsegment)) { + throw new Error('Failed to add subsegment:' + subsegment + ' to subsegment "' + this.name + + '". Not a subsegment.'); + } + + if (_.isUndefined(this.subsegments)) + this.subsegments = []; + + subsegment.segment = this.segment; + subsegment.parent = this; + + if (_.isUndefined(subsegment.end_time)) { + this.incrementCounter(subsegment.counter); + } + + this.subsegments.push(subsegment); +}; + +/** + * Removes the subsegment from the subsegments array, used in subsegment streaming. + */ + +Subsegment.prototype.removeSubsegment = function removeSubsegment(subsegment) { + if (!(subsegment instanceof Subsegment)) { + throw new Error('Failed to remove subsegment:' + subsegment + ' from subsegment "' + this.name + + '". Not a subsegment.'); + } + + if (!_.isUndefined(this.subsegments)) { + var index = this.subsegments.indexOf(subsegment); + + if (index >= 0) + this.subsegments.splice(index, 1); + } +}; + +/** + * Adds a property with associated data into the subsegment. + * @param {string} name - The name of the property to add. + * @param {Object} data - The data of the property to add. + */ + +Subsegment.prototype.addAttribute = function addAttribute(name, data) { + this[name] = data; +}; + +/** + * Adds a subsegement id to record ordering. + * @param {string} id - A subsegment id. + */ + +Subsegment.prototype.addPrecursorId = function(id) { + if (!_.isString(id)) + logger.getLogger().error('Failed to add id:' + id + ' to subsegment ' + this.name + + '. Precursor Ids must be of type string.'); + + if (_.isUndefined(this.precursor_ids)) + this.precursor_ids = []; + + this.precursor_ids.push(id); +}; + +/** + * Adds a key-value pair that can be queryable through GetTraceSummaries. + * Only acceptable types are string, float/int and boolean. + * @param {string} key - The name of key to add. + * @param {boolean|string|number} value - The value to add for the given key. + */ + +Subsegment.prototype.addAnnotation = function(key, value) { + if (!(_.isBoolean(value) || _.isString(value) || _.isFinite(value))) { + throw new Error('Failed to add annotation key: ' + key + ' value: ' + value + ' to subsegment ' + + this.name + '. Value must be of type string, number or boolean.'); + } else if (!_.isString(key)) { + throw new Error('Failed to add annotation key: ' + key + ' value: ' + value + ' to subsegment ' + + this.name + '. Key must be of type string.'); + } + + if (_.isUndefined(this.annotations)) + this.annotations = {}; + + this.annotations[key] = value; +}; + +/** + * Adds a key-value pair to the metadata.default attribute when no namespace is given. + * Metadata is not queryable, but is recorded. + * @param {string} key - The name of the key to add. + * @param {object|null} value - The value of the associated key. + * @param {string} [namespace] - The property name to put the key/value pair under. + */ + +Subsegment.prototype.addMetadata = function(key, value, namespace) { + if (!_.isString(key)) { + throw new Error('Failed to add annotation key: ' + key + ' value: ' + value + ' to subsegment ' + + this.name + '. Key must be of type string.'); + } else if (namespace && !_.isString(namespace)) { + throw new Error('Failed to add annotation key: ' + key + ' value: ' + value + 'namespace: ' + namespace + ' to subsegment ' + + this.name + '. Namespace must be of type string.'); + } + + var ns = namespace || 'default'; + + if (!this.metadata) { + this.metadata = {}; + } + + if (!this.metadata[ns]) { + this.metadata[ns] = {}; + } + + this.metadata[ns][key] = value; +}; + +Subsegment.prototype.addSqlData = function addSqlData(sqlData) { + this.sql = sqlData; +}; + +/** + * Adds an error with associated data into the subsegment. + * To handle propagating errors, the subsegment also sets a copy of the error on the + * root segment. As the error passes up the execution stack, a reference is created + * on each subsegment to the originating subsegment. + * @param {Error|string} err - The error to capture. + * @param {boolean} [remote] - Flag for whether the exception caught was remote or not. + */ + +Subsegment.prototype.addError = function addError(err, remote) { + if (!_.isObject(err) && typeof(err) !== 'string') { + throw new Error('Failed to add error:' + err + ' to subsegment "' + this.name + + '". Not an object or string literal.'); + } + + this.addFaultFlag(); + + if (this.segment && this.segment.exception) { + if (err === this.segment.exception.ex) { + this.fault = true; + this.cause = { id: this.segment.exception.cause }; + return; + } + delete this.segment.exception; + } + + if (this.segment) { + this.segment.exception = { + ex: err, + cause: this.id + }; + } else { + //error, cannot propagate exception if not added to segment + } + + if (_.isUndefined(this.cause)) { + this.cause = { + working_directory: process.cwd(), + exceptions: [] + }; + } + + this.cause.exceptions.unshift(new CapturedException(err, remote)); +}; + +/** + * Adds data for an outgoing HTTP/HTTPS call. + * @param {http.ClientRequest/https.ClientRequest} req - The request object from the HTTP/HTTPS call. + * @param {http.IncomingMessage/https.IncomingMessage} res - The response object from the HTTP/HTTPS call. + * @param {boolean} downstreamXRayEnabled - when true, adds a "traced": true hint to generated subsegments such that the AWS X-Ray service expects a corresponding segment from the downstream service. + */ + +Subsegment.prototype.addRemoteRequestData = function addRemoteRequestData(req, res, downstreamXRayEnabled) { + this.http = new RemoteRequestData(req, res, downstreamXRayEnabled); + if ('traced' in this.http.request) { + this.traced = this.http.request.traced; + delete this.http.request.traced; + } +}; + +/** + * Adds fault flag to the subsegment. + */ + +Subsegment.prototype.addFaultFlag = function addFaultFlag() { + this.fault = true; +}; + +/** + * Adds error flag to the subsegment. + */ + +Subsegment.prototype.addErrorFlag = function addErrorFlag() { + this.error = true; +}; + +/** + * Adds throttle flag to the subsegment. + */ + +Subsegment.prototype.addThrottleFlag = function addThrottleFlag() { + this.throttle = true; +}; + +/** + * Closes the current subsegment. This automatically captures any exceptions and sets the end time. + * @param {Error|string} [err] - The error to capture. + * @param {boolean} [remote] - Flag for whether the exception caught was remote or not. + */ + +Subsegment.prototype.close = function close(err, remote) { + var root = this.segment; + this.end_time = SegmentUtils.getCurrentTime(); + delete this.in_progress; + + if (err) + this.addError(err, remote); + + if (this.parent) + this.parent.decrementCounter(); + + if (root && root.counter > SegmentUtils.getStreamingThreshold()) { + if (this.streamSubsegments() && this.parent) + this.parent.removeSubsegment(this); + } +}; + +/** + * Each subsegment holds a counter of open subsegments. This increments + * the counter such that it can be called from a child and propagate up. + * @param {Number} [additional] - An additional amount to increment. Used when adding subsegment trees. + */ + +Subsegment.prototype.incrementCounter = function incrementCounter(additional) { + this.counter = additional ? this.counter + additional + 1 : this.counter + 1; + + if (this.parent) + this.parent.incrementCounter(additional); +}; + +/** + * Each subsegment holds a counter of its open subsegments. This decrements + * the counter such that it can be called from a child and propagate up. + */ + +Subsegment.prototype.decrementCounter = function decrementCounter() { + this.counter--; + + if (this.parent) + this.parent.decrementCounter(); +}; + +/** + * Returns a boolean indicating whether or not the subsegment has been closed. + * @returns {boolean} - Returns true if the subsegment is closed. + */ + +Subsegment.prototype.isClosed = function isClosed() { + return !this.in_progress; +}; + +/** + * Sends the subsegment to the daemon. + */ + +Subsegment.prototype.flush = function flush() { + if (!this.parent || !this.segment) { + throw new Error('Failed to flush subsegment: ' + this.name + '. Subsegment must be added ' + + 'to a segment chain to flush.'); + } + + if (this.segment.trace_id) { + if (this.segment.notTraced !== true) { + SegmentEmitter.send(this); + } else { + logger.getLogger().debug('Ignoring flush on subsegment ' + this.id + '. Associated segment is marked as not sampled.'); + } + } else { + logger.getLogger().debug('Ignoring flush on subsegment ' + this.id + '. Associated segment is missing a trace ID.'); + } +}; + +/** + * Returns true if the subsegment was streamed in its entirety + */ + +Subsegment.prototype.streamSubsegments = function streamSubsegments() { + if (this.isClosed() && this.counter <= 0) { + this.flush(); + return true; + } else if (this.subsegments && this.subsegments.length > 0) { + var open = []; + + this.subsegments.forEach(function(child) { + if (!child.streamSubsegments()) + open.push(child); + }); + + this.subsegments = open; + } +}; + +/** + * Returns the formatted, trimmed subsegment JSON string to send to the daemon. + */ + +Subsegment.prototype.format = function format() { + this.type = 'subsegment'; + + if (this.parent) + this.parent_id = this.parent.id; + + if (this.segment) + this.trace_id = this.segment.trace_id; + + return JSON.stringify(this); +}; + +/** + * Returns the formatted subsegment JSON string. + */ + +Subsegment.prototype.toString = function toString() { + return JSON.stringify(this); +}; + +Subsegment.prototype.toJSON = function toJSON() { + var ignore = ['segment', 'parent', 'counter']; + + if (_.isEmpty(this.subsegments)) + ignore.push('subsegments'); + + return _.omit(this, ignore); +}; + +module.exports = Subsegment; diff --git a/packages/core/lib/segments/plugins/ec2_plugin.js b/packages/core/lib/segments/plugins/ec2_plugin.js new file mode 100644 index 00000000..8146e801 --- /dev/null +++ b/packages/core/lib/segments/plugins/ec2_plugin.js @@ -0,0 +1,30 @@ +var Plugin = require('./plugin'); + +var logger = require('../../logger'); + +var EC2Plugin = { + /** + * A function to get the instance data from the EC2 metadata service. + * @param {function} callback - The callback for the plugin loader. + */ + + getData: function(callback) { + var METADATA_OPTIONS = { + host: '169.254.169.254', + path: '/latest/dynamic/instance-identity/document' + }; + + Plugin.getPluginMetadata(METADATA_OPTIONS, function(err, data) { + if (err) { + logger.getLogger().error('Error loading EC2 plugin: ', err.stack); + callback(); + } else { + var metadata = { ec2: { instance_id: data.instanceId, availability_zone: data.availabilityZone }}; + callback(metadata); + } + }); + }, + originName: 'AWS::EC2::Instance' +}; + +module.exports = EC2Plugin; diff --git a/packages/core/lib/segments/plugins/ecs_plugin.js b/packages/core/lib/segments/plugins/ecs_plugin.js new file mode 100644 index 00000000..0001a8e7 --- /dev/null +++ b/packages/core/lib/segments/plugins/ecs_plugin.js @@ -0,0 +1,14 @@ +var os = require('os'); + +var ECSPlugin = { + /** + * A function to get the instance data from the ECS instance. + * @param {function} callback - The callback for the plugin loader. + */ + getData: function(callback) { + callback({ ecs: { container: os.hostname() }}); + }, + originName: 'AWS::ECS::Container' +}; + +module.exports = ECSPlugin; diff --git a/packages/core/lib/segments/plugins/elastic_beanstalk_plugin.js b/packages/core/lib/segments/plugins/elastic_beanstalk_plugin.js new file mode 100644 index 00000000..28a8dad9 --- /dev/null +++ b/packages/core/lib/segments/plugins/elastic_beanstalk_plugin.js @@ -0,0 +1,35 @@ +var fs = require('fs'); + +var logger = require('../../logger'); + +var ENV_CONFIG_LOCATION = '/var/elasticbeanstalk/xray/environment.conf'; + +var ElasticBeanstalkPlugin = { + /** + * A function to get data from the Elastic Beanstalk environment configuration file. + * @param {function} callback - The callback for the plugin loader. + */ + getData: function(callback) { + fs.readFile(ENV_CONFIG_LOCATION, 'utf8', function(err, rawData) { + if (err) { + logger.getLogger().error('Error loading Elastic Beanstalk plugin:', err.stack); + callback(); + } else { + var data = JSON.parse(rawData); + + var metadata = { + elastic_beanstalk: { + environment: data.environment_name, + version_label: data.version_label, + deployment_id: data.deployment_id + } + }; + + callback(metadata); + } + }); + }, + originName: 'AWS::ElasticBeanstalk::Environment' +}; + +module.exports = ElasticBeanstalkPlugin; diff --git a/packages/core/lib/segments/plugins/plugin.js b/packages/core/lib/segments/plugins/plugin.js new file mode 100644 index 00000000..d364351a --- /dev/null +++ b/packages/core/lib/segments/plugins/plugin.js @@ -0,0 +1,47 @@ +var http = require('http'); + +var Plugin = { + getPluginMetadata: function(options, callback) { + var METADATA_TIMEOUT = 1000; + var METADATA_RETRY_TIMEOUT = 250; + var METADATA_RETRIES = 20; + + var retries = METADATA_RETRIES; + + var getMetadata = function() { + var httpReq = http.__request ? http.__request : http.request; + + var req = httpReq(options, function(res) { + var body = ''; + + res.on('data', function(chunk) { + body += chunk; + }); + res.on('end', function() { + if (this.statusCode === 200 || this.statusCode === 300) { + body = JSON.parse(body); + callback(null, body); + } else if (retries > 0 && this.statusCode === 400){ + retries--; + setTimeout(getMetadata, METADATA_RETRY_TIMEOUT); + } else { callback(); } + }); + }).on('error', function(err) { + callback(err); + }); + + req.on('socket', function(socket) { + socket.on('timeout', function() { + req.abort(); + }); + socket.setTimeout(METADATA_TIMEOUT); + }); + + req.end(); + }; + + getMetadata(); + } +}; + +module.exports = Plugin; diff --git a/packages/core/lib/segments/segment.js b/packages/core/lib/segments/segment.js new file mode 100644 index 00000000..759b5213 --- /dev/null +++ b/packages/core/lib/segments/segment.js @@ -0,0 +1,354 @@ +var crypto = require('crypto'); +var _ = require('underscore'); + +var CapturedException = require('./attributes/captured_exception'); +var SegmentEmitter = require('../segment_emitter'); +var SegmentUtils = require('./segment_utils'); +var Subsegment = require('./attributes/subsegment'); + +var logger = require('../logger'); + +/** + * Represents a segment. + * @constructor + * @param {string} name - The name of the subsegment. + * @param {string} [rootId] - The trace ID of the spawning parent, included in the 'X-Amzn-Trace-Id' header of the incoming request. If one is not supplied, it will be generated. + * @param {string} [parentId] - The sub/segment ID of the spawning parent, included in the 'X-Amzn-Trace-Id' header of the incoming request. + */ + +function Segment(name, rootId, parentId) { + this.init(name, rootId, parentId); +} + +Segment.prototype.init = function init(name, rootId, parentId) { + if (typeof name != 'string') + throw new Error('Segment name must be of type string.'); + + var traceId = rootId || '1-' + Math.round(new Date().getTime() / 1000).toString(16) + '-' + + crypto.randomBytes(12).toString('hex'); + + var id = crypto.randomBytes(8).toString('hex'); + var startTime = SegmentUtils.getCurrentTime(); + + this.trace_id = traceId; + this.id = id; + this.start_time = startTime; + this.name = name || ''; + this.in_progress = true; + this.counter = 0; + + if (parentId) + this.parent_id = parentId; + + if (SegmentUtils.serviceData) + this.setServiceData(SegmentUtils.serviceData); + + if (SegmentUtils.pluginData) + this.addPluginData(SegmentUtils.pluginData); + + if (SegmentUtils.origin) + this.origin = SegmentUtils.origin; + + if (SegmentUtils.sdkData) + this.setSDKData(SegmentUtils.sdkData); +}; + +/** + * Adds incoming request data to the http block of the segment. + * @param {IncomingRequestData} data - The data of the property to add. + */ + +Segment.prototype.addIncomingRequestData = function addIncomingRequestData(data) { + this.http = data; +}; + +/** + * Adds a key-value pair that can be queryable through GetTraceSummaries. + * Only acceptable types are string, float/int and boolean. + * @param {string} key - The name of key to add. + * @param {boolean|string|number} value - The value to add for the given key. + */ + +Segment.prototype.addAnnotation = function addAnnotation(key, value) { + if (!(_.isBoolean(value) || _.isString(value) || _.isFinite(value))) { + logger.getLogger().error('Add annotation key: ' + key + ' value: ' + value + ' failed.' + + ' Annotations must be of type string, number or boolean.'); + return; + } + + if (_.isUndefined(this.annotations)) + this.annotations = {}; + + this.annotations[key] = value; +}; + +/** + * Adds a key-value pair to the metadata.default attribute when no namespace is given. + * Metadata is not queryable, but is recorded. + * @param {string} key - The name of the key to add. + * @param {object|null} value - The value of the associated key. + * @param {string} [namespace] - The property name to put the key/value pair under. + */ + +Segment.prototype.addMetadata = function(key, value, namespace) { + if (!_.isString(key)) { + throw new Error('Failed to add annotation key: ' + key + ' value: ' + value + ' to subsegment ' + + this.name + '. Key must be of type string.'); + } else if (namespace && !_.isString(namespace)) { + throw new Error('Failed to add annotation key: ' + key + ' value: ' + value + 'namespace: ' + namespace + ' to subsegment ' + + this.name + '. Namespace must be of type string.'); + } + + var ns = namespace || 'default'; + + if (!this.metadata) { + this.metadata = {}; + } + + if (!this.metadata[ns]) { + this.metadata[ns] = {}; + } + + this.metadata[ns][key] = value; +}; + +/** + * Adds data about the AWS X-Ray SDK onto the segment. + * @param {Object} data - Object that contains the version of the SDK, and other information. + */ + +Segment.prototype.setSDKData = function setSDKData(data) { + if (!data) { + logger.getLogger().error('Add SDK data: ' + data + ' failed.' + + 'Must not be empty.'); + return; + } + + if (!this.aws) + this.aws = {}; + + this.aws.xray = data; +}; + +/** + * Adds data about the service into the segment. + * @param {Object} data - Object that contains the version of the application, and other information. + */ + +Segment.prototype.setServiceData = function setServiceData(data) { + if (!data) { + logger.getLogger().error('Add service data: ' + data + ' failed.' + + 'Must not be empty.'); + return; + } + + this.service = data; +}; + +/** + * Adds a service with associated version data into the segment. + * @param {Object} data - The associated AWS data. + */ + +Segment.prototype.addPluginData = function addPluginData(data) { + if (_.isUndefined(this.aws)) + this.aws = {}; + + _.extend(this.aws, data); +}; + +/** + * Adds a new subsegment to the array of subsegments. + * @param {string} name - The name of the new subsegment to append. + */ + +Segment.prototype.addNewSubsegment = function addNewSubsegment(name) { + var subsegment = new Subsegment(name); + this.addSubsegment(subsegment); + return subsegment; +}; + +/** + * Adds a subsegment to the array of subsegments. + * @param {Subsegment} subsegment - The subsegment to append. + */ + +Segment.prototype.addSubsegment = function addSubsegment(subsegment) { + if (!(subsegment instanceof Subsegment)) + throw new Error('Cannot add subsegment: ' + subsegment + '. Not a subsegment.'); + + if (_.isUndefined(this.subsegments)) + this.subsegments = []; + + subsegment.segment = this; + subsegment.parent = this; + this.subsegments.push(subsegment); + + if (!subsegment.end_time) + this.incrementCounter(subsegment.counter); +}; + +/** + * Removes the subsegment from the subsegments array, used in subsegment streaming. + */ + +Segment.prototype.removeSubsegment = function removeSubsegment(subsegment) { + if (!(subsegment instanceof Subsegment)) { + throw new Error('Failed to remove subsegment:' + subsegment + ' from subsegment "' + this.name + + '". Not a subsegment.'); + } + + if (!_.isUndefined(this.subsegments)) { + var index = this.subsegments.indexOf(subsegment); + + if (index >= 0) + this.subsegments.splice(index, 1); + } +}; + +/** + * Adds error data into the segment. + * @param {Error|string} err - The error to capture. + * @param {boolean} [remote] - Flag for whether the exception caught was remote or not. + */ + +Segment.prototype.addError = function addError(err, remote) { + if (!_.isObject(err) && typeof(err) !== 'string') { + throw new Error('Failed to add error:' + err + ' to subsegment "' + this.name + + '". Not an object or string literal.'); + } + + this.addFaultFlag(); + + if (this.exception) { + if (err === this.exception.ex) { + this.cause = { id: this.exception.cause }; + delete this.exception; + return; + } + delete this.exception; + } + + if (_.isUndefined(this.cause)) { + this.cause = { + working_directory: process.cwd(), + exceptions: [] + }; + } + + this.cause.exceptions.push(new CapturedException(err, remote)); +}; + +/** + * Adds fault flag to the subsegment. + */ + +Segment.prototype.addFaultFlag = function addFaultFlag() { + this.fault = true; +}; + +/** + * Adds error flag to the subsegment. + */ + +Segment.prototype.addErrorFlag = function addErrorFlag() { + this.error = true; +}; + +/** + * Adds throttle flag to the subsegment. + */ + +Segment.prototype.addThrottleFlag = function addThrottleFlag() { + this.throttle = true; +}; + +/** + * Returns a boolean indicating whether or not the segment has been closed. + * @returns {boolean} - Returns true if the subsegment is closed. + */ + +Segment.prototype.isClosed = function isClosed() { + return !this.in_progress; +}; + +/** + * Each segment holds a counter of open subsegments. This increments the counter. + * @param {Number} [additional] - An additional amount to increment. Used when adding subsegment trees. + */ + +Segment.prototype.incrementCounter = function incrementCounter(additional) { + this.counter = additional ? this.counter + additional + 1 : this.counter + 1; + + if (this.counter > SegmentUtils.streamingThreshold && this.subsegments && this.subsegments.length > 0) { + var open = []; + + this.subsegments.forEach(function(child) { + if (!child.streamSubsegments()) + open.push(child); + }); + + this.subsegments = open; + } +}; + +/** + * Each segment holds a counter of open subsegments. This decrements + * the counter such that it can be called from a child and propagate up. + */ + +Segment.prototype.decrementCounter = function decrementCounter() { + this.counter--; + + if (this.counter <= 0 && this.isClosed()) { + this.flush(); + } +}; + +/** + * Closes the current segment. This automatically sets the end time. + * @param {Error|string} [err] - The error to capture. + * @param {boolean} [remote] - Flag for whether the exception caught was remote or not. + */ + +Segment.prototype.close = function(err, remote) { + if (!this.end_time) + this.end_time = SegmentUtils.getCurrentTime(); + + if (!_.isUndefined(err)) + this.addError(err, remote); + + delete this.in_progress; + delete this.exception; + + if (this.counter <= 0) { + this.flush(); + } +}; + +/** + * Sends the segment to the daemon. + */ + +Segment.prototype.flush = function flush() { + if (this.notTraced !== true) { + delete this.exception; + SegmentEmitter.send(_.omit(this, ['counter', 'notTraced'])); + } +}; + +Segment.prototype.format = function format() { + var ignore = ['counter', 'notTraced', 'exception']; + + if (_.isEmpty(this.subsegments)) + ignore.push('subsegments'); + + var trimmed = _.omit(this, ignore); + return JSON.stringify(trimmed); +}; + +Segment.prototype.toString = function toString() { + return JSON.stringify(this); +}; + +module.exports = Segment; diff --git a/packages/core/lib/segments/segment_utils.js b/packages/core/lib/segments/segment_utils.js new file mode 100644 index 00000000..293deafc --- /dev/null +++ b/packages/core/lib/segments/segment_utils.js @@ -0,0 +1,53 @@ +var _ = require('underscore'); + +var logger = require('../logger'); + +var DEFAULT_STREAMING_THRESHOLD = 100; + +var utils = { + streamingThreshold: DEFAULT_STREAMING_THRESHOLD, + + getCurrentTime: function getCurrentTime() { + return new Date().getTime()/1000; + }, + + setOrigin: function setOrigin(origin) { + this.origin = origin; + }, + + setPluginData: function setPluginData(pluginData) { + this.pluginData = pluginData; + }, + + setSDKData: function setSDKData(sdkData) { + this.sdkData = sdkData; + }, + + setServiceData: function setServiceData(serviceData) { + this.serviceData = serviceData; + }, + + /** + * Overrides the default streaming threshold (100). + * The threshold represents the maximum number of subsegments on a single segment before + * the SDK beings to send the completed subsegments out of band of the main segment. + * Reduce this threshold if you see the 'Segment too large to send' error. + * @param {number} threshold - The new threshold to use. + * @memberof AWSXRay + */ + + setStreamingThreshold: function setStreamingThreshold(threshold) { + if (_.isFinite(threshold) && threshold >= 0) { + utils.streamingThreshold = threshold; + logger.getLogger().info('Subsegment streaming threshold set to: ' + threshold); + } else { + logger.getLogger().error('Invalid threshold: ' + threshold + '. Must be a whole number >= 0.'); + } + }, + + getStreamingThreshold: function getStreamingThreshold() { + return utils.streamingThreshold; + } +}; + +module.exports = utils; diff --git a/packages/core/lib/utils.js b/packages/core/lib/utils.js new file mode 100644 index 00000000..d0990286 --- /dev/null +++ b/packages/core/lib/utils.js @@ -0,0 +1,173 @@ +/** + * @module utils + */ + +var _ = require('underscore'); + +var logger = require('./logger'); + +var utils = { + + /** + * Checks a HTTP response code, where 4xx are 'error' and 5xx are 'fault'. + * @param {string} status - the HTTP response sattus code. + * @returns [string] - 'error', 'fault' or nothing on no match + * @alias module:utils.getCauseTypeFromHttpStatus + */ + + getCauseTypeFromHttpStatus: function getCauseTypeFromHttpStatus(status) { + var stat = status.toString(); + if (!_.isNull(stat.match(/^[4][0-9]{2}$/))) + return 'error'; + else if (!_.isNull(stat.match(/^[5][0-9]{2}$/))) + return 'fault'; + }, + + /** + * Performs a case-insensitive wildcard match against two strings. This method works with pseduo-regex chars; specifically ? and * are supported. + * An asterisk (*) represents any combination of characters + * A question mark (?) represents any single character + * + * @param {string} pattern - the regex-like pattern to be compared against. + * @param {string} text - the string to compare against the pattern. + * @returns boolean + * @alias module:utils.wildcardMatch + */ + + wildcardMatch: function wildcardMatch(pattern, text) { + if (_.isUndefined(pattern) || _.isUndefined(text)) + return false; + + if (pattern.length === 1 && pattern.charAt(0) === '*') + return true; + + var patternLength = pattern.length; + var textLength = text.length; + var indexOfGlob = pattern.indexOf('*'); + + pattern = pattern.toLowerCase(); + text = text.toLowerCase(); + + // Infix globs are relatively rare, and the below search is expensive especially when + // Balsa is used a lot. Check for infix globs and, in their absence, do the simple thing + if (indexOfGlob === -1 || indexOfGlob === (patternLength - 1)) { + var match = function simpleWildcardMatch() { + var j = 0; + + for(var i = 0; i < patternLength; i++) { + var patternChar = pattern.charAt(i); + if(patternChar === '*') { + // Presumption for this method is that globs only occur at end + return true; + } else if (patternChar === '?') { + if(j === textLength) + return false; // No character to match + + j++; + } else { + if (j >= textLength || patternChar != text.charAt(j)) + return false; + + j++; + } + } + // Ate up all the pattern and didn't end at a glob, so a match will have consumed all + // the text + return j === textLength; + }; + + return match(); + } + + /* + * The matchArray[i] is used to record if there is a match between the first i chars in = + * text and the first j chars in pattern. + * So will return matchArray[textLength+1] in the end + * Loop from the beginning of the pattern + * case not '*': if text[i]==pattern[j] or pattern[j] is '?', and matchArray[i] is true, + * set matchArray[i+1] to true, otherwise false + * case '*': since '*' can match any globing, as long as there is a true in matchArray before i + * all the matchArray[i+1], matchArray[i+2],...,matchArray[textLength] could be true + */ + + var matchArray = []; + matchArray[0] = true; + + for (var j = 0; j < patternLength; j++) { + var i; + var patternChar = pattern.charAt(j); + + if (patternChar != '*') { + for(i = textLength - 1; i >= 0; i--) + matchArray[i+1] = !!matchArray[i] && (patternChar === '?' || (patternChar === text.charAt(i))); + } else { + i = 0; + + while (i <= textLength && !matchArray[i]) + i++; + + for(i; i <= textLength; i++) + matchArray[i] = true; + } + matchArray[0] = (matchArray[0] && patternChar === '*'); + } + + return matchArray[textLength]; + }, + + LambdaUtils: { + validTraceData: function(xAmznTraceId) { + var valid = false; + + if (xAmznTraceId) { + var data = utils.processTraceData(xAmznTraceId); + valid = !!(data && data.Root && data.Parent && data.Sampled); + } + + return valid; + }, + + populateTraceData: function(segment, xAmznTraceId) { + logger.getLogger().debug('Lambda trace data found: ' + xAmznTraceId); + var data = utils.processTraceData(xAmznTraceId); + var populated = false; + + if (data && data.Root && data.Parent && data.Sampled) { + segment.trace_id = data.Root; + segment.id = data.Parent; + + if (!parseInt(data.Sampled)) + segment.notTraced = true; + else + delete segment.notTraced; + + logger.getLogger().debug('Segment started: ' + JSON.stringify(data)); + + populated = true; + } else + logger.getLogger().warn('_X_AMZN_TRACE_ID is missing required data.'); + + return populated; + } + }, + + /** + * Splits out the data from the trace id format. Used by the middleware. + * @param {String} traceData - The additional trace data (typically in req.headers.x-amzn-trace-id). + * @returns {object} + * @alias module:mw_utils.processTraceData + */ + + processTraceData: function processTraceData(traceData) { + var amznTraceData = {}; + + _.each(traceData.split(';'), function(header) { + var pair = header.split('='); + this[pair[0].trim()] = pair[1].trim(); + }, amznTraceData); + + return amznTraceData; + } +}; + +module.exports = utils; diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..da79d7a6 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,49 @@ +{ + "name": "aws-xray-sdk-core", + "version": "1.1.5", + "description": "AWS X-Ray SDK for Javascript", + "author": "Amazon Web Services", + "contributors": [ + "Sandra McMullen " + ], + "main": "lib/index.js", + "engines": { + "node": ">= 0.8.0" + }, + "directories": { + "test": "test" + }, + "peerDependencies": { + "continuation-local-storage": "^3.2.0", + "moment": "^2.15.2", + "pkginfo": "^0.4.0", + "semver": "^5.3.0", + "underscore": "^1.8.3", + "winston": "^2.2.0" + }, + "devDependencies": { + "chai": "^3.5.0", + "eslint": "^3.10.2", + "grunt": "^1.0.1", + "grunt-contrib-clean": "^1.0.0", + "grunt-jsdoc": "^2.1.0", + "mocha": "^3.0.2", + "nock": "^8.0.0", + "sinon": "^1.17.5", + "sinon-chai": "^2.8.0" + }, + "scripts": { + "test": "./node_modules/.bin/mocha --recursive ./test/ -R spec" + }, + "keywords": [ + "amazon", + "api", + "aws", + "core", + "xray", + "x-ray", + "x ray" + ], + "license": "Apache-2.0", + "repository": "/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/core" +} diff --git a/packages/core/test/resources/custom_sampling.json b/packages/core/test/resources/custom_sampling.json new file mode 100644 index 00000000..63b4a222 --- /dev/null +++ b/packages/core/test/resources/custom_sampling.json @@ -0,0 +1,31 @@ +{ + "rules": [ + { + "description": "Root", + "http_method": "GET", + "service_name": "localhost:*", + "url_path": "/", + "fixed_target": 0, + "rate": 0 + }, + { + "http_method": "GET", + "service_name": "*", + "url_path": "/getSQS", + "fixed_target": 10, + "rate": 0.05 + }, + { + "http_method": "GET", + "service_name": "*.foo.com", + "url_path": "/signin/*", + "fixed_target": 10, + "rate": 0.05 + } + ], + "default": { + "fixed_target": 10, + "rate": 0.05 + }, + "version": 1 +} \ No newline at end of file diff --git a/packages/core/test/resources/custom_whitelist.json b/packages/core/test/resources/custom_whitelist.json new file mode 100644 index 00000000..97750e3f --- /dev/null +++ b/packages/core/test/resources/custom_whitelist.json @@ -0,0 +1,14 @@ +{ + "services": { + "s3": { + "operations": { + "getObject": { + "request_parameters": [ + "Bucket", + "Key" + ] + } + } + } + } +} \ No newline at end of file diff --git a/packages/core/test/unit/aws-xray.test.js b/packages/core/test/unit/aws-xray.test.js new file mode 100644 index 00000000..243ea327 --- /dev/null +++ b/packages/core/test/unit/aws-xray.test.js @@ -0,0 +1,89 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var segmentUtils = require('../../lib/segments/segment_utils'); + +chai.should(); +chai.use(sinonChai); + +describe('AWSXRay', function() { + var AWSXRay; + + describe('on load', function() { + var sandbox, setSDKDataStub, setServiceDataStub; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + setSDKDataStub = sandbox.stub(segmentUtils, 'setSDKData'); + setServiceDataStub = sandbox.stub(segmentUtils, 'setServiceData'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should set the segmentUtils version and SDK version', function() { + AWSXRay = require('../../lib/index'); + + setSDKDataStub.should.have.been.calledWithExactly(sinon.match.object); + setServiceDataStub.should.have.been.calledWithExactly(sinon.match.object); + + assert.property(setSDKDataStub.firstCall.args[0], 'sdk'); + assert.property(setSDKDataStub.firstCall.args[0], 'sdk_version'); + assert.property(setSDKDataStub.firstCall.args[0], 'package'); + + assert.property(setServiceDataStub.firstCall.args[0], 'runtime'); + assert.property(setServiceDataStub.firstCall.args[0], 'runtime_version'); + assert.property(setServiceDataStub.firstCall.args[0], 'name'); + assert.property(setServiceDataStub.firstCall.args[0], 'version'); + }); + }); + + describe('#config', function() { + var sandbox, setOriginStub, setPluginDataStub; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + setPluginDataStub = sandbox.stub(segmentUtils, 'setPluginData'); + setOriginStub = sandbox.stub(segmentUtils, 'setOrigin'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should load the given plugins and set the data on segmentUtils', function(done) { + var data = { client: 'data' }; + var pluginStub = { + getData: function(callback) { + callback(data); + } + }; + AWSXRay.config([pluginStub]); + + setTimeout(function() { + setPluginDataStub.should.have.been.calledWithExactly(data); + done(); + }, 50); + }); + + it('should set segmentUtils origin to beanstalk if beanstalk plugin was loaded', function(done) { + var pluginStub = { + getData: function(callback) { + callback('data'); + }, + originName: 'AWS::ElasticBeanstalk::Environment' + }; + AWSXRay.config([pluginStub]); + + setTimeout(function() { + setOriginStub.should.have.been.calledWithExactly('AWS::ElasticBeanstalk::Environment'); + done(); + }, 50); + }); + }); +}); diff --git a/packages/core/test/unit/capture.test.js b/packages/core/test/unit/capture.test.js new file mode 100644 index 00000000..2cc1e030 --- /dev/null +++ b/packages/core/test/unit/capture.test.js @@ -0,0 +1,335 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var contextUtils = require('../../lib/context_utils'); +var Segment = require('../../lib/segments/segment'); + +var captureFunc = require('../../lib/capture').captureFunc; +var captureAsyncFunc = require('../../lib/capture').captureAsyncFunc; +var captureCallbackFunc = require('../../lib/capture').captureCallbackFunc; + +chai.should(); +chai.use(sinonChai); + +describe('Capture', function() { + var sandbox; + + var traceId = '1-57fbe041-2c7ad569f5d6ff149137be86'; + + before(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(Segment.prototype, 'flush'); + }); + + after(function() { + sandbox.restore(); + }); + + describe('when manual mode is enabled', function() { + describe('#captureFunc', function() { + var sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test', traceId); + + sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create an subsegment on the parent', function() { + captureFunc('tracedFcn', function() { + return; + }, segment); + + assert.property(segment, 'subsegments'); + }); + + it('should close the subsegment on completion and record the time', function() { + captureFunc('tracedFcn', function() { + return; + }, segment); + + assert.property(segment.subsegments[0], 'end_time'); + assert.isAtLeast(segment.subsegments[0].end_time, segment.subsegments[0].start_time); + }); + + it('should capture any errors thrown', function() { + var err = new Error('x is not defined'); + + assert.throws(function() { + captureFunc('tracedFcn', function() { + throw err; + }, segment); + }); + + segment.close(err); + assert.property(segment, 'fault'); + assert.equal(segment.cause.id, segment.subsegments[0].id); + assert.equal(segment.subsegments[0].cause.exceptions[0].message, err.message); + }); + + it('should expose the new subsegment', function() { + var subsegment; + + captureFunc('tracedFcn', function(sub) { + subsegment = sub; + }, segment); + + assert.isObject(subsegment); + assert.equal(subsegment, segment.subsegments[0]); + }); + }); + + describe('#captureAsyncFunc', function() { + var sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test', traceId); + + sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create an subsegment on the parent', function(done) { + captureAsyncFunc('tracedFcn', function(seg) { + seg.close(); + }, segment); + + setTimeout(function() { + assert.property(segment, 'subsegments'); + done(); + }, 50); + }); + + it('should record the time on close', function(done) { + captureAsyncFunc('tracedFcn', function(seg) { + seg.close(); + }, segment); + + setTimeout(function() { + assert.property(segment.subsegments[0], 'end_time'); + assert.isAtLeast(segment.subsegments[0].end_time, segment.subsegments[0].start_time); + done(); + }, 50); + }); + + it('should capture any errors thrown', function() { + var err = new Error('x is not defined'); + + assert.throws(function() { + captureAsyncFunc('tracedFcn', function() { + throw err; + }, segment); + }); + + segment.close(err); + assert.property(segment, 'fault'); + assert.equal(segment.cause.id, segment.subsegments[0].id); + assert.equal(segment.subsegments[0].cause.exceptions[0].message, err.message); + }); + + it('should expose the new subsegment', function() { + var subsegment; + + captureAsyncFunc('tracedFcn', function(sub) { + subsegment = sub; + }, segment); + + assert.isObject(subsegment); + assert.equal(subsegment, segment.subsegments[0]); + }); + }); + + describe('#captureCallbackFunc', function() { + var params, sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + params = ['hello', 'there']; + segment = new Segment('test', traceId); + + sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call the original function and pass the params through', function() { + var tracedFcn = function tracedFcn(callback) { + callback(params[0], params[1]); + }; + + var callback = function tracedFcn(param0, param1) { + assert.equal(params[0], param0); + assert.equal(params[1], param1); + }; + + tracedFcn(captureCallbackFunc('callback', callback, segment)); + }); + }); + }); + + describe('when automatic mode is enabled', function() { + describe('#captureFunc', function() { + var sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test', traceId); + + sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create an subsegment on the parent', function() { + captureFunc('tracedFcn', function() { + return; + }); + + assert.property(segment, 'subsegments'); + }); + + it('should close the subsegment on completion and record the time', function() { + captureFunc('tracedFcn', function() { + return; + }); + + assert.property(segment.subsegments[0], 'end_time'); + assert.isAtLeast(segment.subsegments[0].end_time, segment.subsegments[0].start_time); + }); + + it('should capture any errors thrown', function() { + var err = new Error('x is not defined'); + + assert.throws(function() { + captureFunc('tracedFcn', function() { + throw err; + }, segment); + }); + + segment.close(err); + assert.property(segment, 'fault'); + assert.equal(segment.cause.id, segment.subsegments[0].id); + assert.equal(segment.subsegments[0].cause.exceptions[0].message, err.message); + }); + + it('should expose the new subsegment', function() { + var subsegment; + + captureFunc('tracedFcn', function(sub) { + subsegment = sub; + }); + + assert.isObject(subsegment); + assert.equal(subsegment, segment.subsegments[0]); + }); + }); + + describe('#captureAsyncFunc', function() { + var sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test', traceId); + + sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create an subsegment on the parent', function(done) { + captureAsyncFunc('tracedFcn', function(seg) { + seg.close(); + }); + + setTimeout(function() { + assert.property(segment, 'subsegments'); + done(); + }, 50); + }); + + it('should record the time on close', function(done) { + captureAsyncFunc('tracedFcn', function(seg) { + seg.close(); + }); + + setTimeout(function() { + assert.property(segment.subsegments[0], 'end_time'); + assert.isAtLeast(segment.subsegments[0].end_time, segment.subsegments[0].start_time); + done(); + }, 50); + }); + + it('should capture any errors thrown', function() { + var err = new Error('x is not defined'); + + assert.throws(function() { + captureAsyncFunc('tracedFcn', function() { + throw err; + }, segment); + }); + + segment.close(err); + assert.property(segment, 'fault'); + assert.equal(segment.cause.id, segment.subsegments[0].id); + assert.equal(segment.subsegments[0].cause.exceptions[0].message, err.message); + }); + + it('should expose the new subsegment', function() { + var subsegment; + + captureAsyncFunc('tracedFcn', function(sub) { + subsegment = sub; + }, segment); + + assert.isObject(subsegment); + assert.equal(subsegment, segment.subsegments[0]); + }); + }); + + describe('#captureCallbackFunc', function() { + var params, sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + params = ['hello', 'there']; + segment = new Segment('test', traceId); + + sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call the original function and pass the params through', function() { + var tracedFcn = function tracedFcn(callback) { + callback(params[0], params[1]); + }; + + var callback = function tracedFcn(param0, param1) { + assert.equal(params[0], param0); + assert.equal(params[1], param1); + }; + + tracedFcn(captureCallbackFunc('callback', callback)); + }); + }); + }); +}); diff --git a/packages/core/test/unit/context_utils.test.js b/packages/core/test/unit/context_utils.test.js new file mode 100644 index 00000000..42a2f609 --- /dev/null +++ b/packages/core/test/unit/context_utils.test.js @@ -0,0 +1,175 @@ +var assert = require('chai').assert; +var sinon = require('sinon'); + +var Segment = require('../../lib/segments/segment'); +var Subsegment = require('../../lib/segments/attributes/subsegment'); +var ContextUtils = require('../../lib/context_utils'); + +var LOG_ERROR = 'LOG_ERROR'; +var LOG_ERROR_FCN_NAME = 'contextMissingLogError'; +var RUNTIME_ERROR = 'RUNTIME_ERROR'; +var RUNTIME_ERROR_FCN_NAME = 'contextMissingRuntimeError'; + +describe('ContextUtils', function() { + function reloadContextUtils() { + var path = '../../lib/context_utils'; + delete require.cache[require.resolve(path)]; + ContextUtils = require(path); + } + + describe('init', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + delete process.env.AWS_XRAY_CONTEXT_MISSING; + reloadContextUtils(); + }); + + it('should start in automatic mode by creating the X-Ray namespace', function() { + assert.equal(ContextUtils.getNamespace().name, 'AWSXRay'); + }); + + it('should set the contextMissingStrategy to RUNTIME_ERROR by default', function() { + assert.equal(ContextUtils.contextMissingStrategy.contextMissing.name, RUNTIME_ERROR_FCN_NAME); + }); + + it('should set the contextMissingStrategy to the process.env.AWS_XRAY_CONTEXT_MISSING strategy if present', function() { + process.env.AWS_XRAY_CONTEXT_MISSING = LOG_ERROR; + reloadContextUtils(); + + assert.equal(ContextUtils.contextMissingStrategy.contextMissing.name, LOG_ERROR_FCN_NAME); + }); + }); + + describe('#resolveManualSegmentParams', function() { + var autoModeStub, params, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + autoModeStub = sandbox.stub(ContextUtils, 'isAutomaticMode').returns(false); + params = { + Bucket: 'moop', + Key: 'boop' + }; + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should return null if in automatic mode', function() { + autoModeStub.returns(true); + params.XRaySegment = new Segment('moop'); + + assert.isUndefined(ContextUtils.resolveManualSegmentParams(params)); + }); + + it('should return XRaySegment if of type Segment', function() { + var segment = params.XRaySegment = new Segment('moop'); + + assert.equal(ContextUtils.resolveManualSegmentParams(params), segment); + }); + + it('should return XRaySegment if of type Subsegment', function() { + var segment = params.XRaySegment = new Subsegment('moop'); + + assert.equal(ContextUtils.resolveManualSegmentParams(params), segment); + }); + + it('should return null if XRaySegment is not of type Segment or Subsegment', function() { + params.XRaySegment = 'moop'; + + assert.isNull(ContextUtils.resolveManualSegmentParams(params)); + }); + + it('should delete XRaySegment from the params passed', function() { + params.XRaySegment = new Segment('moop'); + ContextUtils.resolveManualSegmentParams(params); + + assert.isUndefined(params.XRaySegment); + }); + + it('should return Segment if of type Segment', function() { + var segment = params.Segment = new Segment('moop'); + + assert.equal(ContextUtils.resolveManualSegmentParams(params), segment); + }); + + it('should return Segment if of type Subsegment', function() { + var segment = params.Segment = new Subsegment('moop'); + + assert.equal(ContextUtils.resolveManualSegmentParams(params), segment); + }); + + it('should return null if Segment is not of type Segment or Subsegment', function() { + params.Segment = 'moop'; + + assert.isNull(ContextUtils.resolveManualSegmentParams(params)); + }); + + it('should delete Segment from the params passed', function() { + params.Segment = new Segment('moop'); + ContextUtils.resolveManualSegmentParams(params); + + assert.isUndefined(params.Segment); + }); + + it('should take XRaySegment as a priority', function() { + params.XRaySegment = 'moop'; + + assert.isNull(ContextUtils.resolveManualSegmentParams(params)); + }); + }); + + describe('#setContextMissingStrategy', function() { + var sandbox; + + beforeEach(function() { + reloadContextUtils(); + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + delete process.env.AWS_XRAY_CONTEXT_MISSING; + }); + + it('should accept and set the LOG_ERROR strategy', function() { + ContextUtils.setContextMissingStrategy(LOG_ERROR); + assert.equal(ContextUtils.contextMissingStrategy.contextMissing.name, LOG_ERROR_FCN_NAME); + }); + + it('should accept and set the RUNTIME_ERROR strategy', function() { + ContextUtils.setContextMissingStrategy(RUNTIME_ERROR); + assert.notEqual(ContextUtils.contextMissingStrategy.contextMissing, RUNTIME_ERROR_FCN_NAME); + }); + + it('should accept and set a custom strategy', function() { + var custom = function() {}; + ContextUtils.setContextMissingStrategy(custom); + assert.equal(ContextUtils.contextMissingStrategy.contextMissing, custom); + }); + + it('should ignore the configuration change if process.env.AWS_XRAY_CONTEXT_MISSING is set', function() { + var custom = function() {}; + process.env.AWS_XRAY_CONTEXT_MISSING = LOG_ERROR; + reloadContextUtils(); + + ContextUtils.setContextMissingStrategy(custom); + assert.notEqual(ContextUtils.contextMissingStrategy.contextMissing, custom); + }); + + it('should throw an error if given an invalid string', function() { + assert.throws(function() { ContextUtils.setContextMissingStrategy('moop'); } ); + }); + + it('should throw an error if given an invalid parameter type', function() { + assert.throws(function() { ContextUtils.setContextMissingStrategy({}); } ); + }); + }); +}); diff --git a/packages/core/test/unit/env/aws_lambda.test.js b/packages/core/test/unit/env/aws_lambda.test.js new file mode 100644 index 00000000..fbe9504b --- /dev/null +++ b/packages/core/test/unit/env/aws_lambda.test.js @@ -0,0 +1,209 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var fs = require('fs'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +chai.use(sinonChai); + +var contextUtils = require('../../../lib/context_utils'); +var Lambda = require('../../../lib/env/aws_lambda'); +var LambdaUtils = require('../../../lib/utils').LambdaUtils; +var Segment = require('../../../lib/segments/segment'); +var SegmentUtils = require('../../../lib/segments/segment_utils'); +var SegmentEmitter = require('../../../lib/segment_emitter'); + +describe('AWSLambda', function() { + var sandbox; + + function resetState() { + delete require.cache[require.resolve('../../../lib/context_utils')]; + contextUtils = require('../../../lib/context_utils'); + + var path = '../../../lib/env/aws_lambda'; + delete require.cache[require.resolve(path)]; + Lambda = require(path); + } + + beforeEach(function() { + resetState(); + + sandbox = sinon.sandbox.create(); + sandbox.stub(contextUtils, 'getNamespace').returns({ + enter: function() {}, + createContext: function() {} + }); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('#init', function() { + var disableReusableSocketStub, openSyncStub, populateStub, sandbox, setSegmentStub, validateStub; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + disableReusableSocketStub = sandbox.stub(SegmentEmitter, 'disableReusableSocket'); + openSyncStub = sandbox.stub(fs, 'openSync'); + sandbox.stub(fs, 'mkdir').yields(); + sandbox.stub(fs, 'closeSync'); + sandbox.stub(fs, 'utimesSync'); + + validateStub = sandbox.stub(LambdaUtils, 'validTraceData').returns(true); + populateStub = sandbox.stub(LambdaUtils, 'populateTraceData').returns(true); + setSegmentStub = sandbox.stub(contextUtils, 'setSegment'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create the AWS XRay init file', function() { + Lambda.init(); + openSyncStub.should.have.been.calledOnce; + }); + + it('should disable reusable socket', function() { + Lambda.init(); + disableReusableSocketStub.should.have.been.calledOnce; + }); + + it('should override the default streaming threshold', function() { + Lambda.init(); + assert.equal(SegmentUtils.streamingThreshold, 0); + }); + + it('should set a facade segment on the context', function() { + Lambda.init(); + setSegmentStub.should.have.been.calledOnce; + setSegmentStub.should.have.been.calledWith(sinon.match.instanceOf(Segment)); + + var facade = setSegmentStub.args[0][0]; + assert.equal(facade.name, 'facade'); + }); + + describe('the facade segment', function() { + afterEach(function() { + populateStub.returns(true); + delete process.env._X_AMZN_TRACE_ID; + validateStub.reset(); + }); + + it('should call validTraceData with process.env._X_AMZN_TRACE_ID', function() { + process.env._X_AMZN_TRACE_ID; + Lambda.init(); + + validateStub.should.have.been.calledWith(process.env._X_AMZN_TRACE_ID); + }); + + it('should call populateTraceData if validTraceData returns true', function() { + Lambda.init(); + + populateStub.should.have.been.calledOnce; + }); + + it('should not call populateTraceData if validTraceData returns false', function() { + validateStub.returns(false); + Lambda.init(); + + populateStub.should.have.not.been.called; + }); + }); + }); + + describe('FacadeSegment', function() { + var populateStub, sandbox, setSegmentStub; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(SegmentEmitter, 'disableReusableSocket'); + sandbox.stub(fs, 'openSync'); + sandbox.stub(fs, 'mkdir').yields(); + sandbox.stub(fs, 'closeSync'); + sandbox.stub(fs, 'utimesSync'); + + sandbox.stub(LambdaUtils, 'validTraceData').returns(true); + populateStub = sandbox.stub(LambdaUtils, 'populateTraceData').returns(true); + setSegmentStub = sandbox.stub(contextUtils, 'setSegment'); + }); + + afterEach(function() { + delete process.env._X_AMZN_TRACE_ID; + sandbox.restore(); + }); + + describe('#reset', function() { + it('should check reset the facade', function() { + Lambda.init(); + + var facade = setSegmentStub.args[0][0]; + facade.trace_id = 'traceIdHere'; + facade.id = 'parentIdHere'; + facade.subsegments = [ { subsegment: 'here' } ]; + facade.trace_id = 'traceIdHere'; + + facade.reset(); + + assert.isNull(facade.trace_id); + assert.isNull(facade.id); + assert.isUndefined(facade.subsegments); + assert.isTrue(facade.notTraced); + }); + }); + + describe('#resolveLambdaTraceData', function() { + var sandbox, traceId; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + traceId = 'xAmznTraceId;xAmznTraceId;xAmznTraceId'; + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should throw an error if _X_AMZN_TRACE_ID is not set', function() { + Lambda.init(); + populateStub.reset(); + + var facade = setSegmentStub.args[0][0]; + assert.throws(facade.resolveLambdaTraceData); + populateStub.should.have.not.been.called; + }); + + it('should call populate if _X_AMZN_TRACE_ID has changed post init', function() { + process.env._X_AMZN_TRACE_ID = traceId; + Lambda.init(); + process.env._X_AMZN_TRACE_ID = 'xAmznTraceId2'; + populateStub.reset(); + var facade = setSegmentStub.args[0][0]; + facade.resolveLambdaTraceData(); + + populateStub.should.have.been.calledOnce; + }); + + it('should call reset if _X_AMZN_TRACE_ID has changed post init', function() { + process.env._X_AMZN_TRACE_ID = traceId; + Lambda.init(); + process.env._X_AMZN_TRACE_ID = 'xAmznTraceId2'; + var facade = setSegmentStub.args[0][0]; + var resetStub = sandbox.stub(facade, 'reset'); + + facade.resolveLambdaTraceData(); + resetStub.should.have.been.calledOnce; + }); + + it('should not call populate if _X_AMZN_TRACE_ID is the same post init', function() { + process.env._X_AMZN_TRACE_ID = traceId; + Lambda.init(); + populateStub.reset(); + + var facade = setSegmentStub.args[0][0]; + facade.resolveLambdaTraceData(); + populateStub.should.have.not.been.called; + }); + }); + }); +}); diff --git a/packages/core/test/unit/middleware/incoming_request_data.test.js b/packages/core/test/unit/middleware/incoming_request_data.test.js new file mode 100644 index 00000000..84fe8319 --- /dev/null +++ b/packages/core/test/unit/middleware/incoming_request_data.test.js @@ -0,0 +1,114 @@ +var assert = require('chai').assert; +var IncomingRequestData = require('../../../lib/middleware/incoming_request_data'); + +describe('IncomingRequestData', function() { + var req; + + beforeEach(function() { + req = { headers: {} }; + req.connection = {}; + req.headers['user-agent'] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)'; + req.headers['host'] = 'reqDatahost:8081'; + req.url = '/'; + }); + + describe('#init', function() { + it('should create a new reqData object', function() { + var reqData = new IncomingRequestData(req); + + assert.isObject(reqData); + }); + + it('should set the user agent', function() { + var reqData = new IncomingRequestData(req); + + assert.equal(reqData.request.user_agent, req.headers['user-agent']); + }); + + it('should set the construct the URL to HTTP by default', function() { + var reqData = new IncomingRequestData(req); + + assert.equal(reqData.request.url, 'http://' + req.headers['host'] + req.url); + }); + + it('should set the construct the URL to HTTPS when req.connection.encrypted is set', function() { + req.connection.encrypted = true; + var reqData = new IncomingRequestData(req); + + assert.equal(reqData.request.url, 'https://' + req.headers['host'] + req.url); + }); + + it('should set the construct the URL to HTTPS when req.connection.secure is set', function() { + req.connection.secure = true; + var reqData = new IncomingRequestData(req); + + assert.equal(reqData.request.url, 'https://' + req.headers['host'] + req.url); + }); + + it('should use the connection.remoteAddress property by for the client IP', function() { + var address = '::1'; + req.connection.remoteAddress = address; + req.connection.socket = { remoteAddress: 'donotuse' }; + + req.socket = { remoteAddress: 'donotuse' }; + + var reqData = new IncomingRequestData(req); + + assert.equal(reqData.request.client_ip, address); + assert.notProperty(reqData.request, 'x_forwarded_for'); + }); + + it('should use the socket.remoteAddress property by for the client IP', function() { + var address = '192.168.1.150:1337'; + req.connection.socket = { remoteAddress: 'donotuse' }; + req.socket = { remoteAddress: address }; + var reqData = new IncomingRequestData(req); + + assert.equal(reqData.request.client_ip, address); + assert.notProperty(reqData.request, 'x_forwarded_for'); + }); + + it('should use the connection.socket.remoteAddress property by for the client IP', function() { + var address = '192.168.1.49:8080'; + req.connection.socket = { remoteAddress: address }; + var reqData = new IncomingRequestData(req); + + assert.equal(reqData.request.client_ip, address); + assert.notProperty(reqData.request, 'x_forwarded_for'); + }); + + it('should use the x-forwarded-for header if used and flag it', function() { + req.headers['x-forwarded-for'] = 'client2, proxy2'; + req.connection.socket = { remoteAddress: 'donotuse' }; + var reqData = new IncomingRequestData(req); + + assert.equal(reqData.request.client_ip, 'client2'); + assert.propertyVal(reqData.request, 'x_forwarded_for', true); + }); + }); + + describe('#close', function() { + var res; + + beforeEach(function() { + res = { headers: {} }; + res.statusCode = 403; + }); + + it('should capture the response status code', function() { + var reqData = new IncomingRequestData(req); + reqData.close(res); + + assert.equal(reqData.response.status, res.statusCode); + assert.notProperty(reqData.response, 'content_length'); + }); + + it('should capture the content-length if set', function() { + res.headers['content-length'] = 5637; + var reqData = new IncomingRequestData(req); + reqData.close(res); + + assert.equal(reqData.response.content_length, res.headers['content-length']); + }); + }); +}); diff --git a/packages/core/test/unit/middleware/mw_utils.test.js b/packages/core/test/unit/middleware/mw_utils.test.js new file mode 100644 index 00000000..c05c65a6 --- /dev/null +++ b/packages/core/test/unit/middleware/mw_utils.test.js @@ -0,0 +1,259 @@ +var assert = require('chai').assert; +var sinon = require('sinon'); + +var MWUtils = require('../../../lib/middleware/mw_utils'); +var SamplingRules = require('../../../lib/middleware/sampling/sampling_rules'); + +//headers are case-insensitive +var XRAY_HEADER = 'x-amzn-trace-id'; + +describe('Middleware utils', function() { + var defaultName = 'defaultName'; + var envVarName = 'envDefaultName'; + var hostName = 'www.myhost.com'; + var traceId = '1-f9194208-2c7ad569f5d6ff149137be86'; + + function reloadMWUtils() { + var path = '../../../lib/middleware/mw_utils'; + delete require.cache[require.resolve(path)]; + MWUtils = require(path); + } + + describe('#enableDynamicNaming', function() { + afterEach(function() { + reloadMWUtils(); + }); + + it('should flag dynamic mode if no pattern is given', function() { + MWUtils.enableDynamicNaming(); + + assert.isTrue(MWUtils.dynamicNaming); + }); + + it('should flag dynamic mode and set the host regex if a pattern is given', function() { + MWUtils.enableDynamicNaming('ww.*-moop.com'); + + assert.isTrue(MWUtils.dynamicNaming); + assert.equal(MWUtils.hostPattern, 'ww.*-moop.com'); + }); + }); + + describe('#processHeaders', function() { + var parentId = '74051af127d2bcba'; + + it('should return an empty array on an undefined request', function() { + var headers = MWUtils.processHeaders(); + + assert.deepEqual(headers, {}); + }); + + it('should return an empty array on an request with an undefined header', function() { + var req = {}; + var headers = MWUtils.processHeaders(req); + + assert.deepEqual(headers, {}); + }); + + it('should return an empty array on an request with an empty header', function() { + var req = { headers: {}}; + var headers = MWUtils.processHeaders(req); + + assert.deepEqual(headers, {}); + }); + + it('should return a split array on an request with an "x-amzn-trace-id" header with a root ID', function() { + var req = { headers: {}}; + req.headers[XRAY_HEADER] = 'Root=' + traceId; + var headers = MWUtils.processHeaders(req); + + assert.deepEqual(headers, {Root: traceId}); + }); + + it('should return a split array on an request with an "x-amzn-trace-id" header with a root ID and parent ID', function() { + var req = { headers: {}}; + req.headers[XRAY_HEADER] = 'Root=' + traceId + '; Parent=' + parentId; + var headers = MWUtils.processHeaders(req); + + assert.deepEqual(headers, {Root: traceId, Parent: parentId}); + }); + + it('should return a split array on an request with an "x-amzn-trace-id" header with a root ID, parent ID and sampling', function() { + var req = { headers: {}}; + req.headers[XRAY_HEADER] = 'Root=' + traceId + '; Parent=' + parentId + '; Sampled=0'; + var headers = MWUtils.processHeaders(req); + + assert.deepEqual(headers, {Root: traceId, Parent: parentId, Sampled: '0'}); + }); + }); + + describe('#resolveName', function() { + beforeEach(function() { + MWUtils.setDefaultName(defaultName); + }); + + afterEach(function() { + reloadMWUtils(); + }); + + describe('when in fixed mode', function() { + it('it should use the default name given when there is a host name', function() { + var name = MWUtils.resolveName(hostName); + + assert.equal(name, defaultName); + }); + + it('it should use the default name given when there is no host name', function() { + var name = MWUtils.resolveName(); + + assert.equal(name, defaultName); + }); + }); + + describe('when in dynamic mode', function() { + it('it should use the default name when no host is provided', function() { + MWUtils.enableDynamicNaming(); + var name = MWUtils.resolveName(); + + assert.equal(name, defaultName); + }); + + describe('and a host name is provided', function() { + it('it should use the host name when no pattern is provided', function() { + MWUtils.enableDynamicNaming(); + var name = MWUtils.resolveName(hostName); + + assert.equal(name, hostName); + }); + + it('it should use the default name when a pattern is provided but not matched', function() { + MWUtils.enableDynamicNaming('ww.*-moop.com'); + var name = MWUtils.resolveName(hostName); + + assert.equal(name, defaultName); + }); + + it('it should use the host name when a pattern is provided and matched', function() { + MWUtils.enableDynamicNaming('www.*.com'); + var name = MWUtils.resolveName(hostName); + + assert.equal(name, hostName); + }); + }); + }); + }); + + describe('#resolveSampling', function() { + var res, sandbox, segment, shouldSampleStub; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + MWUtils.sampler = { shouldSample: function() {}}; + + shouldSampleStub = sandbox.stub(MWUtils.sampler, 'shouldSample').returns(true); + + segment = {}; + res = { + req: { + headers: { host: 'moop.hello.com' }, + url: '/evergreen', + method: 'GET', + }, + header: {} + }; + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should not mark segment as not traced if the sampled header is set to "1"', function() { + var headers = { Root: traceId, Sampled: '1' }; + MWUtils.resolveSampling(headers, segment, res); + + shouldSampleStub.should.have.not.been.called; + + assert.notProperty(segment, 'notTraced'); + }); + + it('should mark segment as not traced if the sampled header is set to "0"', function() { + var headers = { Root: traceId, Sampled: '0' }; + MWUtils.resolveSampling(headers, segment, res); + + shouldSampleStub.should.have.not.been.called; + + assert.equal(segment.notTraced, true); + }); + + it('should do a sampling rules check if no "Sampled" header is set', function() { + var headers = { Root: traceId }; + MWUtils.resolveSampling(headers, segment, res); + + shouldSampleStub.should.have.been.calledWithExactly(res.req.headers.host, res.req.method, res.req.url); + }); + + it('should set the response header with sampling result if header is "?"', function() { + var headers = { Root: traceId, Sampled: '?' }; + MWUtils.resolveSampling(headers, segment, res); + + var expected = new RegExp('^Root=' + traceId + ';Sampled=1$'); + assert.match(res.header[XRAY_HEADER], expected); + }); + + it('should mark segment as not traced if the sampling rules check returns false', function() { + shouldSampleStub.returns(false); + var headers = { Root: traceId }; + + MWUtils.resolveSampling(headers, segment, res); + + assert.equal(segment.notTraced, true); + }); + }); + + describe('#setDefaultName', function() { + it('it should set the default name', function() { + MWUtils.setDefaultName(defaultName); + + assert.equal(MWUtils.defaultName, defaultName); + }); + + it('it should be overidden by the default name set as an environment variable', function() { + process.env.AWS_XRAY_TRACING_NAME = envVarName; + reloadMWUtils(); + MWUtils.setDefaultName(defaultName); + + assert.equal(MWUtils.defaultName, envVarName); + }); + }); + + describe('#setSamplingRules', function() { + var samplingRulesStub, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + samplingRulesStub = sandbox.stub(SamplingRules.prototype, 'init'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should accept a string for location', function() { + var location = '/path/here'; + MWUtils.setSamplingRules(location); + samplingRulesStub.should.have.been.calledWith(location); + }); + + it('should accept a source object', function() { + var source = {}; + MWUtils.setSamplingRules(source); + samplingRulesStub.should.have.been.calledWith(source); + }); + + it('should throw an error on bad values', function() { + assert.throws(function() { MWUtils.setSamplingRules(); }); + assert.throws(function() { MWUtils.setSamplingRules(null); }); + assert.throws(function() { MWUtils.setSamplingRules(0); }); + assert.throws(function() { MWUtils.setSamplingRules(new String('')); }); + }); + }); +}); diff --git a/packages/core/test/unit/patchers/aws_p.test.js b/packages/core/test/unit/patchers/aws_p.test.js new file mode 100644 index 00000000..77aa9ca3 --- /dev/null +++ b/packages/core/test/unit/patchers/aws_p.test.js @@ -0,0 +1,257 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var EventEmitter = require('events'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); +var util = require('util'); + +var Aws = require('../../../lib/segments/attributes/aws'); +var awsPatcher = require('../../../lib/patchers/aws_p'); +var contextUtils = require('../../../lib/context_utils'); +var Segment = require('../../../lib/segments/segment'); +var Utils = require('../../../lib/utils'); + +var logger = require('../../../lib/logger').getLogger(); + +chai.should(); +chai.use(sinonChai); + +var traceId = '1-57fbe041-2c7ad569f5d6ff149137be86'; + +describe('AWS patcher', function() { + describe('#captureAWS', function() { + var customStub, sandbox; + + var awssdk = { + VERSION: '2.7.15', + s3: { + prototype: { + customizeRequests: function() {} + }, + serviceIdentifier: 's3' + } + }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + customStub = sandbox.stub(awssdk.s3.prototype, 'customizeRequests'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call customizeRequests and return the sdk', function() { + var patched = awsPatcher.captureAWS(awssdk); + customStub.should.have.been.calledOnce; + assert.equal(patched, awssdk); + }); + + it('should throw an error if the AWSSDK is below the minimum required version', function() { + awssdk.VERSION = '1.2.5'; + assert.throws(function() { awsPatcher.captureAWS(awssdk); }, Error); + }); + }); + + describe('#captureAWSClient', function() { + var customStub, sandbox; + + var awsClient = { + customizeRequests: function() {} + }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + customStub = sandbox.stub(awsClient, 'customizeRequests'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call customizeRequests and return the service', function() { + var patched = awsPatcher.captureAWSClient(awsClient); + customStub.should.have.been.calledOnce; + assert.equal(patched, awsClient); + }); + }); + + describe('#captureAWSRequest', function() { + var awsClient, awsRequest, MyEmitter, sandbox, segment, stubResolve, stubResolveManual, sub; + + before(function() { + MyEmitter = function() { + EventEmitter.call(this); + }; + + awsClient = { + customizeRequests: function customizeRequests(captureAWSRequest) { this.call = captureAWSRequest; }, + throttledError: function throttledError() {} + }; + awsClient = awsPatcher.captureAWSClient(awsClient); + + util.inherits(MyEmitter, EventEmitter); + }); + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + awsRequest = { + httpRequest: { + method: 'GET', + url: '/', + connection: { + remoteAddress: 'localhost' + }, + headers: {} + }, + response: {} + }; + + awsRequest.on = function(event, fcn) { + if (event === 'complete') + this.emitter.on(event, fcn.bind(this, this.response)); + else + this.emitter.on(event, fcn.bind(this, this)); + return this; + }; + + awsRequest.emitter = new MyEmitter(); + + segment = new Segment('testSegment', traceId); + sub = segment.addNewSubsegment('subseg'); + + stubResolveManual = sandbox.stub(contextUtils, 'resolveManualSegmentParams'); + stubResolve = sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + sandbox.stub(segment, 'addNewSubsegment').returns(sub); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call to resolve any manual params', function() { + awsClient.call(awsRequest); + + stubResolveManual.should.have.been.calledWith(awsRequest.params); + }); + + it('should log an info statement and exit if parent is not found on the context or on the call params', function(done) { + stubResolve.returns(); + var logStub = sandbox.stub(logger, 'info'); + + awsClient.call(awsRequest); + + setTimeout(function() { + logStub.should.have.been.calledOnce; + done(); + }, 50); + }); + + it('should inject the tracing headers', function(done) { + sandbox.stub(contextUtils, 'isAutomaticMode').returns(true); + + awsClient.call(awsRequest); + + awsRequest.emitter.emit('build'); + + setTimeout(function() { + var expected = new RegExp('^Root=' + traceId + ';Parent=' + sub.id + ';Sampled=1$'); + assert.match(awsRequest.httpRequest.headers['X-Amzn-Trace-Id'], expected); + done(); + }, 50); + }); + + it('should close on complete with no errors when code 200 is seen', function(done) { + var closeStub = sandbox.stub(sub, 'close').returns(); + sandbox.stub(contextUtils, 'isAutomaticMode').returns(true); + sandbox.stub(sub, 'addAttribute').returns(); + sandbox.stub(Aws.prototype, 'init').returns(); + + awsRequest.response = { + httpResponse: { statusCode: 200 }, + }; + + awsClient.call(awsRequest); + + awsRequest.emitter.emit('complete'); + + setTimeout(function() { + closeStub.should.have.been.calledWithExactly(); + done(); + }, 50); + }); + + it('should mark the subsegment as throttled and error if code 429 is seen', function(done) { + var throttleStub = sandbox.stub(sub, 'addThrottleFlag').returns(); + + sandbox.stub(contextUtils, 'isAutomaticMode').returns(true); + sandbox.stub(sub, 'addAttribute').returns(); + sandbox.stub(Aws.prototype, 'init').returns(); + + awsRequest.response = { + error: { message: 'throttling', code: 'ThrottlingError' }, + httpResponse: { statusCode: 429 }, + }; + + awsClient.call(awsRequest); + + awsRequest.emitter.emit('complete'); + + setTimeout(function() { + throttleStub.should.have.been.calledOnce; + assert.isTrue(sub.error); + done(); + }, 50); + }); + + it('should mark the subsegment as throttled and error if code service.throttledError returns true, regardless of status code', function(done) { + var throttledCheckStub = sandbox.stub(awsClient, 'throttledError').returns(true); + var throttleStub = sandbox.stub(sub, 'addThrottleFlag').returns(); + + sandbox.stub(contextUtils, 'isAutomaticMode').returns(true); + sandbox.stub(sub, 'addAttribute').returns(); + sandbox.stub(Aws.prototype, 'init').returns(); + + awsRequest.response = { + error: { message: 'throttling', code: 'ProvisionedThroughputException' }, + httpResponse: { statusCode: 400 }, + }; + + awsClient.call(awsRequest); + + awsRequest.emitter.emit('complete'); + + setTimeout(function() { + throttledCheckStub.should.have.been.calledOnce; + throttleStub.should.have.been.calledOnce; + assert.isTrue(sub.error); + done(); + }, 50); + }); + + it('should capture an error on the response and mark exception as remote', function(done) { + var closeStub = sandbox.stub(sub, 'close').returns(); + var getCauseStub = sandbox.stub(Utils, 'getCauseTypeFromHttpStatus').returns(); + + sandbox.stub(contextUtils, 'isAutomaticMode').returns(true); + sandbox.stub(sub, 'addAttribute').returns(); + sandbox.stub(Aws.prototype, 'init').returns(); + + var error = { message: 'big error', code: 'Error' }; + + awsRequest.response.error = error; + awsRequest.response.httpResponse = { statusCode: 500 }; + + awsClient.call(awsRequest); + + awsRequest.emitter.emit('complete'); + + setTimeout(function() { + getCauseStub.should.have.been.calledWithExactly(awsRequest.response.httpResponse.statusCode); + closeStub.should.have.been.calledWithExactly(sinon.match({ message: error.message, name: error.code}), true); + done(); + }, 50); + }); + }); +}); diff --git a/packages/core/test/unit/patchers/call_capturer.test.js b/packages/core/test/unit/patchers/call_capturer.test.js new file mode 100644 index 00000000..493aa0ae --- /dev/null +++ b/packages/core/test/unit/patchers/call_capturer.test.js @@ -0,0 +1,256 @@ +var assert = require('chai').assert; +var sinon = require('sinon'); + +var CallCapturer = require('../../../lib/patchers/call_capturer'); + +describe('CallCapturer', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('#constructor', function() { + var jsonDoc = { + services: { + s3: {} + } + }; + + it('should return a call capturer object loaded with the default JSON document', function() { + var capturer = new CallCapturer(); + + assert.instanceOf(capturer, CallCapturer); + assert.property(capturer.services, 'dynamodb'); + }); + + it('should return a call capturer object loaded with a custom JSON document given a file location', function() { + var capturer = new CallCapturer('./test/resources/custom_whitelist.json'); + + assert.instanceOf(capturer, CallCapturer); + assert.property(capturer.services, 's3'); + }); + + it('should return a call capturer object loaded with a custom source object', function() { + var capturer = new CallCapturer(jsonDoc); + + assert.instanceOf(capturer, CallCapturer); + assert.property(capturer.services, 's3'); + }); + }); + + describe('#append', function() { + var capturer; + + beforeEach(function() { + capturer = new CallCapturer({ services: { s3: {} }}); + }); + + it('should extend the current service list', function() { + capturer.append({ services: { dynamodb: {} }}); + + assert.property(capturer.services, 's3'); + assert.property(capturer.services, 'dynamodb'); + }); + }); + + describe('#capture', function() { + var jsonDocDynamoParams, jsonDocDynamoDesc, jsonDocSQS, responseDynamo, responseSQS; + + beforeEach(function() { + responseDynamo = { + request: { + operation: 'getItem', + params: { + TableName: 'myTable', + ProjectionExpression: 'Table', + ConsistentRead: true, + ExpressionAttributeNames: { + '#attrName': 'SessionID' + } + } + }, + data: { + TableNames: ['hello'], + ConsumedCapacity: '10' + } + }; + + responseSQS = { + request: { + operation: 'sendMessageBatch', + params: {} + }, + data: { + Failed: [1,2,3], + Successful: [1,2,3,4,5,6,7] + } + }; + + jsonDocDynamoParams = { + services: { + dynamodb: { + operations: { + getItem: { + request_parameters: [ 'TableName' ], + response_parameters: [ 'ConsumedCapacity' ] + } + } + } + } + }; + + jsonDocDynamoDesc = { + services: { + dynamodb: { + operations: { + getItem: { + request_descriptors: { + ExpressionAttributeNames: { + get_keys: true, + rename_to: 'attribute_names_substituted' + } + }, + response_descriptors: { + TableNames: { + list: true, + get_count: true + } + } + } + } + } + } + }; + + jsonDocSQS = { + services: { + sqs: { + operations: { + sendMessageBatch: { + response_descriptors: { + Failed: { + list: true, + get_count: true, + }, + Successful: { + list: true, + get_count: true, + }, + } + } + } + } + } + }; + }); + + it('should capture the request and response params noted', function() { + var capturer = new CallCapturer(jsonDocDynamoParams); + var data = capturer.capture('dynamodb', responseDynamo); + + assert.deepEqual(data, { table_name: 'myTable', consumed_capacity: '10' }); + }); + + it('should capture falsey request and response params noted', function() { + responseDynamo.request.params.TableName = false; + + var capturer = new CallCapturer(jsonDocDynamoParams); + var data = capturer.capture('dynamodb', responseDynamo); + + assert.deepEqual(data, { table_name: false, consumed_capacity: '10' }); + }); + + it('should not capture the request param if missing', function() { + delete responseDynamo.request.params.TableName; + + var capturer = new CallCapturer(jsonDocDynamoParams); + var data = capturer.capture('dynamodb', responseDynamo); + + assert.notProperty(data, 'table_name'); + assert.propertyVal(data, 'consumed_capacity', '10'); + }); + + it('should not capture the response param if missing', function() { + delete responseDynamo.data.ConsumedCapacity; + + var capturer = new CallCapturer(jsonDocDynamoParams); + var data = capturer.capture('dynamodb', responseDynamo); + + assert.notProperty(data, 'consumed_capacity'); + }); + + it('should capture the request descriptors as noted', function() { + var capturer = new CallCapturer(jsonDocDynamoDesc); + var data = capturer.capture('dynamodb', responseDynamo); + + assert.deepEqual(data, { attribute_names_substituted: [ '#attrName' ], table_names: 1 }); + }); + + it('should capture falsey request descriptors noted', function() { + delete jsonDocDynamoDesc.services.dynamodb.operations.getItem.request_descriptors.ExpressionAttributeNames.get_keys; + delete jsonDocDynamoDesc.services.dynamodb.operations.getItem.request_descriptors.ExpressionAttributeNames.rename_to; + responseDynamo.request.params.ExpressionAttributeNames = false; + + var capturer = new CallCapturer(jsonDocDynamoDesc); + var data = capturer.capture('dynamodb', responseDynamo); + + assert.propertyVal(data, 'expression_attribute_names', false); + }); + + it('should rename the request descriptor if noted', function() { + var capturer = new CallCapturer(jsonDocDynamoDesc); + var data = capturer.capture('dynamodb', responseDynamo); + + assert.property(data, 'attribute_names_substituted'); + assert.deepEqual(data.attribute_names_substituted, [ '#attrName' ]); + }); + + it('should not capture the request descriptor if missing', function() { + delete responseDynamo.request.params.ExpressionAttributeNames; + + var capturer = new CallCapturer(jsonDocDynamoDesc); + var data = capturer.capture('dynamodb', responseDynamo); + + assert.notProperty(data, 'attribute_names_substituted'); + }); + + it('should capture the response descriptors as noted', function() { + var capturer = new CallCapturer(jsonDocSQS); + var data = capturer.capture('sqs', responseSQS); + + assert.deepEqual(data, { failed: 3, successful: 7 }); + }); + + it('should not capture the response descriptor if missing', function() { + delete responseSQS.data.Failed; + + var capturer = new CallCapturer(jsonDocSQS); + var data = capturer.capture('sqs', responseSQS); + + assert.notProperty(data, 'failed'); + assert.propertyVal(data, 'successful', 7); + }); + + it('should rename the response descriptor if noted', function() { + jsonDocSQS.services.sqs.operations.sendMessageBatch.response_descriptors.Failed.rename_to = 'error'; + + var capturer = new CallCapturer(jsonDocSQS); + var data = capturer.capture('sqs', responseSQS); + + assert.propertyVal(data, 'error', 3); + }); + + it('should ignore response data if null, in the event of an error', function () { + var capturer = new CallCapturer(jsonDocSQS); + responseSQS.data = null; + var data = capturer.capture('sqs', responseSQS); + + assert.deepEqual(data, {}); + }); + }); +}); diff --git a/packages/core/test/unit/patchers/http_p.test.js b/packages/core/test/unit/patchers/http_p.test.js new file mode 100644 index 00000000..15d0b14c --- /dev/null +++ b/packages/core/test/unit/patchers/http_p.test.js @@ -0,0 +1,311 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var captureHTTPs = require('../../../lib/patchers/http_p').captureHTTPs; +var captureHTTPsGlobal = require('../../../lib/patchers/http_p').captureHTTPsGlobal; +var contextUtils = require('../../../lib/context_utils'); +var Utils = require('../../../lib/utils'); +var Segment = require('../../../lib/segments/segment'); +var TestEmitter = require('../test_utils').TestEmitter; + +chai.should(); +chai.use(sinonChai); + +var buildFakeRequest = function() { + var request = new TestEmitter(); + request.method = 'GET'; + request.url = '/'; + request.connection = { remoteAddress: 'myhost' }; + return request; +}; + +var buildFakeResponse = function() { + var response = new TestEmitter(); + return response; +}; + +describe('HTTP/S', function() { + describe('patchers', function () { + var httpClient; + + beforeEach(function() { + httpClient = { + request: function request() {}, + get: function get() {} + }; + }); + + describe('#captureHTTPs', function() { + it('should create a copy of the module', function() { + var capturedHttp = captureHTTPs(httpClient, true); + assert.notEqual(httpClient, capturedHttp); + }); + + it('should stub out the request method for the capture one', function() { + var capturedHttp = captureHTTPs(httpClient, true); + assert.equal(capturedHttp.request.name, 'captureHTTPsRequest'); + assert.equal(capturedHttp.__request.name, 'request'); + }); + + it('should stub out the get method for the capture one', function() { + var capturedHttp = captureHTTPs(httpClient, true); + assert.equal(capturedHttp.get.name, 'captureHTTPsGet'); + assert.equal(capturedHttp.__get.name, 'get'); + }); + }); + + describe('#captureHTTPsGlobal', function() { + it('should stub out the request method for the capture one', function() { + captureHTTPsGlobal(httpClient, true); + assert.equal(httpClient.request.name, 'captureHTTPsRequest'); + assert.equal(httpClient.__request.name, 'request'); + }); + + it('should stub out the get method for the capture one', function() { + captureHTTPsGlobal(httpClient, true); + assert.equal(httpClient.get.name, 'captureHTTPsGet'); + assert.equal(httpClient.__get.name, 'get'); + }); + }); + }); + + describe('#captureHTTPsRequest', function() { + var addRemoteDataStub, closeStub, httpOptions, newSubsegmentStub, resolveManualStub, sandbox, segment, subsegment; + var traceId = '1-57fbe041-2c7ad569f5d6ff149137be86'; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test', traceId); + subsegment = segment.addNewSubsegment('testSub'); + + newSubsegmentStub = sandbox.stub(segment, 'addNewSubsegment').returns(subsegment); + + resolveManualStub = sandbox.stub(contextUtils, 'resolveManualSegmentParams'); + sandbox.stub(contextUtils, 'isAutomaticMode').returns(true); + sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + addRemoteDataStub = sandbox.stub(subsegment, 'addRemoteRequestData').returns(); + closeStub = sandbox.stub(subsegment, 'close').returns(); + + httpOptions = { + host: 'myhost', + path: '/' + }; + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('on invocation', function() { + var capturedHttp, fakeRequest, fakeResponse, httpClient, requestSpy, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test', traceId); + + fakeRequest = buildFakeRequest(); + fakeResponse = buildFakeResponse(); + + httpClient = { request: function(options, callback) { + callback(fakeResponse); + return fakeRequest; + }}; + httpClient.get = httpClient.request; + + requestSpy = sandbox.spy(httpClient, 'request'); + capturedHttp = captureHTTPs(httpClient, true); }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call to resolve any manual params', function() { + var options = {hostname: 'hostname', path: '/'}; + capturedHttp.request(options); + + resolveManualStub.should.have.been.calledWith(options); + }); + + it('should create a new subsegment with name as hostname', function() { + var options = {hostname: 'hostname', path: '/'}; + capturedHttp.request(options); + newSubsegmentStub.should.have.been.calledWith(options.hostname); + }); + + it('should create a new subsegment with name as host when hostname is missing', function() { + capturedHttp.request(httpOptions); + newSubsegmentStub.should.have.been.calledWith(httpOptions.host); + }); + + it('should create a new subsegment with name as "Unknown host" when host and hostname is missing', function() { + capturedHttp.request({path: '/'}); + newSubsegmentStub.should.have.been.calledWith('Unknown host'); + }); + + it('should call the base method', function() { + capturedHttp.request({'Segment': segment}); + assert(requestSpy.called); + }); + + it('should attach an event handler to the "end" event', function() { + capturedHttp.request(httpOptions); + assert.isFunction(fakeResponse._events.end); + }); + + it('should inject the tracing headers', function() { + capturedHttp.request(httpOptions); + + // example: 'Root=1-59138384-82ff54d5ba9282f0c680adb3;Parent=53af362e4e4efeb8;Sampled=1' + var xAmznTraceId = new RegExp('^Root=' + traceId + ';Parent=([a-f0-9]{16});Sampled=1$'); + var options = requestSpy.firstCall.args[0]; + assert.match(options.headers['X-Amzn-Trace-Id'], xAmznTraceId); + }); + + it('should return the request object', function() { + var request = capturedHttp.request(httpOptions); + assert.equal(request, fakeRequest); + }); + }); + + describe('on the "end" event', function() { + var capturedHttp, fakeRequest, fakeResponse, httpClient, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + fakeRequest = buildFakeRequest(); + fakeResponse = buildFakeResponse(); + + httpClient = { request: function(options, callback) { + fakeResponse.req = fakeRequest; + callback(fakeResponse); + return fakeRequest; + }}; + + capturedHttp = captureHTTPs(httpClient); + }); + + afterEach(function() { + sandbox.restore(); + delete segment.notTraced; + }); + + it('should not set "http.traced" if the enableXRayDownstream flag is not set', function(done) { + fakeResponse.statusCode = 200; + capturedHttp.request(httpOptions); + fakeResponse.emit('end'); + + setTimeout(function() { + addRemoteDataStub.should.have.been.calledWithExactly(fakeRequest, fakeResponse, false); + done(); + }, 50); + }); + + it('should set "http.traced" on the subsegment if the root is sampled and enableXRayDownstream is set', function(done) { + capturedHttp = captureHTTPs(httpClient, true); + fakeResponse.statusCode = 200; + capturedHttp.request(httpOptions); + fakeResponse.emit('end'); + + setTimeout(function() { + addRemoteDataStub.should.have.been.calledWithExactly(fakeRequest, fakeResponse, true); + done(); + }, 50); + }); + + it('should close the subsegment', function(done) { + fakeResponse.statusCode = 200; + capturedHttp.request(httpOptions); + fakeResponse.emit('end'); + + setTimeout(function() { + closeStub.should.have.been.calledWithExactly(); + done(); + }, 50); + }); + + it('should flag the subsegment as throttled if status code 429 is seen', function(done) { + var addThrottleStub = sandbox.stub(subsegment, 'addThrottleFlag'); + + fakeResponse.statusCode = 429; + capturedHttp.request(httpOptions); + fakeResponse.emit('end'); + + setTimeout(function() { + addThrottleStub.should.have.been.calledOnce; + done(); + }, 50); + }); + + it('should check the cause of the http status code', function(done) { + var utilsCodeStub = sandbox.stub(Utils, 'getCauseTypeFromHttpStatus'); + + fakeResponse.statusCode = 500; + capturedHttp.request(httpOptions); + fakeResponse.emit('end'); + + setTimeout(function() { + utilsCodeStub.should.have.been.calledWith(fakeResponse.statusCode); + done(); + }, 50); + }); + }); + + describe('when the request "error" event fires', function() { + var capturedHttp, error, fakeRequest, httpClient, req, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + httpClient = { request: function() {} }; + capturedHttp = captureHTTPs(httpClient); + + fakeRequest = buildFakeRequest(); + + sandbox.stub(capturedHttp, '__request').returns(fakeRequest); + error = {}; + + req = capturedHttp.request(httpOptions); + }); + + afterEach(function() { + sandbox.restore(); + }); + + // (request -> ECONNREFUSED -> error event). + // The way I verify if 'end' fired is if the subsegment.http.response was captured on the alternate code path. + // The only way to trigger this is a ECONNREFUSED error, as it is the only event which fires and has no response object. + + it('should capture the request ECONNREFUSED error', function(done) { + fakeRequest.on('error', function() {}); + fakeRequest.emit('error', error); + + setTimeout(function() { + addRemoteDataStub.should.have.been.calledWith(req); + closeStub.should.have.been.calledWithExactly(error); + done(); + }, 50); + }); + + // (request -> end event, then if error -> error event) + // sets subsegment.http = { response: { status: 500 }} to set the state that the 'end' event fired. + + it('should capture the request code error', function(done) { + subsegment.http = { response: { status: 500 }}; + fakeRequest.on('error', function() {}); + fakeRequest.emit('error', error); + + setTimeout(function() { + closeStub.should.have.been.calledWithExactly(error, true); + done(); + }, 50); + }); + + it('should re-emit the error if unhandled', function() { + assert.throws(function() { fakeRequest.emitter.emit('error', error); }); + }); + }); + }); +}); diff --git a/packages/core/test/unit/sampling/sampler.test.js b/packages/core/test/unit/sampling/sampler.test.js new file mode 100644 index 00000000..dae4abcd --- /dev/null +++ b/packages/core/test/unit/sampling/sampler.test.js @@ -0,0 +1,62 @@ +var _ = require('underscore'); +var assert = require('chai').assert; +var expect = require('chai').expect; +var sinon = require('sinon'); + +var Sampler = require('../../../lib/middleware/sampling/sampler'); + +describe('Sampler', function() { + describe('#constructor', function() { + it('should return a new Sampler with fixed target and rate set', function() { + var sampler = new Sampler(5, 0.5); + + assert(!isNaN(sampler.fixedTarget), 'Expected fixed target to be a number.'); + assert(!isNaN(sampler.fallbackRate), 'Expected rate to be a number.'); + }); + + it('should throw an exception if fixed target is a float or a negative number', function() { + expect(function() { new Sampler(123.45, 0.5); }).to.throw(Error, '"fixed_target" must be a non-negative integer.'); + expect(function() { new Sampler(-123, 0.5); }).to.throw(Error, '"fixed_target" must be a non-negative integer.'); + }); + + it('should throw an exception if rate is not a number between 0 and 1', function() { + expect(function() { new Sampler(5, 123); }).to.throw(Error, '"rate" must be a number between 0 and 1 inclusive.'); + expect(function() { new Sampler(5, -0.5); }).to.throw(Error, '"rate" must be a number between 0 and 1 inclusive.'); + }); + }); + + describe('#isSampled', function() { + var sandbox, sampler; + var fixedTarget = 5; + + before(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(Math, 'round').returns(1); + }); + + beforeEach(function() { + sampler = new Sampler(fixedTarget, 0); + }); + + after(function() { + sandbox.restore(); + }); + + it('should return true up to the fixed target set.', function() { + _.times(fixedTarget, function() { + assert.isTrue(sampler.isSampled()); + }); + + assert.isFalse(sampler.isSampled()); + }); + + it('should call Math.random and use the rate set if the fixed target has already been reached.', function() { + sampler.thisSecond = 1; + sampler.usedThisSecond = 5; + var randomStub = sandbox.stub(Math, 'random').returns(1); + + sampler.isSampled(); + randomStub.should.have.been.calledOnce; + }); + }); +}); diff --git a/packages/core/test/unit/sampling/sampling_rules.test.js b/packages/core/test/unit/sampling/sampling_rules.test.js new file mode 100644 index 00000000..31447f7d --- /dev/null +++ b/packages/core/test/unit/sampling/sampling_rules.test.js @@ -0,0 +1,274 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var fs = require('fs'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +chai.should(); +chai.use(sinonChai); + +var SamplingRules = require('../../../lib/middleware/sampling/sampling_rules'); +var Sampler = require('../../../lib/middleware/sampling/sampler'); +var Utils = require('../../../lib/utils'); + +describe('SamplingRules', function() { + var sandbox, stubIsSampled; + + var jsonDoc = { + rules: [ + { + description: 'moop', + http_method: 'GET', + service_name: '*.foo.com', + url_path: '/signin/*', + fixed_target: 0, + rate: 0 + }, + { + description: '', + http_method: 'POST', + service_name: '*.moop.com', + url_path: '/login/*', + fixed_target: 10, + rate: 0.05 + } + ], + default: { + fixed_target: 10, + rate: 0.05 + }, + version: 1 + }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + stubIsSampled = sandbox.stub(Sampler.prototype, 'isSampled').returns(true); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('#constructor', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(Sampler.prototype, 'init').returns(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should return a SamplingRules object loaded with the default rules', function() { + var samplingRules = new SamplingRules(); + + assert(samplingRules); + assert.instanceOf(samplingRules, SamplingRules); + }); + + it('should return a custom SamplingRules object given a custom rules file location', function() { + var samplingRules = new SamplingRules('./test/resources/custom_sampling.json'); + + assert(samplingRules); + assert.instanceOf(samplingRules, SamplingRules); + }); + + it('should return a custom SamplingRules object given a custom rules source object', function() { + var samplingRules = new SamplingRules(jsonDoc); + + assert(samplingRules); + assert.instanceOf(samplingRules, SamplingRules); + }); + + describe('given a config file', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should parse the matchers rules', function() { + var samplingRules = new SamplingRules(jsonDoc); + var rule0 = samplingRules.rules[0]; + var rule1 = samplingRules.rules[1]; + var rule2 = samplingRules.rules[2]; + + assert.equal(rule0.service_name, jsonDoc.rules[0].service_name); + assert.equal(rule0.http_method, jsonDoc.rules[0].http_method); + assert.equal(rule0.url_path, jsonDoc.rules[0].url_path); + assert.instanceOf(rule0.sampler, Sampler); + + assert.equal(rule1.service_name, jsonDoc.rules[1].service_name); + assert.equal(rule1.http_method, jsonDoc.rules[1].http_method); + assert.equal(rule1.url_path, jsonDoc.rules[1].url_path); + assert.instanceOf(rule1.sampler, Sampler); + + assert.isTrue(rule2.default); + assert.instanceOf(rule2.sampler, Sampler); + }); + }); + + it('should accept a default fixed_target of 0 and a rate of 0', function() { + sandbox.stub(fs, 'readFileSync'); + sandbox.stub(JSON, 'parse').returns({ + default: { + fixed_target: 0, + rate: 0 + }, + version: 1 + }); + + var samplingRules = new SamplingRules('/path/here'); + assert.isTrue(samplingRules.rules[0].default); + }); + + it('should throw an error if the file is missing a "version" attribute', function() { + var source = { rules: [] }; + assert.throws(function() { new SamplingRules(source); }, 'Missing "version" attribute.'); + }); + + it('should throw an error if the file the version is not valid', function() { + var source = { rules: [], version: 'moop' }; + assert.throws(function() { new SamplingRules(source); }, 'Unknown version "moop".'); + }); + + it('should throw an error if the file is missing a "default" object', function() { + var source = { rules: [], version: 1 }; + assert.throws(function() { new SamplingRules(source); }, + 'Expecting "default" object to be defined with attributes "fixed_target" and "rate".'); + }); + + it('should throw an error if the "default" object contains an invalid attribute', function() { + var source = { default: { fixed_target: 10, rate: 0.05, url_path: '/signin/*' }, version: 1}; + + assert.throws(function() { new SamplingRules(source); }, + 'Invalid attribute for default: url_path. Valid attributes for default are "fixed_target" and "rate".'); + }); + + it('should throw an error if the "default" object is missing required attributes', function() { + var source = { default: { fixed_target: 10 }, version: 1}; + assert.throws(function() { new SamplingRules(source); }, 'Missing required attributes for default: rate.'); + }); + + it('should throw an error if any rule contains invalid attributes', function() { + var source = { + rules: [{ + service_name: 'www.worththewait.io', + http_method: 'PUT', + url_path: '/signin/*', + moop: 'moop', + fixed_target: 10, + rate: 0.05 + }], + default: { + fixed_target: 10, + rate: 0.05 + }, + version: 1 + }; + + assert.throws(function() { new SamplingRules(source); }, 'has invalid attribute: moop.'); + }); + + it('should throw an error if any rule is missing required attributes', function() { + var source = { + rules: [{ + url_path: '/signin/*', + fixed_target: 10, + rate: 0.05 + }], + default: { + fixed_target: 10, + rate: 0.05 + }, + version: 1 + }; + + assert.throws(function() { new SamplingRules(source); }, 'is missing required attributes: service_name,http_method.'); + }); + + it('should throw an error if any rule attributes have an invalid value', function() { + var source = { + rules: [{ + service_name: 'www.worththewait.io', + http_method: null, + url_path: '/signin/*', + fixed_target: 10, + rate: 0.05 + }], + default: { + fixed_target: 10, + rate: 0.05 + }, + version: 1 + }; + + assert.throws(function() { new SamplingRules(source); }, 'attribute "http_method" has invalid value: null.'); + }); + }); + + describe('#shouldSample', function() { + var sandbox, fakeSampler; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + fakeSampler = new Sampler(10, 0.05); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should match the default rule and return true', function() { + var samplingRules = new SamplingRules(); + samplingRules.rules = [{ + default: true, + sampler: fakeSampler + }]; + + assert.isTrue(samplingRules.shouldSample('hello.moop.com', 'GET', '/home/moop/hello')); + stubIsSampled.should.have.been.calledOnce; + }); + + it('should match the customer rule by calling Utils.wildcardMatch on each attribute', function() { + var matchStub = sandbox.stub(Utils, 'wildcardMatch').returns(true); + + var samplingRules = new SamplingRules(); + samplingRules.rules = [{ + http_method: 'POST', + service_name: '*.moop.com', + url_path: '/login/*', + sampler: fakeSampler + }]; + + samplingRules.shouldSample('hello.moop.com', 'POST', '/login/moop/hello'); + stubIsSampled.should.have.been.calledOnce; + + matchStub.should.have.been.calledThrice; + matchStub.should.have.been.calledWithExactly('/login/*', '/login/moop/hello'); + matchStub.should.have.been.calledWithExactly('POST', 'POST'); + matchStub.should.have.been.calledWithExactly('*.moop.com', 'hello.moop.com'); + }); + + it('should fail to match the customer rule and not call isSampled', function() { + sandbox.stub(Utils, 'wildcardMatch').returns(false); + + var samplingRules = new SamplingRules(); + samplingRules.rules = [{ + http_method: '.', + service_name: '.', + url_path: '.', + sampler: fakeSampler + }]; + + assert.isFalse(samplingRules.shouldSample('hello.moop.com', 'GET', '/login/moop/hello')); + stubIsSampled.should.not.have.been.called; + }); + }); +}); diff --git a/packages/core/test/unit/segment_emitter.test.js b/packages/core/test/unit/segment_emitter.test.js new file mode 100644 index 00000000..c47ff71e --- /dev/null +++ b/packages/core/test/unit/segment_emitter.test.js @@ -0,0 +1,119 @@ +var assert = require('chai').assert; +var expect = require('chai').expect; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +chai.use(sinonChai); + +var dgram = require('dgram'); +var Segment = require('../../lib/segments/segment'); + +describe('SegmentEmitter', function() { + var client, sandbox, SegmentEmitter; + var DEFAULT_DAEMON_ADDRESS = '127.0.0.1'; + var DEFAULT_DAEMON_PORT = 2000; + + var ADDRESS_PROPERTY_NAME = 'daemonAddress'; + var PORT_PROPERTY_NAME = 'daemonPort'; + + function getUncachedEmitter() { + var path = '../../lib/segment_emitter'; + delete require.cache[require.resolve(path)]; + return require(path); + } + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + delete process.env.AWS_XRAY_DAEMON_ADDRESS; + SegmentEmitter = getUncachedEmitter(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('init', function() { + it('should load the default address and port', function() { + assert.equal(SegmentEmitter[ADDRESS_PROPERTY_NAME], DEFAULT_DAEMON_ADDRESS); + assert.equal(SegmentEmitter[PORT_PROPERTY_NAME], DEFAULT_DAEMON_PORT); + }); + + it('should load the environment variables address and port if set', function() { + process.env.AWS_XRAY_DAEMON_ADDRESS = '192.168.0.23:8081'; + SegmentEmitter = getUncachedEmitter(); + + assert.equal(SegmentEmitter[ADDRESS_PROPERTY_NAME], '192.168.0.23'); + assert.equal(SegmentEmitter[PORT_PROPERTY_NAME], 8081); + }); + }); + + describe('#send', function() { + it('should send the segment using the dgram client', function() { + client = dgram.createSocket('udp4'); + sandbox.stub(client, 'send'); + sandbox.stub(dgram, 'createSocket').returns(client); + + SegmentEmitter = getUncachedEmitter(); + + var segment = new Segment('test'); + SegmentEmitter.send(segment); + + expect(client.send).to.have.been.calledOnce; + expect(client.send).to.have.been.calledWithExactly(sinon.match.any, 0, sinon.match.number, + SegmentEmitter[PORT_PROPERTY_NAME], SegmentEmitter[ADDRESS_PROPERTY_NAME], sinon.match.func); + }); + }); + + describe('#setDaemonAddress', function() { + var hostname = 'hostname'; + var ip = '192.168.0.23'; + var port = ':8081'; + + it('should set the IP address', function() { + SegmentEmitter.setDaemonAddress(ip); + + assert.equal(SegmentEmitter[ADDRESS_PROPERTY_NAME], ip); + assert.equal(SegmentEmitter[PORT_PROPERTY_NAME], DEFAULT_DAEMON_PORT); + }); + + it('should set the hostname', function() { + SegmentEmitter.setDaemonAddress(hostname); + + assert.equal(SegmentEmitter[ADDRESS_PROPERTY_NAME], hostname); + assert.equal(SegmentEmitter[PORT_PROPERTY_NAME], DEFAULT_DAEMON_PORT); + }); + + it('should set the port', function() { + SegmentEmitter.setDaemonAddress(port); + + assert.equal(SegmentEmitter[ADDRESS_PROPERTY_NAME], DEFAULT_DAEMON_ADDRESS); + assert.equal(SegmentEmitter[PORT_PROPERTY_NAME], parseInt(port.slice(1))); + }); + + it('should set the IP address and port', function() { + SegmentEmitter.setDaemonAddress(ip + port); + + assert.equal(SegmentEmitter[ADDRESS_PROPERTY_NAME], ip); + assert.equal(SegmentEmitter[PORT_PROPERTY_NAME], parseInt(port.slice(1))); + }); + + it('should set the hostname and port', function() { + SegmentEmitter.setDaemonAddress(hostname + port); + + assert.equal(SegmentEmitter[ADDRESS_PROPERTY_NAME], hostname); + assert.equal(SegmentEmitter[PORT_PROPERTY_NAME], parseInt(port.slice(1))); + }); + + it('should not override the environment variables', function() { + process.env.AWS_XRAY_DAEMON_ADDRESS = '184.88.8.173:4553'; + SegmentEmitter = getUncachedEmitter(); + + SegmentEmitter.setDaemonAddress(ip + port); + + assert.equal(SegmentEmitter[ADDRESS_PROPERTY_NAME], '184.88.8.173'); + assert.equal(SegmentEmitter[PORT_PROPERTY_NAME], 4553); + }); + }); +}); diff --git a/packages/core/test/unit/segments/attributes/aws.test.js b/packages/core/test/unit/segments/attributes/aws.test.js new file mode 100644 index 00000000..3ff8ff5a --- /dev/null +++ b/packages/core/test/unit/segments/attributes/aws.test.js @@ -0,0 +1,123 @@ +var assert = require('chai').assert; +var Aws = require('../../../../lib/segments/attributes/aws'); +var CallCapturer = require('../../../../lib/patchers/call_capturer.js'); +var sinon = require('sinon'); + +describe('Aws', function() { + var serviceName = 's3'; + var req = { + request: { + operation: 'putObject', + httpRequest: { + region: 'us-east-1' + } + }, + requestId: 'C9336616C948DC3C', + retryCount: 3 + }; + + describe('#init', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(Aws.prototype, 'addData'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create a new Aws object', function() { + var aws = new Aws(req, serviceName); + assert.isObject(aws); + }); + + it('should capture the request ID but not the extendedRequestId', function() { + var aws = new Aws(req, serviceName); + assert.propertyVal(aws, 'request_id', req.requestId); + assert.notProperty(aws, 'id_2'); + }); + + it('should capture the special S3 extendedRequestId (x-amz-id-2 header) if set', function() { + req.extendedRequestId = 'AzVdR5vxfKlTwI7SMKu+suvQRfzGrzDtZRy3dU7Te6vbFx/R18U0I/ndTmLfA78sVxgfRo0lDMQ='; + var aws = new Aws(req, serviceName); + + assert.propertyVal(aws, 'id_2', req.extendedRequestId); + }); + + it('should format the operation name', function() { + var aws = new Aws(req, serviceName); + assert.propertyVal(aws, 'operation', 'PutObject'); + }); + }); + + describe('#addData', function() { + it('should append the data to the Aws object', function() { + var aws = new Aws(req, serviceName); + var data1 = { moop: { key: 'value2' } }; + var data2 = { boop: { key: 'value1' } }; + aws.addData(data1); + aws.addData(data2); + + assert.deepEqual(aws.moop, data1.moop); + assert.deepEqual(aws.boop, data2.boop); + }); + }); + + describe('class functions', function() { + var capturerAppendStub, capturerInitStub, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + capturerInitStub = sandbox.stub(CallCapturer.prototype, 'init'); + capturerAppendStub = sandbox.stub(CallCapturer.prototype, 'append'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('setAWSWhitelist', function() { + it('should accept a string for location', function() { + var location = '/path/here'; + Aws.setAWSWhitelist(location); + capturerInitStub.should.have.been.calledWith(location); + }); + + it('should accept a source object', function() { + var source = {}; + Aws.setAWSWhitelist(source); + capturerInitStub.should.have.been.calledWith(source); + }); + + it('should throw an error on bad values', function() { + assert.throws(function() { Aws.setAWSWhitelist(); }); + assert.throws(function() { Aws.setAWSWhitelist(null); }); + assert.throws(function() { Aws.setAWSWhitelist(0); }); + assert.throws(function() { Aws.setAWSWhitelist(new String('')); }); + }); + }); + + describe('appendAWSWhitelist', function() { + it('should accept a string for location', function() { + var location = '/path/here'; + Aws.appendAWSWhitelist(location); + capturerAppendStub.should.have.been.calledWith(location); + }); + + it('should accept a source object', function() { + var source = {}; + Aws.appendAWSWhitelist(source); + capturerAppendStub.should.have.been.calledWith(source); + }); + + it('should throw an error on bad values', function() { + assert.throws(function() { Aws.appendAWSWhitelist(); }); + assert.throws(function() { Aws.appendAWSWhitelist(null); }); + assert.throws(function() { Aws.appendAWSWhitelist(0); }); + assert.throws(function() { Aws.setAWSWhitelist(new String('')); }); + }); + }); + }); +}); diff --git a/packages/core/test/unit/segments/attributes/captured_exception.test.js b/packages/core/test/unit/segments/attributes/captured_exception.test.js new file mode 100644 index 00000000..7ff5800b --- /dev/null +++ b/packages/core/test/unit/segments/attributes/captured_exception.test.js @@ -0,0 +1,73 @@ +var assert = require('chai').assert; +var CapturedException = require('../../../../lib/segments/attributes/captured_exception'); + +describe('CapturedException', function() { + describe('#constructor', function() { + it('should create a CapturedException for a String', function() { + var err = 'Error here!'; + var captured = new CapturedException(err); + + assert.equal(captured.message, err); + assert.equal(captured.type, ''); + assert.deepEqual(captured.stack, []); + }); + + it('should create a CapturedException for an Error', function() { + var err = new Error('Error here!'); + var captured = new CapturedException(err); + + assert.equal(captured.message, err.message); + assert.equal(captured.type, err.name); + assert.isArray(captured.stack); + }); + + it('should create a CapturedException for an Error with no stack trace', function() { + var err = { message: 'Error here!', name: 'Error'}; + var captured = new CapturedException(err); + + assert.deepEqual(captured.stack, []); + }); + + it('should create a CapturedException for an Error with a parsed stack trace', function() { + var err = new Error('Test error'); + err.stack = ('Test error\n at /path/to/file.js:200:15\n ' + + 'at myTestFunction /path/to/another/file.js:20:30\n ' + + 'at myTest [as _myTests] (test.js:10:5)'); + + var stack = [ + { + path: '/path/to/file.js', + line: 200, + label: 'anonymous' + }, + { + path: '/path/to/another/file.js', + line: 20, + label: 'myTestFunction' + }, + { + path: 'test.js', + line: 10, + label: 'myTest [as _myTests]' + } + ]; + + var captured = new CapturedException(err); + assert.deepEqual(captured.stack, stack); + }); + + it('should create a CapturedException with remote false by default', function() { + var err = { message: 'Error here!', name: 'Error'}; + var captured = new CapturedException(err); + + assert.equal(captured.remote, false); + }); + + it('should create a CapturedException with remote true when set', function() { + var err = { message: 'Error here!', name: 'Error'}; + var captured = new CapturedException(err, true); + + assert.equal(captured.remote, true); + }); + }); +}); diff --git a/packages/core/test/unit/segments/attributes/subsegment.test.js b/packages/core/test/unit/segments/attributes/subsegment.test.js new file mode 100644 index 00000000..84b0af2f --- /dev/null +++ b/packages/core/test/unit/segments/attributes/subsegment.test.js @@ -0,0 +1,339 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var CapturedException = require('../../../../lib/segments/attributes/captured_exception'); +var SegmentEmitter = require('../../../../lib/segment_emitter'); +var SegmentUtils = require('../../../../lib/segments/segment_utils'); +var Subsegment = require('../../../../lib/segments/attributes/subsegment'); + +chai.should(); +chai.use(sinonChai); + +describe('Subsegment', function() { + describe('#init', function() { + it('should set the required attributes', function() { + var subsegment = new Subsegment('foo'); + + var expected = new RegExp('^([a-f0-9]{16})$'); + assert.match(subsegment.id, expected); + + assert.property(subsegment, 'start_time'); + assert.propertyVal(subsegment, 'name', 'foo'); + assert.propertyVal(subsegment, 'in_progress', true); + assert.propertyVal(subsegment, 'counter', 0); + }); + }); + + describe('#addMetadata', function() { + var key, subsegment, value; + + beforeEach(function() { + subsegment = new Subsegment('test'); + key = 'key'; + value = [1, 2, 3]; + }); + + it('should add key value pair to metadata.default if no namespace is supplied', function() { + subsegment.addMetadata(key, value); + assert.propertyVal(subsegment.metadata.default, key, value); + }); + + it('should add key value pair to metadata[namespace] if a namespace is supplied', function() { + var namespace = 'hello'; + subsegment.addMetadata(key, value, 'hello'); + assert.propertyVal(subsegment.metadata[namespace], key, value); + }); + }); + + describe('#addSubsegment', function() { + var child, incrementStub, subsegment, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + subsegment = new Subsegment('test'); + child = new Subsegment('child'); + incrementStub = sandbox.stub(subsegment, 'incrementCounter'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should throw an error if trying to add a non-subsegment', function() { + assert.throws( function() { subsegment.addSubsegment({ key: 'x' }); }, Error); + }); + + it('should add the new subsegment to the subsegments array' , function() { + subsegment.addSubsegment(child); + assert.equal(subsegment.subsegments[0], child); + }); + + it('should set the parent and segment properties', function() { + subsegment.segment = 'segment'; + subsegment.addSubsegment(child); + assert.equal(child.parent, subsegment); + assert.equal(child.segment, subsegment.segment); + }); + + it('should call to increment the counter with count from subsegment', function() { + child.counter = 10; + subsegment.addSubsegment(child); + + incrementStub.should.have.been.calledWith(10); + }); + + it('should not call to increment the counter if the subsegment is closed', function() { + child.close(); + subsegment.addSubsegment(child); + + incrementStub.should.have.not.been.called; + }); + }); + + describe('#addError', function() { + var err, exceptionStub, sandbox, subsegment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + exceptionStub = sandbox.stub(CapturedException.prototype, 'init'); + + subsegment = new Subsegment('test'); + err = new Error('Test error'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should accept an object or string', function() { + subsegment.addError(err); + subsegment.addError('error'); + assert.equal(subsegment.cause.exceptions.length, 2); + }); + + it('should throw an error on other types', function() { + assert.throws(function() { subsegment.addError(3); }); + }); + + it('should set fault to true by default', function() { + subsegment.addError(err); + assert.equal(subsegment.fault, true); + }); + + it('should add the cause property with working directory data', function() { + subsegment.addError(err); + assert.property(subsegment.cause, 'working_directory'); + }); + + it('should add a new captured exception', function() { + subsegment.addError(err, true); + exceptionStub.should.have.been.calledWithExactly(err, true); + }); + }); + + describe('#incrementCounter', function() { + var sandbox, stubIncrementParent, subsegment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + subsegment = new Subsegment('test'); + subsegment.parent = { incrementCounter: function() {} }; + + stubIncrementParent = sandbox.stub(subsegment.parent, 'incrementCounter'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should increment the counter and parent', function() { + subsegment.incrementCounter(); + + assert.equal(subsegment.counter, 1); + stubIncrementParent.should.have.been.calledWith(); + }); + + it('should increment the counter and parent plus additional when provided', function() { + var additional = 4; + subsegment.incrementCounter(additional); + + assert.equal(subsegment.counter, additional + 1); + stubIncrementParent.should.have.been.calledWith(additional); + }); + }); + + describe('#close', function() { + var sandbox, segment, stubSegmentDecrement, stubSegmentRemove, stubSubsegmentStream, subsegment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + segment = { + parent_id: '12345abc3456def', + counter: 1, + decrementCounter: function() {}, + removeSubsegment: function() {}, + isClosed: function() {} + }; + + stubSegmentDecrement = sandbox.stub(segment, 'decrementCounter'); + stubSegmentRemove = sandbox.stub(segment, 'removeSubsegment'); + + subsegment = new Subsegment('child'); + subsegment.parent = segment; + subsegment.segment = segment; + stubSubsegmentStream = sandbox.stub(subsegment, 'streamSubsegments').returns(true); + }); + + afterEach(function() { + sandbox.restore(); + SegmentUtils.setStreamingThreshold(100); + }); + + it('should set the end time', function() { + subsegment.close(); + assert.property(subsegment, 'end_time'); + }); + + it('should decrement the parent counter', function() { + subsegment.close(); + stubSegmentDecrement.should.have.been.calledOnce; + }); + + it('should delete the in_progress attribute', function() { + subsegment.close(); + assert.notProperty(subsegment, 'in_progress'); + }); + + it('should call streamSubsegments if the parent counter is higher than the threshold', function() { + SegmentUtils.setStreamingThreshold(0); + subsegment.close(); + + stubSubsegmentStream.should.have.been.calledOnce; + }); + + it('should remove itself from the parent if it was streamed', function() { + SegmentUtils.setStreamingThreshold(0); + subsegment.close(); + + stubSegmentRemove.should.have.been.calledWith(subsegment); + }); + }); + + describe('#flush', function() { + var child, emitStub, parent, sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + segment = { trace_id: '1-58c835af-cf6bfe9f8f2c5b84a6d1f50c', parent_id: '12345abc3456def' }; + parent = new Subsegment('test'); + emitStub = sandbox.stub(SegmentEmitter.socket, 'send'); + + child = parent.addNewSubsegment('child'); + child.segment = segment; + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should throw an error if the subsegment has no parent', function() { + delete child.parent; + assert.throws( function() { child.flush(); }, Error); + }); + + it('should throw an error if the subsegment has no segment', function() { + delete child.segment; + assert.throws( function() { child.flush(); }, Error); + }); + + it('should set the parent_id, trace_id and type properties', function() { + child.flush(); + assert.equal(child.type, 'subsegment'); + assert.equal(child.parent_id, parent.id); + assert.equal(child.trace_id, segment.trace_id); + }); + + it('should not send if the notTraced property evaluates to true', function() { + segment.notTraced = true; + child.flush(); + emitStub.should.have.not.been.called; + }); + + it('should send if the notTraced property evaluates to false', function() { + child.flush(); + emitStub.should.have.been.called; + }); + }); + + describe('#streamSubsegments', function() { + var child1, parent, sandbox, stubFlush, stubStream1; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + parent = new Subsegment('parent'); + child1 = parent.addNewSubsegment('child'); + + stubFlush = sandbox.stub(parent, 'flush'); + stubStream1 = sandbox.stub(child1, 'streamSubsegments'); + }); + + afterEach(function() { + sandbox.restore(); + SegmentUtils.setStreamingThreshold(100); + }); + + describe('if the subsegment is closed and has no open subsegments', function() { + it('should flush itself', function() { + child1.close(); + parent.close(); + parent.streamSubsegments(); + + stubFlush.should.have.been.calledOnce; + }); + + it('should return true', function() { + child1.close(); + parent.close(); + + assert.isTrue(parent.streamSubsegments()); + }); + }); + + describe('if the subsegment not closed or it has open subsegments', function() { + it('should call streamSubsegment on each child subsegment', function() { + var child2 = parent.addNewSubsegment('child2'); + var child3 = parent.addNewSubsegment('child3'); + var stubStream2 = sandbox.stub(child2, 'streamSubsegments'); + var stubStream3 = sandbox.stub(child3, 'streamSubsegments'); + parent.streamSubsegments(); + + stubStream1.should.have.been.calledOnce; + stubStream2.should.have.been.calledOnce; + stubStream3.should.have.been.calledOnce; + }); + + it('should remove the closed subsegments from the subsegment array', function() { + var child2 = parent.addNewSubsegment('child2'); + var child3 = parent.addNewSubsegment('child3'); + var child4 = parent.addNewSubsegment('child4'); + var child5 = parent.addNewSubsegment('child5'); + var child6 = parent.addNewSubsegment('child6'); + stubStream1.returns(true); + sandbox.stub(child2, 'streamSubsegments'); + sandbox.stub(child3, 'streamSubsegments'); + sandbox.stub(child4, 'streamSubsegments').returns(true); + sandbox.stub(child5, 'streamSubsegments'); + sandbox.stub(child6, 'streamSubsegments').returns(true); + parent.streamSubsegments(); + + assert.deepEqual(parent.subsegments, [child2, child3, child5]); + }); + }); + }); +}); diff --git a/packages/core/test/unit/segments/plugins/ec2_plugin.test.js b/packages/core/test/unit/segments/plugins/ec2_plugin.test.js new file mode 100644 index 00000000..2f00af40 --- /dev/null +++ b/packages/core/test/unit/segments/plugins/ec2_plugin.test.js @@ -0,0 +1,47 @@ +var expect = require('chai').expect; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +chai.use(sinonChai); + +var EC2Plugin = require('../../../../lib/segments/plugins/ec2_plugin'); +var Plugin = require('../../../../lib/segments/plugins/plugin'); + +describe('EC2Plugin', function() { + var data = { + availabilityZone: 'us-east-1d', + instanceId: 'i-1234567890abcdef0' + }; + + var getStub, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should return an object holding EC2 metadata if it recieved data', function(done) { + getStub = sandbox.stub(Plugin, 'getPluginMetadata').yields(null, data); + + EC2Plugin.getData(function(data) { + getStub.should.have.been.calledOnce; + expect(data.ec2).not.to.be.empty; + done(); + }); + }); + + it('should return undefined if an error is recieved', function(done) { + var err = new Error('error'); + getStub = sandbox.stub(Plugin, 'getPluginMetadata').yields(err); + + EC2Plugin.getData(function(data) { + getStub.should.have.been.calledOnce; + expect(data).to.be.undefined; + done(); + }); + }); +}); diff --git a/packages/core/test/unit/segments/plugins/ecs_plugin.test.js b/packages/core/test/unit/segments/plugins/ecs_plugin.test.js new file mode 100644 index 00000000..4b5b39ca --- /dev/null +++ b/packages/core/test/unit/segments/plugins/ecs_plugin.test.js @@ -0,0 +1,27 @@ +var expect = require('chai').expect; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +chai.use(sinonChai); + +var ECSPlugin = require('../../../../lib/segments/plugins/ecs_plugin'); + +describe('ECSPlugin', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should return an object holding ECS metadata', function(done) { + ECSPlugin.getData(function(data) { + expect(data.ecs.container).not.to.be.empty; + done(); + }); + }); +}); diff --git a/packages/core/test/unit/segments/plugins/elastic_beanstalk_plugin.test.js b/packages/core/test/unit/segments/plugins/elastic_beanstalk_plugin.test.js new file mode 100644 index 00000000..00029455 --- /dev/null +++ b/packages/core/test/unit/segments/plugins/elastic_beanstalk_plugin.test.js @@ -0,0 +1,49 @@ +var expect = require('chai').expect; +var fs = require('fs'); +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +chai.use(sinonChai); + +var ElasticBeanstalkPlugin = require('../../../../lib/segments/plugins/elastic_beanstalk_plugin'); + +describe('ElasticBeanstalkPlugin', function() { + var err = new Error('Cannot load file.'); + var data = { + deployment_id: 'deployment_id', + version_label: 'version_label', + environment_name: 'my_env' + }; + + var readStub, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should return an object holding Beanstalk metadata if it read data', function(done) { + readStub = sandbox.stub(fs, 'readFile').yields(null, data); + sandbox.stub(JSON, 'parse').returns(data); + + ElasticBeanstalkPlugin.getData(function(data) { + readStub.should.have.been.calledOnce; + expect(data.elastic_beanstalk).not.to.be.empty; + done(); + }); + }); + + it('should return undefined if the read fails', function(done) { + readStub = sandbox.stub(fs, 'readFile').yields(err, null); + + ElasticBeanstalkPlugin.getData(function(data) { + readStub.should.have.been.calledOnce; + expect(data).to.be.undefined; + done(); + }); + }); +}); diff --git a/packages/core/test/unit/segments/plugins/plugin.test.js b/packages/core/test/unit/segments/plugins/plugin.test.js new file mode 100644 index 00000000..a62e47f1 --- /dev/null +++ b/packages/core/test/unit/segments/plugins/plugin.test.js @@ -0,0 +1,75 @@ +var expect = require('chai').expect; +var nock = require('nock'); + +var Plugin = require('../../../../lib/segments/plugins/plugin'); + +describe('Plugin', function() { + describe('#getPluginMetadata', function() { + var METADATA_HOST = 'http://localhost'; + var METADATA_PATH = '/index'; + + var OPTIONS = { + host: 'localhost', + path: '/index' + }; + + var data = { data: 1234 }; + var getPluginMetadata = Plugin.getPluginMetadata; + + var getMetadata; + + it('should return metadata if 200 OK', function(done) { + getMetadata = nock(METADATA_HOST) + .get(METADATA_PATH) + .reply(200, data); + + getPluginMetadata(OPTIONS, function(err, data) { + expect(data.data).not.to.be.empty; + getMetadata.done(); + done(); + }); + }); + + it('should retry on 4xx', function(done) { + getMetadata = nock(METADATA_HOST) + .get(METADATA_PATH) + .times(3) + .reply(400) + .get(METADATA_PATH) + .reply(200, data); + + getPluginMetadata(OPTIONS, function(err, data) { + expect(data.data).not.to.be.empty; + getMetadata.done(); + done(); + }); + }); + + it('should retry on 4xx 20 times then error out', function(done) { + this.timeout(12000); + + getMetadata = nock(METADATA_HOST) + .get(METADATA_PATH) + .times(21) + .reply(400); + + getPluginMetadata(OPTIONS, function(err, data) { + expect(data).to.be.empty; + getMetadata.done(); + done(); + }); + }); + + it('should fast fail on any other status code', function(done) { + getMetadata = nock(METADATA_HOST) + .get(METADATA_PATH) + .reply(500); + + getPluginMetadata(OPTIONS, function(err, data) { + expect(data).to.be.empty; + getMetadata.done(); + done(); + }); + }); + }); +}); diff --git a/packages/core/test/unit/segments/segment.test.js b/packages/core/test/unit/segments/segment.test.js new file mode 100644 index 00000000..548a115b --- /dev/null +++ b/packages/core/test/unit/segments/segment.test.js @@ -0,0 +1,410 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var CapturedException = require('../../../lib/segments/attributes/captured_exception'); +var SegmentEmitter = require('../../../lib/segment_emitter'); +var SegmentUtils = require('../../../lib/segments/segment_utils'); +var Segment = require('../../../lib/segments/segment'); +var Subsegment = require('../../../lib/segments/attributes/subsegment'); + +chai.should(); +chai.use(sinonChai); + +describe('Segment', function() { + describe('#init', function() { + var rootId = '1-57fbe041-2c7ad569f5d6ff149137be86'; + var parentId = 'f9c6e4f0b5116501'; + + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should use the supplied root id as the trace id', function() { + var segment = new Segment('foo', rootId); + assert.equal(segment.trace_id, rootId); + }); + + it('should use the supplied parent id', function() { + var segment = new Segment('foo', null, parentId); + assert.equal(segment.parent_id, parentId); + }); + + it('should generate a new trace id if one was not supplied', function() { + var segment = new Segment('foo'); + var expected = new RegExp('^1-([a-f0-9]{8})-([a-f0-9]{24})$'); + + assert.match(segment.trace_id, expected); + }); + + it('should generate a 16 character hex id', function() { + var segment = new Segment('foo'); + var expected = new RegExp('^([a-f0-9]{16})$'); + + assert.match(segment.id, expected); + }); + + it('should use SegmentUtils origin property to add global attributes', function() { + SegmentUtils.setOrigin('hello'); + var segment = new Segment('foo'); + + assert.equal(segment.origin, 'hello'); + delete SegmentUtils.origin; + }); + + it('should use SegmentUtils pluginData properties call addPluginData', function() { + var data = { data: 'hello' }; + SegmentUtils.setPluginData(data); + var stubAddPluginData = sandbox.stub(Segment.prototype, 'addPluginData'); + new Segment('foo'); + + stubAddPluginData.should.have.been.calledWithExactly(data); + delete SegmentUtils.pluginData; + }); + + it('should use SegmentUtils sdkData properties call setSDKData', function() { + var data = { + sdk: 'X-Ray for Node.js' + }; + SegmentUtils.setSDKData(data); + var stubSetSDKData = sandbox.stub(Segment.prototype, 'setSDKData'); + new Segment('foo'); + + stubSetSDKData.should.have.been.calledWithExactly(data); + delete SegmentUtils.sdkData; + }); + + it('should use SegmentUtils serviceData properties call setServiceData', function() { + var serviceData = '2.3.0'; + SegmentUtils.setServiceData(serviceData); + var stubSetServiceData = sandbox.stub(Segment.prototype, 'setServiceData'); + new Segment('foo'); + + stubSetServiceData.should.have.been.calledWithExactly(serviceData); + delete SegmentUtils.serviceData; + }); + }); + + describe('#addMetadata', function() { + var key, segment, value; + + beforeEach(function() { + segment = new Segment('test'); + key = 'key'; + value = [1, 2, 3]; + }); + + it('should add key value pair to metadata.default if no namespace is supplied', function() { + segment.addMetadata(key, value); + assert.propertyVal(segment.metadata.default, key, value); + }); + + it('should add key value pair to metadata[namespace] if a namespace is supplied', function() { + var namespace = 'hello'; + segment.addMetadata(key, value, 'hello'); + assert.propertyVal(segment.metadata[namespace], key, value); + }); + }); + + describe('#addSDKData', function() { + var segment, version; + + beforeEach(function() { + segment = new Segment('test'); + version = '1.0.0-beta'; + }); + + it('should add SDK data to aws.xray.sdk', function() { + segment.setSDKData({ + sdk_version: version + }); + assert.propertyVal(segment.aws.xray, 'sdk_version', version); + }); + }); + + describe('#addPluginData', function() { + var segment, data; + + beforeEach(function() { + segment = new Segment('test'); + data = { elastic_beanstalk: { environment: 'my_environment_name' }}; + }); + + it('should add plugin data to aws', function() { + segment.addPluginData(data); + assert.deepEqual(segment.aws.elastic_beanstalk, data.elastic_beanstalk); + }); + }); + + describe('#setServiceData', function() { + var segment, data; + + beforeEach(function() { + segment = new Segment('test'); + data = { + version: '2.3.0', + package: 'sample-app' + }; + }); + + it('should add the service version to service.version', function() { + segment.setServiceData(data); + assert.propertyVal(segment.service, 'version', data.version); + assert.propertyVal(segment.service, 'package', data.package); + }); + }); + + describe('#addNewSubsegment', function() { + var segment; + + beforeEach(function() { + segment = new Segment('test'); + }); + + it('should add a new subsegment to the segment', function() { + segment.addNewSubsegment('newSubsegment'); + assert.instanceOf(segment.subsegments[0], Subsegment); + }); + + it('should throw an error if trying to add a non-subsegment', function() { + assert.throws( function() { segment.addNewSubsegment({}); }, Error); + }); + }); + + describe('#addSubsegment', function() { + var incrementStub, subsegment, sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test'); + + subsegment = new Subsegment('new'); + incrementStub = sandbox.stub(segment, 'incrementCounter'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should throw an error if trying to add a non-subsegment', function() { + assert.throws( function() { segment.addSubsegment({ key: 'x' }); }, Error); + }); + + it('should add the new subsegment to the subsegments array' , function() { + segment.addSubsegment(subsegment); + assert.equal(segment.subsegments[0], subsegment); + }); + + it('should set the parent and segment properties', function() { + segment.addSubsegment(subsegment); + assert.equal(subsegment.parent, segment); + assert.equal(subsegment.segment, segment); + }); + + it('should call to increment the counter', function() { + segment.addSubsegment(subsegment); + incrementStub.should.have.been.calledOnce; + }); + + it('should not call to increment the counter if the segment is closed', function() { + subsegment.close(); + segment.addSubsegment(subsegment); + + incrementStub.should.have.not.been.called; + }); + }); + + describe('#addError', function() { + var err, exceptionStub, sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + + exceptionStub = sandbox.stub(CapturedException.prototype, 'init'); + + segment = new Segment('test'); + err = new Error('Test error'); + err.stack = ('Test error\n at /path/to/file.js:200:15\n ' + + 'at myTestFunction /path/to/another/file.js:20:30\n ' + + 'at myTest [as _myTests] (test.js:10:5)'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should accept an object or string', function() { + segment.addError(err); + segment.addError('error'); + assert.equal(segment.cause.exceptions.length, 2); + }); + + it('should throw an error on other types', function() { + assert.throws(function() { segment.addError(3); }); + }); + + it('should set fault to true by default', function() { + segment.addError(err); + assert.equal(segment.fault, true); + }); + + it('should add the cause property with working directory data', function() { + segment.addError(err); + assert.property(segment.cause, 'working_directory'); + }); + + it('should add a new captured exception', function() { + segment.addError(err, true); + exceptionStub.should.have.been.calledWithExactly(err, true); + }); + }); + + describe('#close', function() { + var err, addErrorStub, flushStub, sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test'); + + addErrorStub = sandbox.stub(segment, 'addError'); + flushStub = sandbox.stub(segment, 'flush'); + err = new Error('Test error'); + }); + + afterEach(function() { + sandbox.restore(); + SegmentUtils.setStreamingThreshold(100); + }); + + it('should set the end time if not already set', function() { + segment.close(); + assert.property(segment, 'end_time'); + }); + + it('should not reset the end time if already set', function() { + var end = 111; + + segment.end_time = end; + segment.close(); + assert.equal(segment.end_time, end); + }); + + it('should call "addError" if an error was given', function() { + segment.close(err, true); + addErrorStub.should.have.been.calledWithExactly(err, true); + }); + + it('should not call "addError" if no error was given', function() { + segment.close(); + addErrorStub.should.have.not.been.called; + }); + + it('should delete properties "in_progress" and "exception"', function() { + segment.in_progress = true; + segment.exception = err; + segment.close(); + + assert.notProperty(segment, 'in_progress'); + assert.notProperty(segment, 'exception'); + }); + + it('should flush the segment on close', function() { + segment.close(); + + flushStub.should.have.been.calledOnce; + }); + }); + + describe('#incrementCounter', function() { + var sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test'); + }); + + afterEach(function() { + sandbox.restore(); + SegmentUtils.setStreamingThreshold(100); + }); + + it('should increment the counter', function() { + segment.incrementCounter(); + + assert.equal(segment.counter, 1); + }); + + it('should stream the subsegments when the count is greater than the SegmentUtils threshold', function() { + SegmentUtils.setStreamingThreshold(0); + var child1 = segment.addNewSubsegment('child1'); + var child2 = segment.addNewSubsegment('child2'); + var stubStream1 = sandbox.stub(child1, 'streamSubsegments'); + var stubStream2 = sandbox.stub(child2, 'streamSubsegments'); + + segment.incrementCounter(); + + stubStream1.should.have.been.calledOnce; + stubStream2.should.have.been.calledOnce; + }); + + it('should remove the subsegments streamed from the subsegments array', function() { + SegmentUtils.setStreamingThreshold(0); + var child1 = segment.addNewSubsegment('child1'); + var child2 = segment.addNewSubsegment('child2'); + var child3 = segment.addNewSubsegment('child3'); + var child4 = segment.addNewSubsegment('child4'); + var child5 = segment.addNewSubsegment('child5'); + var child6 = segment.addNewSubsegment('child6'); + sandbox.stub(child1, 'streamSubsegments').returns(true); + sandbox.stub(child2, 'streamSubsegments'); + sandbox.stub(child3, 'streamSubsegments'); + sandbox.stub(child4, 'streamSubsegments').returns(true); + sandbox.stub(child5, 'streamSubsegments'); + sandbox.stub(child6, 'streamSubsegments').returns(true); + + segment.incrementCounter(); + + assert.deepEqual(segment.subsegments, [child2, child3, child5]); + }); + }); + + describe('#flush', function() { + var err, sandbox, segment; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test'); + err = new Error('Test error'); + }); + + afterEach(function() { + sandbox.restore(); + SegmentUtils.setStreamingThreshold(100); + }); + + describe('if traced', function() { + it('should remove properties "notTraced", "counter" and "exception"', function() { + var sendStub = sandbox.stub(SegmentEmitter, 'send'); + segment.notTraced = false; + segment.in_progress = true; + segment.exception = err; + segment.counter = 1; + + segment.flush(); + + sendStub.should.have.been.calledOnce; + var sentSegment = sendStub.lastCall.args; + assert.notProperty(sentSegment, 'exception'); + assert.notProperty(sentSegment, 'counter'); + assert.notProperty(sentSegment, 'notTraced'); + }); + }); + }); +}); diff --git a/packages/core/test/unit/segments/segment_utils.test.js b/packages/core/test/unit/segments/segment_utils.test.js new file mode 100644 index 00000000..04994039 --- /dev/null +++ b/packages/core/test/unit/segments/segment_utils.test.js @@ -0,0 +1,17 @@ +var assert = require('chai').assert; + +var SegmentUtils = require('../../../lib/segments/segment_utils'); + +describe('SegmentUtils', function() { + afterEach(function() { + SegmentUtils.setStreamingThreshold(100); + }); + + describe('#setStreamingThreshold', function() { + it('should override the default streaming threshold', function() { + SegmentUtils.setStreamingThreshold(10); + + assert.equal(SegmentUtils.streamingThreshold, 10); + }); + }); +}); diff --git a/packages/core/test/unit/test_utils.js b/packages/core/test/unit/test_utils.js new file mode 100644 index 00000000..0dfa0074 --- /dev/null +++ b/packages/core/test/unit/test_utils.js @@ -0,0 +1,26 @@ +var EventEmitter = require('events'); +var util = require('util'); + +var TestUtils = {}; + +TestUtils.TestEmitter = function TestEmitter() { + EventEmitter.call(this); +}; + +util.inherits(TestUtils.TestEmitter, EventEmitter); + +TestUtils.onEvent = function onEvent(event, fcn) { + this.emitter.on(event, fcn.bind(this)); + return this; +}; + +TestUtils.randomString = function randomString(length) { + var text = ''; + var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.#-_$%^&@!'; + for(var i = 0; i < length; i++) + text += possible.charAt(Math.floor(Math.random() * possible.length)); + + return text; +}; + +module.exports = TestUtils; diff --git a/packages/core/test/unit/utils.test.js b/packages/core/test/unit/utils.test.js new file mode 100644 index 00000000..0d60f650 --- /dev/null +++ b/packages/core/test/unit/utils.test.js @@ -0,0 +1,264 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +chai.use(sinonChai); + +var TestUtils = require('./test_utils'); +var Utils = require('../../lib/utils'); + +describe('Utils', function() { + describe('#getCauseTypeFromHttpStatus', function() { + it('should return "fault" on 5xx status code', function() { + assert.equal(Utils.getCauseTypeFromHttpStatus(544), 'fault'); + }); + + it('should return "error" on 4xx status code', function() { + assert.equal(Utils.getCauseTypeFromHttpStatus(404), 'error'); + }); + }); + + describe('#processTraceData', function() { + it('should parse X-Amzn-Trace-Id with spaces', function() { + var traceData = 'Root=1-58ed6027-14afb2e09172c337713486c0; Parent=48af77592b6dd73f; Sampled=1'; + + var parsed = Utils.processTraceData(traceData); + assert.propertyVal(parsed, 'Root', '1-58ed6027-14afb2e09172c337713486c0'); + assert.propertyVal(parsed, 'Parent', '48af77592b6dd73f'); + assert.propertyVal(parsed, 'Sampled', '1'); + }); + + it('should parse X-Amzn-Trace-Id without spaces', function() { + var traceData = 'Root=1-58ed6027-14afb2e09172c337713486c0;Parent=48af77592b6dd73f;Sampled=1'; + + var parsed = Utils.processTraceData(traceData); + assert.propertyVal(parsed, 'Root', '1-58ed6027-14afb2e09172c337713486c0'); + assert.propertyVal(parsed, 'Parent', '48af77592b6dd73f'); + assert.propertyVal(parsed, 'Sampled', '1'); + }); + }); + + describe('#processTraceData', function() { + it('should call processTraceData', function() { + assert.equal(Utils.getCauseTypeFromHttpStatus(544), 'fault'); + }); + + it('should return "error" on 4xx status code', function() { + assert.equal(Utils.getCauseTypeFromHttpStatus(404), 'error'); + }); + }); + + describe('#LambdaUtils', function() { + describe('#validTraceData', function() { + var headerData, processStub, sandbox, xAmznTraceId; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + xAmznTraceId = 'moop'; + headerData = { + Root: '1-58e8017e-fd7f0e6deaf6ce16a4841b44', + Parent: 'c2f1d3ad6a9fbd5a', + Sampled: '1' + }; + + processStub = sandbox.stub(Utils, 'processTraceData').returns(headerData); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call processTraceData', function() { + Utils.LambdaUtils.validTraceData(xAmznTraceId); + processStub.should.have.been.calledWith(xAmznTraceId); + }); + + it('should return true if Root, Parent and Sampled are present', function() { + assert.isTrue(Utils.LambdaUtils.validTraceData(headerData)); + }); + + it('should return false any of Root, Parent and Sampled is missing', function() { + delete headerData.Sampled; + assert.isFalse(Utils.LambdaUtils.validTraceData(xAmznTraceId)); + }); + + it('should return false if given no xAmznTraceId', function() { + assert.isFalse(Utils.LambdaUtils.validTraceData()); + }); + }); + + describe('#populateTraceData', function() { + var headerData, processStub, sandbox, segment; + var segmentId = 'b2f698e3dae16fb6'; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = {}; + headerData = { + Root: '1-58e8017e-fd7f0e6deaf6ce16a4841b44', + Parent: 'c2f1d3ad6a9fbd5a', + Sampled: '1' + }; + + processStub = sandbox.stub(Utils, 'processTraceData').returns(headerData); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call processTraceData', function() { + Utils.LambdaUtils.populateTraceData(segment); + processStub.should.have.been.calledOnce; + }); + + it('should return true if data is present', function() { + var populated = Utils.LambdaUtils.populateTraceData(segment); + assert.isTrue(populated); + }); + + it('should return false if data is missing', function() { + delete headerData.Sampled; + var populated = Utils.LambdaUtils.populateTraceData(segment); + assert.isFalse(populated); + }); + + it('should set segment.trace_id', function() { + Utils.LambdaUtils.populateTraceData(segment); + assert.equal(segment.trace_id, headerData.Root); + }); + + it('should not set segment.notTraced', function() { + Utils.LambdaUtils.populateTraceData(segment); + assert.notProperty(segment, 'notTraced'); + }); + + it('should set segment.notTraced as true if sampled is 0', function() { + headerData.Sampled = '0'; + Utils.LambdaUtils.populateTraceData(segment); + assert.propertyVal(segment, 'notTraced', true); + }); + + it('should delete segment.notTraced if sampled is 1', function() { + headerData.Sampled = '1'; + Utils.LambdaUtils.populateTraceData(segment); + assert.isUndefined(segment.notTraced); + }); + + it('should set the segment.id as the parent ID', function() { + segment.id = segmentId; + Utils.LambdaUtils.populateTraceData(segment); + assert.propertyVal(segment, 'id', headerData.Parent); + }); + }); + }); + + describe('#wildcardMatch', function() { + it('should match anything on "*"', function() { + for (var i = 0; i < 10; i++) + assert.equal(Utils.wildcardMatch('*', TestUtils.randomString(50)), true); + }); + + it('should match a single character on "?"', function() { + assert.equal(Utils.wildcardMatch('?', 'a'), true); + }); + + it('should match on exact literals (case insensitive)', function() { + assert.equal(Utils.wildcardMatch('foo', 'foo'), true); + assert.equal(Utils.wildcardMatch('Foo', 'foo'), true); + assert.equal(Utils.wildcardMatch('foo', 'Foo'), true); + }); + + it('should not match on unmatching exact literals', function() { + assert.equal(Utils.wildcardMatch('GET', 'POST'), false); + }); + + it('should match any number of any characters on "*" and the rest of the pattern', function() { + assert.equal(Utils.wildcardMatch('*.amazon.com', 'moop.amazon.com'), true); + assert.equal(Utils.wildcardMatch('hello.*.com', 'hello.moop.com'), true); + assert.equal(Utils.wildcardMatch('hello.amazon.*', 'hello.amazon.org'), true); + }); + + it('should not match on "*" if the rest of the pattern does not match', function() { + assert.equal(Utils.wildcardMatch('*.amazon.com', 'moop.anazon.com'), false); + assert.equal(Utils.wildcardMatch('*.amazon.com', 'moop.amazon.org'), false); + assert.equal(Utils.wildcardMatch('amazon.*.com', 'anazon.moop.com'), false); + assert.equal(Utils.wildcardMatch('amazon.*.com', 'amazon.noop.org'), false); + assert.equal(Utils.wildcardMatch('moop.amazon.*', 'moop.anazon.com'), false); + assert.equal(Utils.wildcardMatch('moop.amazon.*', 'noop.amazon.org'), false); + }); + + it('should match on multiple "*"s and the rest of the pattern', function() { + assert.equal(Utils.wildcardMatch('**', TestUtils.randomString(6)), true); + assert.equal(Utils.wildcardMatch('***', TestUtils.randomString(6)), true); + assert.equal(Utils.wildcardMatch('*a*', TestUtils.randomString(6) + 'a' + TestUtils.randomString(3)), true); + assert.equal(Utils.wildcardMatch('a*a*', 'a' + TestUtils.randomString(6) + 'a' + TestUtils.randomString(4)), true); + assert.equal(Utils.wildcardMatch('*aaa*', '1aaa1'), true); + assert.equal(Utils.wildcardMatch('*aaa*', TestUtils.randomString(6) + 'aaa' + TestUtils.randomString(5)), true); + assert.equal(Utils.wildcardMatch('*.*.com', 'moop.amazon.com'), true); + assert.equal(Utils.wildcardMatch('*moop.*.com', '1moop.amazon.com'), true); + assert.equal(Utils.wildcardMatch('*moop.*.com', '111moop.amazon.com'), true); + }); + + it('should not match on multiple "*"s if the rest of the pattern does not match', function() { + assert.equal(Utils.wildcardMatch('*.*.com', 'xray.amazon.org'), false); + assert.equal(Utils.wildcardMatch('*.*moop.com', 'xray.amazon.com'), false); + assert.equal(Utils.wildcardMatch('moop*.*moop.com', 'xraymoop.amazonmoop.com'), false); + assert.equal(Utils.wildcardMatch('moop.moop*.*', 'moop.moppp.comcom'), false); + }); + + it('should match a single character on "?" and the rest of the pattern', function() { + assert.equal(Utils.wildcardMatch('?b', 'ab'), true); + assert.equal(Utils.wildcardMatch('b?', 'ba'), true); + assert.equal(Utils.wildcardMatch('abc?', 'abca'), true); + assert.equal(Utils.wildcardMatch('?abc', 'aabc'), true); + assert.equal(Utils.wildcardMatch('ab?c', 'abac'), true); + assert.equal(Utils.wildcardMatch('?ray.amazon.com', 'xray.amazon.com'), true); + assert.equal(Utils.wildcardMatch('xray.amazon.?om', 'xray.amazon.xom'), true); + assert.equal(Utils.wildcardMatch('xray.a?azon.com', 'xray.anazon.com'), true); + }); + + it('should not match on "?" if the rest of the pattern does not match', function() { + assert.equal(Utils.wildcardMatch('?b', 'aa'), false); + assert.equal(Utils.wildcardMatch('b?', 'aa'), false); + assert.equal(Utils.wildcardMatch('abc?', 'abaa'), false); + assert.equal(Utils.wildcardMatch('?abc', 'abbc'), false); + assert.equal(Utils.wildcardMatch('ab?c', 'abab'), false); + assert.equal(Utils.wildcardMatch('?ray.amazon.com', 'xray.anazon.com'), false); + assert.equal(Utils.wildcardMatch('xray.amazon.?om', 'xray.amazon.xon'), false); + assert.equal(Utils.wildcardMatch('xray.a?azon.com', 'xray.anazom.com'), false); + assert.equal(Utils.wildcardMatch('?.moop.com', 'xray.amazon.com'), false); + }); + + it('should match on multiple "?"s and the rest of the pattern', function() { + assert.equal(Utils.wildcardMatch('????.amazon.com', 'xray.amazon.com'), true); + assert.equal(Utils.wildcardMatch('hell???mazon.com', 'hello.amazon.com'), true); + assert.equal(Utils.wildcardMatch('hel?o??mazo??com', 'hello.amazon.com'), true); + }); + + it('should match on multiple "?"s spliced into the rest of pattern', function() { + assert.equal(Utils.wildcardMatch('?abc?def?', '1abc2def3'), true); + }); + + it('should not match on multiple "?"s spliced in if the rest of the pattern does not match', function() { + assert.equal(Utils.wildcardMatch('?abc?def?', '1def2ghi3'), false); + }); + + it('should not match on multiple "?"s if the rest of the pattern does not match', function() { + assert.equal(Utils.wildcardMatch('????.amazom.com', 'x-ray.amazon.org'), false); + }); + + it('should match on complex cases when the pattern matches', function() { + assert.equal(Utils.wildcardMatch('?a?b?c?.*.com', '1a2b3c4.myelasticbeanstalkenv.com'), true); + assert.equal(Utils.wildcardMatch('?a?b*b?c?.*.com', '1a2bjkdwfjkewb3c4.myelasticbeanstalkenv.com'), true); + assert.equal(Utils.wildcardMatch('1*?m', '1a2bjkdwfjkewb3c4.myelasticbeanstalkenv.com'), true); + }); + + it('should not match on complex cases when the interspliced literals do not match', function() { + assert.equal(Utils.wildcardMatch('?a?b?c?.*.com', '1a2a3c4.myelasticbeanstalkenv.com'), false); + assert.equal(Utils.wildcardMatch('?a?b?c?.*.com', '1a2a3c4.myelasticbeanstalkenv.com'), false); + assert.equal(Utils.wildcardMatch('?a?b*b?c?.?*.com', '1a2bjkdwfjkewb3c4.myelasticbeanstalkenv.org'), false); + }); + }); +}); diff --git a/packages/express/.eslintrc.json b/packages/express/.eslintrc.json new file mode 100644 index 00000000..a3e0b54b --- /dev/null +++ b/packages/express/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "eol-last": [ + "error", + "always" + ] + } +} diff --git a/packages/express/.npmignore b/packages/express/.npmignore new file mode 100644 index 00000000..afb67596 --- /dev/null +++ b/packages/express/.npmignore @@ -0,0 +1,6 @@ +.npmignore +node_modules +npm-debug.log +docs +AWSXRay.log +Config diff --git a/packages/express/CHANGELOG.md b/packages/express/CHANGELOG.md new file mode 100644 index 00000000..6cc143de --- /dev/null +++ b/packages/express/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog for AWS X-Ray SDK Express for JavaScript + + + +## 1.1.5 +* The X-Ray SDK for Node.js is now an open source project. You can follow the project and submit issues and pull requests on [GitHub](/~https://github.com/aws/aws-xray-sdk-node). + +## 1.1.2 +* bugfix: Changed behavior on a http status code 429. Segment should have been marked as 'throttle' and 'error'. + +## 1.1.1 +* feature: Added debug logs for opening and closing segments. diff --git a/packages/express/LICENSE b/packages/express/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/packages/express/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/express/NOTICE.txt b/packages/express/NOTICE.txt new file mode 100644 index 00000000..7073de6e --- /dev/null +++ b/packages/express/NOTICE.txt @@ -0,0 +1,5 @@ +AWS X-Ray SDK Express for JavaScript +Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). diff --git a/packages/express/README.md b/packages/express/README.md new file mode 100644 index 00000000..99b3e0df --- /dev/null +++ b/packages/express/README.md @@ -0,0 +1,81 @@ + +## Requirements + + AWS X-Ray SDK Core (aws-xray-sdk-core) + Express 4.14.0 or greater + +## AWS X-Ray and Express + +The AWS X-Ray Express package automatically records information for incoming and outgoing +requests and responses, via the middleware functions in this package. + +The AWS X-Ray SDK Core has two modes - `manual` and `automatic`. +Automatic mode uses the Continuation Local Storage package (CLS) and automatically +tracks the current segment and subsegment. This is the default mode. +Manual mode requires that you pass around the segment reference. + +In automatic mode, you can get the current segment/subsegment at any time: + var segment = AWSXRay.getSegment(); + +In manual mode, you can get the base segment off of the request object: + var segment = req.segment; + +## Sampling rates on routes + +Sampling rates are determined by the `aws-xray-sdk-core` package, using the default +sampling file that is provided, or by overriding this with a custom sampling file. +For more information on sampling, see aws-xray-sdk-core [README](/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/core/README.md). + +## Dynamic and fixed naming modes + +The SDK requires that a default segment name is set when using middleware. +If it isn't set, an error is thrown. You can override this value via the `AWS_XRAY_TRACING_NAME` +environment variable. + + app.use(xrayExpress.openSegment('defaultName')); + +The AWS X-Ray SDK Core defaults to a fixed naming mode. This means that each time the middleware creates a new segment for an incoming request, +the name of that segment is set to the default name. In dynamic mode, the segment name can vary between the host header of the request or the default name. +For more information about naming modes, see the aws-xray-sdk-core [README](/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/core/README.md). + +## Automatic mode examples + + var AWSXRay = require('aws-xray-sdk-core'); + var xrayExpress = require('aws-xray-sdk-express'); + var app = express(); + + //... + + app.use(xrayExpress.openSegment('defaultName')); + + app.get('/', function (req, res) { + var segment = AWSXRay.getSegment(); + + //... + + res.render('index'); + }); + + app.use(xrayExpress.closeSegment()); + +## Manual mode examples + + var AWSXRay = require('aws-xray-sdk-core'); + var xrayExpress = require('aws-xray-sdk-express'); + var app = express(); + + //... + + var AWSXRay = require('aws-xray-sdk'); + + app.use(xrayExpress.openSegment('defaultName')); //Required at the start of your routes + + app.get('/', function (req, res) { + var segment = req.segment; + + //... + + res.render('index'); + }); + + app.use(xrayExpress.closeSegment()); //Required at the end of your routes / first in error handling routes diff --git a/packages/express/lib/express_mw.js b/packages/express/lib/express_mw.js new file mode 100644 index 00000000..d0a95277 --- /dev/null +++ b/packages/express/lib/express_mw.js @@ -0,0 +1,105 @@ +/** + * Express middleware module. + * + * Exposes Express middleware functions to enable automated data capturing on a web service. To enable on a Node.js/Express application, + * use 'app.use(AWSXRayExpress.openSegment())' before defining your routes. After your routes, before any extra error + * handling middleware, use 'app.use(AWSXRayExpress.closeSegment())'. + * Use AWSXRay.getSegment() to access the current sub/segment. + * Otherwise, for manual mode, this appends the Segment object to the request object as req.segment. + * @module express_mw + */ + +var AWSXRay = require('aws-xray-sdk-core'); + +var mwUtils = AWSXRay.middleware; +var IncomingRequestData = mwUtils.IncomingRequestData; +var Segment = AWSXRay.Segment; + +var expressMW = { + + /** + * Use 'app.use(AWSXRayExpress.openSegment('defaultName'))' before defining your routes. + * Use AWSXRay.getSegment() to access the current sub/segment. + * Otherwise, for manual mode, this appends the Segment object to the request object as req.segment. + * @param {string} defaultName - The default name for the segment. + * @alias module:express_mw.openSegment + * @returns {function} + */ + + openSegment: function openSegment(defaultName) { + if (!defaultName || typeof defaultName !== 'string') + throw new Error('Default segment name was not supplied. Please provide a string.'); + + mwUtils.setDefaultName(defaultName); + + return function open(req, res, next) { + var amznTraceHeader = mwUtils.processHeaders(req); + var name = mwUtils.resolveName(req.headers.host); + var segment = new Segment(name, amznTraceHeader.Root, amznTraceHeader.Parent); + + mwUtils.resolveSampling(amznTraceHeader, segment, res); + segment.addIncomingRequestData(new IncomingRequestData(req)); + + AWSXRay.getLogger().debug('Starting express segment: { url: ' + req.url + ', name: ' + segment.name + ', trace_id: ' + + segment.trace_id + ', id: ' + segment.id + ', sampled: ' + !segment.notTraced + ' }'); + + res.on('finish', function () { + if (this.statusCode === 429) + segment.addThrottleFlag(); + if (AWSXRay.utils.getCauseTypeFromHttpStatus(this.statusCode)) + segment[AWSXRay.utils.getCauseTypeFromHttpStatus(this.statusCode)] = true; + + segment.http.close(this); + segment.close(); + + AWSXRay.getLogger().debug('Closed express segment successfully: { url: ' + req.url + ', name: ' + segment.name + ', trace_id: ' + + segment.trace_id + ', id: ' + segment.id + ', sampled: ' + !segment.notTraced + ' }'); + }); + + if (AWSXRay.isAutomaticMode()) { + var ns = AWSXRay.getNamespace(); + ns.bindEmitter(req); + ns.bindEmitter(res); + + ns.run(function () { + AWSXRay.setSegment(segment); + + if (next) { next(); } + }); + } else { + req.segment = segment; + if (next) { next(); } + } + }; + }, + + /** + * After your routes, before any extra error handling middleware, use 'app.use(AWSXRayExpress.closeSegment())'. + * @alias module:express_mw.closeSegment + * @returns {function} + */ + + closeSegment: function closeSegment() { + return function close(err, req, res, next) { + var segment = AWSXRay.resolveSegment(req.segment); + + if (segment && err) { + segment.close(err); + + AWSXRay.getLogger().debug('Closed express segment with error: { url: ' + req.url + ', name: ' + segment.name + ', trace_id: ' + + segment.trace_id + ', id: ' + segment.id + ', sampled: ' + !segment.notTraced + ' }'); + + } else if (segment) { + segment.close(); + + AWSXRay.getLogger().debug('Closed express segment successfully: { url: ' + req.url + ', name: ' + segment.name + ', trace_id: ' + + segment.trace_id + ', id: ' + segment.id + ', sampled: ' + !segment.notTraced + ' }'); + } + + if (next) + next(err); + }; + } +}; + +module.exports = expressMW; diff --git a/packages/express/lib/index.js b/packages/express/lib/index.js new file mode 100644 index 00000000..a8060555 --- /dev/null +++ b/packages/express/lib/index.js @@ -0,0 +1,2 @@ +// Convenience file to require the SDK from the root of the repository +module.exports = require('./express_mw'); diff --git a/packages/express/package.json b/packages/express/package.json new file mode 100644 index 00000000..f0bac4b1 --- /dev/null +++ b/packages/express/package.json @@ -0,0 +1,45 @@ +{ + "name": "aws-xray-sdk-express", + "version": "1.1.5", + "description": "AWS X-Ray Middleware for Express (Javascript)", + "author": "Amazon Web Services", + "contributors": [ + "Sandra McMullen " + ], + "main": "lib/index.js", + "engines": { + "node": ">= 0.8.0" + }, + "directories": { + "test": "test" + }, + "peerDependencies": { + "aws-xray-sdk-core": "^1.1.0" + }, + "devDependencies": { + "aws-xray-sdk-core": "^1.1.5", + "chai": "^3.5.0", + "eslint": "^3.10.2", + "grunt": "^1.0.1", + "grunt-contrib-clean": "^1.0.0", + "grunt-jsdoc": "^2.1.0", + "mocha": "^3.0.2", + "nock": "^8.0.0", + "sinon": "^1.17.5", + "sinon-chai": "^2.8.0" + }, + "scripts": { + "test": "./node_modules/.bin/mocha --recursive ./test/ -R spec" + }, + "keywords": [ + "amazon", + "api", + "aws", + "express", + "xray", + "x-ray", + "x ray" + ], + "license": "Apache-2.0", + "repository": "/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/express" +} diff --git a/packages/express/test/test_utils.js b/packages/express/test/test_utils.js new file mode 100644 index 00000000..2057d00f --- /dev/null +++ b/packages/express/test/test_utils.js @@ -0,0 +1,17 @@ +var EventEmitter = require('events'); +var util = require('util'); + +var TestUtils = {}; + +TestUtils.TestEmitter = function TestEmitter() { + EventEmitter.call(this); +}; + +util.inherits(TestUtils.TestEmitter, EventEmitter); + +TestUtils.onEvent = function onEvent(event, fcn) { + this.emitter.on(event, fcn.bind(this)); + return this; +}; + +module.exports = TestUtils; diff --git a/packages/express/test/unit/express_mw.test.js b/packages/express/test/unit/express_mw.test.js new file mode 100644 index 00000000..b9cd3605 --- /dev/null +++ b/packages/express/test/unit/express_mw.test.js @@ -0,0 +1,182 @@ +var xray = require('aws-xray-sdk-core'); +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var expressMW = require('../../lib/express_mw'); +var SegmentEmitter = require('../../../core/lib/segment_emitter.js'); + +var mwUtils = xray.middleware; +var IncomingRequestData = xray.middleware.IncomingRequestData; +var Segment = xray.Segment; + +chai.should(); +chai.use(sinonChai); + +var utils = require('../test_utils'); + +describe('Express middleware', function() { + var defaultName = 'defaultName'; + var hostName = 'expressMiddlewareTest'; + var parentId = '2c7ad569f5d6ff149137be86'; + var traceId = '1-f9194208-2c7ad569f5d6ff149137be86'; + + describe('#openSegment', function() { + var openSegment = expressMW.openSegment; + + it('should throw an error if no default name is supplied', function() { + assert.throws(openSegment); + }); + + it('should return a middleware function', function() { + assert.isFunction(openSegment(defaultName)); + }); + }); + + describe('#open', function() { + var req, res, sandbox; + var open = expressMW.openSegment(defaultName); + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(xray, 'isAutomaticMode').returns(false); + + req = { + method: 'GET', + url: '/', + connection: { + remoteAddress: 'localhost' + }, + headers: { host: 'myHostName' } + }; + + req.emitter = new utils.TestEmitter(); + req.on = utils.onEvent; + + res = { + req: req, + header: {} + }; + res.emitter = new utils.TestEmitter(); + res.on = utils.onEvent; + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('when handling a request', function() { + var addReqDataSpy, newSegmentSpy, onEventStub, processHeadersStub, resolveNameStub, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + newSegmentSpy = sandbox.spy(Segment.prototype, 'init'); + addReqDataSpy = sandbox.spy(Segment.prototype, 'addIncomingRequestData'); + + onEventStub = sandbox.stub(res, 'on'); + + processHeadersStub = sandbox.stub(mwUtils, 'processHeaders').returns({ Root: traceId, Parent: parentId, Sampled: '0' }); + resolveNameStub = sandbox.stub(mwUtils, 'resolveName').returns(defaultName); + + req.headers = { host: hostName }; + }); + + afterEach(function() { + sandbox.restore(); + delete process.env.AWS_XRAY_TRACING_NAME; + }); + + it('should call mwUtils.processHeaders to split the headers, if any', function() { + open(req, res); + + processHeadersStub.should.have.been.calledOnce; + processHeadersStub.should.have.been.calledWithExactly(req); + }); + + it('should call mwUtils.resolveName to find the name of the segment', function() { + open(req, res); + + resolveNameStub.should.have.been.calledOnce; + resolveNameStub.should.have.been.calledWithExactly(req.headers.host); + }); + + it('should create a new segment', function() { + open(req, res); + + newSegmentSpy.should.have.been.calledOnce; + newSegmentSpy.should.have.been.calledWithExactly(defaultName, traceId, parentId); + }); + + it('should add a new http property on the segment', function() { + open(req, res); + + addReqDataSpy.should.have.been.calledOnce; + addReqDataSpy.should.have.been.calledWithExactly(sinon.match.instanceOf(IncomingRequestData)); + }); + + it('should add a finish event to the response', function() { + open(req, res); + + onEventStub.should.have.been.calledOnce; + onEventStub.should.have.been.calledWithExactly('finish', sinon.match.typeOf('function')); + }); + }); + + describe('when the request completes', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(SegmentEmitter); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should add the error flag on the segment on 4xx', function() { + var getCauseStub = sandbox.stub(xray.utils, 'getCauseTypeFromHttpStatus').returns('error'); + open(req, res); + + res.statusCode = 400; + res.emitter.emit('finish'); + + assert.equal(req.segment.error, true); + getCauseStub.should.have.been.calledWith(400); + }); + + it('should add the fault flag on the segment on 5xx', function() { + var getCauseStub = sandbox.stub(xray.utils, 'getCauseTypeFromHttpStatus').returns('fault'); + open(req, res); + + res.statusCode = 500; + res.emitter.emit('finish'); + + assert.equal(req.segment.fault, true); + getCauseStub.should.have.been.calledWith(500); + }); + + it('should add the throttle flag and error flag on the segment on a 429', function() { + open(req, res); + + res.statusCode = 429; + res.emitter.emit('finish'); + + assert.equal(req.segment.throttle, true); + assert.equal(req.segment.error, true); + }); + + it('should add nothing on anything else', function() { + open(req, res); + + res.statusCode = 200; + res.emitter.emit('finish'); + + assert.notProperty(req.segment, 'error'); + assert.notProperty(req.segment, 'fault'); + assert.notProperty(req.segment, 'throttle'); + }); + }); + }); +}); diff --git a/packages/full_sdk/.eslintrc.json b/packages/full_sdk/.eslintrc.json new file mode 100644 index 00000000..a3e0b54b --- /dev/null +++ b/packages/full_sdk/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "eol-last": [ + "error", + "always" + ] + } +} diff --git a/packages/full_sdk/.npmignore b/packages/full_sdk/.npmignore new file mode 100644 index 00000000..afb67596 --- /dev/null +++ b/packages/full_sdk/.npmignore @@ -0,0 +1,6 @@ +.npmignore +node_modules +npm-debug.log +docs +AWSXRay.log +Config diff --git a/packages/full_sdk/CHANGELOG.md b/packages/full_sdk/CHANGELOG.md new file mode 100644 index 00000000..8901df91 --- /dev/null +++ b/packages/full_sdk/CHANGELOG.md @@ -0,0 +1,92 @@ +# Changelog for AWS X-Ray SDK for JavaScript + + + +## 1.1.5 +* The X-Ray SDK for Node.js is now an open source project. You can follow the project and submit issues and pull requests on [GitHub](/~https://github.com/aws/aws-xray-sdk-node). + +## 1.1.4 + +* change: Updated aws-xray-sdk-core to 1.1.4. See aws-xray-sdk-core's CHANGELOG.md for package changes. +* change: Updated aws-xray-sdk-express to 1.1.4. No further changes. +* change: Updated aws-xray-sdk-mysql to 1.1.4. No further changes. +* change: Updated aws-xray-sdk-postgres to 1.1.4. No further changes. + +## 1.1.3 +* change: Updated aws-xray-sdk-core to 1.1.3. See aws-xray-sdk-core's CHANGELOG.md for package changes. +* change: Updated aws-xray-sdk-express to 1.1.3. No further changes. +* change: Updated aws-xray-sdk-mysql to 1.1.3. No further changes. +* change: Updated aws-xray-sdk-postgres to 1.1.3. No further changes. + +## 1.1.2 +* change: Updated aws-xray-sdk-core to 1.1.2. See aws-xray-sdk-core's CHANGELOG.md for package changes. +* change: Updated aws-xray-sdk-express to 1.1.2. See aws-xray-sdk-express's CHANGELOG.md for package changes. +* change: Updated aws-xray-sdk-mysql to 1.1.2. No further changes. +* change: Updated aws-xray-sdk-postgres to 1.1.2. No further changes. + +## 1.1.1 +* change: Updated aws-xray-sdk-core to 1.1.1. See aws-xray-sdk-core's CHANGELOG.md for package changes. +* change: Updated aws-xray-sdk-express to 1.1.1. See aws-xray-sdk-express's CHANGELOG.md for package changes. +* change: Updated aws-xray-sdk-mysql to 1.1.1. No further changes. +* change: Updated aws-xray-sdk-postgres to 1.1.1. No further changes. + +## 1.1.0 +* change: Updated aws-xray-sdk-core from 1.0.0-beta to 1.1.0. See aws-xray-sdk-core's CHANGELOG.md for package changes. +* change: Updated aws-xray-sdk-express from 1.0.0-beta to 1.1.0. No further changes. +* change: Updated aws-xray-sdk-mysql from 1.0.0-beta to 1.1.0. No further changes. +* change: Updated aws-xray-sdk-postgres from 1.0.0-beta to 1.1.0. No further changes. + +## 1.0.6-beta +* **BREAKING** change: added a `setContextMissingStrategy` function to the `AWSXRay` module. This allows configuration of the exception behavior exhibited when trace context is not properly propagated. The behavior can be configured in code. Alternatively, the environment variable `AWS_XRAY_CONTEXT_MISSING` can be used (overrides any modes set in code). Valid values for this environment variable are currently (case insensitive) `RUNTIME_ERROR` and `LOG_ERROR`. The default behavior is changing from `LOG_ERROR` to `RUNTIME_ERROR`, i.e. by default, an exception will be thrown on missing context. +* **BREAKING** change: Renamed the capture module's exported functions `capture`, `captureAsync`, and `captureCallback` to `captureFunc`, `captureAsyncFunc`, and `captureCallbackFunc`, respectively. +* change: Changed the behavior when loading multiple plugins to set the segment origin using the latest-loaded plugin. +* change: Removed the `Subsegment` `addRemote` setter. `Subsegment` namespaces can be set directly using the `namespace` attribute. +* change: Changed the name of the `Segment`/`Subsegment` `addThrottle` method to `addThrottleFlag`. +* change: Removed the `type` parameter from the `Segment`/`Subsegment` `addError` and `close` methods. +* feature: Added `addFaultFlag` and `addErrorFlag` methods to `Segment` and `Subsegment`. +* feature: Added additional version information to the `aws.xray` segment property. +* bugfix: Fixed issue where loading multiple plugins using `XRay.config` did not set all applicable data in the segment's `aws` attribute. + +## 1.0.5-beta +* change: Changed the expected sampling file format. See README for details. +* change: Removed the default file logger. You can set a custom logger via AWSXRay.setLogger(). +* change: Moved the AWSXRay.setSamplingRules() function to AWSXRay.middleware.setSamplingRules(). +* change: Changed various AWS DynamoDB params on the AWS param whitelist file. +* change: Removed 'paths' property on segment and subsegment cause blocks for error capturing. +* change: Changed logging max backlog files to 3 with max size of 300kB each. +* feature: Added AWSXRay.setStreamingThreshold() and partial subsegment streaming. +* feature: Added an 'x_forwarded_for' flag attribute in regard to capturing inbound http request data. +* feature: Added AWS Lambda Invoke and InvokeAsync params to the AWS param whitelist file. +* feature: Added a configuration option to set a custom logger via AWSXRay.setLogger(). +* feature: Added 'error' and 'fault' flags for HTTP response statuses for outbound calls. +* feature: Added 'For Node.js' on SDK version capturing. +* bugfix: Fixed issue with throttle flag on downstream AWS calls. +* bugfix: Fixed issue where 'error' and 'fault' flags were being set improperly. +* bugfix: Fixed issue where sampling rules were not being observed. +* bugfix: Fixed issue where sampling rules validation was not checking the expected format. +* bugfix: Fixed issue where an error loading the AWS Elastic Beanstalk plugin would be improperly logged. +* bugfix: Fixed issue where calling addError and passing a string would throw an error. + +## 1.0.4-beta +* change: Removed microtime dependency. +* change: Improved the detection of throttling errors from AWS services. +* change: Moved the aws.xray.sdk.version segment attribute to aws.xray.sdk_version. + +## 1.0.3-beta +* bugfix: Added microtime dependency. + +## 1.0.2-beta +* change: Added the AWS_XRAY_TRACING_NAME environment variable. XRAY_TRACING_NAME will be deprecated on GA release. +* change: Renamed XRAY_DEBUG_MODE environment variable to AWS_XRAY_DEBUG_MODE. +* change: Removed XRAY_TRACING_DEFAULT_NAME environment variable. +* change: Removed AWSXRay.setDefaultName(). A default name is now required via AWSXRay.express.openSegment(). +* change: Minimum AWS SDK version required for capturing is 2.7.15. +* feature: Added AWS_XRAY_DAEMON_ADDRESS environment variable. +* feature: Added AWSXRay.setDaemonAddress(
) function that accepts an IPv4 address. See README for details. +* feature: Introducing 'fixed' (default) and 'dynamic' naming modes. Enable dynamic mode via AWSXRay.middleware.enableDynamicNaming(). +* feature: Added a 'remote' attribute flag to mark errors from downstream services. +* feature: Added 'service' 'version' attribute to capture NPM module version of your application. +* feature: Added 'aws' 'sdk' version' attribute to capture AWS X-RAY SDK version. +* bugfix: Fixed broken logging statement on AWS Client patcher. +* bugfix: Changed segment emitter to keep UPD socket open instead of closing on complete. +* bugfix: Fixed issue with loading AWS Elastic Beanstalk data and origin name. diff --git a/packages/full_sdk/LICENSE b/packages/full_sdk/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/packages/full_sdk/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/full_sdk/NOTICE.txt b/packages/full_sdk/NOTICE.txt new file mode 100644 index 00000000..48f6992d --- /dev/null +++ b/packages/full_sdk/NOTICE.txt @@ -0,0 +1,5 @@ +AWS X-Ray SDK for JavaScript +Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). diff --git a/packages/full_sdk/README.md b/packages/full_sdk/README.md new file mode 100644 index 00000000..ef881f7a --- /dev/null +++ b/packages/full_sdk/README.md @@ -0,0 +1,31 @@ + +## Requirements + +AWS SDK v2.7.15 or greater (if using `captureAWS` or `captureAWSClient`) +Express 4.14.0 or greater (if using Express and the associated X-Ray middleware) +MySQL 2.12.0 or greater (if using `captureMySQL`) +Postgres 6.1.0 or greater (if using `capturePostgres`) + +## AWS X-Ray + +The AWS X-Ray SDK automatically records information for incoming and outgoing requests and responses (via middleware), as well as local data +such as function calls, time, variables (via metadata and annotations), even EC2 instance data (via plugins). + +Although the AWS X-Ray SDK was originally intended to capture request/response data on a web app, the SDK provides functionality for use cases +outside this as well. The SDK exposes the 'Segment' and 'Subsegment' objects to create your own capturing mechanisms, middleware, etc. + +This package includes all other AWS X-Ray packages except `aws-xray-sdk-restify` which is still in beta. + + aws-xray-sdk-core + aws-xray-sdk-express + aws-xray-sdk-postgres + aws-xray-sdk-mysql + +## Setup + +The core package contains the base SDK functionality. Please see the aws-xray-sdk-core [README.md](/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/core/README.md) for more details. + +### Support for web frameworks + +* [Express](/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/express) +* [Restify](/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/restify)(beta) diff --git a/packages/full_sdk/lib/index.js b/packages/full_sdk/lib/index.js new file mode 100644 index 00000000..5c5e18aa --- /dev/null +++ b/packages/full_sdk/lib/index.js @@ -0,0 +1,18 @@ +// Convenience file to require the SDK from the root of the repository +var AWSXRay = require('aws-xray-sdk-core'); +AWSXRay.express = require('aws-xray-sdk-express'); +AWSXRay.captureMySQL = require('aws-xray-sdk-mysql'); +AWSXRay.capturePostgres = require('aws-xray-sdk-postgres'); + +var UNKNOWN = 'unknown'; +var pkginfo = module.filename ? require('pkginfo') : function() {}; +pkginfo(module); + +(function () { + var sdkData = AWSXRay.SegmentUtils.sdkData || { sdk: 'X-Ray for Node.js' }; + sdkData.sdk_version = (module.exports && module.exports.version) ? module.exports.version : UNKNOWN; + sdkData.package = (module.exports && module.exports.name) ? module.exports.name : UNKNOWN; + AWSXRay.SegmentUtils.setSDKData(sdkData); +})(); + +module.exports = AWSXRay; diff --git a/packages/full_sdk/package.json b/packages/full_sdk/package.json new file mode 100644 index 00000000..4ce23f92 --- /dev/null +++ b/packages/full_sdk/package.json @@ -0,0 +1,30 @@ +{ + "name": "aws-xray-sdk", + "version": "1.1.5", + "description": "AWS X-Ray SDK for Javascript", + "author": "Amazon Web Services", + "contributors": [ + "Sandra McMullen " + ], + "main": "lib/index.js", + "engines": { + "node": ">= 0.8.0" + }, + "dependencies": { + "aws-xray-sdk-core": "^1.1.5", + "aws-xray-sdk-express": "^1.1.5", + "aws-xray-sdk-mysql": "^1.1.5", + "aws-xray-sdk-postgres": "^1.1.5", + "pkginfo": "^0.4.0" + }, + "keywords": [ + "amazon", + "api", + "aws", + "xray", + "x-ray", + "x ray" + ], + "license": "Apache-2.0", + "repository": "/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/full_sdk" +} diff --git a/packages/mysql/.eslintrc.json b/packages/mysql/.eslintrc.json new file mode 100644 index 00000000..a3e0b54b --- /dev/null +++ b/packages/mysql/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "eol-last": [ + "error", + "always" + ] + } +} diff --git a/packages/mysql/.npmignore b/packages/mysql/.npmignore new file mode 100644 index 00000000..afb67596 --- /dev/null +++ b/packages/mysql/.npmignore @@ -0,0 +1,6 @@ +.npmignore +node_modules +npm-debug.log +docs +AWSXRay.log +Config diff --git a/packages/mysql/LICENSE b/packages/mysql/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/packages/mysql/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/mysql/NOTICE.txt b/packages/mysql/NOTICE.txt new file mode 100644 index 00000000..45bd6a94 --- /dev/null +++ b/packages/mysql/NOTICE.txt @@ -0,0 +1,5 @@ +AWS X-Ray SDK MySQL for JavaScript +Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). diff --git a/packages/mysql/README.md b/packages/mysql/README.md new file mode 100644 index 00000000..ce1e9801 --- /dev/null +++ b/packages/mysql/README.md @@ -0,0 +1,70 @@ + +## Requirements + + AWS X-Ray SDK Core + MySQL 2.12.0 or greater + +## AWS X-Ray and MySQL + +The AWS X-Ray MySQL package automatically records query information and request and +response data. Simply patch the MySQL package via `captureMySQL` as shown below. + +The AWS X-Ray SDK Core has two modes - `manual` and `automatic`. +Automatic mode uses the Continuation Local Storage package (CLS) and automatically +tracks the current segment and subsegment. This is the default mode. +Manual mode requires that you pass around the segment reference. See the examples below. + +### Environment variables + + MYSQL_DATABASE_VERSION Sets additional data for the sql subsegment. + MYSQL_DRIVER_VERSION Sets additional data for the sql subsegment. + +## Automatic mode example + + var AWSXRay = require('aws-xray-sdk-core'); + var captureMySQL = require('aws-xray-sdk-mysql'); + + var mysql = captureMySQL(require('mysql')); + + var config = { ... }; + + ... + + var connection = mysql.createConnection(config); + + connection.query('SELECT * FROM cats', function(err, rows) { + //Automatically captures query information and errors (if any) + }); + + ... + + var pool = mysql.createPool(config); + + pool.query('SELECT * FROM cats', function(err, rows, fields) { + //Automatically captures query information and errors (if any) + } + +## Manual mode example + + var AWSXRay = require('aws-xray-sdk-core'); + var captureMySQL = require('aws-xray-sdk-mysql'); + + var mysql = captureMySQL(require('mysql')); + + var config = { ... }; + + ... + + var connection = mysql.createConnection(config); + + connection.query('SELECT * FROM cats', function(err, rows) { + //Automatically captures query information and errors (if any) + }); + + ... + + var pool = mysql.createPool(config); + + pool.query('SELECT * FROM cats', function(err, rows, fields) { + //Automatically captures query information and errors (if any) + }, segment); diff --git a/packages/mysql/lib/index.js b/packages/mysql/lib/index.js new file mode 100644 index 00000000..4812f155 --- /dev/null +++ b/packages/mysql/lib/index.js @@ -0,0 +1,2 @@ +// Convenience file to require the SDK from the root of the repository +module.exports = require('./mysql_p'); diff --git a/packages/mysql/lib/mysql_p.js b/packages/mysql/lib/mysql_p.js new file mode 100644 index 00000000..fd9eddfb --- /dev/null +++ b/packages/mysql/lib/mysql_p.js @@ -0,0 +1,144 @@ +/** + * @module mysql_p + */ + +var AWSXRay = require('aws-xray-sdk-core'); +var SqlData = AWSXRay.database.SqlData; + +var DATABASE_VERS = process.env.MYSQL_DATABASE_VERSION; +var DRIVER_VERS = process.env.MYSQL_DRIVER_VERSION; + +var PREPARED = 'statement'; + +/** + * Patches the Node MySQL client to automatically capture query information for the segment. + * Connection.query and pool.query calls are automatically captured. + * In manual mode, connection.query and pool.query require a segment or subsegment object + * as an additional argument as the last argument for the query. + * @param {mysql} module - The MySQL npm module. + * @returns {mysql} + * @see /~https://github.com/mysqljs/mysql + */ + +module.exports = function captureMySQL(mysql) { + if (mysql.__createConnection) + return mysql; + + patchCreateConnection(mysql); + patchCreatePool(mysql); + + return mysql; +}; + +function patchCreateConnection(mysql) { + var baseFcn = '__createConnection'; + mysql[baseFcn] = mysql['createConnection']; + + mysql['createConnection'] = function patchedCreateConnection() { + var connection = mysql[baseFcn].apply(connection, arguments); + connection.__query = connection.query; + connection.query = captureQuery; + + return connection; + }; +} + +function patchCreatePool(mysql) { + var baseFcn = '__createPool'; + mysql[baseFcn] = mysql['createPool']; + + mysql['createPool'] = function patchedCreatePool() { + var pool = mysql[baseFcn].apply(pool, arguments); + pool.__query = pool.query; + pool.query = captureQuery; + + return pool; + }; +} + +function resolveArguments(argsObj) { + var args = {}; + + if (argsObj && argsObj.length > 0) { + args.sql = argsObj[0]; + args.values = typeof argsObj[1] !== 'function' ? argsObj[1] : null; + args.callback = !args.values ? argsObj[1] : (typeof argsObj[2] === 'function' ? argsObj[2] : null); + args.segment = (argsObj[argsObj.length-1].constructor && (argsObj[argsObj.length-1].constructor.name === 'Segment' || + argsObj[argsObj.length-1].constructor.name === 'Subsegment')) ? argsObj[argsObj.length-1] : null; + } + + return args; +} + +function captureQuery() { + var args = resolveArguments(arguments); + var parent = AWSXRay.resolveSegment(args.segment); + var query; + + if (args.segment) + delete arguments[arguments.length-1]; + + if (!parent) { + AWSXRay.getLogger().info('Failed to capture MySQL. Cannot resolve sub/segment.'); + return this.__query.apply(this, arguments); + } + + var config = this.config.connectionConfig || this.config; + var subsegment = parent.addNewSubsegment(config.database + '@' + config.host); + + if (args.callback) { + var cb = args.callback; + + if (AWSXRay.isAutomaticMode()) { + args.callback = function autoContext(err, data) { + var session = AWSXRay.getNamespace(); + + session.run(function() { + AWSXRay.setSegment(subsegment); + cb(err, data); + }); + + subsegment.close(err); + }; + } else { + args.callback = function wrappedCallback(err, data) { + cb(err, data); + subsegment.close(err); + }; + } + } + + query = this.__query.call(this, args.sql, args.values, args.callback); + + if (!args.callback) { + query.on('end', function() { + subsegment.close(); + }); + + var errorCapturer = function (err) { + subsegment.close(err); + + if (this._events && this._events.error && this._events.error.length === 1) { + this.removeListener('error', errorCapturer); + this.emit('error', err); + } + }; + + query.on('error', errorCapturer); + } + + subsegment.addSqlData(createSqlData(config, query)); + subsegment.namespace = 'remote'; + + return query; +} + +function createSqlData(config, query) { + var queryType = query.values ? PREPARED : null; + + var data = new SqlData(DATABASE_VERS, DRIVER_VERS, config.user, + config.host + ':' + config.port + '/' + config.database, + queryType); + + return data; +} diff --git a/packages/mysql/package.json b/packages/mysql/package.json new file mode 100644 index 00000000..f40bed3b --- /dev/null +++ b/packages/mysql/package.json @@ -0,0 +1,45 @@ +{ + "name": "aws-xray-sdk-mysql", + "version": "1.1.5", + "description": "AWS X-Ray Patcher for MySQL (Javascript)", + "author": "Amazon Web Services", + "contributors": [ + "Sandra McMullen " + ], + "main": "lib/index.js", + "engines": { + "node": ">= 0.8.0" + }, + "directories": { + "test": "test" + }, + "peerDependencies": { + "aws-xray-sdk-core": "^1.1.0" + }, + "devDependencies": { + "aws-xray-sdk-core": "^1.1.5", + "chai": "^3.5.0", + "eslint": "^3.10.2", + "grunt": "^1.0.1", + "grunt-contrib-clean": "^1.0.0", + "grunt-jsdoc": "^2.1.0", + "mocha": "^3.0.2", + "nock": "^8.0.0", + "sinon": "^1.17.5", + "sinon-chai": "^2.8.0" + }, + "scripts": { + "test": "./node_modules/.bin/mocha --recursive ./test/ -R spec" + }, + "keywords": [ + "amazon", + "api", + "aws", + "mysql", + "xray", + "x-ray", + "x ray" + ], + "license": "Apache-2.0", + "repository": "/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/mysql" +} diff --git a/packages/mysql/test/test_utils.js b/packages/mysql/test/test_utils.js new file mode 100644 index 00000000..2057d00f --- /dev/null +++ b/packages/mysql/test/test_utils.js @@ -0,0 +1,17 @@ +var EventEmitter = require('events'); +var util = require('util'); + +var TestUtils = {}; + +TestUtils.TestEmitter = function TestEmitter() { + EventEmitter.call(this); +}; + +util.inherits(TestUtils.TestEmitter, EventEmitter); + +TestUtils.onEvent = function onEvent(event, fcn) { + this.emitter.on(event, fcn.bind(this)); + return this; +}; + +module.exports = TestUtils; diff --git a/packages/mysql/test/unit/mysql_p.test.js b/packages/mysql/test/unit/mysql_p.test.js new file mode 100644 index 00000000..1f35e3df --- /dev/null +++ b/packages/mysql/test/unit/mysql_p.test.js @@ -0,0 +1,148 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var AWSXRay = require('aws-xray-sdk-core'); + +var captureMySQL = require('../../lib/mysql_p'); +var Segment = AWSXRay.Segment; +var SqlData = AWSXRay.database.SqlData; +var TestEmitter = require('../test_utils').TestEmitter; + +chai.should(); +chai.use(sinonChai); + +describe('captureMySQL', function() { + var err = new Error('An error has been encountered.'); + + describe('#captureMySQL', function() { + it('should patch all the create functions the return the module', function() { + var mysql = { + createConnection: function() {}, + createPool: function() {}, + createPoolCluster: function() {} + }; + + var patched = captureMySQL(mysql); + + assert.property(patched, '__createConnection'); + assert.property(patched, '__createPool'); + + assert.equal(patched.createConnection.name, 'patchedCreateConnection'); + assert.equal(patched.createPool.name, 'patchedCreatePool'); + }); + }); + + describe('#captureQuery', function() { + describe('for basic connections', function() { + var conn, connectionObj, mysql, query, queryObj, sandbox, segment, stubAddNew, stubBaseQuery, subsegment; + + before(function() { + conn = { + config: { + user: 'mcmuls', + host: 'database.location', + port: '8080', + database: 'myTestDb' + }, + query: function() {} + }; + + mysql = { createConnection: function() { return conn; } }; + mysql = captureMySQL(mysql); + connectionObj = mysql.createConnection(); + }); + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + segment = new Segment('test'); + subsegment = segment.addNewSubsegment('testSub'); + + queryObj = new TestEmitter(); + queryObj.sql = 'sql statement here'; + queryObj.values = ['hello', 'there']; + + sandbox = sinon.sandbox.create(); + stubBaseQuery = sandbox.stub(connectionObj, '__query').returns(queryObj); + sandbox.stub(AWSXRay, 'getSegment').returns(segment); + stubAddNew = sandbox.stub(segment, 'addNewSubsegment').returns(subsegment); + sandbox.stub(AWSXRay, 'isAutomaticMode').returns(true); + query = connectionObj.query; + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create a new subsegment using database and host', function() { + var config = conn.config; + query.call(connectionObj, 'sql here'); + + stubAddNew.should.have.been.calledWithExactly(config.database + '@' + config.host); + }); + + it('should add the sql data to the subsegment', function() { + var stubAddSql = sandbox.stub(subsegment, 'addSqlData'); + var stubDataInit = sandbox.stub(SqlData.prototype, 'init'); + var config = conn.config; + + query.call(connectionObj, 'sql here'); + + stubDataInit.should.have.been.calledWithExactly(undefined, undefined, config.user, + config.host + ':' + config.port + '/' + config.database, 'statement'); + stubAddSql.should.have.been.calledWithExactly(sinon.match.instanceOf(SqlData)); + }); + + it('should start a new automatic context and close the subsegment via the callback if supplied', function(done) { + var stubClose = sandbox.stub(subsegment, 'close'); + var session = { run: function(fcn) { fcn(); }}; + var stubRun = sandbox.stub(session, 'run'); + + sandbox.stub(AWSXRay, 'getNamespace').returns(session); + query.call(connectionObj, 'sql here', function() {}); + + stubBaseQuery.should.have.been.calledWith(sinon.match.string, null, sinon.match.func); + assert.equal(stubBaseQuery.args[0][2].name, 'autoContext'); + stubBaseQuery.args[0][2].call(queryObj); + + setTimeout(function() { + stubClose.should.always.have.been.calledWith(); + stubRun.should.have.been.calledOnce; + done(); + }, 50); + }); + + it('should capture the error via the callback if supplied', function(done) { + var stubClose = sandbox.stub(subsegment, 'close'); + + query.call(connectionObj, 'sql here', function() {}); + stubBaseQuery.args[0][2].call(queryObj, err); + + setTimeout(function() { + stubClose.should.have.been.calledWithExactly(err); + done(); + }, 50); + }); + + it('should close the subsegment via the event if the callback is missing', function() { + var stubClose = sandbox.stub(subsegment, 'close'); + query.call(connectionObj); + + queryObj.emit('end'); + stubClose.should.always.have.been.calledWithExactly(); + }); + + it('should capture the error via the event if the callback is missing', function() { + var stubClose = sandbox.stub(subsegment, 'close'); + query.call(connectionObj); + + assert.throws(function() { + queryObj.emit('error', err); + }); + + stubClose.should.have.been.calledWithExactly(err); + }); + }); + }); +}); diff --git a/packages/postgres/.eslintrc.json b/packages/postgres/.eslintrc.json new file mode 100644 index 00000000..a3e0b54b --- /dev/null +++ b/packages/postgres/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "eol-last": [ + "error", + "always" + ] + } +} diff --git a/packages/postgres/.npmignore b/packages/postgres/.npmignore new file mode 100644 index 00000000..afb67596 --- /dev/null +++ b/packages/postgres/.npmignore @@ -0,0 +1,6 @@ +.npmignore +node_modules +npm-debug.log +docs +AWSXRay.log +Config diff --git a/packages/postgres/LICENSE b/packages/postgres/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/packages/postgres/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/postgres/NOTICE.txt b/packages/postgres/NOTICE.txt new file mode 100644 index 00000000..4e25278e --- /dev/null +++ b/packages/postgres/NOTICE.txt @@ -0,0 +1,5 @@ +AWS X-Ray SDK Postgres for JavaScript +Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). diff --git a/packages/postgres/README.md b/packages/postgres/README.md new file mode 100644 index 00000000..3a140af5 --- /dev/null +++ b/packages/postgres/README.md @@ -0,0 +1,82 @@ + +## Requirements + + AWS X-Ray SDK Core + Postgres 6.1.0 or greater + +## AWS X-Ray and Postgres + +The AWS X-Ray Postgres package automatically records query information and request +and response data. Simply patch the Postgres package via `capturePostgres` as shown below. + +The AWS X-Ray SDK Core has two modes - `manual` and `automatic`. +Automatic mode uses the Continuation Local Storage package (CLS) and automatically +tracks the current segment and subsegment. This is the default mode. +Manual mode requires that you pass around the segment reference. See the examples below. + +### Environment variables + + POSTGRES_DATABASE_VERSION Sets additional data for the sql subsegment. + POSTGRES_DRIVER_VERSION Sets additional data for the sql subsegment. + +### Automatic mode example + + var AWSXRay = require('aws-xray-sdk-core'); + var capturePostgres = require('aws-xray-sdk-postgres'); + + var pg = capturePostgres(require('pg')); + + ... + + var client = new pg.Client(); + + client.connect(function (err) { + ... + + client.query({name: 'moop', text: 'SELECT $1::text as name'}, ['brianc'], function (err, result) { + //Automatically captures query information and errors (if any) + }); + }); + + ... + + var pool = new pg.Pool(config); + pool.connect(function(err, client, done) { + if(err) { + return console.error('error fetching client from pool', err); + } + var query = client.query('SELECT * FROM mytable', function(err, result) { + //Automatically captures query information and errors (if any) + }); + }); + +### Manual mode example + + var AWSXRay = require('aws-xray-sdk-core'); + var capturePostgres = require('aws-xray-sdk-postgres'); + + var pg = capturePostgres(require('pg')); + + ... + + var client = new pg.Client(); + + client.connect(function (err) { + ... + + client.query({name: 'moop', text: 'SELECT $1::text as name'}, ['mcmuls'], function (err, result) { + //Automatically captures query information and errors (if any) + }); + }); + + ... + + var pool = new pg.Pool(config); + pool.connect(function(err, client, done) { + if(err) { + return console.error('error fetching client from pool', err); + } + var query = client.query('SELECT * FROM mytable', function(err, result) { + //Automatically captures query information and errors (if any) + }, segment)); + }; diff --git a/packages/postgres/lib/index.js b/packages/postgres/lib/index.js new file mode 100644 index 00000000..938ff53a --- /dev/null +++ b/packages/postgres/lib/index.js @@ -0,0 +1,2 @@ +// Convenience file to require the SDK from the root of the repository +module.exports = require('./postgres_p'); diff --git a/packages/postgres/lib/postgres_p.js b/packages/postgres/lib/postgres_p.js new file mode 100644 index 00000000..f81e3b2e --- /dev/null +++ b/packages/postgres/lib/postgres_p.js @@ -0,0 +1,97 @@ +/** + * @module postgres_p + */ + +var AWSXRay = require('aws-xray-sdk-core'); +var SqlData = AWSXRay.database.SqlData; + +var DATABASE_VERS = process.env.POSTGRES_DATABASE_VERSION; +var DRIVER_VERS = process.env.POSTGRES_DRIVER_VERSION; + +var PREPARED = 'statement'; + +/** + * Patches the Node PostreSQL client to automatically capture query information for the segment. + * Client.query calls are automatically captured. + * In manual mode, client.query requires a sub/segment object + * as an additional argument as the last argument for the query. + * @function + * @param {pg} module - The PostgreSQL npm module. + * @returns {pg} + * @see /~https://github.com/brianc/node-postgres + */ + +module.exports = function capturePostgres(pg) { + if (pg.Client.prototype.__query) + return pg; + + pg.Client.prototype.__query = pg.Client.prototype.query; + + pg.Client.prototype.query = function captureQuery(sql, values, callback, segment) { + var parent = AWSXRay.resolveSegment(segment); + delete arguments[3]; + + if (!parent) { + AWSXRay.getLogger().info('Failed to capture Postgres. Cannot resolve sub/segment.'); + return this.__query.apply(this, arguments); + } + + var subsegment = parent.addNewSubsegment(this.database + '@' + this.host); + subsegment.namespace = 'remote'; + + var query = this.__query.apply(this, arguments); + + subsegment.addSqlData(createSqlData(this.connectionParameters, query)); + + if (typeof query.callback === 'function') { + var cb = query.callback; + + if (AWSXRay.isAutomaticMode()) { + query.callback = function autoContext(err, data) { + var session = AWSXRay.getNamespace(); + + session.run(function() { + AWSXRay.setSegment(subsegment); + cb(err, data); + }); + + subsegment.close(err); + }; + } else { + query.callback = function(err, data) { + cb(err, data, subsegment); + subsegment.close(err); + }; + } + } else { + query.on('end', function() { + subsegment.close(); + }); + + var errorCapturer = function (err) { + subsegment.close(err); + + if (this._events && this._events.error && this._events.error.length === 1) { + this.removeListener('error', errorCapturer); + this.emit('error', err); + } + }; + + query.on('error', errorCapturer); + } + + return query; + }; + + return pg; +}; + +function createSqlData(connParams, query) { + var queryType = query.name ? PREPARED : undefined; + + var data = new SqlData(DATABASE_VERS, DRIVER_VERS, connParams.user, + connParams.host + ':' + connParams.port + '/' + connParams.database, + queryType); + + return data; +} diff --git a/packages/postgres/package.json b/packages/postgres/package.json new file mode 100644 index 00000000..846c8081 --- /dev/null +++ b/packages/postgres/package.json @@ -0,0 +1,45 @@ +{ + "name": "aws-xray-sdk-postgres", + "version": "1.1.5", + "description": "AWS X-Ray Patcher for Postgres (Javascript)", + "author": "Amazon Web Services", + "contributors": [ + "Sandra McMullen " + ], + "main": "lib/index.js", + "engines": { + "node": ">= 0.8.0" + }, + "directories": { + "test": "test" + }, + "peerDependencies": { + "aws-xray-sdk-core": "^1.1.0" + }, + "devDependencies": { + "aws-xray-sdk-core": "^1.1.5", + "chai": "^3.5.0", + "eslint": "^3.10.2", + "grunt": "^1.0.1", + "grunt-contrib-clean": "^1.0.0", + "grunt-jsdoc": "^2.1.0", + "mocha": "^3.0.2", + "nock": "^8.0.0", + "sinon": "^1.17.5", + "sinon-chai": "^2.8.0" + }, + "scripts": { + "test": "./node_modules/.bin/mocha --recursive ./test/ -R spec" + }, + "keywords": [ + "amazon", + "api", + "aws", + "postgres", + "xray", + "x-ray", + "x ray" + ], + "license": "Apache-2.0", + "repository": "/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/postgres" +} diff --git a/packages/postgres/test/test_utils.js b/packages/postgres/test/test_utils.js new file mode 100644 index 00000000..2057d00f --- /dev/null +++ b/packages/postgres/test/test_utils.js @@ -0,0 +1,17 @@ +var EventEmitter = require('events'); +var util = require('util'); + +var TestUtils = {}; + +TestUtils.TestEmitter = function TestEmitter() { + EventEmitter.call(this); +}; + +util.inherits(TestUtils.TestEmitter, EventEmitter); + +TestUtils.onEvent = function onEvent(event, fcn) { + this.emitter.on(event, fcn.bind(this)); + return this; +}; + +module.exports = TestUtils; diff --git a/packages/postgres/test/unit/postgres_p.test.js b/packages/postgres/test/unit/postgres_p.test.js new file mode 100644 index 00000000..e06717b6 --- /dev/null +++ b/packages/postgres/test/unit/postgres_p.test.js @@ -0,0 +1,137 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var AWSXRay = require('aws-xray-sdk-core'); + +var capturePostgres = require('../../lib/postgres_p'); +var Segment = AWSXRay.Segment; +var SqlData = AWSXRay.database.SqlData; +var TestEmitter = require('../test_utils').TestEmitter; + +chai.should(); +chai.use(sinonChai); + +describe('capturePostgres', function() { + var err = new Error('An error has been encountered.'); + + describe('#capturePostgres', function() { + it('should patch the query function the return the module', function() { + var postgres = { Client: { prototype: {query: function () {} }}}; + postgres = capturePostgres(postgres); + assert.equal(postgres.Client.prototype.query.name, 'captureQuery'); + }); + }); + + describe('#captureQuery', function() { + var postgres, query, queryObj, sandbox, segment, stubAddNew, subsegment; + + before(function() { + postgres = { Client: { prototype: { + query: function () {}, + host: 'database.location', + database: 'myTestDb', + connectionParameters: { + user: 'mcmuls', + host: 'database.location', + port: '8080', + database: 'myTestDb' + } + }}}; + postgres = capturePostgres(postgres); + + query = postgres.Client.prototype.query; + postgres = postgres.Client.prototype; + }); + + beforeEach(function() { + segment = new Segment('test'); + subsegment = segment.addNewSubsegment('testSub'); + + queryObj = new TestEmitter(); + queryObj.text = 'sql statement here'; + queryObj.values = ['hello', 'there']; + + sandbox = sinon.sandbox.create(); + sandbox.stub(postgres, '__query').returns(queryObj); + sandbox.stub(AWSXRay, 'getSegment').returns(segment); + stubAddNew = sandbox.stub(segment, 'addNewSubsegment').returns(subsegment); + sandbox.stub(AWSXRay, 'isAutomaticMode').returns(true); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create a new subsegment using database and host', function() { + query.call(postgres); + + stubAddNew.should.have.been.calledWithExactly(postgres.database + '@' + postgres.host); + }); + + it('should add the sql data to the subsegment', function() { + var stubAddSql = sandbox.stub(subsegment, 'addSqlData'); + var stubDataInit = sandbox.stub(SqlData.prototype, 'init'); + var conParam = postgres.connectionParameters; + + query.call(postgres); + + stubDataInit.should.have.been.calledWithExactly(undefined, undefined, conParam.user, + conParam.host + ':' + conParam.port + '/' + conParam.database, undefined); + stubAddSql.should.have.been.calledWithExactly(sinon.match.instanceOf(SqlData)); + }); + + it('should start a new automatic context and close the subsegment via the callback if supplied', function(done) { + var stubClose = sandbox.stub(subsegment, 'close'); + var session = { run: function(fcn) { fcn(); }}; + var stubRun = sandbox.stub(session, 'run'); + + sandbox.stub(AWSXRay, 'getNamespace').returns(session); + + queryObj.callback = function() {}; + + query.call(postgres); + assert.equal(queryObj.callback.name, 'autoContext'); + queryObj.callback(); + + setTimeout(function() { + stubClose.should.always.have.been.calledWith(undefined); + stubRun.should.have.been.calledOnce; + done(); + }, 50); + }); + + it('should capture the error via the callback if supplied', function(done) { + var stubClose = sandbox.stub(subsegment, 'close'); + queryObj.callback = function() {}; + + query.call(postgres); + queryObj.callback(err); + + setTimeout(function() { + stubClose.should.have.been.calledWithExactly(err); + done(); + }, 50); + }); + + it('should close the subsegment via the event if the callback is missing', function() { + var stubClose = sandbox.stub(subsegment, 'close'); + query.call(postgres); + + queryObj.emit('end'); + stubClose.should.always.have.been.calledWithExactly(); + }); + + it('should capture the error via the event if the callback is missing', function() { + var stubClose = sandbox.stub(subsegment, 'close'); + query.call(postgres); + + assert.throws(function() { + queryObj.emit('error', err); + }); + + stubClose.should.have.been.calledWithExactly(err); + }); + }); +}); diff --git a/packages/restify/.eslintrc.json b/packages/restify/.eslintrc.json new file mode 100644 index 00000000..a3e0b54b --- /dev/null +++ b/packages/restify/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "node": true, + "mocha": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": [ + "error", + 2 + ], + "linebreak-style": [ + "error", + "unix" + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "always" + ], + "eol-last": [ + "error", + "always" + ] + } +} diff --git a/packages/restify/.npmignore b/packages/restify/.npmignore new file mode 100644 index 00000000..afb67596 --- /dev/null +++ b/packages/restify/.npmignore @@ -0,0 +1,6 @@ +.npmignore +node_modules +npm-debug.log +docs +AWSXRay.log +Config diff --git a/packages/restify/LICENSE b/packages/restify/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/packages/restify/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/restify/NOTICE.txt b/packages/restify/NOTICE.txt new file mode 100644 index 00000000..b54dcbf9 --- /dev/null +++ b/packages/restify/NOTICE.txt @@ -0,0 +1,5 @@ +AWS X-Ray SDK Restify for JavaScript +Copyright 2012-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +This product includes software developed at +Amazon Web Services, Inc. (http://aws.amazon.com/). diff --git a/packages/restify/README.md b/packages/restify/README.md new file mode 100644 index 00000000..216dace3 --- /dev/null +++ b/packages/restify/README.md @@ -0,0 +1,82 @@ + +## Requirements + + AWS X-Ray SDK Core (aws-xray-sdk-core) + Restify 4.3.0 or greater + +## AWS X-Ray and Restify + +The AWS X-Ray Restify package automatically records information for incoming and outgoing +requests and responses, via the 'enable' function in this package. + +The AWS X-Ray SDK Core has two modes - `manual` and `automatic`. +Automatic mode uses the Continuation Local Storage package (CLS) and automatically +tracks the current segment and subsegment. This is the default mode. +Manual mode requires that you pass around the segment reference. + +In automatic mode, you can get the current segment or subsegment at any time: + var segment = AWSXRay.getSegment(); + +In manual mode, you can get the base segment off of the request object: + var segment = req.segment; + + //If the restify context plugin is being used, it is placed under 'XRaySegment' + var segment = req.get('XRaySegment'); + +## Sampling rates on routes + +Sampling rates are determined by the AWS X-Ray SDK Core package, using the default +sampling file that is provided, or by overriding this with a custom sampling file. +For more information about sampling, see the aws-xray-sdk-core [README](/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/core/README.md). + +## Dynamic and fixed naming modes + +The SDK requires that a default segment name is set. If it isn't set, +an error is thrown. You can override this value via the `AWS_XRAY_TRACING_NAME` +environment variable. + + AWSXRayRestify.enable(server, 'defaultName'); + +The AWS X-Ray SDK Core defaults to a fixed naming mode. This means that each time the handler creates a new segment for an incoming request, +the name of that segment is set to the default name. In dynamic mode, the segment name can vary between the host header of the request or the default name. +For more information about naming modes, see the aws-xray-sdk-core [README](/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/core/README.md). + +## Automatic mode examples + + var AWSXRayRestify = require('aws-xray-sdk-restify'); + var restify = require('restify'); + var server = restify.createServer(); + + //... + + AWSXRayRestify.enable(server, 'defaultName'); + + server.get('/', function (req, res) { + var segment = AWSXRay.getSegment(); + + //... + + res.send('hello'); + }); + + //Error capturing is attached to the server's uncaughtException and after events (for both handled and unhandled errors) + +## Manual mode examples + + var AWSXRayRestify = require('aws-xray-sdk-restify'); + var restify = require('restify'); + var server = restify.createServer(); + + //... + + AWSXRayRestify.enable(server, 'defaultName'); //Required at the start of your routes + + server.get('/', function (req, res) { + var segment = req.segment; + + //... + + res.send('hello'); + }); + + //Error capturing is attached to the server's uncaughtException and after events (for both handled and unhandled errors) diff --git a/packages/restify/lib/index.js b/packages/restify/lib/index.js new file mode 100644 index 00000000..173e1a7d --- /dev/null +++ b/packages/restify/lib/index.js @@ -0,0 +1,2 @@ +// Convenience file to require the SDK from the root of the repository +module.exports = require('./restify_mw'); diff --git a/packages/restify/lib/restify_mw.js b/packages/restify/lib/restify_mw.js new file mode 100644 index 00000000..80bac8a6 --- /dev/null +++ b/packages/restify/lib/restify_mw.js @@ -0,0 +1,94 @@ +/** + * Enable X-Ray for Restify module. + * + * Exposes the 'enable' function to enable automated data capturing for a Restify web service. To enable on a Restify server, + * use 'AWSXRayRestify.enable(, )' before defining your routes. + * Use AWSXRay.getSegment() to access the current sub/segment. + * If in manual mode, this appends the Segment object to the request object as req.segment, or in the case of using + * the Restify context plugin, it will be set as 'XRaySegment'. + * @module restify_mw + */ + +var AWSXRay = require('aws-xray-sdk-core'); + +var mwUtils = AWSXRay.middleware; +var IncomingRequestData = mwUtils.IncomingRequestData; +var Segment = AWSXRay.Segment; + +var restifyMW = { + + /** + * Use 'AWSXRayRestify.enable(server, 'defaultName'))' before defining your routes. + * Use AWSXRay.getSegment() to access the current sub/segment. + * If in manual mode, this appends the Segment object to the request object as req.segment, or in the case of using + * the Restify context plugin, it will be set as 'XRaySegment'. + * @param {Server} server - The Restify server instance. + * @param {string} defaultName - The default name for the segment. + * @alias module:restify_mw.enable + */ + + enable: function enable(server, defaultName) { + if (!server) + throw new Error('Restify server instance to enable was not supplied. Please provide a server.'); + + if (!defaultName || typeof defaultName !== 'string') + throw new Error('Default segment name was not supplied. Please provide a string.'); + + mwUtils.setDefaultName(defaultName); + AWSXRay.getLogger().debug('Enabling AWS X-Ray for Restify.'); + + var segment; + + server.use(function open(req, res, next) { + var amznTraceHeader = mwUtils.processHeaders(req); + var name = mwUtils.resolveName(req.headers.host); + segment = new Segment(name, amznTraceHeader.Root, amznTraceHeader.Parent); + + mwUtils.resolveSampling(amznTraceHeader, segment, res); + segment.addIncomingRequestData(new IncomingRequestData(req)); + + res.on('finish', function() { + if (this.statusCode === 429) + segment.addThrottleFlag(); + if (AWSXRay.utils.getCauseTypeFromHttpStatus(this.statusCode)) + segment[AWSXRay.utils.getCauseTypeFromHttpStatus(this.statusCode)] = true; + + segment.http.close(this); + segment.close(); + }); + + if (AWSXRay.isAutomaticMode()) { + var ns = AWSXRay.getNamespace(); + ns.bindEmitter(req); + ns.bindEmitter(res); + + ns.run(function() { + AWSXRay.setSegment(segment); + + if (next) { next(); } + }); + } else { + if (req.set) + req.set('XRaySegment', segment); + else + req.segment = segment; + + if (next) { next(); } + } + }); + + server.on('uncaughtException', function uncaughtError(req, res, route, err) { + if (segment && err) { + segment.close(err); + } + }); + + server.on('after', function handledError(req, res, route, err) { + if (segment && err) { + segment.addError(err); + } + }); + } +}; + +module.exports = restifyMW; diff --git a/packages/restify/package.json b/packages/restify/package.json new file mode 100644 index 00000000..138ef10d --- /dev/null +++ b/packages/restify/package.json @@ -0,0 +1,40 @@ +{ + "name": "aws-xray-sdk-restify", + "version": "1.0.1-beta", + "description": "Enables AWS X-Ray for Restify (Javascript)", + "author": "Amazon Web Services", + "contributors": [ + "Sandra McMullen " + ], + "main": "lib/index.js", + "engines": { + "node": ">= 4.3" + }, + "directories": { + "test": "test" + }, + "peerDependencies": { + "aws-xray-sdk-core": "^1.1.0" + }, + "devDependencies": { + "chai": "^3.5.0", + "eslint": "^3.10.2", + "mocha": "^3.0.2", + "sinon": "^1.17.5", + "sinon-chai": "^2.8.0" + }, + "scripts": { + "test": "./node_modules/.bin/mocha --recursive ./test/ -R spec" + }, + "keywords": [ + "amazon", + "api", + "aws", + "restify", + "xray", + "x-ray", + "x ray" + ], + "license": "Apache-2.0", + "repository": "/~https://github.com/aws/aws-xray-sdk-node/tree/master/packages/restify" +} diff --git a/packages/restify/test/test_utils.js b/packages/restify/test/test_utils.js new file mode 100644 index 00000000..2057d00f --- /dev/null +++ b/packages/restify/test/test_utils.js @@ -0,0 +1,17 @@ +var EventEmitter = require('events'); +var util = require('util'); + +var TestUtils = {}; + +TestUtils.TestEmitter = function TestEmitter() { + EventEmitter.call(this); +}; + +util.inherits(TestUtils.TestEmitter, EventEmitter); + +TestUtils.onEvent = function onEvent(event, fcn) { + this.emitter.on(event, fcn.bind(this)); + return this; +}; + +module.exports = TestUtils; diff --git a/packages/restify/test/unit/restify_mw.test.js b/packages/restify/test/unit/restify_mw.test.js new file mode 100644 index 00000000..89cafd33 --- /dev/null +++ b/packages/restify/test/unit/restify_mw.test.js @@ -0,0 +1,253 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); +var xray = require('aws-xray-sdk-core'); + +var TestEmitter = require('../test_utils').TestEmitter; + +var restifyMW = require('../../lib/restify_mw'); +var SegmentEmitter = require('../../../core/lib/segment_emitter.js'); + +var mwUtils = xray.middleware; +var IncomingRequestData = xray.middleware.IncomingRequestData; +var Segment = xray.Segment; + +chai.should(); +chai.use(sinonChai); + +var utils = require('../test_utils'); + +describe('Express middleware', function() { + var sandbox, server; + var defaultName = 'defaultName'; + var hostName = 'expressMiddlewareTest'; + var parentId = '2c7ad569f5d6ff149137be86'; + var traceId = '1-f9194208-2c7ad569f5d6ff149137be86'; + + beforeEach(function() { + server = new TestEmitter(); + server.use = function(fcn) { this.open = fcn; }; + sandbox = sinon.sandbox.create(); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('#enable', function() { + var enable = restifyMW.enable; + + it('should throw an error if no restify instance is supplied', function() { + assert.throws(enable); + }); + + it('should throw an error if no default name is supplied', function() { + assert.throws(function() { enable(server); }); + }); + + it('should make restify use the open function and set listeners on "uncaughtException" and "after" events', function() { + var serverMock = sandbox.mock(server); + serverMock.expects('use').withArgs(sinon.match.func); + serverMock.expects('on').once().withArgs('after', sinon.match.func); + serverMock.expects('on').once().withArgs('uncaughtException', sinon.match.func); + + enable(server, defaultName); + + serverMock.verify(); + }); + }); + + describe('#open', function() { + var req, res, modeStub, sandbox, setStub; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + modeStub = sandbox.stub(xray, 'isAutomaticMode').returns(true); + setStub = sandbox.stub(xray, 'setSegment'); + + var ns = { + bindEmitter: function() {}, + run: function(fcn) { fcn(); } + }; + sandbox.stub(xray, 'getNamespace').returns(ns); + + req = { + method: 'GET', + url: '/', + connection: { + remoteAddress: 'localhost' + }, + headers: { host: 'myHostName' }, + context: {}, + }; + + req.emitter = new utils.TestEmitter(); + req.on = utils.onEvent; + + res = { + req: req, + header: {} + }; + res.emitter = new utils.TestEmitter(); + res.on = utils.onEvent; + + restifyMW.enable(server, defaultName); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('when handling a request', function() { + var addReqDataSpy, newSegmentSpy, onEventStub, processHeadersStub, resolveNameStub, sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + newSegmentSpy = sandbox.spy(Segment.prototype, 'init'); + addReqDataSpy = sandbox.spy(Segment.prototype, 'addIncomingRequestData'); + + onEventStub = sandbox.stub(res, 'on'); + + processHeadersStub = sandbox.stub(mwUtils, 'processHeaders').returns({ Root: traceId, Parent: parentId, Sampled: '0' }); + resolveNameStub = sandbox.stub(mwUtils, 'resolveName').returns(defaultName); + + req.headers = { host: hostName }; + }); + + afterEach(function() { + sandbox.restore(); + delete process.env.AWS_XRAY_TRACING_NAME; + }); + + it('should call mwUtils.processHeaders to split the headers, if any', function() { + server.open(req, res); + + processHeadersStub.should.have.been.calledOnce; + processHeadersStub.should.have.been.calledWithExactly(req); + }); + + it('should call mwUtils.resolveName to find the name of the segment', function() { + server.open(req, res); + + resolveNameStub.should.have.been.calledOnce; + resolveNameStub.should.have.been.calledWithExactly(req.headers.host); + }); + + it('should create a new segment', function() { + server.open(req, res); + + newSegmentSpy.should.have.been.calledOnce; + newSegmentSpy.should.have.been.calledWithExactly(defaultName, traceId, parentId); + }); + + it('should add a new http property on the segment', function() { + server.open(req, res); + + addReqDataSpy.should.have.been.calledOnce; + addReqDataSpy.should.have.been.calledWithExactly(sinon.match.instanceOf(IncomingRequestData)); + }); + + it('should add a finish event to the response', function() { + server.open(req, res); + + onEventStub.should.have.been.calledOnce; + onEventStub.should.have.been.calledWithExactly('finish', sinon.match.typeOf('function')); + }); + + describe('in automatic mode', function() { + it('should set the segment on the namespace if in automatic mode', function() { + server.open(req, res); + + setStub.should.have.been.calledWith(sinon.match.instanceOf(Segment)); + }); + }); + + describe('in manual mode', function() { + it('should set the segment on the request object if in manual mode', function() { + modeStub.returns(false); + server.open(req, res); + + assert.instanceOf(req.segment, Segment); + }); + + it('should set the segment on the restify context if in manual mode (and available)', function() { + modeStub.returns(false); + req.set = function() {}; + var reqSetStub = sandbox.stub(req, 'set'); + server.open(req, res); + + reqSetStub.should.have.been.calledWith('XRaySegment', sinon.match.instanceOf(Segment)); + }); + }); + + it('should add a finish event to the response', function() { + server.open(req, res); + + onEventStub.should.have.been.calledOnce; + onEventStub.should.have.been.calledWithExactly('finish', sinon.match.typeOf('function')); + }); + }); + + describe('when the request completes', function() { + var sandbox; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + sandbox.stub(SegmentEmitter); + + modeStub.returns(false); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should add the error flag the segment on 4xx', function() { + var getCauseStub = sandbox.stub(xray.utils, 'getCauseTypeFromHttpStatus').returns('error'); + server.open(req, res); + + res.statusCode = 400; + res.emitter.emit('finish'); + + assert.equal(req.segment.error, true); + getCauseStub.should.have.been.calledWith(400); + }); + + it('should add the fault flag the segment on 5xx', function() { + var getCauseStub = sandbox.stub(xray.utils, 'getCauseTypeFromHttpStatus').returns('fault'); + server.open(req, res); + + res.statusCode = 500; + res.emitter.emit('finish'); + + assert.equal(req.segment.fault, true); + getCauseStub.should.have.been.calledWith(500); + }); + + it('should add the throttle flag and error flag on the segment on a 429', function() { + var getCauseStub = sandbox.stub(xray.utils, 'getCauseTypeFromHttpStatus').returns('error'); + server.open(req, res); + + res.statusCode = 429; + res.emitter.emit('finish'); + + assert.equal(req.segment.throttle, true); + getCauseStub.should.have.been.calledWith(429); + }); + + it('should add nothing on anything else', function() { + server.open(req, res); + + res.statusCode = 200; + res.emitter.emit('finish'); + + var segment = req.segment; + + assert.notProperty(segment, 'error'); + assert.notProperty(segment, 'fault'); + assert.notProperty(segment, 'throttle'); + }); + }); + }); +});