├── .drone.jsonnet ├── .gitignore ├── .mocharc.js ├── .npmignore ├── .vscode └── settings.json ├── LICENSE.md ├── NOTES.md ├── README.md ├── bun.lockb ├── docker-compose.yml ├── docs └── README.md ├── examples ├── entrypoint-module │ ├── package.json │ └── probe.js └── standalone-example.js ├── package.json ├── pres ├── io-black.png └── io-white.png ├── src ├── configuration.ts ├── constants.ts ├── featureManager.ts ├── features │ ├── dependencies.ts │ ├── entrypoint.ts │ ├── events.ts │ ├── metrics.ts │ ├── notify.ts │ └── profiling.ts ├── index.ts ├── metrics │ ├── eventLoopMetrics.ts │ ├── httpMetrics.ts │ ├── network.ts │ ├── runtime.ts │ └── v8.ts ├── pmx.ts ├── profilers │ ├── addonProfiler.ts │ └── inspectorProfiler.ts ├── serviceManager.ts ├── services │ ├── actions.ts │ ├── inspector.ts │ ├── metrics.ts │ ├── runtimeStats.ts │ └── transport.ts ├── transports │ ├── IPCTransport.ts │ └── WebsocketTransport.ts └── utils │ ├── BinaryHeap.ts │ ├── EDS.ts │ ├── EWMA.ts │ ├── autocast.ts │ ├── metrics │ ├── counter.ts │ ├── gauge.ts │ ├── histogram.ts │ └── meter.ts │ ├── miscellaneous.ts │ ├── module.ts │ ├── stackParser.ts │ ├── transactionAggregator.ts │ └── units.ts ├── test.sh ├── test ├── api.spec.ts ├── autoExit.spec.ts ├── entrypoint.spec.ts ├── features │ ├── events.spec.ts │ ├── profiling.spec.ts │ └── tracing.spec.ts ├── fixtures │ ├── apiActionsChild.ts │ ├── apiActionsJsonChild.ts │ ├── apiBackwardActionsChild.ts │ ├── apiBackwardConfChild.ts │ ├── apiBackwardEventChild.ts │ ├── apiBackwardExpressChild.ts │ ├── apiInitModuleChild.ts │ ├── apiKoaErrorHandler.ts │ ├── apiMetricsChild.ts │ ├── apiNotifyChild.ts │ ├── apiOnExitChild.ts │ ├── apiOnExitExceptionChild.ts │ ├── autoExitChild.ts │ ├── entrypointChild.ts │ ├── features │ │ ├── eventLoopInspectorChild.ts │ │ ├── eventsChild.ts │ │ └── profilingChild.ts │ └── metrics │ │ ├── gcv8Child.ts │ │ ├── httpWrapperChild.ts │ │ ├── networkChild.ts │ │ ├── networkWithoutDownloadChild.ts │ │ └── tracingChild.ts ├── metrics │ ├── eventloop.spec.ts │ ├── http.spec.ts │ ├── network.spec.ts │ ├── runtime.spec.ts │ └── v8.spec.ts ├── package.json ├── services │ ├── actions.spec.ts │ └── metrics.spec.ts └── standalone │ ├── events.spec.ts │ ├── helper.ts │ └── tracing.spec.ts ├── tsconfig.json └── tslint.json /.drone.jsonnet: -------------------------------------------------------------------------------- 1 | local pipeline(version) = { 2 | kind: "pipeline", 3 | name: "node-v" + version, 4 | steps: [ 5 | { 6 | name: "tests", 7 | image: "node:" + version, 8 | commands: [ 9 | "node -v", 10 | "uname -r", 11 | "npm install", 12 | "npm test" 13 | ] 14 | }, 15 | ], 16 | services: [ 17 | { 18 | name: "mongodb", 19 | image: "mongo:3.4", 20 | environment: { 21 | AUTH: "no" 22 | }, 23 | }, 24 | { 25 | name: "redis", 26 | image: "redis:5", 27 | }, 28 | { 29 | name: "mysql", 30 | image: "mysql:5", 31 | environment: { 32 | MYSQL_DATABASE: "test", 33 | MYSQL_ROOT_PASSWORD: "password" 34 | }, 35 | }, 36 | { 37 | name: "postgres", 38 | image: "postgres:11", 39 | environment: { 40 | POSTGRES_DB: "test", 41 | POSTGRES_PASSWORD: "password" 42 | }, 43 | }, 44 | ], 45 | trigger: { 46 | event: ["push", "pull_request"] 47 | }, 48 | }; 49 | 50 | [ 51 | pipeline("8"), 52 | pipeline("10"), 53 | pipeline("12"), 54 | pipeline("13"), 55 | pipeline("14"), 56 | { 57 | kind: "pipeline", 58 | name: "build & publish", 59 | trigger: { 60 | event: "tag" 61 | }, 62 | steps: [ 63 | { 64 | name: "build", 65 | image: "node:8", 66 | commands: [ 67 | "export PATH=$PATH:./node_modules/.bin/", 68 | "yarn 2> /dev/null", 69 | "mkdir build", 70 | "yarn run build", 71 | ], 72 | }, 73 | { 74 | name: "publish", 75 | image: "plugins/npm", 76 | settings: { 77 | username: { 78 | from_secret: "npm_username" 79 | }, 80 | password: { 81 | from_secret: "npm_password" 82 | }, 83 | email: { 84 | from_secret: "npm_email" 85 | }, 86 | }, 87 | }, 88 | ], 89 | }, 90 | { 91 | kind: "secret", 92 | name: "npm_username", 93 | get: { 94 | path: "secret/drone/npm", 95 | name: "username", 96 | }, 97 | }, 98 | { 99 | kind: "secret", 100 | name: "npm_email", 101 | get: { 102 | path: "secret/drone/npm", 103 | name: "email", 104 | }, 105 | }, 106 | { 107 | kind: "secret", 108 | name: "npm_password", 109 | get: { 110 | path: "secret/drone/npm", 111 | name: "password", 112 | }, 113 | } 114 | ] 115 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | build 4 | src/**.js 5 | package-lock.json 6 | coverage 7 | .nyc_output 8 | *.log 9 | -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_ENV = 'test'; 3 | 4 | module.exports = { 5 | 'allow-uncaught': false, 6 | 'async-only': false, 7 | bail: true, 8 | color: true, 9 | delay: false, 10 | diff: true, 11 | exit: true, 12 | timeout: 10000, 13 | 'trace-warnings': true, 14 | ui: 'bdd', 15 | retries: 2, 16 | require: ['ts-node/register'] 17 | } 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | src 3 | ./config 4 | examples 5 | test 6 | tsconfig.json 7 | tslint.json 8 | .travis.yml 9 | .github 10 | build/temp 11 | build/docs 12 | 13 | coverage 14 | .nyc_output 15 | *.log 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Keymetrics Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | 2 | ## API idea 3 | 4 | require('pm2-bundle-monitoring') 5 | 6 | // or 7 | 8 | require('pm2-io').connect({ 9 | secret: '', 10 | public: '' 11 | }) 12 | 13 | require('pm2-exception-catching') 14 | require('pm2-transaction-tracing').config({ 15 | ignore_route: '/ws' 16 | }) 17 | 18 | require('pm2-frontend-monitoring') 19 | var pm2_metrics = require('pm2-metrics') 20 | 21 | pm2_metrics.variable('BLE pairing mode', permit_join) 22 | pm2_metrics.variable('In memory users', () => Object.keys(users).length) 23 | 24 | NOTES: 25 | - watch parameters is not reset on pm2 restart. only after pm2 delete 26 | 27 | 28 | ---- 29 | 30 | 31 | pm2-io-apm features are in src/features/: 32 | 33 | ``` 34 | src/features/ 35 | ├── dependencies.ts 36 | ├── entrypoint.ts 37 | ├── events.ts 38 | ├── metrics.ts 39 | ├── notify.ts 40 | ├── profiling.ts 41 | └── tracing.ts 42 | ``` 43 | 44 | ## Tracing 45 | 46 | - `./src/census` folder is essentially a dump of https://github.com/census-instrumentation/opencensus-node/tree/master/packages/opencensus-nodejs-base/src/trace with plugins added 47 | - Only traces higher than `MINIMUM_TRACE_DURATION: 1000 * 1000` are sent to transporter (sent in /src/census/exporter.ts:72) 48 | 49 | Trace sent looks like: 50 | 51 | ``` 52 | { 53 | traceId: 'fac7052e9129416185a26d4935229620', 54 | name: '/slow', 55 | id: '66358f0a48be82c5', 56 | parentId: '', 57 | kind: 'SERVER', 58 | timestamp: 1586380086251000, 59 | duration: 2007559, 60 | debug: false, 61 | shared: false, 62 | localEndpoint: { serviceName: 'tototransaction' }, 63 | tags: { 64 | 'http.host': 'localhost', 65 | 'http.method': 'GET', 66 | 'http.path': '/slow', 67 | 'http.route': '/slow', 68 | 'http.user_agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36', 69 | 'http.status_code': '304', 70 | 'result.code': undefined 71 | } 72 | } 73 | ``` 74 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keymetrics/pm2-io-apm/714ec396ca212e43e6129a6e2413b2be2c1b1aa1/bun.lockb -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | mongodb: 5 | image: mongo:3.4 6 | ports: 7 | - "27017:27017" 8 | environment: 9 | AUTH: "no" 10 | redis: 11 | image: eywek/redis:5 12 | ports: 13 | - "6379:6379" 14 | mysql: 15 | image: mysql:5 16 | ports: 17 | - "3306:3306" 18 | environment: 19 | MYSQL_DATABASE: 'test' 20 | MYSQL_ROOT_PASSWORD: 'password' 21 | postgres: 22 | image: postgres:11 23 | ports: 24 | - "5432:5432" 25 | environment: 26 | POSTGRES_DB: 'test' 27 | POSTGRES_PASSWORD: 'password' 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # PM2 APM Architecture 2 | 3 | The APM has been architected for a maximum composability when developing different features, each component have exactly one job and they have access to different low level API (like sendin/receiving data) to be able to do it. 4 | 5 | Here a simple (not exact) architecture diagram: 6 | 7 | ![schema](https://docs.google.com/drawings/d/e/2PACX-1vQdtaYLu1QaVwuhYfqhbknDzpLHAZWZVSKEK-Q3jnn00herQ6bT2FqyTn2-7s_SU6eVelYs21WB711Z/pub?w=1311&h=726) 8 | 9 | ## Concept 10 | 11 | ### Manager 12 | 13 | There are two "Manager" in the implementation: 14 | - The Service Manager which statically store reference to all services so they are available by other component. 15 | - The Feature Manager which handle the configuration and instanciation of each feature. 16 | 17 | ### Service 18 | 19 | A service is an internal component designed to group a speficic logic and provide a simple API to do that logic. 20 | They should not be accessible to user to allow us to change the low level API without breaking anything. 21 | 22 | ### Feature 23 | 24 | Exaclty as it sound, a feature is a component that actually do something, for example: 25 | - The tracing feature handle all the tracing infrastructure from the init to sending all trace data. 26 | - The notify metrics provide an API to send error to the remote infrastructure 27 | 28 | Each feature expose different methods and there are not exposed by default to the user. 29 | 30 | ### Public API 31 | 32 | By public API we mean all the functions that a user can call when instanciating the APM. 33 | Each function that need to be exposed is added into the main instance and then use the feature manager to call the actual method implmentation. 34 | This design help maintaining backward compatibility at the user level and not in the logic implementation. 35 | 36 | ## Services 37 | 38 | ### Transport 39 | 40 | The most important service is the Transport service which offer an interface to implement different type of transport. It also provide an API to create a transport instance. 41 | 42 | ##### IPC Transport 43 | 44 | This is the default transport and it's based on IPC, a shared file descriptor used to transfer data to and from the PM2 daemon. 45 | Every data is sent as specific packet to the PM2 RPC system that will then broadcast them to the agent. 46 | See the agent: https://github.com/keymetrics/pm2-io-agent 47 | 48 | ##### Websocket Transport 49 | 50 | The websocket transport is the new transporter used when we don't use the PM2 daemon and his agent to send data. 51 | It use this agent: https://github.com/keymetrics/pm2-io-agent-node to handle the low level networking to our servers. 52 | 53 | ### Metrics 54 | 55 | The metrics service expose method to register metrics that will be sent to the remote endpoint, it then handle: 56 | - Fetching the value of the metrics every second, and sending them formatted to the transport service 57 | 58 | ### Actions 59 | 60 | The action service, as the metric service, expose an API to register custom action. It handle the listening to the remote connection for new action request, call the actual user function and then send back the user data. 61 | 62 | ### Inspector 63 | 64 | This service doesn't do much apart from offering a `inspector` session to every feature that need it. From Node 8 to node 10, there could be only one session in the whole process so this service is made to avoid having two session opened by different features. 65 | 66 | ### RuntimeStats 67 | 68 | This service is simply a wrapper to the `@pm2/node-runtime-stats` module that allows to get low level metrics about the node runtime. 69 | Since multiple metrics are using it, we made a service out of it so we only have one instance of it. 70 | 71 | ## Features 72 | 73 | ### Notify 74 | 75 | The `notify` provides 3 API: 76 | - `notifyError` to send custom error to the remote server 77 | - `expressErrorHandler` which is a express middleware that catch error and send them 78 | - `koaErrorHandler` same as express middleware but for koa 79 | 80 | It also handle the `unhandledRejection` and `uncaughtException` error event to also send them. In the case of `uncaughtException`, it also `exit` the process. 81 | 82 | ### Profiling 83 | 84 | The profiling feature provide a wrapper on top of two different profilers implementation: 85 | - `inspector` based profiling, which is a built-in API for Node 8 to interact with v8 API 86 | - `addon` based profiling which is seperate c++ addon that need to be installed, it's only here to support Node 6. 87 | 88 | Both register customs actions with the `ActionService` to when a user can remotely start and stop profiles in their processes. 89 | 90 | ### Tracing 91 | 92 | The tracing feature handle the `opencensus` agent which is in `src/census`, for more information about how they works, you should read about [Opencensus Node Agent](https://github.com/census-instrumentation/opencensus-node/) 93 | 94 | We got our own plugins to be able to iterate faster on them and since the opencensus API isn't stable yet, do not break between minor bump of opencensus. 95 | We implemented our own exporter that use the `TransportService` to send trace data to the remote server. 96 | 97 | ### Metrics 98 | 99 | The Metrics service is implemented as a Manager because it only instanciate different metrics and don't do anything more. 100 | Here the current list of metrics implemented: 101 | - Event loop Metrics: when the `@pm2/node-runtime-stats` addon is there, it use it to get metrics directly from libuv otherwise it fallback on computing it with javascript. 102 | - HTTP Metrics: When the `http` or `https` module is required, add custom listener for request and compute latency/volume for them. 103 | - Network Metrics: Patch the `net.Socket` implementation to count how much bytes are sent/received from/to the network 104 | - Runtime Metrics: Only available when the `@pm2/node-runtime-stats` is there, it add metrics about the V8 GC and Linux Kernel metrics 105 | - V8: Fetch from built-in `v8` module some metrics about heap usage. 106 | 107 | -------------------------------------------------------------------------------- /examples/entrypoint-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pm2-agent-monitoring", 3 | "version": "1.5.5", 4 | "description": "", 5 | "main": "probe.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "dependencies": { 10 | "@pm2/io": "^2.3.0", 11 | "find-process": "^1.1.3", 12 | "pm2": "*", 13 | "pmx": "latest", 14 | "shelljs": "*" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "apps": [ 19 | { 20 | "script": "probe.js", 21 | "env": { 22 | "PM2_EXTRA_DISPLAY": "true" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /examples/entrypoint-module/probe.js: -------------------------------------------------------------------------------- 1 | var pmx = require('@pm2/io'); 2 | var Entrypoint = require('@pm2/io').Entrypoint; 3 | var pm2 = require('pm2'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var shelljs = require('shelljs'); 7 | var findprocess = require('find-process'); 8 | 9 | class App extends Entrypoint { 10 | onStart(cb) { 11 | this.initModule({ 12 | pid : this.getPID(path.join(process.env.HOME, '.pm2', 'agent.pid')), 13 | widget : { 14 | type: 'generic', 15 | theme: ['#1d3b4a', '#1B2228', '#22bbe2', '#22bbe2'], 16 | logo: 'https://raw.githubusercontent.com/Unitech/pm2/master/pres/pm2-v4.png', 17 | pid: this.getPID(path.join(process.env.HOME, '.pm2', 'pm2.pid')), 18 | 19 | el : { 20 | probes : true, 21 | actions : true 22 | }, 23 | 24 | block : { 25 | errors : true, 26 | main_probes : ['events rcvd/min', 'Agent Count', 'Version'], 27 | latency : false, 28 | versioning : false, 29 | show_module_meta : false 30 | } 31 | } 32 | }) 33 | 34 | console.log('Monitoring agent initialized') 35 | cb() 36 | } 37 | 38 | actuators () { 39 | this.action('pm2 list', function(reply) { 40 | pm2.list(function(err, procs) { 41 | reply(procs) 42 | }) 43 | }) 44 | 45 | this.action('pm2 link info', function(reply) { 46 | shelljs.exec('pm2 link info', function(err, stdo, stde) { 47 | reply({err, stdo, stde}) 48 | }) 49 | }) 50 | } 51 | 52 | sensors () { 53 | console.log('Building sensors and integrations') 54 | 55 | var version, pm2_procs, proc_nb 56 | 57 | /** 58 | * Events sent from PM2 to the Agent 59 | */ 60 | var event_metric = this.meter({ 61 | name : 'events rcvd/min' 62 | }) 63 | 64 | pm2.connect(function() { 65 | pm2.launchBus(function(err, bus) { 66 | bus.on('*', function(event, data) { 67 | if (event.indexOf('log:') > -1) 68 | event_metric.mark(); 69 | }); 70 | }); 71 | 72 | setInterval(function() { 73 | pm2.list(function(err, procs) { 74 | pm2_procs = procs.length; 75 | }); 76 | }, 2000); 77 | }); 78 | 79 | /** 80 | * Agent Count metric + Version retrieval and integration 81 | */ 82 | this.metric({ 83 | name : 'Agent Count', 84 | alert : { 85 | mode : 'threshold-avg', 86 | value : 2, 87 | cmp : '>' 88 | }, 89 | value : function() { 90 | return proc_nb; 91 | } 92 | }) 93 | 94 | this.metric({ 95 | name : 'Version', 96 | value : function() { 97 | return version; 98 | } 99 | }) 100 | 101 | setInterval(function() { 102 | findprocess('name', 'PM2 Agent') 103 | .then(function (list) { 104 | proc_nb = 0 105 | list.filter(proc => { 106 | if (proc.cmd.indexOf(path.join(process.env.HOME, '.pm2')) > -1) { 107 | version = proc.cmd.split(' ')[2] 108 | proc_nb++ 109 | } 110 | }) 111 | }); 112 | }, 5000) 113 | } 114 | 115 | 116 | onStop(err, cb) { 117 | pm2.disconnect(cb) 118 | } 119 | } 120 | 121 | new App() 122 | -------------------------------------------------------------------------------- /examples/standalone-example.js: -------------------------------------------------------------------------------- 1 | var io = require('@pm2/io') 2 | 3 | var agent = io.init({ 4 | standalone: true, 5 | metrics: { 6 | eventLoop: false, 7 | http: false, 8 | v8: false 9 | }, 10 | apmOptions: { 11 | publicKey: '', 12 | secretKey: '', 13 | appName: 'toto' 14 | } 15 | }) 16 | 17 | var val = agent.metrics({ 18 | name: 'Value Report' 19 | }) 20 | 21 | var i = 0 22 | 23 | setInterval(() => { 24 | val.set(i++) 25 | }, 900) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pm2/io", 3 | "version": "6.1.0", 4 | "description": "PM2.io NodeJS APM", 5 | "main": "build/main/index.js", 6 | "typings": "build/main/index.d.ts", 7 | "types": "build/main/index.d.ts", 8 | "module": "build/module/index.js", 9 | "repository": "https://github.com/keymetrics/pm2-io-apm", 10 | "author": { 11 | "name": "PM2.io tech team", 12 | "email": "tech@pm2.io", 13 | "url": "https://pm2.io" 14 | }, 15 | "contributors": [ 16 | { 17 | "name": "Vincent Vallet", 18 | "url": "https://github.com/wallet77" 19 | } 20 | ], 21 | "license": "Apache-2", 22 | "scripts": { 23 | "build": "tsc -p tsconfig.json", 24 | "build:module": "tsc -p config/exports/tsconfig.module.json", 25 | "lint": "tslint --project . src/**/*.ts", 26 | "unit": "npm run build && bash test.sh", 27 | "mono": "mocha --exit --require ts-node/register", 28 | "test": "npm run unit", 29 | "watch": "tsc -w", 30 | "prepublishOnly": "npm run build" 31 | }, 32 | "scripts-info": { 33 | "build": "(Trash and re)build the library", 34 | "lint": "Lint all typescript source files", 35 | "unit": "Build the library and run unit tests", 36 | "test": "Lint, build, and test the library", 37 | "watch": "Watch source files, rebuild library on changes, rerun relevant tests" 38 | }, 39 | "engines": { 40 | "node": ">=6.0" 41 | }, 42 | "devDependencies": { 43 | "@types/chai": "4.1.4", 44 | "@types/express": "~4.16.1", 45 | "@types/ioredis": "~4.0.6", 46 | "@types/mocha": "5.2.5", 47 | "@types/mongodb": "~3.1.19", 48 | "@types/node": "~10.12.21", 49 | "@types/redis": "~2.8.10", 50 | "chai": "4.1.2", 51 | "express": "^4.17.1", 52 | "ioredis": "^4.16.3", 53 | "koa": "^2.11.0", 54 | "mocha": "~7.1.0", 55 | "mongodb-core": "^3.2.7", 56 | "mysql": "~2.18.1", 57 | "mysql2": "~2.1.0", 58 | "nock": "~10.0.6", 59 | "nyc": "~13.1.0", 60 | "pg": "^7.18.2", 61 | "redis": "^3.0.2", 62 | "source-map-support": "~0.5.9", 63 | "ts-node": "~7.0.1", 64 | "tslint": "~5.11.0", 65 | "tslint-config-standard": "~8.0.1", 66 | "typescript": "^5.2.2", 67 | "vue": "^2.6.11", 68 | "vue-server-renderer": "^2.6.11" 69 | }, 70 | "keywords": [], 71 | "nyc": { 72 | "extension": [ 73 | ".ts" 74 | ], 75 | "exclude": [ 76 | "build/", 77 | "config/", 78 | "examples/", 79 | "test/" 80 | ], 81 | "cache": true, 82 | "all": true 83 | }, 84 | "dependencies": { 85 | "async": "~2.6.1", 86 | "debug": "~4.3.1", 87 | "eventemitter2": "^6.3.1", 88 | "require-in-the-middle": "^5.0.0", 89 | "semver": "~7.5.4", 90 | "shimmer": "^1.2.0", 91 | "signal-exit": "^3.0.3", 92 | "tslib": "1.9.3" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /pres/io-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keymetrics/pm2-io-apm/714ec396ca212e43e6129a6e2413b2be2c1b1aa1/pres/io-black.png -------------------------------------------------------------------------------- /pres/io-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keymetrics/pm2-io-apm/714ec396ca212e43e6129a6e2413b2be2c1b1aa1/pres/io-white.png -------------------------------------------------------------------------------- /src/configuration.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | const debug = Debug('axm:configuration') 3 | 4 | import { ServiceManager } from './serviceManager' 5 | import Autocast from './utils/autocast' 6 | import * as path from 'path' 7 | import * as fs from 'fs' 8 | 9 | export default class Configuration { 10 | 11 | static configureModule (opts) { 12 | if (ServiceManager.get('transport')) ServiceManager.get('transport').setOptions(opts) 13 | } 14 | 15 | static findPackageJson () { 16 | try { 17 | require.main = Configuration.getMain() 18 | } catch (_e) { 19 | // Ignore error when getter is set on require.main, but no setter 20 | } 21 | 22 | if (!require.main) { 23 | return 24 | } 25 | 26 | if (!require.main.paths) { 27 | return 28 | } 29 | 30 | let pkgPath = path.resolve(path.dirname(require.main.filename), 'package.json') 31 | try { 32 | fs.statSync(pkgPath) 33 | } catch (e) { 34 | try { 35 | pkgPath = path.resolve(path.dirname(require.main.filename), '..', 'package.json') 36 | fs.statSync(pkgPath) 37 | } catch (e) { 38 | debug('Cannot find package.json') 39 | try { 40 | pkgPath = path.resolve(path.dirname(require.main.filename), '..', '..', 'package.json') 41 | fs.statSync(pkgPath) 42 | } catch (e) { 43 | debug('Cannot find package.json') 44 | return null 45 | } 46 | } 47 | return pkgPath 48 | } 49 | 50 | return pkgPath 51 | } 52 | 53 | static init (conf, doNotTellPm2?) { 54 | const packageFilepath = Configuration.findPackageJson() 55 | let packageJson 56 | 57 | if (!conf.module_conf) { 58 | conf.module_conf = {} 59 | } 60 | conf.apm = { 61 | type: 'node', 62 | version: null 63 | } 64 | 65 | try { 66 | const prefix = __dirname.replace(/\\/g,'/').indexOf('/build/') >= 0 ? '../../' : '../' 67 | const pkg = require(prefix + 'package.json') 68 | conf.apm.version = pkg.version || null 69 | } catch (err) { 70 | debug('Failed to fetch current apm version: ', err.message) 71 | } 72 | 73 | if (conf.isModule === true) { 74 | /** 75 | * Merge package.json metadata 76 | */ 77 | try { 78 | packageJson = require(packageFilepath || '') 79 | 80 | conf.module_version = packageJson.version 81 | conf.module_name = packageJson.name 82 | conf.description = packageJson.description 83 | 84 | if (packageJson.config) { 85 | conf = Object.assign(conf, packageJson.config) 86 | conf.module_conf = packageJson.config 87 | } 88 | } catch (e) { 89 | throw new Error(e) 90 | } 91 | } else { 92 | conf.module_name = process.env.name || 'outside-pm2' 93 | try { 94 | packageJson = require(packageFilepath || '') 95 | 96 | conf.module_version = packageJson.version 97 | 98 | if (packageJson.config) { 99 | conf = Object.assign(conf, packageJson.config) 100 | conf.module_conf = packageJson.config 101 | } 102 | } catch (e) { 103 | debug(e.message) 104 | } 105 | } 106 | 107 | /** 108 | * If custom variables has been set, merge with returned configuration 109 | */ 110 | try { 111 | if (process.env[conf.module_name]) { 112 | const castedConf = new Autocast().autocast(JSON.parse(process.env[conf.module_name] || '')) 113 | conf = Object.assign(conf, castedConf) 114 | // Do not display probe configuration in Keymetrics 115 | delete castedConf.probes 116 | // This is the configuration variable modifiable from keymetrics 117 | conf.module_conf = JSON.parse(JSON.stringify(Object.assign(conf.module_conf, castedConf))) 118 | 119 | // Obfuscate passwords 120 | Object.keys(conf.module_conf).forEach(function (key) { 121 | if ((key === 'password' || key === 'passwd') && 122 | conf.module_conf[key].length >= 1) { 123 | conf.module_conf[key] = 'Password hidden' 124 | } 125 | 126 | }) 127 | } 128 | } catch (e) { 129 | debug(e) 130 | } 131 | 132 | if (doNotTellPm2 === true) return conf 133 | 134 | Configuration.configureModule(conf) 135 | return conf 136 | } 137 | 138 | static getMain (): any { 139 | return require.main || { filename: './somefile.js' } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as semver from 'semver' 2 | 3 | export default { 4 | METRIC_INTERVAL: 990 5 | } 6 | 7 | export function canUseInspector () { 8 | // @ts-ignore 9 | const isBun = typeof Bun !== 'undefined' 10 | const isAboveNode10 = semver.satisfies(process.version, '>= 10.1.0') 11 | const isAboveNode8 = semver.satisfies(process.version, '>= 8.0.0') 12 | const canUseInNode8 = process.env.FORCE_INSPECTOR === '1' 13 | || process.env.FORCE_INSPECTOR === 'true' || process.env.NODE_ENV === 'production' 14 | 15 | return !isBun && (isAboveNode10 || (isAboveNode8 && canUseInNode8)) 16 | } 17 | -------------------------------------------------------------------------------- /src/featureManager.ts: -------------------------------------------------------------------------------- 1 | 2 | import { NotifyFeature } from './features/notify' 3 | import { ProfilingFeature } from './features/profiling' 4 | import { EventsFeature } from './features/events' 5 | import { IOConfig } from './pmx' 6 | import { MetricsFeature } from './features/metrics' 7 | import { DependenciesFeature } from './features/dependencies' 8 | import * as Debug from 'debug' 9 | 10 | export function getObjectAtPath (context: Object, path: string): any { 11 | if (path.indexOf('.') === -1 && path.indexOf('[') === -1) { 12 | return context[path] 13 | } 14 | 15 | let crumbs = path.split(/\.|\[|\]/g) 16 | let i = -1 17 | let len = crumbs.length 18 | let result 19 | 20 | while (++i < len) { 21 | if (i === 0) result = context 22 | if (!crumbs[i]) continue 23 | if (result === undefined) break 24 | result = result[crumbs[i]] 25 | } 26 | 27 | return result 28 | } 29 | 30 | class AvailableFeature { 31 | /** 32 | * Name of the feature 33 | */ 34 | name: string 35 | /** 36 | * The non-instancied class of the feature, used to init it 37 | */ 38 | module: { new(): Feature } 39 | /** 40 | * Option path is the path of the configuration for this feature 41 | * Possibles values: 42 | * - undefined: the feature doesn't need any configuration 43 | * - '.': the feature need the top level configuration 44 | * - everything else: the path to the value that contains the config, it can any anything 45 | */ 46 | optionsPath?: string 47 | /** 48 | * Current instance of the feature used 49 | */ 50 | instance?: Feature 51 | } 52 | 53 | const availablesFeatures: AvailableFeature[] = [ 54 | { 55 | name: 'notify', 56 | optionsPath: '.', 57 | module: NotifyFeature 58 | }, 59 | { 60 | name: 'profiler', 61 | optionsPath: 'profiling', 62 | module: ProfilingFeature 63 | }, 64 | { 65 | name: 'events', 66 | module: EventsFeature 67 | }, 68 | { 69 | name: 'metrics', 70 | optionsPath: 'metrics', 71 | module: MetricsFeature 72 | }, 73 | { 74 | name: 'dependencies', 75 | module: DependenciesFeature 76 | } 77 | ] 78 | 79 | export class FeatureManager { 80 | 81 | private logger: Function = Debug('axm:features') 82 | /** 83 | * Construct all the features and init them with their respective configuration 84 | * It will return a map with each public API method 85 | */ 86 | init (options: IOConfig): void { 87 | for (let availableFeature of availablesFeatures) { 88 | this.logger(`Creating feature ${availableFeature.name}`) 89 | const feature = new availableFeature.module() 90 | let config: any = undefined 91 | if (typeof availableFeature.optionsPath !== 'string') { 92 | config = {} 93 | } else if (availableFeature.optionsPath === '.') { 94 | config = options 95 | } else { 96 | config = getObjectAtPath(options, availableFeature.optionsPath) 97 | } 98 | this.logger(`Init feature ${availableFeature.name}`) 99 | // @ts-ignore 100 | // thanks mr typescript but we don't know the shape that the 101 | // options will be, so we just ignore the warning there 102 | feature.init(config) 103 | availableFeature.instance = feature 104 | } 105 | } 106 | 107 | /** 108 | * Get a internal implementation of a feature method 109 | * WARNING: should only be used by user facing API 110 | */ 111 | get (name: string): Feature { 112 | const feature = availablesFeatures.find(feature => feature.name === name) 113 | if (feature === undefined || feature.instance === undefined) { 114 | throw new Error(`Tried to call feature ${name} which doesn't exist or wasn't initiated`) 115 | } 116 | return feature.instance 117 | } 118 | 119 | destroy () { 120 | for (let availableFeature of availablesFeatures) { 121 | if (availableFeature.instance === undefined) continue 122 | this.logger(`Destroy feature ${availableFeature.name}`) 123 | availableFeature.instance.destroy() 124 | } 125 | } 126 | } 127 | 128 | // just to be able to cast 129 | export class FeatureConfig { } 130 | 131 | export interface Feature { 132 | init (config?: any): void 133 | destroy (): void 134 | } 135 | -------------------------------------------------------------------------------- /src/features/dependencies.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from '../serviceManager' 2 | import * as Debug from 'debug' 3 | import { Feature } from '../featureManager' 4 | import { Transport } from '../services/transport' 5 | import Configuration from '../configuration' 6 | import { readFile } from 'fs' 7 | 8 | type PkgDependencies = { [name: string]: string } 9 | type DependencyList = { [name: string]: { version: string }} 10 | 11 | export class DependenciesFeature implements Feature { 12 | 13 | private transport: Transport 14 | private logger: Function = Debug('axm:features:dependencies') 15 | 16 | init (): void { 17 | this.transport = ServiceManager.get('transport') 18 | this.logger('init') 19 | 20 | const pkgPath = Configuration.findPackageJson() 21 | if (typeof pkgPath !== 'string') return this.logger('failed to found pkg.json path') 22 | 23 | this.logger(`found pkg.json in ${pkgPath}`) 24 | readFile(pkgPath, (err, data) => { 25 | if (err) return this.logger(`failed to read pkg.json`, err) 26 | try { 27 | const pkg = JSON.parse(data.toString()) 28 | if (typeof pkg.dependencies !== 'object') { 29 | return this.logger(`failed to find deps in pkg.json`) 30 | } 31 | const dependencies = Object.keys(pkg.dependencies as PkgDependencies) 32 | .reduce((list: DependencyList, name: string) => { 33 | list[name] = { version: pkg.dependencies[name] } 34 | return list 35 | }, {} as DependencyList) 36 | this.logger(`collected ${Object.keys(dependencies).length} dependencies`) 37 | this.transport.send('application:dependencies', dependencies) 38 | this.logger('sent dependencies list') 39 | } catch (err) { 40 | return this.logger(`failed to parse pkg.json`, err) 41 | } 42 | }) 43 | } 44 | 45 | destroy () { 46 | this.logger('destroy') 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/features/entrypoint.ts: -------------------------------------------------------------------------------- 1 | 2 | import IO, { IOConfig } from '../pmx' 3 | const IO_KEY = Symbol.for('@pm2/io') 4 | 5 | export class Entrypoint { 6 | private io: IO 7 | 8 | constructor () { 9 | try { 10 | this.io = global[IO_KEY].init(this.conf()) 11 | 12 | this.onStart(err => { 13 | if (err) { 14 | console.error(err) 15 | process.exit(1) 16 | } 17 | 18 | this.sensors() 19 | this.events() 20 | this.actuators() 21 | 22 | this.io.onExit((code, signal) => { 23 | this.onStop(err, () => { 24 | this.io.destroy() 25 | }, code, signal) 26 | }) 27 | 28 | if (process && process.send) process.send('ready') 29 | }) 30 | } catch (e) { 31 | // properly exit in case onStart/onStop method has not been override 32 | if (this.io) { 33 | this.io.destroy() 34 | } 35 | 36 | throw (e) 37 | } 38 | } 39 | 40 | events () { 41 | return 42 | } 43 | 44 | sensors () { 45 | return 46 | } 47 | 48 | actuators () { 49 | return 50 | } 51 | 52 | onStart (cb: Function) { 53 | throw new Error('Entrypoint onStart() not specified') 54 | } 55 | 56 | onStop (err: Error, cb: Function, code: number, signal: string) { 57 | return cb() 58 | } 59 | 60 | conf (): IOConfig | undefined { 61 | return undefined 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/features/events.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from '../serviceManager' 2 | import { Feature } from '../featureManager' 3 | import { Transport } from '../services/transport' 4 | import * as Debug from 'debug' 5 | 6 | export class EventsFeature implements Feature { 7 | 8 | private transport: Transport | undefined 9 | private logger: Function = Debug('axm:features:events') 10 | 11 | init (): void { 12 | this.transport = ServiceManager.get('transport') 13 | this.logger('init') 14 | } 15 | 16 | emit (name?: string, data?: any) { 17 | if (typeof name !== 'string') { 18 | console.error('event name must be a string') 19 | return console.trace() 20 | } 21 | if (typeof data !== 'object') { 22 | console.error('event data must be an object') 23 | return console.trace() 24 | } 25 | if (data instanceof Array) { 26 | console.error(`event data cannot be an array`) 27 | return console.trace() 28 | } 29 | 30 | let inflightObj: Object | any = {} 31 | try { 32 | inflightObj = JSON.parse(JSON.stringify(data)) 33 | } catch (err) { 34 | return console.log('Failed to serialize the event data', err.message) 35 | } 36 | 37 | inflightObj.__name = name 38 | if (this.transport === undefined) { 39 | return this.logger('Failed to send event as transporter isnt available') 40 | } 41 | this.transport.send('human:event', inflightObj) 42 | } 43 | 44 | destroy () { 45 | this.logger('destroy') 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/features/metrics.ts: -------------------------------------------------------------------------------- 1 | import Debug from 'debug' 2 | import { Feature, getObjectAtPath } from '../featureManager' 3 | import EventLoopHandlesRequestsMetric, { EventLoopMetricOption } from '../metrics/eventLoopMetrics' 4 | import NetworkMetric, { NetworkTrafficConfig } from '../metrics/network' 5 | import HttpMetrics, { HttpMetricsConfig } from '../metrics/httpMetrics' 6 | import V8Metric, { V8MetricsConfig } from '../metrics/v8' 7 | import RuntimeMetrics, { RuntimeMetricsOptions } from '../metrics/runtime' 8 | 9 | export const defaultMetricConf: MetricConfig = { 10 | eventLoop: true, 11 | network: false, 12 | http: true, 13 | runtime: true, 14 | v8: true 15 | } 16 | 17 | export class MetricConfig { 18 | /** 19 | * Toggle metrics about the V8 Heap 20 | */ 21 | v8?: V8MetricsConfig | boolean 22 | /** 23 | * Toggle metrics about the event loop and GC 24 | * 25 | * Note: need to install @pm2/node-runtime-stats as a dependency 26 | */ 27 | runtime?: RuntimeMetricsOptions | boolean 28 | /** 29 | * Toggle metrics about http/https requests 30 | */ 31 | http?: HttpMetricsConfig | boolean 32 | /** 33 | * Toggle network about network usage in your app 34 | */ 35 | network?: NetworkTrafficConfig | boolean 36 | /** 37 | * Toggle metrics about the event loop 38 | */ 39 | eventLoop?: EventLoopMetricOption | boolean 40 | } 41 | 42 | class AvailableMetric { 43 | /** 44 | * Name of the feature 45 | */ 46 | name: string 47 | /** 48 | * The non-instancied class of the feature, used to init it 49 | */ 50 | module: { new(): MetricInterface } 51 | /** 52 | * Option path is the path of the configuration for this feature 53 | * Possibles values: 54 | * - undefined: the feature doesn't need any configuration 55 | * - '.': the feature need the top level configuration 56 | * - everything else: the path to the value that contains the config, it can any anything 57 | */ 58 | optionsPath?: string 59 | /** 60 | * Current instance of the feature used 61 | */ 62 | instance?: MetricInterface 63 | } 64 | 65 | const availableMetrics: AvailableMetric[] = [ 66 | { 67 | name: 'eventloop', 68 | module: EventLoopHandlesRequestsMetric, 69 | optionsPath: 'eventLoop' 70 | }, 71 | { 72 | name: 'http', 73 | module: HttpMetrics, 74 | optionsPath: 'http' 75 | }, 76 | { 77 | name: 'network', 78 | module: NetworkMetric, 79 | optionsPath: 'network' 80 | }, 81 | { 82 | name: 'v8', 83 | module: V8Metric, 84 | optionsPath: 'v8' 85 | }, 86 | { 87 | name: 'runtime', 88 | module: RuntimeMetrics, 89 | optionsPath: 'runtime' 90 | } 91 | ] 92 | 93 | export interface MetricInterface { 94 | init (config?: Object | boolean): void 95 | destroy (): void 96 | } 97 | 98 | export class MetricsFeature implements Feature { 99 | 100 | private logger: Function = Debug('axm:features:metrics') 101 | 102 | init (options?: Object) { 103 | if (typeof options !== 'object') options = {} 104 | this.logger('init') 105 | 106 | for (let availableMetric of availableMetrics) { 107 | const metric = new availableMetric.module() 108 | let config: any = undefined 109 | if (typeof availableMetric.optionsPath !== 'string') { 110 | config = {} 111 | } else if (availableMetric.optionsPath === '.') { 112 | config = options 113 | } else { 114 | config = getObjectAtPath(options, availableMetric.optionsPath) 115 | } 116 | // @ts-ignore 117 | // thanks mr typescript but we don't know the shape that the 118 | // options will be, so we just ignore the warning there 119 | metric.init(config) 120 | availableMetric.instance = metric 121 | } 122 | } 123 | 124 | get (name: string): MetricInterface | undefined { 125 | const metric = availableMetrics.find(metric => metric.name === name) 126 | if (metric === undefined) return undefined 127 | return metric.instance 128 | } 129 | 130 | destroy () { 131 | this.logger('destroy') 132 | for (let availableMetric of availableMetrics) { 133 | if (availableMetric.instance === undefined) continue 134 | availableMetric.instance.destroy() 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/features/notify.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { Feature } from '../featureManager' 4 | import Configuration from '../configuration' 5 | import { ServiceManager } from '../serviceManager' 6 | import Debug from 'debug' 7 | import { Transport } from '../services/transport' 8 | import * as semver from 'semver' 9 | import { Cache, StackTraceParser, StackContext } from '../utils/stackParser' 10 | import * as fs from 'fs' 11 | import * as path from 'path' 12 | 13 | export class NotifyOptions { 14 | catchExceptions: boolean 15 | } 16 | 17 | export class ErrorContext { 18 | /** 19 | * Add http context to your error 20 | */ 21 | http?: Object 22 | /** 23 | * Add any context that you may need to debug your issue 24 | * example: the id of the user that made the request 25 | */ 26 | custom?: Object 27 | } 28 | 29 | const optionsDefault: NotifyOptions = { 30 | catchExceptions: true 31 | } 32 | 33 | export class NotifyFeature implements Feature { 34 | 35 | private logger: Function = Debug('axm:features:notify') 36 | private transport: Transport | undefined 37 | private cache: Cache 38 | private stackParser: StackTraceParser 39 | 40 | init (options?: NotifyOptions) { 41 | if (options === undefined) { 42 | options = optionsDefault 43 | } 44 | this.logger('init') 45 | this.transport = ServiceManager.get('transport') 46 | if (this.transport === undefined) { 47 | return this.logger(`Failed to load transporter service`) 48 | } 49 | 50 | Configuration.configureModule({ 51 | error : true 52 | }) 53 | if (options.catchExceptions === false) return 54 | this.logger('Registering hook to catch unhandled exception/rejection') 55 | this.cache = new Cache({ 56 | miss: (key) => { 57 | try { 58 | const content = fs.readFileSync(path.resolve(key)) 59 | return content.toString().split(/\r?\n/) 60 | } catch (err) { 61 | this.logger('Error while trying to get file from FS : %s', err.message || err) 62 | return null 63 | } 64 | }, 65 | ttl: 30 * 60 66 | }) 67 | this.stackParser = new StackTraceParser({ 68 | cache: this.cache, 69 | contextSize: 5 70 | }) 71 | this.catchAll() 72 | } 73 | 74 | destroy () { 75 | process.removeListener('uncaughtException', this.onUncaughtException) 76 | process.removeListener('unhandledRejection', this.onUnhandledRejection) 77 | this.logger('destroy') 78 | } 79 | 80 | getSafeError (err): Error { 81 | if (err instanceof Error) return err 82 | 83 | let message: string 84 | try { 85 | message = `Non-error value: ${JSON.stringify(err)}` 86 | } catch (e) { 87 | // We might land here if the error value was not serializable (it might contain 88 | // circular references for example), or if user code was ran as part of the 89 | // serialization and that code threw an error. 90 | // As alternative, we can try converting the error to a string instead: 91 | try { 92 | message = `Unserializable non-error value: ${String(e)}` 93 | // Intentionally not logging the second error anywhere, because we would need 94 | // to protect against errors while doing *that* as well. (For example, the second 95 | // error's "toString" property may crash or not exist.) 96 | } catch (e2) { 97 | // That didn't work. So, report a totally unknown error that resists being converted 98 | // into any usable form. 99 | // Again, we don't even attempt to look at that third error for the same reason as 100 | // described above. 101 | message = `Unserializable non-error value that cannot be converted to a string` 102 | } 103 | } 104 | if (message.length > 1000) message = message.substr(0, 1000) + '...' 105 | 106 | return new Error(message) 107 | } 108 | 109 | notifyError (err: Error | string | {}, context?: ErrorContext) { 110 | // set default context 111 | if (typeof context !== 'object') { 112 | context = { } 113 | } 114 | 115 | if (this.transport === undefined) { 116 | return this.logger(`Tried to send error without having transporter available`) 117 | } 118 | 119 | const safeError = this.getSafeError(err) 120 | let stackContext: StackContext | null = null 121 | if (err instanceof Error) { 122 | stackContext = this.stackParser.retrieveContext(err) 123 | } 124 | 125 | const payload = Object.assign({ 126 | message: safeError.message, 127 | stack: safeError.stack, 128 | name: safeError.name, 129 | metadata: context 130 | }, stackContext === null ? {} : stackContext) 131 | 132 | return this.transport.send('process:exception', payload) 133 | } 134 | 135 | private onUncaughtException (error) { 136 | if (semver.satisfies(process.version, '< 6')) { 137 | console.error(error.stack) 138 | } else { 139 | console.error(error) 140 | } 141 | 142 | const safeError = this.getSafeError(error) 143 | let stackContext: StackContext | null = null 144 | if (error instanceof Error) { 145 | stackContext = this.stackParser.retrieveContext(error) 146 | } 147 | 148 | const payload = Object.assign({ 149 | message: safeError.message, 150 | stack: safeError.stack, 151 | name: safeError.name 152 | }, stackContext === null ? {} : stackContext) 153 | 154 | if (ServiceManager.get('transport')) { 155 | ServiceManager.get('transport').send('process:exception', payload) 156 | } 157 | if (process.listeners('uncaughtException').length === 1) { // if it's only us, exit 158 | process.exit(1) 159 | } 160 | } 161 | 162 | private onUnhandledRejection (error) { 163 | // see https://github.com/keymetrics/pm2-io-apm/issues/223 164 | if (error === undefined) return 165 | 166 | console.error(error) 167 | 168 | const safeError = this.getSafeError(error) 169 | let stackContext: StackContext | null = null 170 | if (error instanceof Error) { 171 | stackContext = this.stackParser.retrieveContext(error) 172 | } 173 | 174 | const payload = Object.assign({ 175 | message: safeError.message, 176 | stack: safeError.stack, 177 | name: safeError.name 178 | }, stackContext === null ? {} : stackContext) 179 | 180 | if (ServiceManager.get('transport')) { 181 | ServiceManager.get('transport').send('process:exception', payload) 182 | } 183 | } 184 | 185 | private catchAll (): Boolean | void { 186 | if (process.env.exec_mode === 'cluster_mode') { 187 | return false 188 | } 189 | 190 | process.on('uncaughtException', this.onUncaughtException.bind(this)) 191 | process.on('unhandledRejection', this.onUnhandledRejection.bind(this)) 192 | } 193 | 194 | expressErrorHandler () { 195 | const self = this 196 | Configuration.configureModule({ 197 | error : true 198 | }) 199 | return function errorHandler (err, req, res, next) { 200 | const safeError = self.getSafeError(err) 201 | const payload = { 202 | message: safeError.message, 203 | stack: safeError.stack, 204 | name: safeError.name, 205 | metadata: { 206 | http: { 207 | url: req.url, 208 | params: req.params, 209 | method: req.method, 210 | query: req.query, 211 | body: req.body, 212 | path: req.path, 213 | route: req.route && req.route.path ? req.route.path : undefined 214 | }, 215 | custom: { 216 | user: typeof req.user === 'object' ? req.user.id : undefined 217 | } 218 | } 219 | } 220 | 221 | if (ServiceManager.get('transport')) { 222 | ServiceManager.get('transport').send('process:exception', payload) 223 | } 224 | return next(err) 225 | } 226 | } 227 | 228 | koaErrorHandler () { 229 | const self = this 230 | Configuration.configureModule({ 231 | error : true 232 | }) 233 | return async function (ctx, next) { 234 | try { 235 | await next() 236 | } catch (err) { 237 | const safeError = self.getSafeError(err) 238 | const payload = { 239 | message: safeError.message, 240 | stack: safeError.stack, 241 | name: safeError.name, 242 | metadata: { 243 | http: { 244 | url: ctx.request.url, 245 | params: ctx.params, 246 | method: ctx.request.method, 247 | query: ctx.request.query, 248 | body: ctx.request.body, 249 | path: ctx.request.path, 250 | route: ctx._matchedRoute 251 | }, 252 | custom: { 253 | user: typeof ctx.user === 'object' ? ctx.user.id : undefined 254 | } 255 | } 256 | } 257 | if (ServiceManager.get('transport')) { 258 | ServiceManager.get('transport').send('process:exception', payload) 259 | } 260 | throw err 261 | } 262 | } 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/features/profiling.ts: -------------------------------------------------------------------------------- 1 | import { Feature } from '../featureManager' 2 | import AddonProfiler from '../profilers/addonProfiler' 3 | import InspectorProfiler from '../profilers/inspectorProfiler' 4 | import { canUseInspector } from '../constants' 5 | import * as Debug from 'debug' 6 | 7 | export interface ProfilerType { 8 | init (): void 9 | register (): void 10 | destroy (): void 11 | } 12 | 13 | export class ProfilingConfig { 14 | cpuJS: boolean 15 | heapSnapshot: boolean 16 | heapSampling: boolean 17 | implementation?: string 18 | } 19 | 20 | const defaultProfilingConfig: ProfilingConfig = { 21 | cpuJS: true, 22 | heapSnapshot: true, 23 | heapSampling: true, 24 | implementation: 'both' 25 | } 26 | 27 | const disabledProfilingConfig: ProfilingConfig = { 28 | cpuJS: false, 29 | heapSnapshot: false, 30 | heapSampling: false, 31 | implementation: 'none' 32 | } 33 | 34 | export class ProfilingFeature implements Feature { 35 | 36 | private profiler: ProfilerType | undefined 37 | private logger: Function = Debug('axm:features:profiling') 38 | 39 | init (config?: ProfilingConfig | boolean) { 40 | if (config === true) { 41 | config = defaultProfilingConfig 42 | } else if (config === false) { 43 | config = disabledProfilingConfig 44 | } else if (config === undefined) { 45 | config = defaultProfilingConfig 46 | } 47 | 48 | // allow to force the fallback to addon via the environment 49 | if (process.env.PM2_PROFILING_FORCE_FALLBACK === 'true') { 50 | config.implementation = 'addon' 51 | } 52 | // by default we check for the best suited one 53 | if (config.implementation === undefined || config.implementation === 'both') { 54 | config.implementation = canUseInspector() === true ? 'inspector' : 'addon' 55 | } 56 | 57 | switch (config.implementation) { 58 | case 'inspector': { 59 | this.logger('using inspector implementation') 60 | this.profiler = new InspectorProfiler() 61 | break 62 | } 63 | case 'addon': { 64 | this.logger('using addon implementation') 65 | this.profiler = new AddonProfiler() 66 | break 67 | } 68 | default: { 69 | return this.logger(`Invalid profiler implementation choosen: ${config.implementation}`) 70 | } 71 | } 72 | this.logger('init') 73 | this.profiler.init() 74 | } 75 | 76 | destroy () { 77 | this.logger('destroy') 78 | if (this.profiler === undefined) return 79 | this.profiler.destroy() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import PMX from './pmx' 3 | 4 | const IO_KEY = Symbol.for('@pm2/io') 5 | const isAlreadyHere = (Object.getOwnPropertySymbols(global).indexOf(IO_KEY) > -1) 6 | 7 | const io: PMX = isAlreadyHere ? global[IO_KEY] as PMX : new PMX().init() 8 | global[IO_KEY] = io 9 | 10 | export = io 11 | -------------------------------------------------------------------------------- /src/metrics/eventLoopMetrics.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { MetricService, InternalMetric, MetricType } from '../services/metrics' 4 | import { ServiceManager } from '../serviceManager' 5 | import * as Debug from 'debug' 6 | import { MetricInterface } from '../features/metrics' 7 | import Histogram from '../utils/metrics/histogram' 8 | import { RuntimeStatsService } from '../services/runtimeStats' 9 | 10 | export class EventLoopMetricOption { 11 | /** 12 | * Toggle the metrics about the actives handles/requests in the event loop 13 | * see http://docs.libuv.org/en/v1.x/design.html#handles-and-requests 14 | */ 15 | eventLoopActive: boolean 16 | /** 17 | * Toggle the metrics about how much time the event loop use to make one loop 18 | * see http://docs.libuv.org/en/v1.x/design.html#the-i-o-loop 19 | */ 20 | eventLoopDelay: boolean 21 | } 22 | 23 | const defaultOptions: EventLoopMetricOption = { 24 | eventLoopActive: true, 25 | eventLoopDelay: true 26 | } 27 | 28 | export default class EventLoopHandlesRequestsMetric implements MetricInterface { 29 | 30 | private metricService: MetricService | undefined 31 | private logger: any = Debug('axm:features:metrics:eventloop') 32 | private requestTimer: NodeJS.Timer | undefined 33 | private handleTimer: NodeJS.Timer | undefined 34 | private delayTimer: NodeJS.Timer | undefined 35 | private delayLoopInterval: number = 1000 36 | private runtimeStatsService: RuntimeStatsService | undefined 37 | private handle: (data: any) => void | undefined 38 | 39 | init (config?: EventLoopMetricOption | boolean) { 40 | if (config === false) return 41 | if (config === undefined) { 42 | config = defaultOptions 43 | } 44 | if (config === true) { 45 | config = defaultOptions 46 | } 47 | this.metricService = ServiceManager.get('metrics') 48 | if (this.metricService === undefined) return this.logger('Failed to load metric service') 49 | 50 | this.logger('init') 51 | if (typeof (process as any)._getActiveRequests === 'function' && config.eventLoopActive === true) { 52 | const requestMetric = this.metricService.metric({ 53 | name : 'Active requests', 54 | id: 'internal/libuv/requests', 55 | historic: true 56 | }) 57 | this.requestTimer = setInterval(_ => { 58 | requestMetric.set((process as any)._getActiveRequests().length) 59 | }, 1000) 60 | this.requestTimer.unref() 61 | } 62 | 63 | if (typeof (process as any)._getActiveHandles === 'function' && config.eventLoopActive === true) { 64 | const handleMetric = this.metricService.metric({ 65 | name : 'Active handles', 66 | id: 'internal/libuv/handles', 67 | historic: true 68 | }) 69 | this.handleTimer = setInterval(_ => { 70 | handleMetric.set((process as any)._getActiveHandles().length) 71 | }, 1000) 72 | this.handleTimer.unref() 73 | } 74 | 75 | if (config.eventLoopDelay === false) return 76 | 77 | const histogram = new Histogram() 78 | 79 | const uvLatencyp50: InternalMetric = { 80 | name: 'Event Loop Latency', 81 | id: 'internal/libuv/latency/p50', 82 | type: MetricType.histogram, 83 | historic: true, 84 | implementation: histogram, 85 | handler: function () { 86 | const percentiles = this.implementation.percentiles([ 0.5 ]) 87 | if (percentiles[0.5] === null) return null 88 | return percentiles[0.5].toFixed(2) 89 | }, 90 | unit: 'ms' 91 | } 92 | const uvLatencyp95: InternalMetric = { 93 | name: 'Event Loop Latency p95', 94 | id: 'internal/libuv/latency/p95', 95 | type: MetricType.histogram, 96 | historic: true, 97 | implementation: histogram, 98 | handler: function () { 99 | const percentiles = this.implementation.percentiles([ 0.95 ]) 100 | if (percentiles[0.95] === null) return null 101 | return percentiles[0.95].toFixed(2) 102 | }, 103 | unit: 'ms' 104 | } 105 | 106 | this.metricService.registerMetric(uvLatencyp50) 107 | this.metricService.registerMetric(uvLatencyp95) 108 | 109 | this.runtimeStatsService = ServiceManager.get('runtimeStats') 110 | if (this.runtimeStatsService === undefined) { 111 | this.logger('runtimeStats module not found, fallbacking into pure js method') 112 | let oldTime = process.hrtime() 113 | this.delayTimer = setInterval(() => { 114 | const newTime = process.hrtime() 115 | const delay = (newTime[0] - oldTime[0]) * 1e3 + (newTime[1] - oldTime[1]) / 1e6 - this.delayLoopInterval 116 | oldTime = newTime 117 | histogram.update(delay) 118 | }, this.delayLoopInterval) 119 | 120 | this.delayTimer.unref() 121 | } else { 122 | this.logger('using runtimeStats module as data source for event loop latency') 123 | this.handle = (stats: any) => { 124 | if (typeof stats !== 'object' || !Array.isArray(stats.ticks)) return 125 | stats.ticks.forEach((tick: number) => { 126 | histogram.update(tick) 127 | }) 128 | } 129 | this.runtimeStatsService.on('data', this.handle) 130 | } 131 | } 132 | 133 | destroy () { 134 | if (this.requestTimer !== undefined) { 135 | clearInterval(this.requestTimer) 136 | } 137 | if (this.handleTimer !== undefined) { 138 | clearInterval(this.handleTimer) 139 | } 140 | if (this.delayTimer !== undefined) { 141 | clearInterval(this.delayTimer) 142 | } 143 | if (this.runtimeStatsService !== undefined) { 144 | this.runtimeStatsService.removeListener('data', this.handle) 145 | } 146 | this.logger('destroy') 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/metrics/httpMetrics.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import * as shimmer from 'shimmer' 4 | import Debug from 'debug' 5 | import Configuration from '../configuration' 6 | import { MetricInterface } from '../features/metrics' 7 | import { ServiceManager } from '../serviceManager' 8 | import Meter from '../utils/metrics/meter' 9 | import Histogram from '../utils/metrics/histogram' 10 | import * as requireMiddle from 'require-in-the-middle' 11 | 12 | import { 13 | MetricService, 14 | InternalMetric, 15 | MetricType, 16 | Metric 17 | } from '../services/metrics' 18 | 19 | export class HttpMetricsConfig { 20 | http: boolean 21 | } 22 | 23 | export default class HttpMetrics implements MetricInterface { 24 | 25 | private defaultConf: HttpMetricsConfig = { 26 | http: true 27 | } 28 | private metrics: Map = new Map() 29 | private logger: any = Debug('axm:features:metrics:http') 30 | private metricService: MetricService | undefined 31 | private modules: any = {} 32 | private hooks 33 | 34 | init (config?: HttpMetricsConfig | boolean) { 35 | if (config === false) return 36 | if (config === undefined) { 37 | config = this.defaultConf 38 | } 39 | if (typeof config !== 'object') { 40 | config = this.defaultConf 41 | } 42 | this.logger('init') 43 | Configuration.configureModule({ 44 | latency: true 45 | }) 46 | this.metricService = ServiceManager.get('metrics') 47 | if (this.metricService === undefined) return this.logger(`Failed to load metric service`) 48 | 49 | this.logger('hooking to require') 50 | this.hookRequire() 51 | } 52 | 53 | private registerHttpMetric () { 54 | if (this.metricService === undefined) return this.logger(`Failed to load metric service`) 55 | const histogram = new Histogram() 56 | const p50: InternalMetric = { 57 | name: `HTTP Mean Latency`, 58 | id: 'internal/http/builtin/latency/p50', 59 | type: MetricType.histogram, 60 | historic: true, 61 | implementation: histogram, 62 | unit: 'ms', 63 | handler: () => { 64 | const percentiles = histogram.percentiles([ 0.5 ]) 65 | return percentiles[0.5] 66 | } 67 | } 68 | const p95: InternalMetric = { 69 | name: `HTTP P95 Latency`, 70 | id: 'internal/http/builtin/latency/p95', 71 | type: MetricType.histogram, 72 | historic: true, 73 | implementation: histogram, 74 | handler: () => { 75 | const percentiles = histogram.percentiles([ 0.95 ]) 76 | return percentiles[0.95] 77 | }, 78 | unit: 'ms' 79 | } 80 | const meter: Metric = { 81 | name: 'HTTP', 82 | historic: true, 83 | id: 'internal/http/builtin/reqs', 84 | unit: 'req/min' 85 | } 86 | this.metricService.registerMetric(p50) 87 | this.metricService.registerMetric(p95) 88 | this.metrics.set('http.latency', histogram) 89 | this.metrics.set('http.meter', this.metricService.meter(meter)) 90 | } 91 | 92 | private registerHttpsMetric () { 93 | if (this.metricService === undefined) return this.logger(`Failed to load metric service`) 94 | const histogram = new Histogram() 95 | const p50: InternalMetric = { 96 | name: `HTTPS Mean Latency`, 97 | id: 'internal/https/builtin/latency/p50', 98 | type: MetricType.histogram, 99 | historic: true, 100 | implementation: histogram, 101 | unit: 'ms', 102 | handler: () => { 103 | const percentiles = histogram.percentiles([ 0.5 ]) 104 | return percentiles[0.5] 105 | } 106 | } 107 | const p95: InternalMetric = { 108 | name: `HTTPS P95 Latency`, 109 | id: 'internal/https/builtin/latency/p95', 110 | type: MetricType.histogram, 111 | historic: true, 112 | implementation: histogram, 113 | handler: () => { 114 | const percentiles = histogram.percentiles([ 0.95 ]) 115 | return percentiles[0.95] 116 | }, 117 | unit: 'ms' 118 | } 119 | const meter: Metric = { 120 | name: 'HTTPS', 121 | historic: true, 122 | id: 'internal/https/builtin/reqs', 123 | unit: 'req/min' 124 | } 125 | this.metricService.registerMetric(p50) 126 | this.metricService.registerMetric(p95) 127 | this.metrics.set('https.latency', histogram) 128 | this.metrics.set('https.meter', this.metricService.meter(meter)) 129 | } 130 | 131 | destroy () { 132 | if (this.modules.http !== undefined) { 133 | this.logger('unwraping http module') 134 | shimmer.unwrap(this.modules.http, 'emit') 135 | this.modules.http = undefined 136 | } 137 | if (this.modules.https !== undefined) { 138 | this.logger('unwraping https module') 139 | shimmer.unwrap(this.modules.https, 'emit') 140 | this.modules.https = undefined 141 | } 142 | if (this.hooks) { 143 | this.hooks.unhook() 144 | } 145 | this.logger('destroy') 146 | } 147 | 148 | /** 149 | * Hook the http emit event emitter to be able to track response latency / request count 150 | */ 151 | private hookHttp (nodule: any, name: string) { 152 | if (nodule.Server === undefined || nodule.Server.prototype === undefined) return 153 | if (this.modules[name] !== undefined) return this.logger(`Module ${name} already hooked`) 154 | this.logger(`Hooking to ${name} module`) 155 | this.modules[name] = nodule.Server.prototype 156 | // register the metrics 157 | if (name === 'http') { 158 | this.registerHttpMetric() 159 | } else if (name === 'https') { 160 | this.registerHttpsMetric() 161 | } 162 | const self = this 163 | // wrap the emitter 164 | shimmer.wrap(nodule.Server.prototype, 'emit', (original: Function) => { 165 | return function (event: string, req: any, res: any) { 166 | // only handle http request 167 | if (event !== 'request') return original.apply(this, arguments) 168 | 169 | const meter: Meter | undefined = self.metrics.get(`${name}.meter`) 170 | if (meter !== undefined) { 171 | meter.mark() 172 | } 173 | const latency: Histogram | undefined = self.metrics.get(`${name}.latency`) 174 | if (latency === undefined) return original.apply(this, arguments) 175 | if (res === undefined || res === null) return original.apply(this, arguments) 176 | const startTime = Date.now() 177 | // wait for the response to set the metrics 178 | res.once('finish', _ => { 179 | latency.update(Date.now() - startTime) 180 | }) 181 | return original.apply(this, arguments) 182 | } 183 | }) 184 | } 185 | 186 | private hookRequire () { 187 | this.hooks = requireMiddle(['http', 'https'], (exports, name) => { 188 | this.hookHttp(exports, name) 189 | return exports 190 | }) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/metrics/network.ts: -------------------------------------------------------------------------------- 1 | import * as netModule from 'net' 2 | import { MetricService, MetricType } from '../services/metrics' 3 | import { MetricInterface } from '../features/metrics' 4 | import * as Debug from 'debug' 5 | import Meter from '../utils/metrics/meter' 6 | import * as shimmer from 'shimmer' 7 | import { ServiceManager } from '../serviceManager' 8 | 9 | export class NetworkTrafficConfig { 10 | upload: boolean 11 | download: boolean 12 | } 13 | 14 | const defaultConfig: NetworkTrafficConfig = { 15 | upload: false, 16 | download: false 17 | } 18 | 19 | const allEnabled: NetworkTrafficConfig = { 20 | upload: true, 21 | download: true 22 | } 23 | 24 | export default class NetworkMetric implements MetricInterface { 25 | private metricService: MetricService | undefined 26 | private timer: NodeJS.Timer | undefined 27 | private logger: Function = Debug('axm:features:metrics:network') 28 | private socketProto: any 29 | 30 | init (config?: NetworkTrafficConfig | boolean) { 31 | if (config === false) return 32 | if (config === true) { 33 | config = allEnabled 34 | } 35 | if (config === undefined) { 36 | config = defaultConfig 37 | } 38 | 39 | this.metricService = ServiceManager.get('metrics') 40 | if (this.metricService === undefined) { 41 | return this.logger(`Failed to load metric service`) 42 | } 43 | 44 | if (config.download === true) { 45 | this.catchDownload() 46 | } 47 | if (config.upload === true) { 48 | this.catchUpload() 49 | } 50 | this.logger('init') 51 | } 52 | 53 | destroy () { 54 | if (this.timer !== undefined) { 55 | clearTimeout(this.timer) 56 | } 57 | 58 | if (this.socketProto !== undefined && this.socketProto !== null) { 59 | shimmer.unwrap(this.socketProto, 'read') 60 | shimmer.unwrap(this.socketProto, 'write') 61 | } 62 | 63 | this.logger('destroy') 64 | } 65 | 66 | private catchDownload () { 67 | if (this.metricService === undefined) return this.logger(`Failed to load metric service`) 68 | const downloadMeter = new Meter({}) 69 | 70 | this.metricService.registerMetric({ 71 | name: 'Network In', 72 | id: 'internal/network/in', 73 | historic: true, 74 | type: MetricType.meter, 75 | implementation: downloadMeter, 76 | unit: 'kb/s', 77 | handler: function () { 78 | return Math.floor(this.implementation.val() / 1024 * 1000) / 1000 79 | } 80 | }) 81 | 82 | setTimeout(() => { 83 | const property = netModule.Socket.prototype.read 84 | // @ts-ignore thanks mr typescript but we are monkey patching here 85 | const isWrapped = property && property.__wrapped === true 86 | if (isWrapped) { 87 | return this.logger(`Already patched socket read, canceling`) 88 | } 89 | shimmer.wrap(netModule.Socket.prototype, 'read', function (original) { 90 | return function () { 91 | this.on('data', (data) => { 92 | if (typeof data.length === 'number') { 93 | downloadMeter.mark(data.length) 94 | } 95 | }) 96 | return original.apply(this, arguments) 97 | } 98 | }) 99 | }, 500) 100 | } 101 | 102 | private catchUpload () { 103 | if (this.metricService === undefined) return this.logger(`Failed to load metric service`) 104 | const uploadMeter = new Meter() 105 | this.metricService.registerMetric({ 106 | name: 'Network Out', 107 | id: 'internal/network/out', 108 | type: MetricType.meter, 109 | historic: true, 110 | implementation: uploadMeter, 111 | unit: 'kb/s', 112 | handler: function () { 113 | return Math.floor(this.implementation.val() / 1024 * 1000) / 1000 114 | } 115 | }) 116 | 117 | setTimeout(() => { 118 | const property = netModule.Socket.prototype.write 119 | // @ts-ignore thanks mr typescript but we are monkey patching here 120 | const isWrapped = property && property.__wrapped === true 121 | if (isWrapped) { 122 | return this.logger(`Already patched socket write, canceling`) 123 | } 124 | shimmer.wrap(netModule.Socket.prototype, 'write', function (original) { 125 | return function (data) { 126 | if (typeof data.length === 'number') { 127 | uploadMeter.mark(data.length) 128 | } 129 | return original.apply(this, arguments) 130 | } 131 | }) 132 | }, 500) 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/metrics/runtime.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { MetricService, MetricType, MetricMeasurements } from '../services/metrics' 4 | import { ServiceManager } from '../serviceManager' 5 | import * as Debug from 'debug' 6 | import { MetricInterface } from '../features/metrics' 7 | import Histogram from '../utils/metrics/histogram' 8 | import { RuntimeStatsService } from '../services/runtimeStats' 9 | 10 | export class RuntimeMetricsOptions { 11 | gcOldPause: boolean 12 | gcNewPause: boolean 13 | /** 14 | * Toggle metrics about the page reclaims (soft and hard) 15 | * see https://en.wikipedia.org/wiki/Page_fault 16 | */ 17 | pageFaults: boolean 18 | /** 19 | * Toggle metrics about CPU context switch 20 | * see https://en.wikipedia.org/wiki/Context_switch 21 | */ 22 | contextSwitchs: boolean 23 | } 24 | 25 | const defaultOptions: RuntimeMetricsOptions = { 26 | gcNewPause: true, 27 | gcOldPause: true, 28 | pageFaults: true, 29 | contextSwitchs: true 30 | } 31 | 32 | export default class RuntimeMetrics implements MetricInterface { 33 | 34 | private metricService: MetricService | undefined 35 | private logger: any = Debug('axm:features:metrics:runtime') 36 | private runtimeStatsService: RuntimeStatsService | undefined 37 | private handle: (data: Object) => void | undefined 38 | private metrics: Map = new Map() 39 | 40 | init (config?: RuntimeMetricsOptions | boolean) { 41 | if (config === false) return 42 | if (config === undefined) { 43 | config = defaultOptions 44 | } 45 | if (config === true) { 46 | config = defaultOptions 47 | } 48 | 49 | this.metricService = ServiceManager.get('metrics') 50 | if (this.metricService === undefined) return this.logger('Failed to load metric service') 51 | 52 | this.runtimeStatsService = ServiceManager.get('runtimeStats') 53 | if (this.runtimeStatsService === undefined) return this.logger('Failed to load runtime stats service') 54 | 55 | this.logger('init') 56 | 57 | const newHistogram = new Histogram() 58 | if (config.gcNewPause === true) { 59 | this.metricService.registerMetric({ 60 | name: 'GC New Space Pause', 61 | id: 'internal/v8/gc/new/pause/p50', 62 | type: MetricType.histogram, 63 | historic: true, 64 | implementation: newHistogram, 65 | unit: 'ms', 66 | handler: function () { 67 | const percentiles = this.implementation.percentiles([ 0.5 ]) 68 | return percentiles[0.5] 69 | } 70 | }) 71 | this.metricService.registerMetric({ 72 | name: 'GC New Space Pause p95', 73 | id: 'internal/v8/gc/new/pause/p95', 74 | type: MetricType.histogram, 75 | historic: true, 76 | implementation: newHistogram, 77 | unit: 'ms', 78 | handler: function () { 79 | const percentiles = this.implementation.percentiles([ 0.95 ]) 80 | return percentiles[0.95] 81 | } 82 | }) 83 | } 84 | 85 | const oldHistogram = new Histogram() 86 | if (config.gcOldPause === true) { 87 | this.metricService.registerMetric({ 88 | name: 'GC Old Space Pause', 89 | id: 'internal/v8/gc/old/pause/p50', 90 | type: MetricType.histogram, 91 | historic: true, 92 | implementation: oldHistogram, 93 | unit: 'ms', 94 | handler: function () { 95 | const percentiles = this.implementation.percentiles([ 0.5 ]) 96 | return percentiles[0.5] 97 | } 98 | }) 99 | this.metricService.registerMetric({ 100 | name: 'GC Old Space Pause p95', 101 | id: 'internal/v8/gc/old/pause/p95', 102 | type: MetricType.histogram, 103 | historic: true, 104 | implementation: oldHistogram, 105 | unit: 'ms', 106 | handler: function () { 107 | const percentiles = this.implementation.percentiles([ 0.95 ]) 108 | return percentiles[0.95] 109 | } 110 | }) 111 | } 112 | 113 | if (config.contextSwitchs === true) { 114 | const volontarySwitchs = this.metricService.histogram({ 115 | name: 'Volontary CPU Context Switch', 116 | id: 'internal/uv/cpu/contextswitch/volontary', 117 | measurement: MetricMeasurements.mean 118 | }) 119 | const inVolontarySwitchs = this.metricService.histogram({ 120 | name: 'Involuntary CPU Context Switch', 121 | id: 'internal/uv/cpu/contextswitch/involontary', 122 | measurement: MetricMeasurements.mean 123 | }) 124 | this.metrics.set('inVolontarySwitchs', inVolontarySwitchs) 125 | this.metrics.set('volontarySwitchs', volontarySwitchs) 126 | } 127 | 128 | if (config.pageFaults === true) { 129 | const softPageFault = this.metricService.histogram({ 130 | name: 'Minor Page Fault', 131 | id: 'internal/uv/memory/pagefault/minor', 132 | measurement: MetricMeasurements.mean 133 | }) 134 | const hardPageFault = this.metricService.histogram({ 135 | name: 'Major Page Fault', 136 | id: 'internal/uv/memory/pagefault/major', 137 | measurement: MetricMeasurements.mean 138 | }) 139 | this.metrics.set('softPageFault', softPageFault) 140 | this.metrics.set('hardPageFault', hardPageFault) 141 | } 142 | 143 | this.handle = (stats: any) => { 144 | if (typeof stats !== 'object' || typeof stats.gc !== 'object') return 145 | newHistogram.update(stats.gc.newPause) 146 | oldHistogram.update(stats.gc.oldPause) 147 | if (typeof stats.usage !== 'object') return 148 | const volontarySwitchs = this.metrics.get('volontarySwitchs') 149 | if (volontarySwitchs !== undefined) { 150 | volontarySwitchs.update(stats.usage.ru_nvcsw) 151 | } 152 | const inVolontarySwitchs = this.metrics.get('inVolontarySwitchs') 153 | if (inVolontarySwitchs !== undefined) { 154 | inVolontarySwitchs.update(stats.usage.ru_nivcsw) 155 | } 156 | const softPageFault = this.metrics.get('softPageFault') 157 | if (softPageFault !== undefined) { 158 | softPageFault.update(stats.usage.ru_minflt) 159 | } 160 | const hardPageFault = this.metrics.get('hardPageFault') 161 | if (hardPageFault !== undefined) { 162 | hardPageFault.update(stats.usage.ru_majflt) 163 | } 164 | } 165 | 166 | this.runtimeStatsService.on('data', this.handle) 167 | } 168 | 169 | destroy () { 170 | if (this.runtimeStatsService !== undefined) { 171 | this.runtimeStatsService.removeListener('data', this.handle) 172 | } 173 | this.logger('destroy') 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/metrics/v8.ts: -------------------------------------------------------------------------------- 1 | import * as v8 from 'v8' 2 | import { MetricService, Metric } from '../services/metrics' 3 | import { MetricInterface } from '../features/metrics' 4 | import Debug from 'debug' 5 | import { ServiceManager } from '../serviceManager' 6 | import Gauge from '../utils/metrics/gauge' 7 | 8 | /* tslint:disable */ 9 | export class V8MetricsConfig { 10 | new_space: boolean 11 | old_space: boolean 12 | map_space: boolean 13 | code_space: boolean 14 | large_object_space: boolean 15 | heap_total_size: boolean 16 | heap_used_size: boolean 17 | heap_used_percent: boolean 18 | } 19 | /* tslint:enable */ 20 | 21 | const defaultOptions: V8MetricsConfig = { 22 | new_space: false, 23 | old_space: false, 24 | map_space: false, 25 | code_space: false, 26 | large_object_space: false, 27 | heap_total_size: true, 28 | heap_used_size: true, 29 | heap_used_percent: true 30 | } 31 | 32 | export default class V8Metric implements MetricInterface { 33 | 34 | private timer: NodeJS.Timer | undefined 35 | private TIME_INTERVAL: number = 800 36 | private metricService: MetricService | undefined 37 | private logger: Function = Debug('axm:features:metrics:v8') 38 | private metricStore: Map = new Map() 39 | 40 | private unitKB = 'MiB' 41 | 42 | private metricsDefinitions = { 43 | /* 44 | new_space: { 45 | name: 'New space used size', 46 | id: 'internal/v8/heap/space/new', 47 | unit: this.unitKB, 48 | historic: true 49 | }, 50 | old_space: { 51 | name: 'Old space used size', 52 | id: 'internal/v8/heap/space/old', 53 | unit: this.unitKB, 54 | historic: true 55 | }, 56 | map_space: { 57 | name: 'Map space used size', 58 | id: 'internal/v8/heap/space/map', 59 | unit: this.unitKB, 60 | historic: false 61 | }, 62 | code_space: { 63 | name: 'Code space used size', 64 | id: 'internal/v8/heap/space/code', 65 | unit: this.unitKB, 66 | historic: false 67 | }, 68 | large_object_space: { 69 | name: 'Large object space used size', 70 | id: 'internal/v8/heap/space/large', 71 | unit: this.unitKB, 72 | historic: false 73 | },*/ 74 | total_heap_size: { 75 | name: 'Heap Size', 76 | id: 'internal/v8/heap/total', 77 | unit: this.unitKB, 78 | historic: true 79 | }, 80 | heap_used_percent: { 81 | name: 'Heap Usage', 82 | id: 'internal/v8/heap/usage', 83 | unit: '%', 84 | historic: true 85 | }, 86 | used_heap_size: { 87 | name: 'Used Heap Size', 88 | id: 'internal/v8/heap/used', 89 | unit: this.unitKB, 90 | historic: true 91 | } 92 | } 93 | 94 | init (config?: V8MetricsConfig | boolean) { 95 | if (config === false) return 96 | if (config === undefined) { 97 | config = defaultOptions 98 | } 99 | if (config === true) { 100 | config = defaultOptions 101 | } 102 | 103 | this.metricService = ServiceManager.get('metrics') 104 | if (this.metricService === undefined) return this.logger('Failed to load metric service') 105 | this.logger('init') 106 | 107 | if (!v8.hasOwnProperty('getHeapStatistics')) { 108 | return this.logger(`V8.getHeapStatistics is not available, aborting`) 109 | } 110 | 111 | for (let metricName in this.metricsDefinitions) { 112 | if (config[metricName] === false) continue 113 | const isEnabled: boolean = config[metricName] 114 | if (isEnabled === false) continue 115 | let metric: Metric = this.metricsDefinitions[metricName] 116 | this.metricStore.set(metricName, this.metricService.metric(metric)) 117 | } 118 | 119 | this.timer = setInterval(() => { 120 | const stats = v8.getHeapStatistics() 121 | // update each metrics that we declared 122 | for (let metricName in this.metricsDefinitions) { 123 | if (typeof stats[metricName] !== 'number') continue 124 | const gauge = this.metricStore.get(metricName) 125 | if (gauge === undefined) continue 126 | gauge.set(this.formatMiBytes(stats[metricName])) 127 | } 128 | // manually compute the heap usage 129 | const usage = (stats.used_heap_size / stats.total_heap_size * 100).toFixed(2) 130 | const usageMetric = this.metricStore.get('heap_used_percent') 131 | if (usageMetric !== undefined) { 132 | usageMetric.set(parseFloat(usage)) 133 | } 134 | }, this.TIME_INTERVAL) 135 | 136 | this.timer.unref() 137 | } 138 | 139 | destroy () { 140 | if (this.timer !== undefined) { 141 | clearInterval(this.timer) 142 | } 143 | this.logger('destroy') 144 | } 145 | 146 | private formatMiBytes (val: number) { 147 | return (val / 1024 / 1024).toFixed(2) 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/profilers/addonProfiler.ts: -------------------------------------------------------------------------------- 1 | import { ProfilerType } from '../features/profiling' 2 | import utils from '../utils/module' 3 | import Configuration from '../configuration' 4 | import { ServiceManager } from '../serviceManager' 5 | import { Transport } from '../services/transport' 6 | import { ActionService } from '../services/actions' 7 | import MiscUtils from '../utils/miscellaneous' 8 | import * as Debug from 'debug' 9 | 10 | class CurrentProfile { 11 | uuid: string 12 | startTime: number 13 | initiated: string 14 | } 15 | 16 | export default class AddonProfiler implements ProfilerType { 17 | 18 | private profiler: any = null 19 | /** 20 | * List of modules that we can require as profiler 21 | * the v8-profiler module has segfault for node > 8 22 | * so we want to be able to use the newer one in priority 23 | */ 24 | private modules = [ 'v8-profiler-node8', 'v8-profiler' ] 25 | private actionService: ActionService | undefined 26 | private transport: Transport | undefined 27 | private currentProfile: CurrentProfile | null = null 28 | private logger: Function = Debug('axm:features:profiling:addon') 29 | 30 | init () { 31 | for (const moduleName of this.modules) { 32 | let path = utils.detectModule(moduleName) 33 | // continue to search if we dont find it 34 | if (path === null) continue 35 | let profiler = utils.loadModule(moduleName) 36 | // we can fail to require it for some reasons 37 | if (profiler instanceof Error) continue 38 | this.profiler = profiler 39 | break 40 | } 41 | if (this.profiler === null) { 42 | Configuration.configureModule({ 43 | heapdump: false, 44 | 'feature.profiler.heap_snapshot': false, 45 | 'feature.profiler.heap_sampling': false, 46 | 'feature.profiler.cpu_js': false 47 | }) 48 | return this.logger(`Failed to require the profiler via addon, disabling profiling ...`) 49 | } 50 | this.logger('init') 51 | 52 | this.actionService = ServiceManager.get('actions') 53 | if (this.actionService === undefined) { 54 | return this.logger(`Fail to get action service`) 55 | } 56 | this.transport = ServiceManager.get('transport') 57 | if (this.transport === undefined) { 58 | return this.logger(`Fail to get transport service`) 59 | } 60 | 61 | Configuration.configureModule({ 62 | heapdump: true, 63 | 'feature.profiler.heapsnapshot': true, 64 | 'feature.profiler.heapsampling': false, 65 | 'feature.profiler.cpu_js': true 66 | }) 67 | this.register() 68 | } 69 | 70 | register () { 71 | if (this.actionService === undefined) { 72 | return this.logger(`Fail to get action service`) 73 | } 74 | this.logger('register') 75 | this.actionService.registerAction('km:heapdump', this.onHeapdump.bind(this)) 76 | this.actionService.registerAction('km:cpu:profiling:start', this.onCPUProfileStart.bind(this)) 77 | this.actionService.registerAction('km:cpu:profiling:stop', this.onCPUProfileStop.bind(this)) 78 | } 79 | 80 | destroy () { 81 | this.logger('Addon Profiler destroyed !') 82 | if (this.profiler === null) return 83 | this.profiler.deleteAllProfiles() 84 | } 85 | 86 | private onCPUProfileStart (opts, cb) { 87 | if (typeof cb !== 'function') { 88 | cb = opts 89 | opts = {} 90 | } 91 | if (typeof opts !== 'object' || opts === null) { 92 | opts = {} 93 | } 94 | 95 | if (this.currentProfile !== null) { 96 | return cb({ 97 | err: 'A profiling is already running', 98 | success: false 99 | }) 100 | } 101 | this.currentProfile = new CurrentProfile() 102 | this.currentProfile.uuid = MiscUtils.generateUUID() 103 | this.currentProfile.startTime = Date.now() 104 | this.currentProfile.initiated = typeof opts.initiated === 'string' 105 | ? opts.initiated : 'manual' 106 | 107 | // run the callback to acknowledge that we received the action 108 | cb({ success: true, uuid: this.currentProfile.uuid }) 109 | 110 | this.profiler.startProfiling() 111 | 112 | if (isNaN(parseInt(opts.timeout, 10))) return 113 | // if the duration is included, handle that ourselves 114 | const duration = parseInt(opts.timeout, 10) 115 | setTimeout(_ => { 116 | // it will send the profiling itself 117 | this.onCPUProfileStop(_ => { 118 | return 119 | }) 120 | }, duration) 121 | } 122 | 123 | private onCPUProfileStop (cb) { 124 | if (this.currentProfile === null) { 125 | return cb({ 126 | err: 'No profiling are already running', 127 | success: false 128 | }) 129 | } 130 | if (this.transport === undefined) { 131 | return cb({ 132 | err: 'No profiling are already running', 133 | success: false 134 | }) 135 | } 136 | const profile = this.profiler.stopProfiling() 137 | const data = JSON.stringify(profile) 138 | 139 | // run the callback to acknowledge that we received the action 140 | cb({ success: true, uuid: this.currentProfile.uuid }) 141 | 142 | // send the profile to the transporter 143 | this.transport.send('profilings', { 144 | uuid: this.currentProfile.uuid, 145 | duration: Date.now() - this.currentProfile.startTime, 146 | at: this.currentProfile.startTime, 147 | data, 148 | dump_file_size: data.length, 149 | success: true, 150 | initiated: this.currentProfile.initiated, 151 | type: 'cpuprofile', 152 | cpuprofile: true 153 | }) 154 | this.currentProfile = null 155 | } 156 | 157 | /** 158 | * Custom action implementation to make a heap snapshot 159 | */ 160 | private onHeapdump (opts, cb) { 161 | if (typeof cb !== 'function') { 162 | cb = opts 163 | opts = {} 164 | } 165 | if (typeof opts !== 'object' || opts === null) { 166 | opts = {} 167 | } 168 | 169 | // run the callback to acknowledge that we received the action 170 | cb({ success: true }) 171 | 172 | // wait few ms to be sure we sended the ACK because the snapshot stop the world 173 | setTimeout(() => { 174 | const startTime = Date.now() 175 | this.takeSnapshot() 176 | .then((data: string) => { 177 | // @ts-ignore thanks mr typescript but its not possible 178 | return this.transport.send('profilings', { 179 | data, 180 | at: startTime, 181 | initiated: typeof opts.initiated === 'string' ? opts.initiated : 'manual', 182 | duration: Date.now() - startTime, 183 | type: 'heapdump' 184 | }) 185 | }).catch(err => { 186 | return cb({ 187 | success: err.message, 188 | err: err 189 | }) 190 | }) 191 | }, 200) 192 | } 193 | 194 | private takeSnapshot () { 195 | return new Promise((resolve, reject) => { 196 | const snapshot = this.profiler.takeSnapshot() 197 | snapshot.export((err, data) => { 198 | if (err) { 199 | reject(err) 200 | } else { 201 | resolve(data) 202 | } 203 | // delete the snapshot as soon as we have serialized it 204 | snapshot.delete() 205 | }) 206 | }) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/profilers/inspectorProfiler.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ProfilerType } from '../features/profiling' 3 | import Configuration from '../configuration' 4 | import { ServiceManager } from '../serviceManager' 5 | import { Transport } from '../services/transport' 6 | import { ActionService } from '../services/actions' 7 | import MiscUtils from '../utils/miscellaneous' 8 | import { InspectorService } from '../services/inspector' 9 | import * as inspector from 'inspector' 10 | import * as Debug from 'debug' 11 | import * as semver from 'semver' 12 | 13 | class CurrentProfile { 14 | uuid: string 15 | startTime: number 16 | initiated: string 17 | } 18 | 19 | export default class InspectorProfiler implements ProfilerType { 20 | 21 | private profiler: InspectorService | undefined = undefined 22 | private actionService: ActionService | undefined 23 | private transport: Transport | undefined 24 | private currentProfile: CurrentProfile | null = null 25 | private logger: Function = Debug('axm:features:profiling:inspector') 26 | private isNode11: boolean = semver.satisfies(semver.clean(process.version), '>11.x') 27 | 28 | init () { 29 | this.profiler = ServiceManager.get('inspector') 30 | if (this.profiler === undefined) { 31 | Configuration.configureModule({ 32 | heapdump: false, 33 | 'feature.profiler.heap_snapshot': false, 34 | 'feature.profiler.heap_sampling': false, 35 | 'feature.profiler.cpu_js': false 36 | }) 37 | return console.error(`Failed to require the profiler via inspector, disabling profiling ...`) 38 | } 39 | 40 | this.profiler.getSession().post('Profiler.enable') 41 | this.profiler.getSession().post('HeapProfiler.enable') 42 | this.logger('init') 43 | 44 | this.actionService = ServiceManager.get('actions') 45 | if (this.actionService === undefined) { 46 | return this.logger(`Fail to get action service`) 47 | } 48 | this.transport = ServiceManager.get('transport') 49 | if (this.transport === undefined) { 50 | return this.logger(`Fail to get transport service`) 51 | } 52 | 53 | Configuration.configureModule({ 54 | heapdump: true, 55 | 'feature.profiler.heapsnapshot': !this.isNode11, 56 | 'feature.profiler.heapsampling': true, 57 | 'feature.profiler.cpu_js': true 58 | }) 59 | this.register() 60 | } 61 | 62 | register () { 63 | if (this.actionService === undefined) { 64 | return this.logger(`Fail to get action service`) 65 | } 66 | this.logger('register') 67 | this.actionService.registerAction('km:heapdump', this.onHeapdump.bind(this)) 68 | this.actionService.registerAction('km:cpu:profiling:start', this.onCPUProfileStart.bind(this)) 69 | this.actionService.registerAction('km:cpu:profiling:stop', this.onCPUProfileStop.bind(this)) 70 | this.actionService.registerAction('km:heap:sampling:start', this.onHeapProfileStart.bind(this)) 71 | this.actionService.registerAction('km:heap:sampling:stop', this.onHeapProfileStop.bind(this)) 72 | } 73 | 74 | destroy () { 75 | this.logger('Inspector Profiler destroyed !') 76 | if (this.profiler === undefined) return 77 | this.profiler.getSession().post('Profiler.disable') 78 | this.profiler.getSession().post('HeapProfiler.disable') 79 | } 80 | 81 | private onHeapProfileStart (opts, cb) { 82 | if (typeof cb !== 'function') { 83 | cb = opts 84 | opts = {} 85 | } 86 | if (typeof opts !== 'object' || opts === null) { 87 | opts = {} 88 | } 89 | 90 | // not possible but thanks mr typescript 91 | if (this.profiler === undefined) { 92 | return cb({ 93 | err: 'Profiler not available', 94 | success: false 95 | }) 96 | } 97 | 98 | if (this.currentProfile !== null) { 99 | return cb({ 100 | err: 'A profiling is already running', 101 | success: false 102 | }) 103 | } 104 | this.currentProfile = new CurrentProfile() 105 | this.currentProfile.uuid = MiscUtils.generateUUID() 106 | this.currentProfile.startTime = Date.now() 107 | this.currentProfile.initiated = typeof opts.initiated === 'string' 108 | ? opts.initiated : 'manual' 109 | 110 | // run the callback to acknowledge that we received the action 111 | cb({ success: true, uuid: this.currentProfile.uuid }) 112 | 113 | const defaultSamplingInterval = 16384 114 | this.profiler.getSession().post('HeapProfiler.startSampling', { 115 | samplingInterval: typeof opts.samplingInterval === 'number' 116 | ? opts.samplingInterval : defaultSamplingInterval 117 | }) 118 | 119 | if (isNaN(parseInt(opts.timeout, 10))) return 120 | // if the duration is included, handle that ourselves 121 | const duration = parseInt(opts.timeout, 10) 122 | setTimeout(_ => { 123 | // it will send the profiling itself 124 | this.onHeapProfileStop(_ => { 125 | return 126 | }) 127 | }, duration) 128 | } 129 | 130 | private onHeapProfileStop (cb) { 131 | if (this.currentProfile === null) { 132 | return cb({ 133 | err: 'No profiling are already running', 134 | success: false 135 | }) 136 | } 137 | // not possible but thanks mr typescript 138 | if (this.profiler === undefined) { 139 | return cb({ 140 | err: 'Profiler not available', 141 | success: false 142 | }) 143 | } 144 | 145 | // run the callback to acknowledge that we received the action 146 | cb({ success: true, uuid: this.currentProfile.uuid }) 147 | 148 | this.profiler.getSession().post('HeapProfiler.stopSampling', (_: Error, { profile }: inspector.HeapProfiler.StopSamplingReturnType) => { 149 | // not possible but thanks mr typescript 150 | if (this.currentProfile === null) return 151 | if (this.transport === undefined) return 152 | 153 | const data = JSON.stringify(profile) 154 | 155 | this.transport.send('profilings', { 156 | uuid: this.currentProfile.uuid, 157 | duration: Date.now() - this.currentProfile.startTime, 158 | at: this.currentProfile.startTime, 159 | data, 160 | success: true, 161 | initiated: this.currentProfile.initiated, 162 | type: 'heapprofile', 163 | heapprofile: true 164 | }) 165 | this.currentProfile = null 166 | }) 167 | } 168 | 169 | private onCPUProfileStart (opts, cb) { 170 | if (typeof cb !== 'function') { 171 | cb = opts 172 | opts = {} 173 | } 174 | if (typeof opts !== 'object' || opts === null) { 175 | opts = {} 176 | } 177 | // not possible but thanks mr typescript 178 | if (this.profiler === undefined) { 179 | return cb({ 180 | err: 'Profiler not available', 181 | success: false 182 | }) 183 | } 184 | 185 | if (this.currentProfile !== null) { 186 | return cb({ 187 | err: 'A profiling is already running', 188 | success: false 189 | }) 190 | } 191 | this.currentProfile = new CurrentProfile() 192 | this.currentProfile.uuid = MiscUtils.generateUUID() 193 | this.currentProfile.startTime = Date.now() 194 | this.currentProfile.initiated = typeof opts.initiated === 'string' 195 | ? opts.initiated : 'manual' 196 | 197 | // run the callback to acknowledge that we received the action 198 | cb({ success: true, uuid: this.currentProfile.uuid }) 199 | 200 | // start the idle time reporter to tell V8 when node is idle 201 | // See https://github.com/nodejs/node/issues/19009#issuecomment-403161559. 202 | if (process.hasOwnProperty('_startProfilerIdleNotifier') === true) { 203 | (process as any)._startProfilerIdleNotifier() 204 | } 205 | 206 | this.profiler.getSession().post('Profiler.start') 207 | 208 | if (isNaN(parseInt(opts.timeout, 10))) return 209 | // if the duration is included, handle that ourselves 210 | const duration = parseInt(opts.timeout, 10) 211 | setTimeout(_ => { 212 | // it will send the profiling itself 213 | this.onCPUProfileStop(_ => { 214 | return 215 | }) 216 | }, duration) 217 | } 218 | 219 | private onCPUProfileStop (cb) { 220 | if (this.currentProfile === null) { 221 | return cb({ 222 | err: 'No profiling are already running', 223 | success: false 224 | }) 225 | } 226 | // not possible but thanks mr typescript 227 | if (this.profiler === undefined) { 228 | return cb({ 229 | err: 'Profiler not available', 230 | success: false 231 | }) 232 | } 233 | 234 | // run the callback to acknowledge that we received the action 235 | cb({ success: true, uuid: this.currentProfile.uuid }) 236 | 237 | // stop the idle time reporter to tell V8 when node is idle 238 | // See https://github.com/nodejs/node/issues/19009#issuecomment-403161559. 239 | if (process.hasOwnProperty('_stopProfilerIdleNotifier') === true) { 240 | (process as any)._stopProfilerIdleNotifier() 241 | } 242 | 243 | this.profiler.getSession().post('Profiler.stop', (_: Error, res: any) => { 244 | // not possible but thanks mr typescript 245 | if (this.currentProfile === null) return 246 | if (this.transport === undefined) return 247 | 248 | const profile: inspector.Profiler.Profile = res.profile 249 | const data = JSON.stringify(profile) 250 | 251 | // send the profile to the transporter 252 | this.transport.send('profilings', { 253 | uuid: this.currentProfile.uuid, 254 | duration: Date.now() - this.currentProfile.startTime, 255 | at: this.currentProfile.startTime, 256 | data, 257 | success: true, 258 | initiated: this.currentProfile.initiated, 259 | type: 'cpuprofile', 260 | cpuprofile: true 261 | }) 262 | this.currentProfile = null 263 | }) 264 | } 265 | 266 | /** 267 | * Custom action implementation to make a heap snapshot 268 | */ 269 | private onHeapdump (opts, cb) { 270 | if (typeof cb !== 'function') { 271 | cb = opts 272 | opts = {} 273 | } 274 | if (typeof opts !== 'object' || opts === null) { 275 | opts = {} 276 | } 277 | // not possible but thanks mr typescript 278 | if (this.profiler === undefined) { 279 | return cb({ 280 | err: 'Profiler not available', 281 | success: false 282 | }) 283 | } 284 | 285 | // run the callback to acknowledge that we received the action 286 | cb({ success: true }) 287 | 288 | // wait few ms to be sure we sended the ACK because the snapshot stop the world 289 | setTimeout(() => { 290 | const startTime = Date.now() 291 | this.takeSnapshot() 292 | .then(data => { 293 | // @ts-ignore thanks mr typescript but its not possible 294 | return this.transport.send('profilings', { 295 | data, 296 | at: startTime, 297 | initiated: typeof opts.initiated === 'string' ? opts.initiated : 'manual', 298 | duration: Date.now() - startTime, 299 | type: 'heapdump' 300 | }) 301 | }).catch(err => { 302 | return cb({ 303 | success: err.message, 304 | err: err 305 | }) 306 | }) 307 | }, 200) 308 | } 309 | 310 | takeSnapshot () { 311 | return new Promise(async (resolve, reject) => { 312 | // not possible but thanks mr typescript 313 | if (this.profiler === undefined) return reject(new Error(`Profiler not available`)) 314 | 315 | const chunks: Array = [] 316 | const chunkHandler = (raw: any) => { 317 | const data = raw.params as inspector.HeapProfiler.AddHeapSnapshotChunkEventDataType 318 | chunks.push(data.chunk) 319 | } 320 | this.profiler.getSession().on('HeapProfiler.addHeapSnapshotChunk', chunkHandler) 321 | // tslint:disable-next-line 322 | await this.profiler.getSession().post('HeapProfiler.takeHeapSnapshot', { 323 | reportProgress: false 324 | }) 325 | // remove the listeners 326 | this.profiler.getSession().removeListener('HeapProfiler.addHeapSnapshotChunk', chunkHandler) 327 | return resolve(chunks.join('')) 328 | }) 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/serviceManager.ts: -------------------------------------------------------------------------------- 1 | 2 | const services: Map = new Map() 3 | 4 | export class Service {} 5 | 6 | export class ServiceManager { 7 | 8 | public static get (serviceName: string): any | undefined { 9 | return services.get(serviceName) 10 | } 11 | 12 | public static set (serviceName: string, service: Service) { 13 | return services.set(serviceName, service) 14 | } 15 | 16 | public static reset (serviceName: string) { 17 | return services.delete(serviceName) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/services/actions.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from '../serviceManager' 2 | import { Transport } from './transport' 3 | import * as Debug from 'debug' 4 | 5 | export class Action { 6 | handler: Function 7 | name: string 8 | type: string 9 | isScoped: boolean 10 | callback?: Function 11 | arity: number 12 | opts: Object | null | undefined 13 | } 14 | 15 | export class ActionService { 16 | 17 | private timer: NodeJS.Timer | undefined = undefined 18 | private transport: Transport | undefined = undefined 19 | private actions: Map = new Map() 20 | private logger: Function = Debug('axm:services:actions') 21 | 22 | private listener (data) { 23 | this.logger(`Received new message from reverse`) 24 | if (!data) return false 25 | 26 | const actionName = data.msg ? data.msg : data.action_name ? data.action_name : data 27 | let action = this.actions.get(actionName) 28 | if (typeof action !== 'object') { 29 | return this.logger(`Received action ${actionName} but failed to find the implementation`) 30 | } 31 | 32 | // handle normal custom action 33 | if (!action.isScoped) { 34 | this.logger(`Succesfully called custom action ${action.name} with arity ${action.handler.length}`) 35 | // In case 2 arguments has been set but no options has been transmitted 36 | if (action.handler.length === 2) { 37 | let params = {} 38 | if (typeof data === 'object') { 39 | params = data.opts 40 | } 41 | return action.handler(params, action.callback) 42 | } 43 | return action.handler(action.callback) 44 | } 45 | 46 | // handle scoped actions 47 | if (data.uuid === undefined) { 48 | return this.logger(`Received scoped action ${action.name} but without uuid`) 49 | } 50 | 51 | // create a simple object that represent a stream 52 | const stream = { 53 | send : (dt) => { 54 | // @ts-ignore thanks mr typescript but i already checked above 55 | this.transport.send('axm:scoped_action:stream', { 56 | data: dt, 57 | uuid: data.uuid, 58 | action_name: actionName 59 | }) 60 | }, 61 | error : (dt) => { 62 | // @ts-ignore thanks mr typescript but i already checked above 63 | this.transport.send('axm:scoped_action:error', { 64 | data: dt, 65 | uuid: data.uuid, 66 | action_name: actionName 67 | }) 68 | }, 69 | end : (dt) => { 70 | // @ts-ignore thanks mr typescript but i already checked above 71 | this.transport.send('axm:scoped_action:end', { 72 | data: dt, 73 | uuid: data.uuid, 74 | action_name: actionName 75 | }) 76 | } 77 | } 78 | 79 | this.logger(`Succesfully called scoped action ${action.name}`) 80 | return action.handler(data.opts || {}, stream) 81 | } 82 | 83 | init (): void { 84 | this.transport = ServiceManager.get('transport') 85 | // tslint:disable-next-line 86 | if (this.transport === undefined) { 87 | return this.logger(`Failed to load transport service`) 88 | } 89 | this.actions.clear() 90 | this.transport.on('data', this.listener.bind(this)) 91 | } 92 | 93 | destroy (): void { 94 | if (this.timer !== undefined) { 95 | clearInterval(this.timer) 96 | } 97 | // tslint:disable-next-line 98 | if (this.transport !== undefined) { 99 | this.transport.removeListener('data', this.listener.bind(this)) 100 | } 101 | } 102 | 103 | /** 104 | * Register a custom action that will be called when we receive a call for this actionName 105 | */ 106 | registerAction (actionName?: string, opts?: Object | undefined | Function, handler?: Function): void { 107 | if (typeof opts === 'function') { 108 | handler = opts 109 | opts = undefined 110 | } 111 | 112 | if (typeof actionName !== 'string') { 113 | console.error(`You must define an name when registering an action`) 114 | return 115 | } 116 | if (typeof handler !== 'function') { 117 | console.error(`You must define an callback when registering an action`) 118 | return 119 | } 120 | if (this.transport === undefined) { 121 | return this.logger(`Failed to load transport service`) 122 | } 123 | 124 | let type = 'custom' 125 | 126 | if (actionName.indexOf('km:') === 0 || actionName.indexOf('internal:') === 0) { 127 | type = 'internal' 128 | } 129 | 130 | const reply = (data) => { 131 | // @ts-ignore thanks mr typescript but i already checked above 132 | this.transport.send('axm:reply', { 133 | at: new Date().getTime(), 134 | action_name: actionName, 135 | return: data 136 | }) 137 | } 138 | 139 | const action: Action = { 140 | name: actionName, 141 | callback: reply, 142 | handler, 143 | type, 144 | isScoped: false, 145 | arity: handler.length, 146 | opts 147 | } 148 | this.logger(`Succesfully registered custom action ${action.name}`) 149 | this.actions.set(actionName, action) 150 | this.transport.addAction(action) 151 | } 152 | 153 | /** 154 | * Register a scoped action that will be called when we receive a call for this actionName 155 | */ 156 | scopedAction (actionName?: string, handler?: Function) { 157 | if (typeof actionName !== 'string') { 158 | console.error(`You must define an name when registering an action`) 159 | return -1 160 | } 161 | if (typeof handler !== 'function') { 162 | console.error(`You must define an callback when registering an action`) 163 | return -1 164 | } 165 | if (this.transport === undefined) { 166 | return this.logger(`Failed to load transport service`) 167 | } 168 | 169 | const action: Action = { 170 | name: actionName, 171 | handler, 172 | type: 'scoped', 173 | isScoped: true, 174 | arity: handler.length, 175 | opts: null 176 | } 177 | this.logger(`Succesfully registered scoped action ${action.name}`) 178 | this.actions.set(actionName, action) 179 | this.transport.addAction(action) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/services/inspector.ts: -------------------------------------------------------------------------------- 1 | import * as inspector from 'inspector' 2 | import Debug from 'debug' 3 | 4 | export class InspectorService { 5 | 6 | private session: inspector.Session | null = null 7 | private logger: Function = Debug('axm:services:inspector') 8 | 9 | init (): inspector.Session { 10 | this.logger(`Creating new inspector session`) 11 | this.session = new inspector.Session() 12 | this.session.connect() 13 | this.logger('Connected to inspector') 14 | this.session.post('Profiler.enable') 15 | this.session.post('HeapProfiler.enable') 16 | return this.session 17 | } 18 | 19 | getSession (): inspector.Session { 20 | if (this.session === null) { 21 | this.session = this.init() 22 | return this.session 23 | } else { 24 | return this.session 25 | } 26 | } 27 | 28 | destroy () { 29 | if (this.session !== null) { 30 | this.session.post('Profiler.disable') 31 | this.session.post('HeapProfiler.disable') 32 | this.session.disconnect() 33 | this.session = null 34 | } else { 35 | this.logger('No open session') 36 | } 37 | } 38 | } 39 | 40 | module.exports = InspectorService 41 | -------------------------------------------------------------------------------- /src/services/metrics.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Meter from '../utils/metrics/meter' 4 | import Counter from '../utils/metrics/counter' 5 | import Histogram from '../utils/metrics/histogram' 6 | import { ServiceManager, Service } from '../serviceManager' 7 | import constants from '../constants' 8 | import { Transport } from './transport' 9 | import * as Debug from 'debug' 10 | import Gauge from '../utils/metrics/gauge' 11 | 12 | export enum MetricType { 13 | 'meter' = 'meter', 14 | 'histogram' = 'histogram', 15 | 'counter' = 'counter', 16 | 'gauge' = 'gauge', 17 | 'metric' = 'metric' // deprecated, must use gauge 18 | } 19 | 20 | export enum MetricMeasurements { 21 | 'min' = 'min', 22 | 'max' = 'max', 23 | 'sum' = 'sum', 24 | 'count' = 'count', 25 | 'variance' = 'variance', 26 | 'mean' = 'mean', 27 | 'stddev' = 'stddev', 28 | 'median' = 'median', 29 | 'p75' = 'p75', 30 | 'p95' = 'p95', 31 | 'p99' = 'p99', 32 | 'p999' = 'p999' 33 | } 34 | 35 | export interface InternalMetric { 36 | /** 37 | * Display name of the metric, it should be a clear name that everyone can understand 38 | */ 39 | name?: string 40 | type?: MetricType 41 | /** 42 | * An precise identifier for your metric, exemple: 43 | * The heap usage can be shown to user as 'Heap Usage' but internally 44 | * we want to put few namespace to be sure we talking about the main heap of v8 45 | * so we would choose something like: process/v8/heap/usage 46 | */ 47 | id?: string 48 | /** 49 | * Choose if the metrics will be saved in our datastore or only be used in realtime 50 | */ 51 | historic?: boolean 52 | /** 53 | * Unit of the metric 54 | */ 55 | unit?: string 56 | /** 57 | * The handler is the function that will be called to get the current value 58 | * of the metrics 59 | */ 60 | handler: Function 61 | /** 62 | * The implementation is the instance of the class that handle the computation 63 | * of the metric value 64 | */ 65 | implementation: any 66 | /** 67 | * Last known value of the metric 68 | */ 69 | value?: number 70 | } 71 | 72 | export class Metric { 73 | /** 74 | * Display name of the metric, it should be a clear name that everyone can understand 75 | */ 76 | name?: string 77 | /** 78 | * An precise identifier for your metric, exemple: 79 | * The heap usage can be shown to user as 'Heap Usage' but internally 80 | * we want to put few namespace to be sure we talking about the main heap of v8 81 | * so we would choose something like: process/v8/heap/usage 82 | */ 83 | id?: string 84 | /** 85 | * Choose if the metrics will be saved in our datastore or only be used in realtime 86 | */ 87 | historic?: boolean 88 | /** 89 | * Unit of the metric 90 | */ 91 | unit?: string 92 | /** 93 | * Allow to automatically update the metric value 94 | * Note: only available with io.metric 95 | * Note: if you use this property, you will not be able to set the value with .set 96 | */ 97 | value?: () => number 98 | } 99 | 100 | export class MetricBulk extends Metric { 101 | type: MetricType 102 | } 103 | 104 | export class HistogramOptions extends Metric { 105 | measurement: MetricMeasurements 106 | } 107 | 108 | export class MetricService implements Service { 109 | 110 | private metrics: Map = new Map() 111 | private timer: NodeJS.Timer | null = null 112 | private transport: Transport | null = null 113 | private logger: any = Debug('axm:services:metrics') 114 | 115 | init (): void { 116 | this.transport = ServiceManager.get('transport') 117 | if (this.transport === null) return this.logger('Failed to init metrics service cause no transporter') 118 | 119 | this.logger('init') 120 | this.timer = setInterval(() => { 121 | if (this.transport === null) return this.logger('Abort metrics update since transport is not available') 122 | this.logger('refreshing metrics value') 123 | for (let metric of this.metrics.values()) { 124 | metric.value = metric.handler() 125 | } 126 | this.logger('sending update metrics value to transporter') 127 | // send all the metrics value to the transporter 128 | const metricsToSend = Array.from(this.metrics.values()) 129 | .filter(metric => { 130 | // thanks tslint but user can be dumb sometimes 131 | /* tslint:disable */ 132 | if (metric === null || metric === undefined) return false 133 | if (metric.value === undefined || metric.value === null) return false 134 | 135 | const isNumber = typeof metric.value === 'number' 136 | const isString = typeof metric.value === 'string' 137 | const isBoolean = typeof metric.value === 'boolean' 138 | const isValidNumber = !isNaN(metric.value) 139 | /* tslint:enable */ 140 | // we send it only if it's a string or a valid number 141 | return isString || isBoolean || (isNumber && isValidNumber) 142 | }) 143 | this.transport.setMetrics(metricsToSend) 144 | }, constants.METRIC_INTERVAL) 145 | this.timer.unref() 146 | } 147 | 148 | registerMetric (metric: InternalMetric): void { 149 | // thanks tslint but user can be dumb sometimes 150 | /* tslint:disable */ 151 | if (typeof metric.name !== 'string') { 152 | console.error(`Invalid metric name declared: ${metric.name}`) 153 | return console.trace() 154 | } else if (typeof metric.type !== 'string') { 155 | console.error(`Invalid metric type declared: ${metric.type}`) 156 | return console.trace() 157 | } else if (typeof metric.handler !== 'function') { 158 | console.error(`Invalid metric handler declared: ${metric.handler}`) 159 | return console.trace() 160 | } 161 | /* tslint:enable */ 162 | if (typeof metric.historic !== 'boolean') { 163 | metric.historic = true 164 | } 165 | this.logger(`Registering new metric: ${metric.name}`) 166 | this.metrics.set(metric.name, metric) 167 | } 168 | 169 | meter (opts: Metric): Meter { 170 | const metric: InternalMetric = { 171 | name: opts.name, 172 | type: MetricType.meter, 173 | id: opts.id, 174 | historic: opts.historic, 175 | implementation: new Meter(opts), 176 | unit: opts.unit, 177 | handler: function () { 178 | return this.implementation.isUsed() ? this.implementation.val() : NaN 179 | } 180 | } 181 | this.registerMetric(metric) 182 | 183 | return metric.implementation 184 | } 185 | 186 | counter (opts: Metric): Counter { 187 | const metric: InternalMetric = { 188 | name: opts.name, 189 | type: MetricType.counter, 190 | id: opts.id, 191 | historic: opts.historic, 192 | implementation: new Counter(opts), 193 | unit: opts.unit, 194 | handler: function () { 195 | return this.implementation.isUsed() ? this.implementation.val() : NaN 196 | } 197 | } 198 | this.registerMetric(metric) 199 | 200 | return metric.implementation 201 | } 202 | 203 | histogram (opts: HistogramOptions): Histogram { 204 | // tslint:disable-next-line 205 | if (opts.measurement === undefined || opts.measurement === null) { 206 | opts.measurement = MetricMeasurements.mean 207 | } 208 | const metric: InternalMetric = { 209 | name: opts.name, 210 | type: MetricType.histogram, 211 | id: opts.id, 212 | historic: opts.historic, 213 | implementation: new Histogram(opts), 214 | unit: opts.unit, 215 | handler: function () { 216 | return this.implementation.isUsed() ? 217 | (Math.round(this.implementation.val() * 100) / 100) : NaN 218 | } 219 | } 220 | this.registerMetric(metric) 221 | 222 | return metric.implementation 223 | } 224 | 225 | metric (opts: Metric): Gauge { 226 | let metric: InternalMetric 227 | if (typeof opts.value === 'function') { 228 | metric = { 229 | name: opts.name, 230 | type: MetricType.gauge, 231 | id: opts.id, 232 | implementation: undefined, 233 | historic: opts.historic, 234 | unit: opts.unit, 235 | handler: opts.value 236 | } 237 | } else { 238 | metric = { 239 | name: opts.name, 240 | type: MetricType.gauge, 241 | id: opts.id, 242 | historic: opts.historic, 243 | implementation: new Gauge(), 244 | unit: opts.unit, 245 | handler: function () { 246 | return this.implementation.isUsed() ? this.implementation.val() : NaN 247 | } 248 | } 249 | } 250 | 251 | this.registerMetric(metric) 252 | 253 | return metric.implementation 254 | } 255 | 256 | deleteMetric (name: string) { 257 | return this.metrics.delete(name) 258 | } 259 | 260 | destroy () { 261 | if (this.timer !== null) { 262 | clearInterval(this.timer) 263 | } 264 | this.metrics.clear() 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/services/runtimeStats.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Debug from 'debug' 4 | import utils from '../utils/module' 5 | import { EventEmitter2 } from 'eventemitter2' 6 | 7 | export class RuntimeStatsService extends EventEmitter2 { 8 | 9 | private logger: any = Debug('axm:services:runtimeStats') 10 | private handle: (data: Object) => void | undefined 11 | private noduleInstance: any 12 | private enabled: boolean = false 13 | 14 | init () { 15 | this.logger('init') 16 | if (process.env.PM2_APM_DISABLE_RUNTIME_STATS === 'true') { 17 | return this.logger('disabling service because of the environment flag') 18 | } 19 | // try to find the module 20 | const modulePath = utils.detectModule('@pm2/node-runtime-stats') 21 | if (typeof modulePath !== 'string') return 22 | // if we find it we can try to require it 23 | const RuntimeStats = utils.loadModule(modulePath) 24 | if (RuntimeStats instanceof Error) { 25 | return this.logger(`Failed to require module @pm2/node-runtime-stats: ${RuntimeStats.message}`) 26 | } 27 | this.noduleInstance = new RuntimeStats({ 28 | delay: 1000 29 | }) 30 | this.logger('starting runtime stats') 31 | this.noduleInstance.start() 32 | this.handle = (data) => { 33 | this.logger('received runtime stats', data) 34 | this.emit('data', data) 35 | } 36 | // seriously i just created it two lines above 37 | // @ts-ignore 38 | this.noduleInstance.on('sense', this.handle) 39 | this.enabled = true 40 | } 41 | 42 | /** 43 | * Is the service ready to send metrics about the runtime 44 | */ 45 | isEnabled (): boolean { 46 | return this.enabled 47 | } 48 | 49 | destroy () { 50 | if (this.noduleInstance !== undefined && this.noduleInstance !== null) { 51 | this.logger('removing listener on runtime stats service') 52 | this.noduleInstance.removeListener('sense', this.handle) 53 | this.noduleInstance.stop() 54 | } 55 | this.logger('destroy') 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/services/transport.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Action } from './actions' 3 | import { Metric, InternalMetric } from './metrics' 4 | import { IPCTransport } from '../transports/IPCTransport' 5 | // import { WebsocketTransport } from '../transports/WebsocketTransport' 6 | import { EventEmitter2 } from 'eventemitter2' 7 | 8 | export class TransportConfig { 9 | /** 10 | * public key of the bucket to which the agent need to connect 11 | */ 12 | publicKey: string 13 | /** 14 | * Secret key of the bucket to which the agent need to connect 15 | */ 16 | secretKey: string 17 | /** 18 | * The name of the application/service that will be reported to PM2 Enterprise 19 | */ 20 | appName: string 21 | /** 22 | * The name of the server as reported in PM2 Enterprise 23 | * 24 | * default is os.hostname() 25 | */ 26 | serverName?: string 27 | /** 28 | * Broadcast all the logs from your application to our backend 29 | */ 30 | sendLogs?: Boolean 31 | /** 32 | * Since logs can be forwared to our backend you may want to ignore specific 33 | * logs (containing sensitive data for example) 34 | */ 35 | logFilter?: string | RegExp 36 | /** 37 | * Avoid to broadcast any logs from your application to our backend 38 | * Even if the sendLogs option set to false, you can still see some logs 39 | * when going to the log interface (it automatically trigger broacasting log) 40 | */ 41 | disableLogs?: Boolean 42 | /** 43 | * Proxy URI to use when reaching internet 44 | * Supporting socks5,http,https,pac,socks4 45 | * see https://github.com/TooTallNate/node-proxy-agent 46 | * 47 | * example: socks5://username:password@some-socks-proxy.com:9050 48 | */ 49 | proxy?: string 50 | } 51 | 52 | export interface Transport extends EventEmitter2 { 53 | /** 54 | * Init the transporter (connection, listeners etc) 55 | */ 56 | init: (config: TransportConfig) => Transport 57 | /** 58 | * Destroy the instance (disconnect, cleaning listeners etc) 59 | */ 60 | destroy: () => void 61 | /** 62 | * Send data to remote endpoint 63 | */ 64 | send: (channel: string, payload: Object) => void 65 | /** 66 | * Declare available actions 67 | */ 68 | addAction: (action: Action) => void 69 | /** 70 | * Declare metrics 71 | */ 72 | setMetrics: (metrics: InternalMetric[]) => void 73 | /** 74 | * Declare options for process 75 | */ 76 | setOptions: (options: any) => void 77 | } 78 | 79 | /** 80 | * Init a transporter implementation with a specific config 81 | */ 82 | export function createTransport (name: string, config: TransportConfig): Transport { 83 | const transport = new IPCTransport() 84 | transport.init(config) 85 | return transport 86 | // switch (name) { 87 | // case 'ipc': { 88 | // const transport = new IPCTransport() 89 | // transport.init(config) 90 | // return transport 91 | // } 92 | // case 'websocket': { 93 | // const transport = new WebsocketTransport() 94 | // transport.init(config) 95 | // return transport 96 | // } 97 | // } 98 | // console.error(`Failed to find transport implementation: ${name}`) 99 | // return process.exit(1) 100 | } 101 | -------------------------------------------------------------------------------- /src/transports/IPCTransport.ts: -------------------------------------------------------------------------------- 1 | import { Transport, TransportConfig } from '../services/transport' 2 | import * as Debug from 'debug' 3 | import { Action } from '../services/actions' 4 | import { InternalMetric } from '../services/metrics' 5 | import { EventEmitter2 } from 'eventemitter2' 6 | import * as cluster from 'cluster' 7 | 8 | export class IPCTransport extends EventEmitter2 implements Transport { 9 | 10 | private initiated: Boolean = false // tslint:disable-line 11 | private logger: Function = Debug('axm:transport:ipc') 12 | private onMessage: any | undefined 13 | private autoExitHandle: NodeJS.Timer | undefined 14 | 15 | init (config?: TransportConfig): Transport { 16 | this.logger('Init new transport service') 17 | if (this.initiated === true) { 18 | console.error(`Trying to re-init the transport, please avoid`) 19 | return this 20 | } 21 | this.initiated = true 22 | this.logger('Agent launched') 23 | this.onMessage = (data?: Object) => { 24 | this.logger(`Received reverse message from IPC`) 25 | this.emit('data', data) 26 | } 27 | process.on('message', this.onMessage) 28 | 29 | // if the process is standalone, the fact that there is a listener attached 30 | // forbid the event loop to exit when there are no other task there 31 | if (cluster.isWorker === false) { 32 | this.autoExitHook() 33 | } 34 | return this 35 | } 36 | 37 | private autoExitHook () { 38 | // clean listener if event loop is empty 39 | // important to ensure apm will not prevent application to stop 40 | this.autoExitHandle = setInterval(() => { 41 | let currentProcess: any = (cluster.isWorker) ? cluster.worker.process : process 42 | 43 | if (currentProcess._getActiveHandles().length === 3) { 44 | let handlers: any = currentProcess._getActiveHandles().map(h => h.constructor.name) 45 | 46 | if (handlers.includes('Pipe') === true && 47 | handlers.includes('Socket') === true) { 48 | process.removeListener('message', this.onMessage) 49 | let tmp = setTimeout(_ => { 50 | this.logger(`Still alive, listen back to IPC`) 51 | process.on('message', this.onMessage) 52 | }, 200) 53 | tmp.unref() 54 | } 55 | } 56 | }, 3000) 57 | 58 | this.autoExitHandle.unref() 59 | } 60 | 61 | setMetrics (metrics: InternalMetric[]) { 62 | const serializedMetric = metrics.reduce((object, metric: InternalMetric) => { 63 | if (typeof metric.name !== 'string') return object 64 | object[metric.name] = { 65 | historic: metric.historic, 66 | unit: metric.unit, 67 | type: metric.id, 68 | value: metric.value 69 | } 70 | return object 71 | }, {}) 72 | this.send('axm:monitor', serializedMetric) 73 | } 74 | 75 | addAction (action: Action) { 76 | this.logger(`Add action: ${action.name}:${action.type}`) 77 | this.send('axm:action', { 78 | action_name: action.name, 79 | action_type: action.type, 80 | arity: action.arity, 81 | opts: action.opts 82 | }) 83 | } 84 | 85 | setOptions (options) { 86 | this.logger(`Set options: [${Object.keys(options).join(',')}]`) 87 | return this.send('axm:option:configuration', options) 88 | } 89 | 90 | send (channel, payload) { 91 | if (typeof process.send !== 'function') return -1 92 | if (process.connected === false) { 93 | console.error('Process disconnected from parent! (not connected)') 94 | return process.exit(1) 95 | } 96 | 97 | try { 98 | process.send({ type: channel, data: payload }) 99 | } catch (err) { 100 | this.logger('Process disconnected from parent !') 101 | this.logger(err) 102 | return process.exit(1) 103 | } 104 | } 105 | 106 | destroy () { 107 | if (this.onMessage !== undefined) { 108 | process.removeListener('message', this.onMessage) 109 | } 110 | if (this.autoExitHandle !== undefined) { 111 | clearInterval(this.autoExitHandle) 112 | } 113 | this.logger('destroy') 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/transports/WebsocketTransport.ts: -------------------------------------------------------------------------------- 1 | // import { Transport, TransportConfig } from '../services/transport' 2 | // import Debug from 'debug' 3 | // import { Action } from '../services/actions' 4 | // import { InternalMetric } from '../services/metrics' 5 | // import { EventEmitter2 } from 'eventemitter2' 6 | 7 | // class SerializedAction { 8 | // action_name: string // tslint:disable-line 9 | // action_type: string // tslint:disable-line 10 | // opts: Object | null | undefined 11 | // arity: number 12 | // } 13 | 14 | // export class ProcessMetadata { 15 | // axm_actions: SerializedAction[] // tslint:disable-line 16 | // axm_monitor: Object // tslint:disable-line 17 | // axm_options: Object // tslint:disable-line 18 | // axm_dynamic?: Object // tslint:disable-line 19 | // interpreter?: string 20 | // versionning?: Object 21 | // } 22 | 23 | // export class WebsocketTransport extends EventEmitter2 implements Transport { 24 | 25 | // private config: TransportConfig 26 | // private agent: any 27 | // private process: ProcessMetadata 28 | // private initiated: Boolean = false // tslint:disable-line 29 | // private logger: Function = Debug('axm:transport:websocket') 30 | 31 | // init (config: TransportConfig): Transport { 32 | // if (this.initiated === true) { 33 | // console.error(`Trying to re-init the transport, please avoid`) 34 | // return this 35 | // } 36 | // this.initiated = true 37 | // const AgentNode = require('@pm2/agent-node') 38 | // this.logger('Init new transport service') 39 | // this.config = config 40 | // this.process = { 41 | // axm_actions: [], 42 | // axm_options: {}, 43 | // axm_monitor: {} 44 | // } 45 | // this.agent = new AgentNode(this.config, this.process) 46 | // if (this.agent instanceof Error) { 47 | // throw this.agent 48 | // } 49 | // this.agent.sendLogs = config.sendLogs || false 50 | // this.agent.start() 51 | // this.agent.transport.on('**', (data) => { 52 | // this.logger(`Received reverse message from websocket transport`) 53 | // this.emit('data', data) 54 | // }) 55 | // this.logger('Agent launched') 56 | // return this 57 | // } 58 | 59 | // setMetrics (metrics: InternalMetric[]) { 60 | // return this.process.axm_monitor = metrics.reduce((object, metric: InternalMetric) => { 61 | // if (typeof metric.name !== 'string') return object 62 | // object[metric.name] = { 63 | // unit: metric.unit, 64 | // type: metric.id, 65 | // value: metric.value 66 | // } 67 | // if (metric.historic == false) 68 | // object[metric.name] = false 69 | // return object 70 | // }, {}) 71 | // } 72 | 73 | // addAction (action: Action) { 74 | // this.logger(`Add action: ${action.name}:${action.type}`) 75 | // const serializedAction: SerializedAction = { 76 | // action_name: action.name, 77 | // action_type: action.type, 78 | // arity: action.arity, 79 | // opts: action.opts 80 | // } 81 | // this.process.axm_actions.push(serializedAction) 82 | // } 83 | 84 | // setOptions (options) { 85 | // this.logger(`Set options: [${Object.keys(options).join(',')}]`) 86 | // return this.process.axm_options = Object.assign(this.process.axm_options, options) 87 | // } 88 | 89 | // private getFormattedPayload (channel: string, payload: any) { 90 | // // Reformat for backend 91 | // switch (channel) { 92 | // case 'axm:reply': 93 | // return { data: payload } 94 | // case 'process:exception': 95 | // return { data: payload } 96 | // case 'human:event': { 97 | // const name = payload.__name 98 | // payload.__name = undefined 99 | // return { name, data: payload } 100 | // } 101 | // } 102 | // return payload 103 | // } 104 | 105 | // send (channel: string, payload: Object) { 106 | // return this.agent.send(channel, this.getFormattedPayload(channel, payload)) ? 0 : -1 107 | // } 108 | 109 | // destroy () { 110 | // this.agent.transport.disconnect() 111 | // this.logger('destroy') 112 | // } 113 | 114 | // removeListener () { 115 | // return this.agent.transport.removeListener.apply(this, arguments) 116 | // } 117 | 118 | // removeAllListeners () { 119 | // return this.agent.transport.removeAllListeners.apply(this, arguments) 120 | // } 121 | 122 | // on () { 123 | // return this.agent.transport.on.apply(this, arguments) 124 | // } 125 | // } 126 | -------------------------------------------------------------------------------- /src/utils/BinaryHeap.ts: -------------------------------------------------------------------------------- 1 | export default class BinaryHeap { 2 | 3 | private _elements 4 | 5 | constructor (options) { 6 | options = options || {} 7 | 8 | this._elements = options.elements || [] 9 | this._score = options.score || this._score 10 | } 11 | 12 | add () { 13 | for (let i = 0; i < arguments.length; i++) { 14 | const element = arguments[i] 15 | 16 | this._elements.push(element) 17 | this._bubble(this._elements.length - 1) 18 | } 19 | } 20 | 21 | first () { 22 | return this._elements[0] 23 | } 24 | 25 | removeFirst () { 26 | const root = this._elements[0] 27 | const last = this._elements.pop() 28 | 29 | if (this._elements.length > 0) { 30 | this._elements[0] = last 31 | this._sink(0) 32 | } 33 | 34 | return root 35 | } 36 | 37 | clone () { 38 | return new BinaryHeap({ 39 | elements: this.toArray(), 40 | score: this._score 41 | }) 42 | } 43 | 44 | toSortedArray () { 45 | const array: any[] = [] 46 | const clone = this.clone() 47 | 48 | while (true) { 49 | const element = clone.removeFirst() 50 | if (element === undefined) break 51 | 52 | array.push(element) 53 | } 54 | 55 | return array 56 | } 57 | 58 | toArray () { 59 | return [].concat(this._elements) 60 | } 61 | 62 | size () { 63 | return this._elements.length 64 | } 65 | 66 | _bubble (bubbleIndex) { 67 | const bubbleElement = this._elements[bubbleIndex] 68 | const bubbleScore = this._score(bubbleElement) 69 | 70 | while (bubbleIndex > 0) { 71 | const parentIndex = this._parentIndex(bubbleIndex) 72 | const parentElement = this._elements[parentIndex] 73 | const parentScore = this._score(parentElement) 74 | 75 | if (bubbleScore <= parentScore) break 76 | 77 | this._elements[parentIndex] = bubbleElement 78 | this._elements[bubbleIndex] = parentElement 79 | bubbleIndex = parentIndex 80 | } 81 | } 82 | 83 | _sink (sinkIndex) { 84 | const sinkElement = this._elements[sinkIndex] 85 | const sinkScore = this._score(sinkElement) 86 | const length = this._elements.length 87 | 88 | while (true) { 89 | let swapIndex 90 | let swapScore 91 | let swapElement = null 92 | const childIndexes = this._childIndexes(sinkIndex) 93 | 94 | for (let i = 0; i < childIndexes.length; i++) { 95 | const childIndex = childIndexes[i] 96 | 97 | if (childIndex >= length) break 98 | 99 | const childElement = this._elements[childIndex] 100 | const childScore = this._score(childElement) 101 | 102 | if (childScore > sinkScore) { 103 | if (swapScore === undefined || swapScore < childScore) { 104 | swapIndex = childIndex 105 | swapScore = childScore 106 | swapElement = childElement 107 | } 108 | } 109 | } 110 | 111 | if (swapIndex === undefined) break 112 | 113 | this._elements[swapIndex] = sinkElement 114 | this._elements[sinkIndex] = swapElement 115 | sinkIndex = swapIndex 116 | } 117 | } 118 | 119 | _parentIndex (index) { 120 | return Math.floor((index - 1) / 2) 121 | } 122 | 123 | _childIndexes (index) { 124 | return [ 125 | 2 * index + 1, 126 | 2 * index + 2 127 | ] 128 | } 129 | 130 | _score (element) { 131 | return element.valueOf() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/utils/EDS.ts: -------------------------------------------------------------------------------- 1 | import BinaryHeap from './BinaryHeap' 2 | import units from './units' 3 | 4 | export default class ExponentiallyDecayingSample { 5 | private RESCALE_INTERVAL = 1 * units.HOURS 6 | private ALPHA = 0.015 7 | private SIZE = 1028 8 | 9 | private _elements 10 | private _rescaleInterval 11 | private _alpha 12 | private _size 13 | private _landmark 14 | private _nextRescale 15 | private _mean 16 | 17 | constructor (options?) { 18 | options = options || {} 19 | 20 | this._elements = new BinaryHeap({ 21 | score: function (element) { 22 | return -element.priority 23 | } 24 | }) 25 | 26 | this._rescaleInterval = options.rescaleInterval || this.RESCALE_INTERVAL 27 | this._alpha = options.alpha || this.ALPHA 28 | this._size = options.size || this.SIZE 29 | this._random = options.random || this._random 30 | this._landmark = null 31 | this._nextRescale = null 32 | this._mean = null 33 | } 34 | 35 | update (value, timestamp?) { 36 | const now = Date.now() 37 | if (!this._landmark) { 38 | this._landmark = now 39 | this._nextRescale = this._landmark + this._rescaleInterval 40 | } 41 | 42 | timestamp = timestamp || now 43 | 44 | const newSize = this._elements.size() + 1 45 | 46 | const element = { 47 | priority: this._priority(timestamp - this._landmark), 48 | value: value 49 | } 50 | 51 | if (newSize <= this._size) { 52 | this._elements.add(element) 53 | } else if (element.priority > this._elements.first().priority) { 54 | this._elements.removeFirst() 55 | this._elements.add(element) 56 | } 57 | 58 | if (now >= this._nextRescale) this._rescale(now) 59 | } 60 | 61 | toSortedArray () { 62 | return this._elements 63 | .toSortedArray() 64 | .map(function (element) { 65 | return element.value 66 | }) 67 | } 68 | 69 | toArray () { 70 | return this._elements 71 | .toArray() 72 | .map(function (element) { 73 | return element.value 74 | }) 75 | } 76 | 77 | _weight (age) { 78 | // We divide by 1000 to not run into huge numbers before reaching a 79 | // rescale event. 80 | return Math.exp(this._alpha * (age / 1000)) 81 | } 82 | 83 | _priority (age) { 84 | return this._weight(age) / this._random() 85 | } 86 | 87 | _random () { 88 | return Math.random() 89 | } 90 | 91 | _rescale (now) { 92 | now = now || Date.now() 93 | 94 | const self = this 95 | const oldLandmark = this._landmark 96 | this._landmark = now || Date.now() 97 | this._nextRescale = now + this._rescaleInterval 98 | 99 | const factor = self._priority(-(self._landmark - oldLandmark)) 100 | 101 | this._elements 102 | .toArray() 103 | .forEach(function (element) { 104 | element.priority *= factor 105 | }) 106 | } 107 | 108 | avg (now) { 109 | let sum = 0 110 | this._elements 111 | .toArray() 112 | .forEach(function (element) { 113 | sum += element.value 114 | }) 115 | return (sum / this._elements.size()) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/utils/EWMA.ts: -------------------------------------------------------------------------------- 1 | import units from './units' 2 | 3 | export default class ExponentiallyWeightedMovingAverage { 4 | private _timePeriod: number 5 | private _tickInterval: number 6 | private _alpha: number 7 | private _count: number = 0 8 | private _rate: number = 0 9 | 10 | private TICK_INTERVAL: number = 5 * units.SECONDS 11 | 12 | constructor (timePeriod?: number, tickInterval?: number) { 13 | this._timePeriod = timePeriod || 1 * units.MINUTES 14 | this._tickInterval = tickInterval || this.TICK_INTERVAL 15 | this._alpha = 1 - Math.exp(-this._tickInterval / this._timePeriod) 16 | } 17 | 18 | update (n) { 19 | this._count += n 20 | } 21 | 22 | tick () { 23 | const instantRate = this._count / this._tickInterval 24 | this._count = 0 25 | 26 | this._rate += (this._alpha * (instantRate - this._rate)) 27 | } 28 | 29 | rate (timeUnit) { 30 | return (this._rate || 0) * timeUnit 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/utils/autocast.ts: -------------------------------------------------------------------------------- 1 | export default class Autocast { 2 | /** 3 | * Common strings to cast 4 | */ 5 | commonStrings = { 6 | 'true': true, 7 | 'false': false, 8 | 'undefined': undefined, 9 | 'null': null, 10 | 'NaN': NaN 11 | } 12 | 13 | process (key,value, o) { 14 | if (typeof(value) === 'object') return 15 | o[key] = this._cast(value) 16 | } 17 | 18 | traverse (o,func) { 19 | for (let i in o) { 20 | func.apply(this,[i,o[i], o]) 21 | if (o[i] !== null && typeof(o[i]) === 'object') { 22 | // going on step down in the object tree!! 23 | this.traverse(o[i],func) 24 | } 25 | } 26 | } 27 | 28 | /** 29 | * Given a value, try and cast it 30 | */ 31 | autocast (s) { 32 | if (typeof(s) === 'object') { 33 | this.traverse(s, this.process) 34 | return s 35 | } 36 | 37 | return this._cast(s) 38 | } 39 | 40 | private _cast (s) { 41 | let key 42 | 43 | // Don't cast Date objects 44 | if (s instanceof Date) return s 45 | if (typeof s === 'boolean') return s 46 | 47 | // Try to cast it to a number 48 | if (!isNaN(s)) return Number(s) 49 | 50 | // Try to make it a common string 51 | for (key in this.commonStrings) { 52 | if (s === key) return this.commonStrings[key] 53 | } 54 | 55 | // Give up 56 | return s 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/metrics/counter.ts: -------------------------------------------------------------------------------- 1 | export default class Counter { 2 | private _count: number 3 | private used: boolean = false 4 | 5 | constructor (opts?) { 6 | opts = opts || {} 7 | this._count = opts.count || 0 8 | } 9 | 10 | val () { 11 | return this._count 12 | } 13 | 14 | inc (n?: number) { 15 | this.used = true 16 | this._count += (n || 1) 17 | } 18 | 19 | dec (n?: number) { 20 | this.used = true 21 | this._count -= (n || 1) 22 | } 23 | 24 | reset (count?: number) { 25 | this._count = count || 0 26 | } 27 | 28 | isUsed () { 29 | return this.used 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/metrics/gauge.ts: -------------------------------------------------------------------------------- 1 | export default class Gauge { 2 | private value = 0 3 | private used = false 4 | 5 | val () { 6 | return this.value 7 | } 8 | 9 | set (value) { 10 | this.used = true 11 | this.value = value 12 | } 13 | 14 | isUsed () { 15 | return this.used 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/utils/metrics/histogram.ts: -------------------------------------------------------------------------------- 1 | import EDS from '../EDS' 2 | 3 | export default class Histogram { 4 | private _measurement 5 | private _callFn 6 | 7 | private _sample = new EDS() 8 | private _min 9 | private _max 10 | private _count: number = 0 11 | private _sum: number = 0 12 | 13 | // These are for the Welford algorithm for calculating running variance 14 | // without floating-point doom. 15 | private _varianceM: number = 0 16 | private _varianceS: number = 0 17 | private _ema: number = 0 18 | 19 | private used: boolean = false 20 | 21 | constructor (opts?) { 22 | opts = opts || {} 23 | 24 | this._measurement = opts.measurement 25 | this._callFn = null 26 | 27 | const methods = { 28 | min : this.getMin, 29 | max : this.getMax, 30 | sum : this.getSum, 31 | count : this.getCount, 32 | variance : this._calculateVariance, 33 | mean : this._calculateMean, 34 | // stddev : this._calculateStddev, 35 | ema : this.getEma() 36 | } 37 | 38 | if (methods.hasOwnProperty(this._measurement)) { 39 | this._callFn = methods[this._measurement] 40 | } else { 41 | this._callFn = function () { 42 | const percentiles = this.percentiles([0.5, 0.75, 0.95, 0.99, 0.999]) 43 | 44 | const medians = { 45 | median : percentiles[0.5], 46 | p75 : percentiles[0.75], 47 | p95 : percentiles[0.95], 48 | p99 : percentiles[0.99], 49 | p999 : percentiles[0.999] 50 | } 51 | 52 | return medians[this._measurement] 53 | } 54 | } 55 | } 56 | 57 | update (value: number) { 58 | this.used = true 59 | this._count++ 60 | this._sum += value 61 | 62 | this._sample.update(value) 63 | this._updateMin(value) 64 | this._updateMax(value) 65 | this._updateVariance(value) 66 | this._updateEma(value) 67 | } 68 | 69 | percentiles (percentiles) { 70 | const values = this._sample 71 | .toArray() 72 | .sort(function (a, b) { 73 | return (a === b) 74 | ? 0 75 | : a - b 76 | }) 77 | 78 | const results = {} 79 | for (let i = 0; i < percentiles.length; i++) { 80 | const percentile = percentiles[i] 81 | if (!values.length) { 82 | results[percentile] = null 83 | continue 84 | } 85 | 86 | const pos = percentile * (values.length + 1) 87 | 88 | if (pos < 1) { 89 | results[percentile] = values[0] 90 | } else if (pos >= values.length) { 91 | results[percentile] = values[values.length - 1] 92 | } else { 93 | const lower = values[Math.floor(pos) - 1] 94 | const upper = values[Math.ceil(pos) - 1] 95 | 96 | results[percentile] = lower + (pos - Math.floor(pos)) * (upper - lower) 97 | } 98 | } 99 | 100 | return results 101 | } 102 | 103 | val () { 104 | if (typeof(this._callFn) === 'function') { 105 | return this._callFn() 106 | } else { 107 | return this._callFn 108 | } 109 | } 110 | 111 | getMin () { 112 | return this._min 113 | } 114 | 115 | getMax () { 116 | return this._max 117 | } 118 | 119 | getSum () { 120 | return this._sum 121 | } 122 | 123 | getCount () { 124 | return this._count 125 | } 126 | 127 | getEma () { 128 | return this._ema 129 | } 130 | 131 | fullResults () { 132 | const percentiles = this.percentiles([0.5, 0.75, 0.95, 0.99, 0.999]) 133 | 134 | return { 135 | min : this._min, 136 | max : this._max, 137 | sum : this._sum, 138 | variance : this._calculateVariance(), 139 | mean : this._calculateMean(), 140 | // stddev : this._calculateStddev(), 141 | count : this._count, 142 | median : percentiles[0.5], 143 | p75 : percentiles[0.75], 144 | p95 : percentiles[0.95], 145 | p99 : percentiles[0.99], 146 | p999 : percentiles[0.999], 147 | ema : this._ema 148 | } 149 | } 150 | 151 | _updateMin (value) { 152 | if (this._min === undefined || value < this._min) { 153 | this._min = value 154 | } 155 | } 156 | 157 | _updateMax (value) { 158 | if (this._max === undefined || value > this._max) { 159 | this._max = value 160 | } 161 | } 162 | 163 | _updateVariance (value) { 164 | if (this._count === 1) return this._varianceM = value 165 | 166 | const oldM = this._varianceM 167 | 168 | this._varianceM += ((value - oldM) / this._count) 169 | this._varianceS += ((value - oldM) * (value - this._varianceM)) 170 | } 171 | 172 | _updateEma (value) { 173 | if (this._count <= 1) return this._ema = this._calculateMean() 174 | const alpha = 2 / (1 + this._count) 175 | this._ema = value * alpha + this._ema * (1 - alpha) 176 | } 177 | 178 | _calculateMean () { 179 | return (this._count === 0) 180 | ? 0 181 | : this._sum / this._count 182 | } 183 | 184 | _calculateVariance () { 185 | return (this._count <= 1) 186 | ? null 187 | : this._varianceS / (this._count - 1) 188 | } 189 | 190 | isUsed () { 191 | return this.used 192 | } 193 | 194 | // TODO still used ? 195 | // _calculateStddev () { 196 | // return (this._count < 1) 197 | // ? null 198 | // : Math.sqrt(this._calculateVariance()) 199 | // } 200 | } 201 | -------------------------------------------------------------------------------- /src/utils/metrics/meter.ts: -------------------------------------------------------------------------------- 1 | import EWMA from '../EWMA' 2 | import units from '../units' 3 | 4 | export default class Meter { 5 | 6 | private _tickInterval: number 7 | private _samples: number 8 | private _timeframe: number 9 | private _rate 10 | private _interval 11 | private used: boolean = false 12 | 13 | constructor (opts?: any) { 14 | const self = this 15 | 16 | if (typeof opts !== 'object') { 17 | opts = {} 18 | } 19 | 20 | this._samples = opts.samples || opts.seconds || 1 21 | this._timeframe = opts.timeframe || 60 22 | this._tickInterval = opts.tickInterval || 5 * units.SECONDS 23 | 24 | this._rate = new EWMA(this._timeframe * units.SECONDS, this._tickInterval) 25 | 26 | if (opts.debug && opts.debug === true) { 27 | return 28 | } 29 | 30 | this._interval = setInterval(function () { 31 | self._rate.tick() 32 | }, this._tickInterval) 33 | 34 | this._interval.unref() 35 | } 36 | 37 | mark = function (n: number = 1) { 38 | this.used = true 39 | this._rate.update(n) 40 | } 41 | 42 | val = function () { 43 | return Math.round(this._rate.rate(this._samples * units.SECONDS) * 100) / 100 44 | } 45 | 46 | isUsed () { 47 | return this.used 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/miscellaneous.ts: -------------------------------------------------------------------------------- 1 | import { ServiceManager } from '../serviceManager' 2 | 3 | export default class MiscUtils { 4 | static generateUUID (): string { 5 | return Math.random().toString(36).substr(2, 16) 6 | } 7 | 8 | static getValueFromDump (property, parentProperty?): number { 9 | if (!parentProperty) { 10 | parentProperty = 'handles' 11 | } 12 | const dump = ServiceManager.get('eventLoopService').inspector.dump() 13 | return dump[parentProperty].hasOwnProperty(property) ? dump[parentProperty][property].length : 0 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/module.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as Debug from 'debug' 3 | import * as path from 'path' 4 | 5 | const debug = Debug('axm:utils:module') 6 | 7 | export default class ModuleUtils { 8 | /** 9 | * Try to load a module from its path 10 | */ 11 | static loadModule (modulePath: string, args?: Object): any | Error { 12 | let nodule 13 | try { 14 | if (args) { 15 | nodule = require(modulePath).apply(this, args) 16 | } else { 17 | nodule = require(modulePath) 18 | } 19 | debug(`Succesfully required module at path ${modulePath}`) 20 | return nodule 21 | } catch (err) { 22 | debug(`Failed to load module at path ${modulePath}: ${err.message}`) 23 | return err 24 | } 25 | } 26 | 27 | /** 28 | * Try to detect the path of a specific module 29 | */ 30 | static detectModule (moduleName: string): string | null { 31 | const fakePath = ['./node_modules', '/node_modules'] 32 | if (!require.main) { 33 | return null 34 | } 35 | const paths = typeof require.main.paths === 'undefined' ? fakePath : require.main.paths // tslint:disable-line 36 | 37 | const requirePaths = paths.slice() 38 | 39 | return ModuleUtils._lookForModule(requirePaths, moduleName) 40 | } 41 | 42 | /** 43 | * Lookup in each require path for the module name 44 | */ 45 | private static _lookForModule (requirePaths: Array, moduleName: string): string | null { 46 | // in older node version, the constants where at the top level 47 | const fsConstants = fs.constants || fs 48 | // check for every path if we can find the module 49 | for (let requirePath of requirePaths) { 50 | const completePath = path.join(requirePath, moduleName) 51 | debug(`Looking for module ${moduleName} in ${completePath}`) 52 | try { 53 | fs.accessSync(completePath, fsConstants.R_OK) 54 | debug(`Found module ${moduleName} in path ${completePath}`) 55 | return completePath 56 | } catch (err) { 57 | debug(`module ${moduleName} not found in path ${completePath}`) 58 | continue 59 | } 60 | } 61 | return null 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/stackParser.ts: -------------------------------------------------------------------------------- 1 | 2 | export type MissFunction = (key: string) => any 3 | export type CacheOptions = { 4 | miss: MissFunction 5 | ttl?: number 6 | } 7 | export type StackContext = { 8 | callsite: string, 9 | context: string 10 | } 11 | 12 | export type FrameMetadata = { 13 | line_number: number 14 | file_name: string 15 | } 16 | 17 | /** 18 | * Simple cache implementation 19 | * 20 | * @param {Object} opts cache options 21 | * @param {Function} opts.miss function called when a key isn't found in the cache 22 | */ 23 | export class Cache { 24 | 25 | private cache: { [key: string]: any } = {} 26 | private ttlCache: { [key: string]: number } = {} 27 | private worker: NodeJS.Timer 28 | private tllTime: number 29 | private onMiss: MissFunction 30 | 31 | constructor (opts: CacheOptions) { 32 | this.onMiss = opts.miss 33 | this.tllTime = opts.ttl || -1 34 | 35 | if (opts.ttl) { 36 | this.worker = setInterval(this.workerFn.bind(this), 1000) 37 | this.worker.unref() 38 | } 39 | } 40 | 41 | workerFn () { 42 | let keys = Object.keys(this.ttlCache) 43 | for (let i = 0; i < keys.length; i++) { 44 | let key = keys[i] 45 | let value = this.ttlCache[key] 46 | if (Date.now() > value) { 47 | delete this.cache[key] 48 | delete this.ttlCache[key] 49 | } 50 | } 51 | } 52 | 53 | /** 54 | * Get a value from the cache 55 | * 56 | * @param {String} key 57 | */ 58 | get (key: string) { 59 | if (!key) return null 60 | let value = this.cache[key] 61 | if (value) return value 62 | 63 | value = this.onMiss(key) 64 | 65 | if (value) { 66 | this.set(key, value) 67 | } 68 | return value 69 | } 70 | 71 | /** 72 | * Set a value in the cache 73 | * 74 | * @param {String} key 75 | * @param {Mixed} value 76 | */ 77 | set (key: string, value: any) { 78 | if (!key || !value) return false 79 | this.cache[key] = value 80 | if (this.tllTime > 0) { 81 | this.ttlCache[key] = Date.now() + this.tllTime 82 | } 83 | return true 84 | } 85 | 86 | reset () { 87 | this.cache = {} 88 | this.ttlCache = {} 89 | } 90 | } 91 | 92 | export type StackTraceParserOptions = { 93 | cache: Cache, 94 | contextSize: number 95 | } 96 | 97 | /** 98 | * StackTraceParser is used to parse callsite from stacktrace 99 | * and get from FS the context of the error (if available) 100 | * 101 | * @param {Cache} cache cache implementation used to query file from FS and get context 102 | */ 103 | export class StackTraceParser { 104 | 105 | private cache: Cache 106 | private contextSize: number = 3 107 | 108 | constructor (options: StackTraceParserOptions) { 109 | this.cache = options.cache 110 | this.contextSize = options.contextSize 111 | } 112 | 113 | isAbsolute (path) { 114 | if (process.platform === 'win32') { 115 | // https://github.com/nodejs/node/blob/b3fcc245fb25539909ef1d5eaa01dbf92e168633/lib/path.js#L56 116 | let splitDeviceRe = /^([a-zA-Z]:|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)?([\\/])?([\s\S]*?)$/ 117 | let result = splitDeviceRe.exec(path) 118 | if (result === null) return path.charAt(0) === '/' 119 | let device = result[1] || '' 120 | let isUnc = Boolean(device && device.charAt(1) !== ':') 121 | // UNC paths are always absolute 122 | return Boolean(result[2] || isUnc) 123 | } else { 124 | return path.charAt(0) === '/' 125 | } 126 | } 127 | 128 | parse (stack: FrameMetadata[]): StackContext | null { 129 | if (stack.length === 0) return null 130 | 131 | const userFrame = stack.find(frame => { 132 | const type = this.isAbsolute(frame.file_name) || frame.file_name[0] === '.' ? 'user' : 'core' 133 | return type !== 'core' && frame.file_name.indexOf('node_modules') < 0 && frame.file_name.indexOf('@pm2/io') < 0 134 | }) 135 | if (userFrame === undefined) return null 136 | 137 | // get the whole context (all lines) and cache them if necessary 138 | const context = this.cache.get(userFrame.file_name) as string[] | null 139 | const source: string[] = [] 140 | if (context === null || context.length === 0) return null 141 | // get line before the call 142 | const preLine = userFrame.line_number - this.contextSize - 1 143 | const start = preLine > 0 ? preLine : 0 144 | context.slice(start, userFrame.line_number - 1).forEach(function (line) { 145 | source.push(line.replace(/\t/g, ' ')) 146 | }) 147 | // get the line where the call has been made 148 | if (context[userFrame.line_number - 1]) { 149 | source.push(context[userFrame.line_number - 1].replace(/\t/g, ' ').replace(' ', '>>')) 150 | } 151 | // and get the line after the call 152 | const postLine = userFrame.line_number + this.contextSize 153 | context.slice(userFrame.line_number, postLine).forEach(function (line) { 154 | source.push(line.replace(/\t/g, ' ')) 155 | }) 156 | return { 157 | context: source.join('\n'), 158 | callsite: [ userFrame.file_name, userFrame.line_number ].join(':') 159 | } 160 | } 161 | 162 | retrieveContext (error: Error): StackContext | null { 163 | if (error.stack === undefined) return null 164 | const frameRegex = /(\/[^\\\n]*)/g 165 | let tmp: any 166 | let frames: string[] = [] 167 | 168 | while ((tmp = frameRegex.exec(error.stack))) { // tslint:disable-line 169 | frames.push(tmp[1]) 170 | } 171 | const stackFrames = frames.map((callsite) => { 172 | if (callsite[callsite.length - 1] === ')') { 173 | callsite = callsite.substr(0, callsite.length - 1) 174 | } 175 | let location = callsite.split(':') 176 | 177 | return { 178 | file_name: location[0], 179 | line_number: parseInt(location[1], 10) 180 | } as FrameMetadata 181 | }) 182 | 183 | return this.parse(stackFrames) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/utils/units.ts: -------------------------------------------------------------------------------- 1 | const MILLISECONDS = 1 2 | const SECONDS = 1000 * MILLISECONDS 3 | const MINUTES = 60 * SECONDS 4 | const HOURS = 60 * MINUTES 5 | 6 | export default { 7 | NANOSECONDS: 1 / (1000 * 1000), 8 | MICROSECONDS: 1 / 1000, 9 | MILLISECONDS: MILLISECONDS, 10 | SECONDS: SECONDS, 11 | MINUTES: MINUTES, 12 | HOURS: HOURS, 13 | DAYS: 24 * HOURS 14 | } 15 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | NODE_ENV='test' 4 | MOCHA='npx mocha' 5 | 6 | trap "exit" INT 7 | set -e 8 | npm run build 9 | 10 | #### Unit tests 11 | 12 | $MOCHA ./test/autoExit.spec.ts 13 | $MOCHA ./test/api.spec.ts 14 | $MOCHA ./test/metrics/http.spec.ts 15 | $MOCHA ./test/metrics/runtime.spec.ts 16 | $MOCHA ./test/entrypoint.spec.ts 17 | # $MOCHA ./test/standalone/tracing.spec.ts 18 | # $MOCHA ./test/standalone/events.spec.ts 19 | $MOCHA ./test/features/events.spec.ts 20 | $MOCHA ./test/features/tracing.spec.ts 21 | $MOCHA ./test/metrics/eventloop.spec.ts 22 | $MOCHA ./test/metrics/network.spec.ts 23 | $MOCHA ./test/metrics/v8.spec.ts 24 | $MOCHA ./test/services/actions.spec.ts 25 | $MOCHA ./test/services/metrics.spec.ts 26 | 27 | #### Tracing tests 28 | 29 | # Enable tests 30 | export OPENCENSUS_MONGODB_TESTS="1" 31 | export OPENCENSUS_REDIS_TESTS="1" 32 | export OPENCENSUS_MYSQL_TESTS="1" 33 | export OPENCENSUS_PG_TESTS="1" 34 | 35 | if [ -z "$DRONE" ] 36 | then 37 | export OPENCENSUS_REDIS_HOST="localhost" 38 | export OPENCENSUS_MONGODB_HOST="localhost" 39 | export OPENCENSUS_MYSQL_HOST="localhost" 40 | export OPENCENSUS_PG_HOST="localhost" 41 | else 42 | export OPENCENSUS_REDIS_HOST="redis" 43 | export OPENCENSUS_MONGODB_HOST="mongodb" 44 | export OPENCENSUS_MYSQL_HOST="mysql" 45 | export OPENCENSUS_PG_HOST="postgres" 46 | fi 47 | 48 | $MOCHA src/census/plugins/__tests__/http.spec.ts 49 | $MOCHA src/census/plugins/__tests__/http2.spec.ts 50 | $MOCHA src/census/plugins/__tests__/https.spec.ts 51 | $MOCHA src/census/plugins/__tests__/mongodb.spec.ts 52 | $MOCHA src/census/plugins/__tests__/mysql.spec.ts 53 | $MOCHA src/census/plugins/__tests__/mysql2.spec.ts 54 | $MOCHA src/census/plugins/__tests__/ioredis.spec.ts 55 | $MOCHA src/census/plugins/__tests__/vue.spec.ts 56 | $MOCHA src/census/plugins/__tests__/express.spec.ts 57 | $MOCHA src/census/plugins/__tests__/net.spec.ts 58 | $MOCHA src/census/plugins/__tests__/redis.spec.ts 59 | 60 | SUPV14=`node -e "require('semver').gte(process.versions.node, '14.0.0') ? console.log('>=14') : console.log('>6')"` 61 | 62 | if [ $SUPV14 == '>=14' ]; then 63 | exit 64 | fi 65 | 66 | $MOCHA src/census/plugins/__tests__/pg.spec.ts 67 | #$MOCHA ./test/features/profiling.spec.ts 68 | -------------------------------------------------------------------------------- /test/autoExit.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { exec } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | const launch = fixture => { 6 | return exec(`node -r ts-node/register ${resolve(__dirname, fixture)}`) 7 | } 8 | 9 | describe('API', function () { 10 | this.timeout(20000) 11 | 12 | describe('AutoExit program', () => { 13 | it('should exit program when it has no more tasks to process', (done) => { 14 | const child = launch('fixtures/autoExitChild') 15 | child.on('exit', () => { 16 | done() 17 | }) 18 | }) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/entrypoint.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { assert, expect } from 'chai' 3 | import { exec, fork } from 'child_process' 4 | import { resolve } from 'path' 5 | import * as pmx from '../src' 6 | 7 | const launch = (fixture) => { 8 | return fork(resolve(__dirname, fixture), [], { 9 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ] 10 | }) 11 | } 12 | 13 | describe('Entrypoint', function () { 14 | this.timeout(20000) 15 | 16 | describe('Empty class', () => { 17 | it('should fail cause no onStart method', () => { 18 | try { 19 | const entrypoint = new pmx.Entrypoint() 20 | } catch (err) { 21 | expect(err.message).to.equal('Entrypoint onStart() not specified') 22 | } 23 | }) 24 | }) 25 | 26 | describe('Basic class', () => { 27 | it('should instantiate a basic entrypoint', (done) => { 28 | const child = launch('fixtures/entrypointChild') 29 | let hasConfig = false 30 | 31 | child.on('message', res => { 32 | 33 | if (res.type && res.type === 'axm:option:configuration' && res.data && res.data.metrics && res.data.metrics.eventLoop === false) { 34 | hasConfig = true 35 | } 36 | 37 | if (res === 'ready') { 38 | assert(hasConfig === true, 'should have both the good config and is ready sent') 39 | child.kill('SIGINT') 40 | } 41 | }) 42 | 43 | let exited = 0 44 | 45 | child.on('exit', (code, signal) => { 46 | if (!exited) { 47 | exited = 1 48 | expect(code).to.equal(null) 49 | expect(signal).to.equal('SIGINT') 50 | done() 51 | } 52 | }) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/features/events.spec.ts: -------------------------------------------------------------------------------- 1 | import { fork } from 'child_process' 2 | import { expect } from 'chai' 3 | import 'mocha' 4 | 5 | import { resolve } from 'path' 6 | 7 | const launch = (fixture) => { 8 | return fork(resolve(__dirname, fixture), [], { 9 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ] 10 | }) 11 | } 12 | 13 | describe('EventsFeature', function () { 14 | this.timeout(5000) 15 | describe('emit', () => { 16 | 17 | it('should emit an event', (done) => { 18 | const child = launch('../fixtures/features/eventsChild') 19 | child.on('message', res => { 20 | if (res.type === 'human:event') { 21 | child.kill('SIGKILL') 22 | expect(res.data.__name).to.equal('myEvent') 23 | expect(res.data.prop1).to.equal('value1') 24 | done() 25 | } 26 | }) 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /test/features/profiling.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { expect } from 'chai' 3 | import { fork, exec } from 'child_process' 4 | import * as semver from 'semver' 5 | import { resolve } from 'path' 6 | // for node 8 7 | process.env.FORCE_INSPECTOR = '1' 8 | 9 | const launch = (fixture) => { 10 | return fork(resolve(__dirname, fixture), [], { 11 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ] 12 | }) 13 | } 14 | 15 | describe('ProfilingAction', function () { 16 | this.timeout(50000) 17 | 18 | describe('CPU', () => { 19 | 20 | it('should get cpu profile data', (done) => { 21 | const child = launch('../fixtures/features/profilingChild') 22 | let uuid 23 | 24 | child.on('message', res => { 25 | 26 | if (res.type === 'axm:action') { 27 | expect(res.data.action_type).to.equal('internal') 28 | } 29 | 30 | if (res.type === 'axm:reply') { 31 | expect(res.data.return.success).to.equal(true) 32 | if (res.data.action_name === 'km:cpu:profiling:start') { 33 | uuid = res.data.return.uuid 34 | } 35 | } 36 | if (res.type === 'profilings') { 37 | expect(typeof res.data.data).to.equal('string') 38 | 39 | expect(res.data.type).to.equal('cpuprofile') 40 | 41 | child.kill('SIGINT') 42 | done() 43 | } 44 | 45 | if (res === 'initialized') { 46 | child.send('km:cpu:profiling:start') 47 | 48 | setTimeout(function () { 49 | child.send('km:cpu:profiling:stop') 50 | }, 500) 51 | } 52 | }) 53 | }) 54 | 55 | it('should get cpu profile data with timeout', (done) => { 56 | const child = launch('../fixtures/features/profilingChild') 57 | let uuid 58 | 59 | child.on('message', res => { 60 | 61 | if (res.type === 'axm:action') { 62 | expect(res.data.action_type).to.equal('internal') 63 | } 64 | 65 | if (res.type === 'axm:reply') { 66 | if (res.data.action_name === 'km:cpu:profiling:start') { 67 | expect(res.data.return.success).to.equal(true) 68 | uuid = res.data.return.uuid 69 | } 70 | } 71 | if (res.type === 'profilings') { 72 | expect(typeof res.data.data).to.equal('string') 73 | 74 | expect(res.data.type).to.equal('cpuprofile') 75 | 76 | child.kill('SIGINT') 77 | done() 78 | } 79 | 80 | if (res === 'initialized') { 81 | child.send({ 82 | msg: 'km:cpu:profiling:start', 83 | opts: { timeout: 500 } 84 | }) 85 | } 86 | }) 87 | }) 88 | 89 | if (semver.satisfies(process.version, '8.x')) { 90 | it('should get cpu profile data (force inspector on node 8)', (done) => { 91 | const child = launch('../fixtures/features/profilingChild') 92 | let uuid 93 | 94 | child.on('message', res => { 95 | 96 | if (res.type === 'axm:action') { 97 | expect(res.data.action_type).to.equal('internal') 98 | } 99 | 100 | if (res.type === 'axm:reply') { 101 | expect(res.data.return.success).to.equal(true) 102 | if (res.data.action_name === 'km:cpu:profiling:start') { 103 | uuid = res.data.return.uuid 104 | } 105 | } 106 | if (res.type === 'profilings') { 107 | expect(typeof res.data.data).to.equal('string') 108 | expect(res.data.type).to.equal('cpuprofile') 109 | 110 | child.kill('SIGINT') 111 | done() 112 | } 113 | 114 | if (res === 'initialized') { 115 | child.send('km:cpu:profiling:start') 116 | 117 | setTimeout(function () { 118 | child.send('km:cpu:profiling:stop') 119 | }, 500) 120 | } 121 | }) 122 | }) 123 | } 124 | }) 125 | 126 | describe('Heap', () => { 127 | if (semver.satisfies(semver.clean(process.version), '>8.x')) { 128 | it('should get heap profile data', (done) => { 129 | const child = launch('../fixtures/features/profilingChild') 130 | let uuid 131 | 132 | child.on('message', res => { 133 | 134 | if (res.type === 'axm:action') { 135 | expect(res.data.action_type).to.equal('internal') 136 | } 137 | 138 | if (res.type === 'axm:reply') { 139 | expect(res.data.return.success).to.equal(true) 140 | if (res.data.action_name === 'km:heap:sampling:start') { 141 | uuid = res.data.return.uuid 142 | } 143 | } 144 | if (res.type === 'profilings') { 145 | expect(typeof res.data.data).to.equal('string') 146 | 147 | expect(res.data.type).to.equal('heapprofile') 148 | child.kill('SIGINT') 149 | } 150 | 151 | if (res === 'initialized') { 152 | setTimeout(function () { 153 | child.send('km:heap:sampling:start') 154 | }, 100) 155 | 156 | setTimeout(function () { 157 | child.send('km:heap:sampling:stop') 158 | }, 500) 159 | } 160 | }) 161 | 162 | child.on('exit', function () { 163 | done() 164 | }) 165 | }) 166 | 167 | it('should get heap profile data with timeout', (done) => { 168 | const child = launch('../fixtures/features/profilingChild') 169 | let uuid 170 | 171 | child.on('message', res => { 172 | 173 | if (res.type === 'axm:action') { 174 | expect(res.data.action_type).to.equal('internal') 175 | } 176 | 177 | if (res.type === 'axm:reply') { 178 | expect(res.data.return.success).to.equal(true) 179 | 180 | if (res.data.action_name === 'km:heap:sampling:start') { 181 | uuid = res.data.return.uuid 182 | } 183 | } 184 | if (res.type === 'profilings') { 185 | expect(typeof res.data.data).to.equal('string') 186 | 187 | expect(res.data.type).to.equal('heapprofile') 188 | child.kill('SIGINT') 189 | } 190 | 191 | if (res === 'initialized') { 192 | setTimeout(function () { 193 | child.send({ 194 | msg: 'km:heap:sampling:start', 195 | opts: { timeout: 500 } 196 | }) 197 | }, 100) 198 | } 199 | }) 200 | 201 | child.on('exit', function () { 202 | done() 203 | }) 204 | }) 205 | } 206 | 207 | if (semver.satisfies(semver.clean(process.version), '<11.x')) { 208 | it('should get heap dump data', (done) => { 209 | const child = launch('../fixtures/features/profilingChild') 210 | 211 | child.on('message', res => { 212 | 213 | if (res.type === 'axm:action') { 214 | expect(res.data.action_type).to.equal('internal') 215 | } 216 | 217 | if (res.type === 'axm:reply') { 218 | 219 | expect(res.data.return.success).to.equal(true) 220 | } 221 | if (res.type === 'profilings') { 222 | expect(res.data.type).to.equal('heapdump') 223 | expect(typeof res.data.data).to.equal('string') 224 | 225 | child.kill('SIGINT') 226 | } 227 | 228 | if (res === 'initialized') { 229 | setTimeout(function () { 230 | child.send('km:heapdump') 231 | }, 500) 232 | } 233 | }) 234 | 235 | child.on('exit', function () { 236 | done() 237 | }) 238 | }) 239 | } 240 | }) 241 | }) 242 | -------------------------------------------------------------------------------- /test/features/tracing.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, assert } from 'chai' 2 | import { fork } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | const launch = (fixture) => { 6 | return fork(resolve(__dirname, fixture), [], { 7 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ], 8 | env: { NODE_ENV: 'test' } 9 | }) 10 | } 11 | 12 | describe('Tracing with IPC transport', function () { 13 | this.timeout(10000) 14 | 15 | it('should use tracing system', (done) => { 16 | const child = launch('../fixtures/metrics/tracingChild') 17 | const spans: any[] = [] 18 | child.on('message', pck => { 19 | if (pck.type !== 'trace-span') return 20 | expect(pck.data.hasOwnProperty('id')).to.equal(true) 21 | expect(pck.data.hasOwnProperty('traceId')).to.equal(true) 22 | spans.push(pck.data) 23 | if (spans.length === 4) { 24 | assert(spans.filter(span => span.name === 'http-get').length === 1) // client 25 | assert(spans.filter(span => span.name === '/toto').length === 1) // server 26 | assert(spans.filter(span => span.name === 'customspan').length === 1) // custom span using api 27 | child.kill('SIGKILL') 28 | return done() 29 | } 30 | }) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /test/fixtures/apiActionsChild.ts: -------------------------------------------------------------------------------- 1 | import * as pmx from '../../src' 2 | 3 | pmx.action('testAction', function (reply) { 4 | reply({ data: 'testActionReply' }) 5 | }) 6 | -------------------------------------------------------------------------------- /test/fixtures/apiActionsJsonChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | 4 | // @ts-ignore 5 | pmx.action({ 6 | name: 'testActionWithConf', 7 | action: function (reply) { reply({ data: 'testActionWithConfReply' }) } 8 | }) 9 | -------------------------------------------------------------------------------- /test/fixtures/apiBackwardActionsChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | 4 | pmx.init({ 5 | actions: { 6 | eventLoopDump: true 7 | }, 8 | profiling: true 9 | }) 10 | 11 | process.on('SIGINT', function () { 12 | pmx.destroy() 13 | process.exit(0) 14 | }) 15 | -------------------------------------------------------------------------------- /test/fixtures/apiBackwardConfChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | let timer 4 | let server 5 | 6 | pmx.init({ 7 | metrics: { 8 | network: true, 9 | v8: true, 10 | http: true 11 | }, 12 | tracing: { 13 | enabled: true 14 | }, 15 | profiling: false 16 | }) 17 | 18 | const express = require('express') 19 | const app = express() 20 | 21 | const httpModule = require('http') 22 | 23 | app.get('/', function (req, res) { 24 | res.send('home') 25 | }) 26 | 27 | server = app.listen(3001, function () { 28 | timer = setInterval(function () { 29 | httpModule.get('http://localhost:' + server.address().port) 30 | }, 100) 31 | }) 32 | 33 | process.on('SIGINT', function () { 34 | clearInterval(timer) 35 | server.close() 36 | pmx.destroy() 37 | }) 38 | -------------------------------------------------------------------------------- /test/fixtures/apiBackwardEventChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | 4 | pmx.emit('myEvent', { prop1: 'value1' }) 5 | -------------------------------------------------------------------------------- /test/fixtures/apiBackwardExpressChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | 4 | const express = require('express') 5 | const app = express() 6 | 7 | app.get('/', function (req, res) { 8 | res.send('Hello World') 9 | }) 10 | 11 | app.get('/error', function (req, res, next) { 12 | next(new Error('toto')) 13 | }) 14 | 15 | pmx.onExit(() => { 16 | pmx.destroy() 17 | }) 18 | 19 | app.use(pmx.expressErrorHandler()) 20 | 21 | app.listen(3003, () => { 22 | if (typeof process.send === 'function') { 23 | process.send('expressReady') 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /test/fixtures/apiInitModuleChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | 4 | process.env.fixtures = JSON.stringify({ 5 | envVar: 'value', 6 | password: 'toto' 7 | }) 8 | 9 | const conf = pmx.initModule({ 10 | test: 'toto' 11 | }) 12 | -------------------------------------------------------------------------------- /test/fixtures/apiKoaErrorHandler.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | 4 | const Koa = require('koa') 5 | const app = new Koa() 6 | 7 | app.use(pmx.koaErrorHandler()) 8 | 9 | // @ts-ignore 10 | app.use(async ctx => { 11 | ctx.throw(new Error('toto')) 12 | }) 13 | 14 | pmx.onExit(() => { 15 | pmx.destroy() 16 | }) 17 | 18 | app.listen(3003, () => { 19 | if (typeof process.send === 'function') { 20 | process.send('ready') 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /test/fixtures/apiMetricsChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as io from '../../src' 3 | import { MetricType } from '../../src/services/metrics' 4 | 5 | const [ one, two, three ] = io.metrics( 6 | [ 7 | { 8 | name: 'metricHistogram', 9 | type: MetricType.histogram, 10 | id: 'metric/custom' 11 | }, 12 | { 13 | name: 'metric with spaces', 14 | type: MetricType.histogram, 15 | id: 'metric/custom' 16 | }, 17 | { 18 | name: 'metric wi!th special chars % ///', 19 | type: MetricType.histogram, 20 | id: 'metric/custom' 21 | } 22 | ] 23 | ) 24 | 25 | one.update(10) 26 | 27 | // test inline declaration 28 | // @ts-ignore 29 | const metric = io.metric('metricInline') 30 | metric.set(11) 31 | 32 | io.metric({ 33 | name: 'toto', 34 | value: () => 42 35 | }) 36 | 37 | // set something into event loop. Else test will exit immediately 38 | const timer = setInterval(function () { 39 | return 40 | }, 5000) 41 | 42 | process.on('SIGINT', function () { 43 | clearInterval(timer) 44 | io.destroy() 45 | }) 46 | -------------------------------------------------------------------------------- /test/fixtures/apiNotifyChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src/index' 3 | 4 | pmx.init() 5 | try { 6 | throw new Error('myNotify') 7 | } catch (err) { 8 | pmx.notifyError(err) 9 | } -------------------------------------------------------------------------------- /test/fixtures/apiOnExitChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | 4 | pmx.onExit(function () { 5 | if (process && process.send) process.send('callback') 6 | }) 7 | -------------------------------------------------------------------------------- /test/fixtures/apiOnExitExceptionChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../src' 3 | 4 | pmx.onExit(function () { 5 | if (process && process.send) process.send('callback') 6 | }) 7 | 8 | setTimeout(function () { 9 | let toto 10 | 11 | console.log(toto.titi) 12 | }, 500) 13 | -------------------------------------------------------------------------------- /test/fixtures/autoExitChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as io from '../../src' 3 | 4 | io.init() 5 | -------------------------------------------------------------------------------- /test/fixtures/entrypointChild.ts: -------------------------------------------------------------------------------- 1 | import * as io from '../../src' 2 | import { IOConfig } from '../../src/pmx' 3 | 4 | class MyEntrypoint extends io.Entrypoint { 5 | 6 | onStart (cb: Function) { 7 | return cb() 8 | } 9 | 10 | conf (): IOConfig { 11 | return { 12 | metrics: { 13 | eventLoop: false 14 | } 15 | } 16 | } 17 | } 18 | 19 | const entrypoint = new MyEntrypoint() 20 | -------------------------------------------------------------------------------- /test/fixtures/features/eventLoopInspectorChild.ts: -------------------------------------------------------------------------------- 1 | import * as pmx from '../../../src' 2 | pmx.init({ 3 | actions: { 4 | eventLoopDump: true 5 | } 6 | }) 7 | 8 | setInterval(_ => { 9 | return 10 | }, 10000) 11 | -------------------------------------------------------------------------------- /test/fixtures/features/eventsChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../../src' 3 | 4 | pmx.init({ 5 | profiling: true 6 | }) 7 | 8 | setInterval(_ => { 9 | pmx.emit('myEvent', { prop1: 'value1' }) 10 | }, 100) 11 | -------------------------------------------------------------------------------- /test/fixtures/features/profilingChild.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as pmx from '../../../src' 3 | pmx.init({ 4 | profiling: true 5 | }) 6 | if (process && process.send) { 7 | process.send('initialized') 8 | } 9 | 10 | setInterval(_ => { 11 | let str = 0 12 | for (let i = 0; i < 100; i++) { 13 | str = str + str 14 | } 15 | }, 1000) 16 | -------------------------------------------------------------------------------- /test/fixtures/metrics/gcv8Child.ts: -------------------------------------------------------------------------------- 1 | import * as pmx from '../../../src' 2 | pmx.init({ 3 | metrics: { 4 | eventLoop: true, 5 | runtime: true, 6 | v8: true 7 | } 8 | }) 9 | 10 | setInterval(_ => { 11 | let str = 0 12 | for (let i = 0; i < 1000; i++) { 13 | str = str + str 14 | } 15 | }, 100) 16 | -------------------------------------------------------------------------------- /test/fixtures/metrics/httpWrapperChild.ts: -------------------------------------------------------------------------------- 1 | import * as pmx from '../../../src' 2 | pmx.init({ 3 | metrics: { 4 | eventLoop: true, 5 | runtime: true, 6 | v8: true, 7 | http: true 8 | } 9 | }) 10 | 11 | const httpModule = require('http') 12 | 13 | // test http outbound 14 | let timer 15 | 16 | const server = httpModule.createServer((req, res) => { 17 | res.writeHead(200) 18 | res.end('hey') 19 | }).listen(() => { 20 | timer = setInterval(function () { 21 | httpModule.get('http://localhost:' + server.address().port) 22 | httpModule.get('http://localhost:' + server.address().port + '/toto') 23 | }, 100) 24 | }) 25 | 26 | process.on('SIGINT', function () { 27 | clearInterval(timer) 28 | server.close() 29 | }) 30 | -------------------------------------------------------------------------------- /test/fixtures/metrics/networkChild.ts: -------------------------------------------------------------------------------- 1 | import * as pmx from '../../../src' 2 | pmx.init({ 3 | metrics: { 4 | eventLoop: true, 5 | v8: true, 6 | network: true 7 | } 8 | }) 9 | 10 | const httpModule = require('http') 11 | 12 | let timer 13 | 14 | const server = httpModule.createServer((req, res) => { 15 | res.writeHead(200) 16 | res.end('hey') 17 | }).listen(0, () => { 18 | timer = setInterval(function () { 19 | httpModule.get('http://localhost:' + server.address().port) 20 | httpModule.get('http://localhost:' + server.address().port + '/toto') 21 | }, 10) 22 | timer.unref() 23 | }) -------------------------------------------------------------------------------- /test/fixtures/metrics/networkWithoutDownloadChild.ts: -------------------------------------------------------------------------------- 1 | import * as pmx from '../../../src' 2 | pmx.init({ 3 | metrics: { 4 | network: { 5 | upload: true, 6 | download: false 7 | } 8 | } 9 | }) 10 | 11 | const httpModule = require('http') 12 | 13 | let timer 14 | 15 | const server = httpModule.createServer((req, res) => { 16 | res.writeHead(200) 17 | res.end('hey') 18 | }).listen(0, () => { 19 | timer = setInterval(function () { 20 | httpModule.get('http://localhost:' + server.address().port) 21 | httpModule.get('http://localhost:' + server.address().port + '/toto') 22 | }, 100) 23 | timer.unref() 24 | }) 25 | -------------------------------------------------------------------------------- /test/fixtures/metrics/tracingChild.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV='test' 2 | 3 | import * as pmx from '../../../src' 4 | pmx.init({ 5 | tracing: { 6 | enabled: true, 7 | samplingRate: 1 8 | } 9 | }) 10 | 11 | // @ts-ignore added in ci only 12 | import * as express from 'express' 13 | import { SpanKind } from '@opencensus/core' 14 | import { AddressInfo } from 'net' 15 | const app = express() 16 | 17 | const http = require('http') 18 | const https = require('https') 19 | 20 | // test http outbound 21 | let timer 22 | 23 | app.get('/', function (req, res) { 24 | http.get('http://localhost:' + (server.address() as AddressInfo).port + '/toto', (_) => { 25 | const tracer = pmx.getTracer() 26 | if (tracer === undefined) throw new Error('tracer undefined') 27 | const customSpan = tracer.startChildSpan('customspan', SpanKind.CLIENT) 28 | customSpan.addAttribute('test', true) 29 | setTimeout(_ => { 30 | customSpan.end() 31 | res.send('home') 32 | }, 100) 33 | }) 34 | }) 35 | 36 | app.get('/toto', function (req, res) { 37 | res.send('toto') 38 | }) 39 | 40 | const server = app.listen(3001, function () { 41 | console.log('App listening') 42 | timer = setInterval(function () { 43 | console.log('Running query') 44 | http.get('http://localhost:' + (server.address() as AddressInfo).port, (_) => { 45 | return 46 | }) 47 | https.get('https://google.fr') 48 | }, 500) 49 | }) 50 | 51 | process.on('SIGINT', function () { 52 | clearInterval(timer) 53 | server.close() 54 | }) 55 | -------------------------------------------------------------------------------- /test/metrics/eventloop.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, assert } from 'chai' 2 | import { fork } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | //process.env.DEBUG = 'axm:services:runtimeStats,axm:features:metrics:eventloop' 6 | 7 | const launch = (fixture) => { 8 | return fork(resolve(__dirname, fixture), [], { 9 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ] 10 | }) 11 | } 12 | 13 | const includes = (array: string[], value: string): boolean => { 14 | return array.some(tmp => tmp === value) 15 | } 16 | 17 | describe('EventLoopHandlesRequests', function () { 18 | this.timeout(10000) 19 | 20 | it('should send event loop with runtime stats', (done) => { 21 | const child = launch('../fixtures/metrics/gcv8Child') 22 | 23 | child.on('message', pck => { 24 | if (pck.type === 'axm:monitor') { 25 | const metricsName = Object.keys(pck.data) 26 | const metricsThatShouldBeThere = [ 27 | 'Event Loop Latency', 28 | 'Event Loop Latency p95', 29 | 'Active handles', 30 | 'Active requests' 31 | ] 32 | if (metricsName.filter(name => includes(metricsThatShouldBeThere, name)).length === metricsThatShouldBeThere.length) { 33 | child.kill('SIGINT') 34 | done() 35 | } 36 | } 37 | }) 38 | }) 39 | it('should send event without runtime stats', (done) => { 40 | process.env.PM2_APM_DISABLE_RUNTIME_STATS = 'true' 41 | const child = launch('../fixtures/metrics/gcv8Child') 42 | 43 | child.on('message', pck => { 44 | if (pck.type === 'axm:monitor') { 45 | const metricsName = Object.keys(pck.data) 46 | const metricsThatShouldBeThere = [ 47 | 'Event Loop Latency', 48 | 'Active handles', 49 | 'Active requests', 50 | 'Event Loop Latency p95' 51 | ] 52 | if (metricsName.filter(name => includes(metricsThatShouldBeThere, name)).length === metricsThatShouldBeThere.length) { 53 | child.kill('SIGINT') 54 | done() 55 | } 56 | } 57 | }) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /test/metrics/http.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { expect, assert } from 'chai' 3 | import { fork } from 'child_process' 4 | import { resolve } from 'path' 5 | 6 | const launch = (fixture) => { 7 | return fork(resolve(__dirname, fixture), [], { 8 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ] 9 | }) 10 | } 11 | describe('HttpWrapper', function () { 12 | this.timeout(10000) 13 | it('should wrap http and send basic metric', (done) => { 14 | const child = launch('../fixtures/metrics/httpWrapperChild') 15 | let called = false 16 | 17 | child.on('message', pck => { 18 | 19 | if (pck.type === 'axm:monitor') { 20 | if (called === true) return 21 | called = true 22 | expect(pck.data.HTTP.type).to.equal('internal/http/builtin/reqs') 23 | expect(pck.data.HTTP.unit).to.equal('req/min') 24 | 25 | expect(pck.data['HTTP Mean Latency'].type).to.equal('internal/http/builtin/latency/p50') 26 | expect(pck.data['HTTP Mean Latency'].unit).to.equal('ms') 27 | 28 | child.kill('SIGINT') 29 | done() 30 | } 31 | }) 32 | }) 33 | 34 | it('should use tracing system', (done) => { 35 | const child = launch('../fixtures/metrics/tracingChild') 36 | let called = false 37 | child.on('message', pck => { 38 | if (pck.type === 'trace-span' && called === false) { 39 | called = true 40 | expect(pck.data.hasOwnProperty('id')).to.equal(true) 41 | expect(pck.data.hasOwnProperty('traceId')).to.equal(true) 42 | expect(pck.data.tags['http.method']).to.equal('GET') 43 | expect(pck.data.tags['http.status_code']).to.equal('200') 44 | 45 | child.kill('SIGINT') 46 | done() 47 | } 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/metrics/network.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, assert } from 'chai' 2 | import { fork } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | const launch = (fixture) => { 6 | return fork(resolve(__dirname, fixture), [], { 7 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ] 8 | }) 9 | } 10 | 11 | describe('Network', function () { 12 | this.timeout(10000) 13 | 14 | it('should send network data', (done) => { 15 | const child = launch('../fixtures/metrics/networkChild') 16 | 17 | child.on('message', pck => { 18 | 19 | if (pck.type === 'axm:monitor' && pck.data['Network Out']) { 20 | child.kill('SIGKILL') 21 | 22 | expect(pck.data.hasOwnProperty('Network In')).to.equal(true) 23 | expect(pck.data['Network In'].historic).to.equal(true) 24 | 25 | expect(pck.data.hasOwnProperty('Network Out')).to.equal(true) 26 | expect(pck.data['Network Out'].historic).to.equal(true) 27 | 28 | done() 29 | } 30 | }) 31 | }) 32 | 33 | it('should only send upload data', (done) => { 34 | const child = launch('../fixtures/metrics/networkWithoutDownloadChild') 35 | 36 | child.on('message', pck => { 37 | 38 | if (pck.type === 'axm:monitor' && pck.data['Network Out'] && pck.data['Network Out'].value !== '0 B/sec') { 39 | child.kill('SIGKILL') 40 | 41 | expect(pck.data.hasOwnProperty('Network Out')).to.equal(true) 42 | expect(pck.data['Network Out'].historic).to.equal(true) 43 | 44 | expect(pck.data.hasOwnProperty('Open ports')).to.equal(false) 45 | done() 46 | } 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /test/metrics/runtime.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, assert } from 'chai' 2 | import { fork } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | //process.env.DEBUG = 'axm:services:runtimeStats,axm:features:metrics:runtime' 6 | 7 | const launch = (fixture) => { 8 | return fork(resolve(__dirname, fixture), [], { 9 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ] 10 | }) 11 | } 12 | 13 | describe.skip('RuntimeStatsMetrics', function () { 14 | this.timeout(5000) 15 | 16 | it('should get GC stats', (done) => { 17 | const child = launch('../fixtures/metrics/gcv8Child') 18 | 19 | child.on('message', pck => { 20 | if (pck.type === 'axm:monitor') { 21 | const metricsName = Object.keys(pck.data) 22 | const hasGCMetrics = metricsName.some(name => !!name.match(/GC/)) 23 | const hasPFMetrics = metricsName.some(name => !!name.match(/Page Fault/)) 24 | const hasContextSwitchMetrics = metricsName.some(name => !!name.match(/Context Switch/)) 25 | if (hasGCMetrics && hasContextSwitchMetrics && hasPFMetrics) { 26 | console.log(`found GC metrics: ${metricsName.filter(name => !!name.match(/GC/)).join(',')}`) 27 | child.kill('SIGINT') 28 | done() 29 | } 30 | } 31 | }) 32 | }) 33 | 34 | it('should not crash if runtime stats is disabled', (done) => { 35 | process.env.PM2_APM_DISABLE_RUNTIME_STATS = 'true' 36 | const child = launch('../fixtures/metrics/gcv8Child') 37 | 38 | setTimeout(_ => { 39 | child.on('message', pck => { 40 | if (pck.type === 'axm:monitor') { 41 | const metricsName = Object.keys(pck.data) 42 | assert(metricsName.every(name => !name.match(/GC/)), 'should have no GC metrics') 43 | assert(metricsName.every(name => !name.match(/Page Fault/)), 'should have no Page fault metrics') 44 | assert(metricsName.every(name => !name.match(/Context Switch/)), 'should have no context switch metrics') 45 | child.kill('SIGINT') 46 | } 47 | }) 48 | }, 1000) 49 | child.on('exit', (code, signal) => { 50 | assert(code === null, 'should not have exit code') 51 | assert(signal === 'SIGINT', 'should have exit via sigint') 52 | done() 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/metrics/v8.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, assert } from 'chai' 2 | import { fork, exec } from 'child_process' 3 | import { resolve } from 'path' 4 | 5 | const launch = (fixture) => { 6 | return fork(resolve(__dirname, fixture), [], { 7 | execArgv: process.env.NYC_ROOT_ID ? process.execArgv : [ '-r', 'ts-node/register' ] 8 | }) 9 | } 10 | 11 | describe('V8', function () { 12 | this.timeout(5000) 13 | it('should send all data with v8 heap info', (done) => { 14 | const child = launch('../fixtures/metrics/gcv8Child.ts') 15 | let receive = false 16 | 17 | child.on('message', pck => { 18 | 19 | if (pck.type === 'axm:monitor' && receive === false) { 20 | receive = true 21 | expect(isNaN(pck.data['Heap Size'].value)).to.equal(false) 22 | expect(isNaN(pck.data['Used Heap Size'].value)).to.equal(false) 23 | expect(pck.data['Heap Usage'].value).to.not.equal(undefined) 24 | 25 | child.kill('SIGINT') 26 | done() 27 | } 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fixtures", 3 | "version": "0.0.1", 4 | "config": { 5 | "prop1": "value1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/services/actions.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Action, ActionService } from '../../src/services/actions' 3 | import { IPCTransport } from '../../src/transports/IPCTransport' 4 | import { ServiceManager } from '../../src/serviceManager' 5 | import * as assert from 'assert' 6 | 7 | describe('ActionsService', function () { 8 | 9 | const transport = new IPCTransport() 10 | transport.init() 11 | ServiceManager.set('transport', transport) 12 | const service = new ActionService() 13 | service.init() 14 | const newAction = { 15 | name: 'toto', 16 | handler: (cb) => { 17 | return cb('data') 18 | } 19 | } 20 | 21 | describe('basic', () => { 22 | it('should register action', (done) => { 23 | transport.addAction = function (action: Action) { 24 | assert(action.name === newAction.name) 25 | return done() 26 | } 27 | service.registerAction(newAction.name, newAction.handler) 28 | }) 29 | it('should call it', (done) => { 30 | transport.send = (channel, payload) => { 31 | assert(channel === 'axm:reply') 32 | assert(payload.action_name === 'toto') 33 | assert(payload.return === 'data') 34 | done() 35 | return undefined 36 | } 37 | transport.emit('data', 'toto') 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/services/metrics.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { InternalMetric, MetricService, MetricMeasurements } from '../../src/services/metrics' 3 | import { IPCTransport } from '../../src/transports/IPCTransport' 4 | import { ServiceManager } from '../../src/serviceManager' 5 | import * as assert from 'assert' 6 | 7 | describe('MetricsService', function () { 8 | this.timeout(5000) 9 | 10 | const transport = new IPCTransport() 11 | transport.init() 12 | ServiceManager.set('transport', transport) 13 | const service = new MetricService() 14 | service.init() 15 | 16 | describe('basic', () => { 17 | it('register gauge', (done) => { 18 | transport.setMetrics = function (metrics: InternalMetric[]) { 19 | const gauge = metrics.find(metric => metric.name === 'gauge') 20 | assert(gauge !== undefined) 21 | return done() 22 | } 23 | const gauge = service.metric({ 24 | name: 'gauge' 25 | }) 26 | gauge.set(10) 27 | }) 28 | it('register gauge (with custom handler)', (done) => { 29 | transport.setMetrics = function (metrics: InternalMetric[]) { 30 | const gauge = metrics.find(metric => metric.name === 'gauge') 31 | assert(gauge !== undefined) 32 | return done() 33 | } 34 | const gauge = service.metric({ 35 | name: 'gauge', 36 | value: () => 10 37 | }) 38 | }) 39 | it('register meter', (done) => { 40 | transport.setMetrics = function (metrics: InternalMetric[]) { 41 | const meter = metrics.find(metric => metric.name === 'meter') 42 | assert(meter !== undefined) 43 | return done() 44 | } 45 | const meter = service.meter({ 46 | name: 'meter' 47 | }) 48 | meter.mark() 49 | }) 50 | it('register histogram', (done) => { 51 | transport.setMetrics = function (metrics: InternalMetric[]) { 52 | const histogram = metrics.find(metric => metric.name === 'histogram') 53 | assert(histogram !== undefined) 54 | return done() 55 | } 56 | const histogram = service.histogram({ 57 | name: 'histogram', 58 | measurement: MetricMeasurements.min 59 | }) 60 | histogram.update(10000) 61 | }) 62 | it('register counter', (done) => { 63 | transport.setMetrics = function (metrics: InternalMetric[]) { 64 | const counter = metrics.find(metric => metric.name === 'counter') 65 | assert(counter !== undefined) 66 | return done() 67 | } 68 | const counter = service.counter({ 69 | name: 'counter' 70 | }) 71 | counter.inc() 72 | }) 73 | it('should send value for all metrics', (done) => { 74 | let called = false 75 | transport.setMetrics = function (metrics: InternalMetric[]) { 76 | if (called === true) return 77 | called = true 78 | 79 | const counter = metrics.find(metric => metric.name === 'counter') 80 | const histogram = metrics.find(metric => metric.name === 'histogram') 81 | const meter = metrics.find(metric => metric.name === 'meter') 82 | const gauge = metrics.find(metric => metric.name === 'gauge') 83 | assert(counter !== undefined && counter.value === 1) 84 | assert(meter !== undefined) 85 | // @ts-ignore 86 | assert(histogram !== undefined && histogram.value > 0) 87 | assert(gauge !== undefined && gauge.value === 10) 88 | return done() 89 | } 90 | }) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /test/standalone/events.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as assert from 'assert' 3 | import 'mocha' 4 | import * as io from '../../src' 5 | // install patch before requiring the helpers 6 | io.init() 7 | import { WSServer, HandshakeServer } from './helper' 8 | 9 | describe('Event Spec', function () { 10 | this.timeout(10000) 11 | let httpServer 12 | let wsServer 13 | let apm 14 | 15 | before(() => { 16 | httpServer = new HandshakeServer() 17 | wsServer = new WSServer() 18 | process.env.PM2_SECRET_KEY = 'bb' 19 | process.env.PM2_PUBLIC_KEY = 'aa' 20 | process.env.PM2_APP_NAME = 'service' 21 | process.env.KEYMETRICS_NODE = 'http://localhost:5934' 22 | }) 23 | 24 | after(() => { 25 | io.destroy() 26 | httpServer.destroy() 27 | wsServer.destroy() 28 | process.env.PM2_SECRET_KEY = process.env.PM2_PUBLIC_KEY = process.env.PM2_APP_NAME = process.env.KEYMETRICS_NODE = undefined 29 | }) 30 | 31 | it('should init agent', () => { 32 | io.init() // the standalone mode should be enabled automaticaly 33 | }) 34 | 35 | it('should receive status', (done) => { 36 | wsServer.once('message', (data) => { 37 | const packet = JSON.parse(data) 38 | assert(packet.channel === 'status' 39 | || packet.channel === 'application:dependencies') 40 | return done() 41 | }) 42 | }) 43 | 44 | it('should send event and receive data', (done) => { 45 | wsServer.on('message', (data) => { 46 | const packet = JSON.parse(data) 47 | if (packet.channel !== 'human:event') return 48 | assert(typeof packet.payload.name === 'string') 49 | assert(typeof packet.payload.data.custom === 'number') 50 | assert(typeof packet.payload.data.nested === 'object') 51 | wsServer.removeAllListeners() 52 | return done() 53 | }) 54 | io.emit('test', { 55 | custom: 1, 56 | number: 2, 57 | nested: { 58 | afrg: 2 59 | } 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/standalone/helper.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as WebSocket from 'ws' 3 | import { EventEmitter2 } from 'eventemitter2' 4 | import { createServer, Server } from 'http' 5 | import * as express from 'express' 6 | 7 | export class WSServer extends EventEmitter2 { 8 | private wss 9 | 10 | constructor (port = 3405) { 11 | super() 12 | // @ts-ignore 13 | this.wss = new WebSocket.Server({ port }) 14 | this.wss.on('connection', (ws) => { 15 | this.emit('connection', ws) 16 | ws.on('message', data => { 17 | this.emit('message', data) 18 | }) 19 | }) 20 | } 21 | 22 | destroy () { 23 | this.wss.close() 24 | } 25 | } 26 | 27 | export class HandshakeServer { 28 | private server: any 29 | constructor (wsEndpoint: number = 3405, httpEndpoint: number = 5934) { 30 | const app = express() 31 | app.use((req, res) => { 32 | return res.status(200).json({ 33 | disabled: false, 34 | active: true, 35 | endpoints: { 36 | 'ws': `ws://localhost:${wsEndpoint}` 37 | } 38 | }) 39 | }) 40 | this.server = app.listen(httpEndpoint) 41 | } 42 | 43 | destroy () { 44 | this.server.close() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/standalone/tracing.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | process.env.NODE_ENV='test' 3 | 4 | import * as io from '../../src/' 5 | // install patch before requiring the helpers 6 | process.env.KEYMETRICS_NODE = 'http://localhost:5934' 7 | io.init({ 8 | standalone: true, 9 | apmOptions: { 10 | publicKey: 'aa', 11 | secretKey: 'bb', 12 | appName: 'service' 13 | }, 14 | tracing: { 15 | enabled: true, 16 | samplingRate: 1 17 | } 18 | }) 19 | 20 | import * as http from 'http' 21 | import * as assert from 'assert' 22 | import 'mocha' 23 | 24 | import { WSServer, HandshakeServer } from './helper' 25 | 26 | describe('Standalone Tracing', function () { 27 | this.timeout(10000) 28 | let httpServer 29 | let wsServer 30 | 31 | before(() => { 32 | httpServer = new HandshakeServer() 33 | wsServer = new WSServer() 34 | }) 35 | 36 | after(() => { 37 | io.destroy() 38 | httpServer.destroy() 39 | wsServer.destroy() 40 | }) 41 | 42 | it('should receive status', (done) => { 43 | wsServer.on('message', (data) => { 44 | const packet = JSON.parse(data) 45 | if (packet.channel === 'status') { 46 | wsServer.removeAllListeners() 47 | return done() 48 | } 49 | assert(packet.channel === 'status' || packet.channel === 'trace-span' 50 | || packet.channel === 'application:dependencies') 51 | }) 52 | }) 53 | 54 | it('should trace requests', (done) => { 55 | let spans = 0 56 | wsServer.on('message', (data) => { 57 | const packet = JSON.parse(data) 58 | if (packet.channel === 'trace-span') spans++ 59 | if (spans === 3) { 60 | wsServer.removeAllListeners() 61 | return done() 62 | } 63 | }) 64 | http.get('http://localhost:5934/fdsafdsg', () => { 65 | return 66 | }) 67 | http.get('http://localhost:5934/nhyjkuyjyt', () => { 68 | return 69 | }) 70 | http.get('http://localhost:5934/qswswde', () => { 71 | return 72 | }) 73 | }) 74 | }) 75 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "outDir": "build/main", 5 | "rootDirs": ["src"], 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "importHelpers": true, 10 | "inlineSourceMap": true, 11 | "strictNullChecks": true, 12 | "traceResolution": false, 13 | "downlevelIteration": true, 14 | "resolveJsonModule": true, 15 | "removeComments": true, 16 | "lib" : [ 17 | "es6" 18 | ], 19 | "types": [ 20 | "node", 21 | "mocha" 22 | ], 23 | "baseUrl": "." // required for "paths" 24 | }, 25 | "include": [ 26 | "src/**/*.ts" 27 | ], 28 | "exclude": [ 29 | "node_modules/**", 30 | "test/**/*.ts", 31 | "src/census/plugins/__tests__/**" 32 | ], 33 | "compileOnSave": false 34 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint-config-standard" 3 | } 4 | --------------------------------------------------------------------------------