├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .jsdoc.json ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Readme.md ├── documentation ├── assets │ ├── measured.license.md │ ├── measured.png │ └── measured.svg └── docstrap_customized │ └── template │ └── publish.js ├── lerna.json ├── package.json ├── packages ├── measured-core │ ├── README.md │ ├── lib │ │ ├── Collection.js │ │ ├── index.js │ │ ├── metrics │ │ │ ├── CachedGauge.js │ │ │ ├── Counter.js │ │ │ ├── Gauge.js │ │ │ ├── Histogram.js │ │ │ ├── Meter.js │ │ │ ├── Metric.js │ │ │ ├── NoOpMeter.js │ │ │ ├── SettableGauge.js │ │ │ └── Timer.js │ │ ├── util │ │ │ ├── BinaryHeap.js │ │ │ ├── ExponentiallyDecayingSample.js │ │ │ ├── ExponentiallyMovingWeightedAverage.js │ │ │ ├── Stopwatch.js │ │ │ └── units.js │ │ └── validators │ │ │ └── metricValidators.js │ ├── package.json │ └── test │ │ ├── common.js │ │ ├── integration │ │ └── test-Collection_end.js │ │ └── unit │ │ ├── metrics │ │ ├── test-CachedGauge.js │ │ ├── test-Counter.js │ │ ├── test-Gauge.js │ │ ├── test-Histogram.js │ │ ├── test-Meter.js │ │ ├── test-NoOpMeter.js │ │ ├── test-SettableGauge.js │ │ └── test-Timer.js │ │ ├── test-Collection.js │ │ └── util │ │ ├── test-BinaryHeap.js │ │ ├── test-ExponentiallyDecayingSample.js │ │ ├── test-ExponentiallyMovingWeightedAverage.js │ │ └── test-Stopwatch.js ├── measured-node-metrics │ ├── README.md │ ├── lib │ │ ├── index.js │ │ ├── nodeHttpRequestMetrics.js │ │ ├── nodeOsMetrics.js │ │ ├── nodeProcessMetrics.js │ │ └── utils │ │ │ └── CpuUtils.js │ ├── package.json │ └── test │ │ ├── integration │ │ ├── test-express-middleware.js │ │ └── test-koa-middleware.js │ │ └── unit │ │ ├── TestReporter.js │ │ ├── test-nodeHttpRequestMetrics.js │ │ ├── test-nodeOsMetrics.js │ │ ├── test-nodeProcessMetrics.js │ │ └── utils │ │ └── test-CpuUtils.js ├── measured-reporting │ ├── README.md │ ├── lib │ │ ├── @types │ │ │ └── types.js │ │ ├── index.js │ │ ├── registries │ │ │ ├── DimensionAwareMetricsRegistry.js │ │ │ └── SelfReportingMetricsRegistry.js │ │ ├── reporters │ │ │ ├── LoggingReporter.js │ │ │ └── Reporter.js │ │ └── validators │ │ │ └── inputValidators.js │ ├── package.json │ └── test │ │ └── unit │ │ ├── registries │ │ ├── test-DimensionAwareMetricsRegistry.js │ │ └── test-SelfReportingMetricsRegistry.js │ │ ├── reporters │ │ ├── test-LoggingReporter.js │ │ └── test-Reporter.js │ │ └── validators │ │ └── test-inputValidators.js └── measured-signalfx-reporter │ ├── README.md │ ├── lib │ ├── SignalFxEventCategories.js │ ├── index.js │ ├── registries │ │ └── SignalFxSelfReportingMetricsRegistry.js │ ├── reporters │ │ └── SignalFxMetricsReporter.js │ └── validators │ │ └── inputValidators.js │ ├── package.json │ └── test │ ├── unit │ ├── registries │ │ └── test-SignalFxSelfReportingMetricsRegistry.js │ ├── reporters │ │ └── test-SignalFxMetricsReporter.js │ └── validators │ │ └── test-inputValidators.js │ └── user-acceptance-test │ └── index.js ├── scripts ├── generate-docs.sh └── publish.sh ├── tutorials ├── SignalFx Express Full End to End Example.md └── SignalFx Koa Full End to End Example.md └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb/base", 3 | "env": { 4 | "jest": true 5 | }, 6 | "rules": { 7 | "max-len": 0, 8 | "no-underscore-dangle": 0, 9 | "no-use-before-define": 0, 10 | "object-shorthand": 0, 11 | "comma-dangle": 0, 12 | "class-methods-use-this": 0, 13 | "no-param-reassign": 0, 14 | "no-constant-condition": 0, 15 | "no-plusplus": 0, 16 | "one-var-declaration-per-line": 0, 17 | "one-var": 0, 18 | "prefer-destructuring": ["error", { 19 | "array": false, 20 | "object": true 21 | }], 22 | "arrow-body-style": 0, 23 | "no-mixed-operators": 0, 24 | "arrow-parens": 0, 25 | "function-paren-newline": 0, 26 | "no-unused-vars": 0, 27 | "object-curly-newline": 0, 28 | "spaced-comment": 0 29 | } 30 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # next.js build output 63 | .next 64 | 65 | # Idea 66 | .idea 67 | 68 | **build/ 69 | 70 | *.un~ 71 | 72 | **/.DS_Store -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "plugins": ["plugins/markdown"], 6 | "templates": { 7 | "logoFile": "img/measured.png", 8 | "cleverLinks": false, 9 | "monospaceLinks": false, 10 | "dateFormat": "ddd MMM Do YYYY", 11 | "outputSourceFiles": true, 12 | "outputSourcePath": true, 13 | "systemName": "Measured", 14 | "copyright": "License: MIT. Measured Copyright © 2012-2017 Felix Geisendörfer and Contributors, © 2018 Yet Another Org and Contributors.
Icon by SimpleIcon [CC BY 3.0], via Wikimedia Commons", 15 | "navType": "vertical", 16 | "theme": "cosmo", 17 | "linenums": true, 18 | "collapseSymbols": false, 19 | "inverseNav": true, 20 | "protocol": "html://", 21 | "methodHeadingReturns": false, 22 | "index": { 23 | "root": "./", 24 | "measured-core": "../../", 25 | "measured-reporting": "../../", 26 | "measured-signalfx-reporter": "../../" 27 | }, 28 | "analytics":{"ua":"UA-119781742-1", "domain":"yaorg.github.io"} 29 | }, 30 | "markdown": { 31 | "parser": "gfm", 32 | "hardwrap": true 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | tabWidth: 2 3 | semi: true 4 | singleQuote: true 5 | bracketSpacing: true -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 11 4 | - 10 5 | - 8 6 | before_install: 7 | - sudo apt-get -qq update 8 | - sudo apt-get install -y gawk jq 9 | jobs: 10 | include: 11 | - stage: publish code coverage 12 | node_js: "8" 13 | script: 14 | - npm run test:node:coverage 15 | - yarn coverage 16 | - stage: release docs 17 | if: type != pull_request AND branch = master 18 | node_js: "8" 19 | script: 20 | - yarn generate-docs 21 | - yarn travis-deploy-github-pages 22 | - stage: lerna publish and npm release 23 | if: type != pull_request AND tag IS present 24 | node_js: "8" 25 | script: 26 | - scripts/publish.sh 27 | sudo: required 28 | dist: xenial 29 | addons: 30 | chrome: stable 31 | env: 32 | global: 33 | - secure: ZKLEofNJCHSzJzfmayPgRdDWqD5N5g7OPs+gJELmwopAKCUaoOH4H5iH0eg1TZzcAoPL/qwU+gheEW1V7Io4PmVB2WUXUdBP9vaqZlrIw/sa6RD/51jpx4MkXd6ciXl1vmiWMnqPBj+S0NuYjKOt1tGMPDiOtK96UUVNA8uRMrs= 34 | - secure: sAfy698oA7zJdVEYt+MrBFlmbXIC0Xg+PSIA6bc6klp11yfgXS3vIXM5cW4MuijLJhxvPDmGzhsgVHB+pIQXffeVYKQypCp6JcoDG6OyUxUglvHoLaIfpMD+jQzu2I05EFCxyRZWrq+i5tMWZjkUPUfRJrXtnXEYqm2A5cWPOHs= 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Pull requests are welcome, please keep conversations and commit messages civil. 4 | 5 | 1. Fork, then clone the repo. 6 | 7 | 1. Set up your machine. 8 | 9 | ```bash 10 | yarn && yarn bootstrap 11 | ``` 12 | 13 | 1. Make sure the tests pass. 14 | 15 | ```bash 16 | yarn test 17 | ``` 18 | 19 | 1. Make your change. Add tests for your change. Make the linter and tests pass. 20 | 21 | 1. Document changes using the [jsdoc standards](http://usejsdoc.org/) to ensure that our code self-generates good documentation. 22 | 23 | build the jsdoc site, the files will be outputted to `./build/docs/` 24 | 25 | ```bash 26 | yarn generate-docs 27 | ``` 28 | 29 | Validate that your generated documentation is what you intended, as documentation gets autogenerated from master. 30 | 31 | 1. Run prettier to format code to comply with code style (Modified AirBnB Rule set: [ES Lint Config](.eslintrc.json)) 32 | 33 | ```bash 34 | yarn format 35 | ``` 36 | 37 | 1. Make sure sure the linter and unit tests pass and the docs can be generated. 38 | 39 | ```bash 40 | yarn test 41 | ``` 42 | 43 | 1. Push to your fork and submit a pull request and wait for peer review. 44 | 45 | We may suggest some changes or improvements or alternatives. 46 | 47 | Some things that will increase the chance that your pull request is accepted: 48 | 49 | 1. Your code must be documented inline via jsdoc annotations, tested, and pass the linter. 50 | 1. Your changes must not break the existing library API without good justification. 51 | 1. Your commit messages should be reasonable. (`git rebase -i head~n` choose the r option to reword you commits). 52 | 53 | This guide is loosely based off of [factory_bot_rails contributing.md](https://github.com/thoughtbot/factory_bot_rails/blob/master/CONTRIBUTING.md) which is referenced here [GitHub's Contributing Guidelines blog post](https://blog.github.com/2012-09-17-contributing-guidelines/) 54 | 55 | ## Releasing a new version 56 | 57 | Once a pull request has been reviewed and merged new versions of all packages can be released by any of the maintainers. This is an automated process driven by [Github Release](https://github.com/yaorg/node-measured/releases). 58 | 59 | 1. Check the [latest version number under releases](https://github.com/yaorg/node-measured/releases) and decide if the changes to be released require a MAJOR, MINOR or PATCH release according to [semantic versioning](https://semver.org/): 60 | 61 | 1. MAJOR version when you make incompatible API changes (e.g. `1.15.0` -> `2.0.0`) 62 | 2. MINOR version when you add functionality in a backwards-compatible manner (e.g. `1.15.0` -> `1.16.0`) 63 | 3. PATCH version when you make backwards-compatible bug fixes (e.g. `1.15.0` -> `1.15.1` 64 | 65 | 2. Create a new release by incrementing the correct version number using the [Github Releases user interface](https://github.com/yaorg/node-measured/releases/new). 66 | 67 | 1. Be sure not to save any draft releases! (as they'll also trigger step 3 below since Github creates a new tag for draft releases). 68 | 2. The tag version and release title is expected to be the same and in the following format: `v1.43.0` 69 | 70 | 3. The new tag will kick off a Travis build and using will automatically release all packages using the new version specified. (See [scripts/publish.sh](scripts/publish.sh) for details). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yet Another Org and Contributors 4 | Copyright (c) 2012-2017 Felix Geisendörfer and Contributors 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Measured 2 | 3 | Node libraries for measuring and reporting application-level metrics. 4 | 5 | Measured is heavily inspired by Coda Hale, Yammer Inc's [Dropwizard Metrics Libraries](https://github.com/dropwizard/metrics) 6 | 7 | [![Build Status](https://secure.travis-ci.org/yaorg/node-measured.png?branch=master)](http://travis-ci.org/yaorg/node-measured) [![Coverage Status](https://coveralls.io/repos/github/yaorg/node-measured/badge.svg?branch=master)](https://coveralls.io/github/yaorg/node-measured?branch=master) 8 | 9 | ## Available packages 10 | 11 | ### [Measured Core](packages/measured-core) 12 | 13 | **The core measured library that has the Metric interfaces and implementations.** 14 | 15 | [![npm](https://img.shields.io/npm/v/measured-core.svg)](https://www.npmjs.com/package/measured-core) [![downloads](https://img.shields.io/npm/dm/measured-core.svg)](https://www.npmjs.com/package/measured-core) 16 | 17 | ### [Measured Reporting](packages/measured-reporting) 18 | 19 | **The registry and reporting library that has the classes needed to create a dimension aware, self reporting metrics registry.** 20 | 21 | [![npm](https://img.shields.io/npm/v/measured-reporting.svg)](https://www.npmjs.com/package/measured-reporting) [![downloads](https://img.shields.io/npm/dm/measured-reporting.svg)](https://www.npmjs.com/package/measured-reporting) 22 | 23 | ### [Measured Node Metrics](packages/measured-node-metrics) 24 | 25 | **Various metrics generators and http framework middlewares that can be used with a self reporting metrics registry to easily instrument metrics for a node app.** 26 | 27 | [![npm](https://img.shields.io/npm/v/measured-node-metrics.svg)](https://www.npmjs.com/package/measured-node-metrics) [![downloads](https://img.shields.io/npm/dm/measured-node-metrics.svg)](https://www.npmjs.com/package/measured-node-metrics) 28 | 29 | ### [Measured SignalFx Reporter](packages/measured-signalfx-reporter) 30 | 31 | **A reporter that can be used with measured-reporting to send metrics to [SignalFx](https://signalfx.com/).** 32 | 33 | [![npm](https://img.shields.io/npm/v/measured-signalfx-reporter.svg)](https://www.npmjs.com/package/measured-signalfx-reporter) [![downloads](https://img.shields.io/npm/dm/measured-signalfx-reporter.svg)](https://www.npmjs.com/package/measured-signalfx-reporter) 34 | 35 | ### Measured Datadog reporter 36 | 37 | **Not implemented, community contribution wanted.** 38 | 39 | ### Measured Graphite reporter 40 | 41 | **Not implemented, community contribution wanted.** 42 | 43 | 44 | ## Development and Contributing 45 | 46 | See [Development and Contributing](https://github.com/yaorg/node-measured/blob/master/CONTRIBUTING.md) 47 | 48 | ## License 49 | 50 | This project Measured and all of its modules are licensed under the [MIT license](https://github.com/yaorg/node-measured/blob/master/LICENSE). 51 | -------------------------------------------------------------------------------- /documentation/assets/measured.license.md: -------------------------------------------------------------------------------- 1 | measured.png and measured.svg by [SimpleIcon](http://www.simpleicon.com/) available at [flaticon](http://www.flaticon.com/packs/simpleicon-business) 2 | Licenced by [CC BY 3.0](https://creativecommons.org/licenses/by/3.0), via Wikimedia Commons -------------------------------------------------------------------------------- /documentation/assets/measured.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaorg/node-measured/916cb3ae8aa76d8c3afcb776b3d1c001d3c57947/documentation/assets/measured.png -------------------------------------------------------------------------------- /documentation/assets/measured.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "version": "2.0.0" 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "coveralls": "^3.0.1", 5 | "eslint": "^4.19.1", 6 | "eslint-config-airbnb": "^16.1.0", 7 | "eslint-config-prettier": "^2.9.0", 8 | "eslint-plugin-import": "^2.11.0", 9 | "eslint-plugin-jsx-a11y": "^6.0.3", 10 | "eslint-plugin-prettier": "^2.6.0", 11 | "eslint-plugin-react": "^7.8.1", 12 | "gh-pages": "^1.1.0", 13 | "ink-docstrap": "^1.3.2", 14 | "jsdoc": "^3.5.5", 15 | "koa": "^2.13.0", 16 | "koa-bodyparser": "^4.3.0", 17 | "koa-router": "^9.1.0", 18 | "lerna": "^2.11.0", 19 | "mocha": "^5.1.1", 20 | "mocha-lcov-reporter": "^1.3.0", 21 | "mochify": "^5.6.1", 22 | "nyc": "^11.8.0", 23 | "prettier": "^1.12.1", 24 | "showdown": "^1.8.6", 25 | "sinon": "^5.0.7" 26 | }, 27 | "scripts": { 28 | "bootstrap": "lerna bootstrap", 29 | "lint": "lerna exec yarn lint", 30 | "pretest": "yarn lint", 31 | "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text lerna exec yarn test:node", 32 | "test:browser": "lerna exec yarn test:browser", 33 | "test": "yarn test:node:coverage && yarn test:browser", 34 | "format": "lerna exec yarn format", 35 | "clean": "rm -fr build && lerna exec yarn clean", 36 | "generate-docs": "./scripts/generate-docs.sh", 37 | "deploy-docs": "gh-pages -d build/docs", 38 | "travis-deploy-github-pages": "gh-pages -r \"https://${GH_TOKEN}@github.com/yaorg/node-measured.git\" -d build/docs", 39 | "coverage": "nyc report --reporter=text-lcov | coveralls" 40 | }, 41 | "repository": { 42 | "url": "git://github.com/yaorg/node-measured.git" 43 | }, 44 | "homepage": "https://yaorg.github.io/node-measured/", 45 | "license": "MIT", 46 | "workspaces": [ 47 | "packages/*" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /packages/measured-core/README.md: -------------------------------------------------------------------------------- 1 | # Measured Core 2 | 3 | The core measured library that has the Metric interfaces and implementations. 4 | 5 | [![npm](https://img.shields.io/npm/v/measured-core.svg)](https://www.npmjs.com/package/measured-core) 6 | 7 | ## Install 8 | 9 | ``` 10 | npm install measured-core 11 | ``` 12 | 13 | ## What is in this package 14 | 15 | ### Metric Implemenations 16 | 17 | The core library has the following metrics classes: 18 | 19 | #### [Gauge](https://yaorg.github.io/node-measured/packages/measured-core/Gauge.html) 20 | Values that can be read instantly via a supplied call back. 21 | 22 | #### [SettableGauge](https://yaorg.github.io/node-measured/packages/measured-core/SettableGauge.html) 23 | Just like a Gauge but its value is set directly rather than supplied by a callback. 24 | 25 | #### [CachedGauge](https://yaorg.github.io/node-measured/packages/measured-core/CachedGauge.html) 26 | Like a mix of the regular and settable Gauge it takes a call back that returns a promise that will resolve the cached value and an interval that it should call the callback on to update its cached value. 27 | 28 | #### [Counter](https://yaorg.github.io/node-measured/packages/measured-core/Counter.html) 29 | Counters are things that increment or decrement. 30 | 31 | #### [Timer](https://yaorg.github.io/node-measured/packages/measured-core/Timer.html) 32 | Timers are a combination of Meters and Histograms. They measure the rate as well as distribution of scalar events. 33 | 34 | #### [Histogram](https://yaorg.github.io/node-measured/packages/measured-core/Histogram.html) 35 | Keeps a reservoir of statistically relevant values to explore their distribution. 36 | 37 | #### [Meter](https://yaorg.github.io/node-measured/packages/measured-core/Meter.html) 38 | Things that are measured as events / interval. 39 | 40 | ### Registry 41 | 42 | The core library comes with a basic registry class 43 | 44 | #### [Collection](https://yaorg.github.io/node-measured/packages/measured-core/Collection.html) 45 | 46 | that is not aware of dimensions / tags and leaves reporting up to you. 47 | 48 | #### See the [measured-reporting](../measured-reporting/) module for more advanced and featured registries. 49 | 50 | ### Other 51 | 52 | See The [measured-core](https://yaorg.github.io/node-measured/packages/measured-core/module-measured-core.html) modules for the full list of exports for require('measured-core'). 53 | 54 | ## Usage 55 | 56 | **Step 1:** Add measurements to your code. For example, lets track the 57 | requests/sec of a http server: 58 | 59 | ```js 60 | var http = require('http'); 61 | var stats = require('measured').createCollection(); 62 | 63 | http.createServer(function(req, res) { 64 | stats.meter('requestsPerSecond').mark(); 65 | res.end('Thanks'); 66 | }).listen(3000); 67 | ``` 68 | 69 | **Step 2:** Show the collected measurements (more advanced examples follow later): 70 | 71 | ```js 72 | setInterval(function() { 73 | console.log(stats.toJSON()); 74 | }, 1000); 75 | ``` 76 | 77 | This will output something like this every second: 78 | 79 | ``` 80 | { requestsPerSecond: 81 | { mean: 1710.2180279856818, 82 | count: 10511, 83 | 'currentRate': 1941.4893498239829, 84 | '1MinuteRate': 168.08263156623656, 85 | '5MinuteRate': 34.74630977619571, 86 | '15MinuteRate': 11.646507524106095 } } 87 | ``` 88 | 89 | **Step 3:** Aggregate the data into your backend of choice. 90 | Here are a few time series data aggregators. 91 | - [Graphite](http://graphite.wikidot.com/) 92 | - A free and open source, self hosted and managed solution for time series data. 93 | - [SignalFx](https://signalfx.com/) 94 | - An enterprise SASS offering for time series data. 95 | - [Datadog](https://www.datadoghq.com/) 96 | - An enterprise SASS offering for time series data. 97 | -------------------------------------------------------------------------------- /packages/measured-core/lib/Collection.js: -------------------------------------------------------------------------------- 1 | const Optional = require('optional-js'); 2 | const Counter = require('./metrics/Counter'); 3 | const Gauge = require('./metrics/Gauge'); 4 | const SettableGauge = require('./metrics/SettableGauge'); 5 | const CachedGauge = require('./metrics/CachedGauge'); 6 | const Histogram = require('./metrics/Histogram'); 7 | const Meter = require('./metrics/Meter'); 8 | const Timer = require('./metrics/Timer'); 9 | const { MetricTypes } = require('./metrics/Metric'); 10 | 11 | /** 12 | * A Simple collection that stores names and a {@link Metric} instances with a few convenience methods for 13 | * creating / registering and then gathering all data the registered metrics. 14 | * @example 15 | * var { Collection } = require('measured'); 16 | * const collection = new Collection('node-process-metrics'); 17 | * const gauge = collection.gauge('node.process.heap_used', () => { 18 | * return process.memoryUsage().heapUsed; 19 | * }); 20 | */ 21 | class Collection { 22 | /** 23 | * Creates a named collection of metrics 24 | * @param {string} [name] The name to use for this collection. 25 | */ 26 | constructor(name) { 27 | this.name = name; 28 | 29 | /** 30 | * internal map of metric name to {@link Metric} 31 | * @type {Object.} 32 | * @private 33 | */ 34 | this._metrics = {}; 35 | } 36 | 37 | /** 38 | * register a metric that was created outside the provided convenience methods of this collection 39 | * @param name The metric name 40 | * @param metric The {@link Metric} implementation 41 | * @example 42 | * var { Collection, Gauge } = require('measured'); 43 | * const collection = new Collection('node-process-metrics'); 44 | * const gauge = new Gauge(() => { 45 | * return process.memoryUsage().heapUsed; 46 | * }); 47 | * collection.register('node.process.heap_used', gauge); 48 | */ 49 | register(name, metric) { 50 | this._metrics[name] = metric; 51 | } 52 | 53 | /** 54 | * Fetches the data/values from all registered metrics 55 | * @return {Object} The combined JSON object 56 | */ 57 | toJSON() { 58 | const json = {}; 59 | 60 | Object.keys(this._metrics).forEach(metric => { 61 | if (Object.prototype.hasOwnProperty.call(this._metrics, metric)) { 62 | json[metric] = this._metrics[metric].toJSON(); 63 | } 64 | }); 65 | 66 | if (!this.name) { 67 | return json; 68 | } 69 | 70 | const wrapper = {}; 71 | wrapper[this.name] = json; 72 | 73 | return wrapper; 74 | } 75 | 76 | /** 77 | * Gets or creates and registers a {@link Gauge} 78 | * @param {string} name The metric name 79 | * @param {function} readFn See {@link Gauge} 80 | * @return {Gauge} 81 | */ 82 | gauge(name, readFn) { 83 | this._validateName(name); 84 | 85 | let gauge; 86 | this._getMetricForNameAndType(name, MetricTypes.GAUGE).ifPresentOrElse( 87 | registeredMetric => { 88 | gauge = registeredMetric; 89 | }, 90 | () => { 91 | gauge = new Gauge(readFn); 92 | this.register(name, gauge); 93 | } 94 | ); 95 | return gauge; 96 | } 97 | 98 | /** 99 | * Gets or creates and registers a {@link Counter} 100 | * @param {string} name The metric name 101 | * @param {CounterProperties} [properties] See {@link CounterProperties} 102 | * @return {Counter} 103 | */ 104 | counter(name, properties) { 105 | this._validateName(name); 106 | 107 | let counter; 108 | this._getMetricForNameAndType(name, MetricTypes.COUNTER).ifPresentOrElse( 109 | registeredMetric => { 110 | counter = registeredMetric; 111 | }, 112 | () => { 113 | counter = new Counter(properties); 114 | this.register(name, counter); 115 | } 116 | ); 117 | return counter; 118 | } 119 | 120 | /** 121 | * Gets or creates and registers a {@link Histogram} 122 | * @param {string} name The metric name 123 | * @param {HistogramProperties} [properties] See {@link HistogramProperties} 124 | * @return {Histogram} 125 | */ 126 | histogram(name, properties) { 127 | this._validateName(name); 128 | 129 | let histogram; 130 | this._getMetricForNameAndType(name, MetricTypes.HISTOGRAM).ifPresentOrElse( 131 | registeredMetric => { 132 | histogram = registeredMetric; 133 | }, 134 | () => { 135 | histogram = new Histogram(properties); 136 | this.register(name, histogram); 137 | } 138 | ); 139 | return histogram; 140 | } 141 | 142 | /** 143 | * Gets or creates and registers a {@link Timer} 144 | * @param {string} name The metric name 145 | * @param {TimerProperties} [properties] See {@link TimerProperties} 146 | * @return {Timer} 147 | */ 148 | timer(name, properties) { 149 | this._validateName(name); 150 | 151 | let timer; 152 | this._getMetricForNameAndType(name, MetricTypes.TIMER).ifPresentOrElse( 153 | registeredMetric => { 154 | timer = registeredMetric; 155 | }, 156 | () => { 157 | timer = new Timer(properties); 158 | this.register(name, timer); 159 | } 160 | ); 161 | return timer; 162 | } 163 | 164 | /** 165 | * Gets or creates and registers a {@link Meter} 166 | * @param {string} name The metric name 167 | * @param {MeterProperties} [properties] See {@link MeterProperties} 168 | * @return {Meter} 169 | */ 170 | meter(name, properties) { 171 | this._validateName(name); 172 | 173 | let meter; 174 | this._getMetricForNameAndType(name, MetricTypes.METER).ifPresentOrElse( 175 | registeredMetric => { 176 | meter = registeredMetric; 177 | }, 178 | () => { 179 | meter = new Meter(properties); 180 | this.register(name, meter); 181 | } 182 | ); 183 | return meter; 184 | } 185 | 186 | /** 187 | * Gets or creates and registers a {@link SettableGauge} 188 | * @param {string} name The metric name 189 | * @param {SettableGaugeProperties} [properties] See {@link SettableGaugeProperties} 190 | * @return {SettableGauge} 191 | */ 192 | settableGauge(name, properties) { 193 | this._validateName(name); 194 | 195 | let settableGauge; 196 | this._getMetricForNameAndType(name, MetricTypes.GAUGE).ifPresentOrElse( 197 | registeredMetric => { 198 | settableGauge = registeredMetric; 199 | }, 200 | () => { 201 | settableGauge = new SettableGauge(properties); 202 | this.register(name, settableGauge); 203 | } 204 | ); 205 | return settableGauge; 206 | } 207 | 208 | /** 209 | * Gets or creates and registers a {@link SettableGauge} 210 | * @param {string} name The metric name 211 | * @param {function} valueProducingPromiseCallback A function that returns a promise than when 212 | * resolved supplies the value that should be cached in this gauge. 213 | * @param {number} updateIntervalInSeconds How often the cached gauge should update it's value. 214 | * @return {CachedGauge} 215 | */ 216 | cachedGauge(name, valueProducingPromiseCallback, updateIntervalInSeconds) { 217 | this._validateName(name); 218 | 219 | let cachedGauge; 220 | this._getMetricForNameAndType(name, MetricTypes.GAUGE).ifPresentOrElse( 221 | registeredMetric => { 222 | cachedGauge = registeredMetric; 223 | }, 224 | () => { 225 | cachedGauge = new CachedGauge(valueProducingPromiseCallback, updateIntervalInSeconds); 226 | this.register(name, cachedGauge); 227 | } 228 | ); 229 | return cachedGauge; 230 | } 231 | 232 | /** 233 | * Checks the registry for a metric with a given name and type, if it exists in the registry as a 234 | * different type an error is thrown. 235 | * @param {string} name The metric name 236 | * @param {string} requestedType The metric type 237 | * @return {Optional} 238 | * @private 239 | */ 240 | _getMetricForNameAndType(name, requestedType) { 241 | if (this._metrics[name]) { 242 | const metric = this._metrics[name]; 243 | const actualType = metric.getType(); 244 | if (requestedType !== actualType) { 245 | throw new Error( 246 | `You requested a metric of type: ${requestedType} with name: ${name}, but it exists in the registry as type: ${actualType}` 247 | ); 248 | } 249 | return Optional.of(metric); 250 | } 251 | return Optional.empty(); 252 | } 253 | 254 | /** 255 | * Validates that the provided name is valid. 256 | * 257 | * @param name The provided metric name param. 258 | * @private 259 | */ 260 | _validateName(name) { 261 | if (!name || typeof name !== 'string') { 262 | throw new Error('You must supply a metric name'); 263 | } 264 | } 265 | 266 | /** 267 | * Calls end on all metrics in the registry that support end() 268 | */ 269 | end() { 270 | const metrics = this._metrics; 271 | Object.keys(metrics).forEach(name => { 272 | const metric = metrics[name]; 273 | if (metric.end) { 274 | metric.end(); 275 | } 276 | }); 277 | } 278 | } 279 | 280 | module.exports = Collection; 281 | -------------------------------------------------------------------------------- /packages/measured-core/lib/index.js: -------------------------------------------------------------------------------- 1 | const Collection = require('./Collection'); 2 | const Counter = require('./metrics/Counter'); 3 | const Gauge = require('./metrics/Gauge'); 4 | const SettableGauge = require('./metrics/SettableGauge'); 5 | const CachedGauge = require('./metrics/CachedGauge'); 6 | const Histogram = require('./metrics/Histogram'); 7 | const Meter = require('./metrics/Meter'); 8 | const NoOpMeter = require('./metrics/NoOpMeter'); 9 | const Timer = require('./metrics/Timer'); 10 | const BinaryHeap = require('./util/BinaryHeap'); 11 | const ExponentiallyDecayingSample = require('./util/ExponentiallyDecayingSample'); 12 | const ExponentiallyMovingWeightedAverage = require('./util/ExponentiallyMovingWeightedAverage'); 13 | const Stopwatch = require('./util/Stopwatch'); 14 | const units = require('./util/units'); 15 | const { MetricTypes } = require('./metrics/Metric'); 16 | const metricValidators = require('./validators/metricValidators'); 17 | 18 | /** 19 | * The main measured-core module that is referenced when require('measured-core') is used. 20 | * @module measured-core 21 | */ 22 | module.exports = { 23 | /** 24 | * See {@link Collection} 25 | * @type {Collection} 26 | */ 27 | Collection, 28 | 29 | /** 30 | * See {@link Counter} 31 | * @type {Counter} 32 | */ 33 | Counter, 34 | 35 | /** 36 | * See {@link Gauge} 37 | * @type {Gauge} 38 | */ 39 | Gauge, 40 | 41 | /** 42 | * See {@link SettableGauge} 43 | * @type {SettableGauge} 44 | */ 45 | SettableGauge, 46 | 47 | /** 48 | * See {@link CachedGauge} 49 | * @type {CachedGauge} 50 | */ 51 | CachedGauge, 52 | 53 | /** 54 | * See {@link Histogram} 55 | * @type {Histogram} 56 | */ 57 | Histogram, 58 | 59 | /** 60 | * See {@link Meter} 61 | * @type {Meter} 62 | */ 63 | Meter, 64 | 65 | /** 66 | * See {@link NoOpMeter} 67 | * @type {NoOpMeter} 68 | */ 69 | NoOpMeter, 70 | 71 | /** 72 | * See {@link Timer} 73 | * @type {Timer} 74 | */ 75 | Timer, 76 | 77 | /** 78 | * See {@link BinaryHeap} 79 | * @type {BinaryHeap} 80 | */ 81 | BinaryHeap, 82 | 83 | /** 84 | * See {@link ExponentiallyDecayingSample} 85 | * @type {ExponentiallyDecayingSample} 86 | */ 87 | ExponentiallyDecayingSample, 88 | 89 | /** 90 | * See {@link ExponentiallyMovingWeightedAverage} 91 | * @type {ExponentiallyMovingWeightedAverage} 92 | */ 93 | ExponentiallyMovingWeightedAverage, 94 | 95 | /** 96 | * See {@link Stopwatch} 97 | * @type {Stopwatch} 98 | */ 99 | Stopwatch, 100 | 101 | /** 102 | * See {@link MetricTypes} 103 | * @type {MetricTypes} 104 | */ 105 | MetricTypes, 106 | 107 | /** 108 | * See {@link units} 109 | * @type {units} 110 | */ 111 | units, 112 | 113 | /** 114 | * See {@link units} 115 | * @type {units} 116 | */ 117 | TimeUnits: units, 118 | 119 | /** 120 | * See {@link module:metricValidators} 121 | * @type {Object.} 122 | */ 123 | metricValidators, 124 | 125 | /** 126 | * Creates a named collection. See {@link Collection} for more details 127 | * 128 | * @param name The name for the collection 129 | * @return {Collection} 130 | */ 131 | createCollection: name => { 132 | return new Collection(name); 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/CachedGauge.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('./Metric'); 2 | const TimeUnits = require('../util/units'); 3 | 4 | /** 5 | * A Cached Gauge takes a function that returns a promise that resolves a 6 | * value that should be cached and updated on a given interval. 7 | * 8 | * toJSON() will return the currently cached value. 9 | * 10 | * @example 11 | * const cpuAverageCachedGauge = new CachedGauge(() => { 12 | * return new Promise(resolve => { 13 | * //Grab first CPU Measure 14 | * const startMeasure = cpuAverage(); 15 | * setTimeout(() => { 16 | * //Grab second Measure 17 | * const endMeasure = cpuAverage(); 18 | * const percentageCPU = calculateCpuUsagePercent(startMeasure, endMeasure); 19 | * resolve(percentageCPU); 20 | * }, sampleTimeInSeconds); 21 | * }); 22 | * }, updateIntervalInSeconds); 23 | * 24 | * @implements {Metric} 25 | */ 26 | class CachedGauge { 27 | /** 28 | * @param {function} valueProducingPromiseCallback A function that returns a promise than when 29 | * resolved supplies the value that should be cached in this gauge. 30 | * @param {number} updateIntervalInSeconds How often the cached gauge should update it's value. 31 | * @param {number} [timeUnitOverride] by default this function takes updateIntervalInSeconds and multiplies it by TimeUnits.SECONDS (1000), 32 | * You can override it here. 33 | */ 34 | constructor(valueProducingPromiseCallback, updateIntervalInSeconds, timeUnitOverride) { 35 | const timeUnit = timeUnitOverride || TimeUnits.SECONDS; 36 | 37 | this._valueProducingPromiseCallback = valueProducingPromiseCallback; 38 | this._value = 0; 39 | this._updateValue(); 40 | this._interval = setInterval(() => { 41 | this._updateValue(); 42 | }, updateIntervalInSeconds * timeUnit); 43 | } 44 | 45 | /** 46 | * Calls the promise producing callback and sets the value when it gets resolved. 47 | * @private 48 | */ 49 | _updateValue() { 50 | this._valueProducingPromiseCallback().then(value => { 51 | this._value = value; 52 | }); 53 | } 54 | 55 | /** 56 | * @return {number} Gauges directly return the value which should be a number. 57 | */ 58 | toJSON() { 59 | return this._value; 60 | } 61 | 62 | /** 63 | * The type of the Metric Impl. {@link MetricTypes}. 64 | * @return {string} The type of the Metric Impl. 65 | */ 66 | getType() { 67 | return MetricTypes.GAUGE; 68 | } 69 | 70 | /** 71 | * Clears the interval, so that it doesn't keep any processes alive. 72 | */ 73 | end() { 74 | clearInterval(this._interval); 75 | this._interval = null; 76 | } 77 | } 78 | 79 | module.exports = CachedGauge; 80 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/Counter.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('./Metric'); 2 | 3 | /** 4 | * Counters are things that increment or decrement 5 | * @implements {Metric} 6 | * @example 7 | * var Measured = require('measured') 8 | * var activeUploads = new Measured.Counter(); 9 | * http.createServer(function(req, res) { 10 | * activeUploads.inc(); 11 | * req.on('end', function() { 12 | * activeUploads.dec(); 13 | * }); 14 | * }); 15 | */ 16 | class Counter { 17 | /** 18 | * @param {CounterProperties} [properties] see {@link CounterProperties} 19 | */ 20 | constructor(properties) { 21 | properties = properties || {}; 22 | 23 | this._count = properties.count || 0; 24 | } 25 | 26 | /** 27 | * Counters directly return their currently value. 28 | * @return {number} 29 | */ 30 | toJSON() { 31 | return this._count; 32 | } 33 | 34 | /** 35 | * Increments the counter. 36 | * @param {number} n Increment the counter by n. Defaults to 1. 37 | */ 38 | inc(n) { 39 | this._count += arguments.length ? n : 1; 40 | } 41 | 42 | /** 43 | * Decrements the counter 44 | * @param {number} n Decrement the counter by n. Defaults to 1. 45 | */ 46 | dec(n) { 47 | this._count -= arguments.length ? n : 1; 48 | } 49 | 50 | /** 51 | * Resets the counter back to count Defaults to 0. 52 | * @param {number} count Resets the counter back to count Defaults to 0. 53 | */ 54 | reset(count) { 55 | this._count = count || 0; 56 | } 57 | 58 | /** 59 | * The type of the Metric Impl. {@link MetricTypes}. 60 | * @return {string} The type of the Metric Impl. 61 | */ 62 | getType() { 63 | return MetricTypes.COUNTER; 64 | } 65 | } 66 | 67 | module.exports = Counter; 68 | 69 | /** 70 | * Properties that can be supplied to the constructor of a {@link Counter} 71 | * 72 | * @interface CounterProperties 73 | * @typedef CounterProperties 74 | * @type {Object} 75 | * @property {number} count An initial count for the counter. Defaults to 0. 76 | * @example 77 | * // Creates a counter that starts at 5. 78 | * const counter = new Counter({ count: 5 }) 79 | */ 80 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/Gauge.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('./Metric'); 2 | 3 | /** 4 | * Values that can be read instantly 5 | * @implements {Metric} 6 | * @example 7 | * var Measured = require('measured') 8 | * var gauge = new Measured.Gauge(function() { 9 | * return process.memoryUsage().rss; 10 | * }); 11 | */ 12 | class Gauge { 13 | /** 14 | * @param {function} readFn A function that returns the numeric value for this gauge. 15 | */ 16 | constructor(readFn) { 17 | this._readFn = readFn; 18 | } 19 | 20 | /** 21 | * @return {number} Gauges directly return the value from the callback which should be a number. 22 | */ 23 | toJSON() { 24 | return this._readFn(); 25 | } 26 | 27 | /** 28 | * The type of the Metric Impl. {@link MetricTypes}. 29 | * @return {string} The type of the Metric Impl. 30 | */ 31 | getType() { 32 | return MetricTypes.GAUGE; 33 | } 34 | } 35 | 36 | module.exports = Gauge; 37 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/Histogram.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('./Metric'); 2 | const binarySearch = require('binary-search'); 3 | const EDS = require('../util/ExponentiallyDecayingSample'); 4 | 5 | /** 6 | * Keeps a reservoir of statistically relevant values biased towards the last 5 minutes to explore their distribution. 7 | * @implements {Metric} 8 | * @example 9 | * var Measured = require('measured') 10 | * var histogram = new Measured.Histogram(); 11 | * http.createServer(function(req, res) { 12 | * if (req.headers['content-length']) { 13 | * histogram.update(parseInt(req.headers['content-length'], 10)); 14 | * } 15 | * }); 16 | */ 17 | class Histogram { 18 | /** 19 | @param {HistogramProperties} [properties] see {@link HistogramProperties}. 20 | */ 21 | constructor(properties) { 22 | this._properties = properties || {}; 23 | this._initializeState(); 24 | } 25 | 26 | _initializeState() { 27 | this._sample = this._properties.sample || new EDS(); 28 | this._percentilesMethod = this._properties.percentilesMethod || this._percentiles; 29 | this._min = null; 30 | this._max = null; 31 | this._count = 0; 32 | this._sum = 0; 33 | 34 | // These are for the Welford algorithm for calculating running constiance 35 | // without floating-point doom. 36 | this._constianceM = 0; 37 | this._constianceS = 0; 38 | } 39 | 40 | /** 41 | * Pushes value into the sample. timestamp defaults to Date.now(). 42 | * @param {number} value 43 | */ 44 | update(value) { 45 | this._count++; 46 | this._sum += value; 47 | 48 | this._sample.update(value); 49 | this._updateMin(value); 50 | this._updateMax(value); 51 | this._updateVariance(value); 52 | } 53 | 54 | _percentiles(percentiles) { 55 | const values = this._sample.toArray().sort((a, b) => { 56 | return a === b ? 0 : a - b; 57 | }); 58 | 59 | const results = {}; 60 | 61 | let i, percentile, pos, lower, upper; 62 | for (i = 0; i < percentiles.length; i++) { 63 | percentile = percentiles[i]; 64 | if (values.length) { 65 | pos = percentile * (values.length + 1); 66 | if (pos < 1) { 67 | results[percentile] = values[0]; 68 | } else if (pos >= values.length) { 69 | results[percentile] = values[values.length - 1]; 70 | } else { 71 | lower = values[Math.floor(pos) - 1]; 72 | upper = values[Math.ceil(pos) - 1]; 73 | results[percentile] = lower + (pos - Math.floor(pos)) * (upper - lower); 74 | } 75 | } else { 76 | results[percentile] = null; 77 | } 78 | } 79 | 80 | return results; 81 | } 82 | 83 | weightedPercentiles(percentiles) { 84 | const values = this._sample.toArrayWithWeights().sort((a, b) => { 85 | return a.value === b.value ? 0 : a.value - b.value; 86 | }); 87 | 88 | const sumWeight = values.reduce((sum, sample) => { 89 | return sum + sample.priority; 90 | }, 0); 91 | 92 | const normWeights = values.map(value => { 93 | return value.priority / sumWeight; 94 | }); 95 | 96 | const quantiles = [0]; 97 | let i; 98 | for (i = 1; i < values.length; i++) { 99 | quantiles[i] = quantiles[i - 1] + normWeights[i - 1]; 100 | } 101 | 102 | function gt(a, b) { 103 | return a - b; 104 | } 105 | 106 | const results = {}; 107 | let percentile, pos; 108 | for (i = 0; i < percentiles.length; i++) { 109 | percentile = percentiles[i]; 110 | if (values.length) { 111 | pos = binarySearch(quantiles, percentile, gt); 112 | if (pos < 0) { 113 | results[percentile] = values[-pos - 1 - 1].value; 114 | } else if (pos < 1) { 115 | results[percentile] = values[0].value; 116 | } else if (pos >= values.length) { 117 | results[percentile] = values[values.length - 1].value; 118 | } 119 | } else { 120 | results[percentile] = null; 121 | } 122 | } 123 | return results; 124 | } 125 | 126 | /** 127 | * Resets all values. Histograms initialized with custom options will be reset to the default settings (patch welcome). 128 | */ 129 | reset() { 130 | // while this is technically a bug?, copying existing logic to maintain current api, 131 | // TODO reset should reset the sample, not override it with a new EDS() 132 | this._properties.sample = new EDS(); 133 | 134 | this._initializeState(); 135 | } 136 | 137 | /** 138 | * Checks whether the histogram contains values. 139 | * @return {boolean} Whether the histogram contains values. 140 | */ 141 | hasValues() { 142 | return this._count > 0; 143 | } 144 | 145 | /** 146 | * @return {HistogramData} 147 | */ 148 | toJSON() { 149 | const percentiles = this._percentilesMethod([0.5, 0.75, 0.95, 0.99, 0.999]); 150 | 151 | return { 152 | min: this._min, 153 | max: this._max, 154 | sum: this._sum, 155 | variance: this._calculateVariance(), 156 | mean: this._calculateMean(), 157 | stddev: this._calculateStddev(), 158 | count: this._count, 159 | median: percentiles[0.5], 160 | p75: percentiles[0.75], 161 | p95: percentiles[0.95], 162 | p99: percentiles[0.99], 163 | p999: percentiles[0.999] 164 | }; 165 | } 166 | 167 | _updateMin(value) { 168 | if (this._min === null || value < this._min) { 169 | this._min = value; 170 | } 171 | } 172 | 173 | _updateMax(value) { 174 | if (this._max === null || value > this._max) { 175 | this._max = value; 176 | } 177 | } 178 | 179 | _updateVariance(value) { 180 | if (this._count === 1) { 181 | this._constianceM = value; 182 | return value; 183 | } 184 | 185 | const oldM = this._constianceM; 186 | 187 | this._constianceM += (value - oldM) / this._count; 188 | this._constianceS += (value - oldM) * (value - this._constianceM); 189 | 190 | // TODO is this right, above it returns in the if statement but does nothing but update internal state for the else case? 191 | return undefined; 192 | } 193 | 194 | /** 195 | * 196 | * @return {number|null} 197 | * @private 198 | */ 199 | _calculateMean() { 200 | return this._count === 0 ? 0 : this._sum / this._count; 201 | } 202 | 203 | /** 204 | * @return {number|null} 205 | * @private 206 | */ 207 | _calculateVariance() { 208 | return this._count <= 1 ? null : this._constianceS / (this._count - 1); 209 | } 210 | 211 | /** 212 | * @return {number|null} 213 | * @private 214 | */ 215 | _calculateStddev() { 216 | return this._count < 1 ? null : Math.sqrt(this._calculateVariance()); 217 | } 218 | 219 | /** 220 | * The type of the Metric Impl. {@link MetricTypes}. 221 | * @return {string} The type of the Metric Impl. 222 | */ 223 | getType() { 224 | return MetricTypes.HISTOGRAM; 225 | } 226 | } 227 | 228 | module.exports = Histogram; 229 | 230 | /** 231 | * Properties to create a {@link Histogram} with. 232 | * 233 | * @interface HistogramProperties 234 | * @typedef HistogramProperties 235 | * @type {Object} 236 | * @property {object} sample The sample reservoir to use. Defaults to an ExponentiallyDecayingSample. 237 | */ 238 | 239 | /** 240 | * The data returned from Histogram::toJSON() 241 | * @interface HistogramData 242 | * @typedef HistogramData 243 | * @typedef {object} 244 | * @property {number|null} min The lowest observed value. 245 | * @property {number|null} max The highest observed value. 246 | * @property {number|null} sum The sum of all observed values. 247 | * @property {number|null} variance The variance of all observed values. 248 | * @property {number|null} mean The average of all observed values. 249 | * @property {number|null} stddev The stddev of all observed values. 250 | * @property {number} count The number of observed values. 251 | * @property {number} median 50% of all values in the resevoir are at or below this value. 252 | * @property {number} p75 See median, 75% percentile. 253 | * @property {number} p95 See median, 95% percentile. 254 | * @property {number} p99 See median, 99% percentile. 255 | * @property {number} p999 See median, 99.9% percentile. 256 | */ 257 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/Meter.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('./Metric'); 2 | const units = require('../util/units'); 3 | const EWMA = require('../util/ExponentiallyMovingWeightedAverage'); 4 | 5 | const RATE_UNIT = units.SECONDS; 6 | const TICK_INTERVAL = 5 * units.SECONDS; 7 | 8 | /** 9 | * Things that are measured as events / interval. 10 | * @implements {Metric} 11 | * @example 12 | * var Measured = require('measured') 13 | * var meter = new Measured.Meter(); 14 | * http.createServer(function(req, res) { 15 | * meter.mark(); 16 | * }); 17 | */ 18 | class Meter { 19 | /** 20 | * @param {MeterProperties} [properties] see {@link MeterProperties}. 21 | */ 22 | constructor(properties) { 23 | this._properties = properties || {}; 24 | this._initializeState(); 25 | 26 | if (!this._properties.keepAlive) { 27 | this.unref(); 28 | } 29 | } 30 | 31 | /** 32 | * Initializes the state of this Metric 33 | * @private 34 | */ 35 | _initializeState() { 36 | this._rateUnit = this._properties.rateUnit || RATE_UNIT; 37 | this._tickInterval = this._properties.tickInterval || TICK_INTERVAL; 38 | if (this._properties.getTime) { 39 | this._getTime = this._properties.getTime; 40 | } 41 | 42 | this._m1Rate = this._properties.m1Rate || new EWMA(units.MINUTES, this._tickInterval); 43 | this._m5Rate = this._properties.m5Rate || new EWMA(5 * units.MINUTES, this._tickInterval); 44 | this._m15Rate = this._properties.m15Rate || new EWMA(15 * units.MINUTES, this._tickInterval); 45 | this._count = 0; 46 | this._currentSum = 0; 47 | this._startTime = this._getTime(); 48 | this._lastToJSON = this._getTime(); 49 | this._interval = setInterval(this._tick.bind(this), TICK_INTERVAL); 50 | } 51 | 52 | /** 53 | * Register n events as having just occured. Defaults to 1. 54 | * @param {number} [n] 55 | */ 56 | mark(n) { 57 | if (!this._interval) { 58 | this.start(); 59 | } 60 | 61 | n = n || 1; 62 | 63 | this._count += n; 64 | this._currentSum += n; 65 | this._m1Rate.update(n); 66 | this._m5Rate.update(n); 67 | this._m15Rate.update(n); 68 | } 69 | 70 | start() {} 71 | 72 | end() { 73 | clearInterval(this._interval); 74 | this._interval = null; 75 | } 76 | 77 | /** 78 | * Refs the backing timer again. Idempotent. 79 | */ 80 | ref() { 81 | if (this._interval && this._interval.ref) { 82 | this._interval.ref(); 83 | } 84 | } 85 | 86 | /** 87 | * Unrefs the backing timer. The meter will not keep the event loop alive. Idempotent. 88 | */ 89 | unref() { 90 | if (this._interval && this._interval.unref) { 91 | this._interval.unref(); 92 | } 93 | } 94 | 95 | _tick() { 96 | this._m1Rate.tick(); 97 | this._m5Rate.tick(); 98 | this._m15Rate.tick(); 99 | } 100 | 101 | /** 102 | * Resets all values. Meters initialized with custom options will be reset to the default settings (patch welcome). 103 | */ 104 | reset() { 105 | this.end(); 106 | this._initializeState(); 107 | } 108 | 109 | meanRate() { 110 | if (this._count === 0) { 111 | return 0; 112 | } 113 | 114 | const elapsed = this._getTime() - this._startTime; 115 | return this._count / elapsed * this._rateUnit; 116 | } 117 | 118 | currentRate() { 119 | const currentSum = this._currentSum; 120 | const duration = this._getTime() - this._lastToJSON; 121 | const currentRate = currentSum / duration * this._rateUnit; 122 | 123 | this._currentSum = 0; 124 | this._lastToJSON = this._getTime(); 125 | 126 | // currentRate could be NaN if duration was 0, so fix that 127 | return currentRate || 0; 128 | } 129 | 130 | /** 131 | * @return {MeterData} 132 | */ 133 | toJSON() { 134 | return { 135 | mean: this.meanRate(), 136 | count: this._count, 137 | currentRate: this.currentRate(), 138 | '1MinuteRate': this._m1Rate.rate(this._rateUnit), 139 | '5MinuteRate': this._m5Rate.rate(this._rateUnit), 140 | '15MinuteRate': this._m15Rate.rate(this._rateUnit) 141 | }; 142 | } 143 | 144 | _getTime() { 145 | if (!process.hrtime) { 146 | return new Date().getTime(); 147 | } 148 | 149 | const hrtime = process.hrtime(); 150 | return hrtime[0] * 1000 + hrtime[1] / (1000 * 1000); 151 | } 152 | 153 | /** 154 | * The type of the Metric Impl. {@link MetricTypes}. 155 | * @return {string} The type of the Metric Impl. 156 | */ 157 | getType() { 158 | return MetricTypes.METER; 159 | } 160 | } 161 | 162 | module.exports = Meter; 163 | 164 | /** 165 | * 166 | * @interface MeterProperties 167 | * @typedef MeterProperties 168 | * @type {Object} 169 | * @property {number} rateUnit The rate unit. Defaults to 1000 (1 sec). 170 | * @property {number} tickInterval The interval in which the averages are updated. Defaults to 5000 (5 sec). 171 | * @property {boolean} keepAlive Optional flag to unref the associated timer. Defaults to `false`. 172 | * @example 173 | * const meter = new Meter({ rateUnit: 1000, tickInterval: 5000}) 174 | */ 175 | 176 | /** 177 | * The data returned from Meter::toJSON() 178 | * @interface MeterData 179 | * @typedef MeterData 180 | * @typedef {object} 181 | * @property {number} mean The average rate since the meter was started. 182 | * @property {number} count The total of all values added to the meter. 183 | * @property {number} currentRate The rate of the meter since the last toJSON() call. 184 | * @property {number} 1MinuteRate The rate of the meter biased towards the last 1 minute. 185 | * @property {number} 5MinuteRate The rate of the meter biased towards the last 5 minutes. 186 | * @property {number} 15MinuteRate The rate of the meter biased towards the last 15 minutes. 187 | */ 188 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/Metric.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface for Metric types. 3 | * 4 | * Implementations 5 | *

6 | *

  • Counter, things that increment or decrement.
  • 7 | *
  • Gauge, values that can be read instantly via a supplied call back.
  • 8 | *
  • Histogram, keeps a reservoir of statistically relevant values to explore their distribution.
  • 9 | *
  • Meter, things that are measured as events / interval.
  • 10 | *
  • NoOpMeter, an empty impl of meter, useful for supplying to a Timer, when you only care about the Histogram.
  • 11 | *
  • SettableGauge, just like a Gauge but its value is set directly rather than supplied by a callback.
  • 12 | *
  • CachedGauge, A Cached Gauge takes a function that returns a promise that resolves a value that should be cached and updated on a given interval.
  • 13 | *
  • Timer, timers are a combination of Meters and Histograms. They measure the rate as well as distribution of scalar events.
  • 14 | *

    15 | * 16 | * @interface Metric 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | class Metric { 20 | /** 21 | * Please note that dispite its name, this method can return raw numbers on 22 | * certain implementations such as counters and gauges. 23 | * 24 | * @return {any} Returns the data from the Metric 25 | */ 26 | toJSON() {} 27 | 28 | /** 29 | * The type of the Metric Impl. {@link MetricTypes}. 30 | * @return {string} The type of the Metric Impl. 31 | */ 32 | getType() {} 33 | } 34 | 35 | /** 36 | * An enum like object that is the set of core metric types that all implementors of {@link Metric} are. 37 | * 38 | * @typedef MetricTypes 39 | * @interface MetricTypes 40 | * @type {Object.} 41 | * @property {COUNTER} The type for Counters. 42 | * @property {GAUGE} The type for Gauges. 43 | * @property {HISTOGRAM} The type for Histograms. 44 | * @property {METER} The type for Meters. 45 | * @property {TIMER} The type for Timers. 46 | */ 47 | const MetricTypes = { 48 | COUNTER: 'Counter', 49 | GAUGE: 'Gauge', 50 | HISTOGRAM: 'Histogram', 51 | METER: 'Meter', 52 | TIMER: 'Timer' 53 | }; 54 | 55 | module.exports = { 56 | MetricTypes 57 | }; 58 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/NoOpMeter.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('./Metric'); 2 | 3 | /** 4 | * A No-Op Impl of Meter that can be used with a timer, to only create histogram data. 5 | * This is useful for some time series aggregators that can calculate rates for you just off of sent count. 6 | * 7 | * @implements {Metric} 8 | * @example 9 | * const { NoOpMeter, Timer } = require('measured') 10 | * const meter = new NoOpMeter(); 11 | * const timer = new Timer({meter: meter}); 12 | * ... 13 | * // do some stuff with the timer and stopwatch api 14 | * ... 15 | */ 16 | // eslint-disable-next-line padded-blocks 17 | class NoOpMeter { 18 | /** 19 | * No-Op impl 20 | * @param {number} n Number of events to mark. 21 | */ 22 | // eslint-disable-next-line no-unused-vars 23 | mark(n) {} 24 | 25 | /** 26 | * No-Op impl 27 | */ 28 | start() {} 29 | 30 | /** 31 | * No-Op impl 32 | */ 33 | end() {} 34 | 35 | /** 36 | * No-Op impl 37 | */ 38 | ref() {} 39 | 40 | /** 41 | * No-Op impl 42 | */ 43 | unref() {} 44 | 45 | /** 46 | * No-Op impl 47 | */ 48 | reset() {} 49 | 50 | /** 51 | * No-Op impl 52 | */ 53 | meanRate() {} 54 | 55 | /** 56 | * No-Op impl 57 | */ 58 | currentRate() {} 59 | 60 | /** 61 | * Returns an empty object 62 | * @return {{}} 63 | */ 64 | toJSON() { 65 | return {}; 66 | } 67 | 68 | /** 69 | * The type of the Metric Impl. {@link MetricTypes}. 70 | * @return {string} The type of the Metric Impl. 71 | */ 72 | getType() { 73 | return MetricTypes.METER; 74 | } 75 | } 76 | 77 | module.exports = NoOpMeter; 78 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/SettableGauge.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('./Metric'); 2 | 3 | /** 4 | * Works like a {@link Gauge}, but rather than getting its value from a callback, the value 5 | * is set when needed. This can be useful for setting a gauges value for asynchronous operations. 6 | * @implements {Metric} 7 | * @example 8 | * const settableGauge = new SettableGauge(); 9 | * // Update the settable gauge ever 10'ish seconds 10 | * setInterval(() => { 11 | * calculateSomethingAsync().then((value) => { 12 | * settableGauge.setValue(value); 13 | * }); 14 | * }, 10000); 15 | */ 16 | class SettableGauge { 17 | /** 18 | * @param {SettableGaugeProperties} [options] See {@link SettableGaugeProperties}. 19 | */ 20 | constructor(options) { 21 | options = options || {}; 22 | this._value = options.initialValue || 0; 23 | } 24 | 25 | setValue(value) { 26 | this._value = value; 27 | } 28 | 29 | /** 30 | * @return {number} Settable Gauges directly return there current value. 31 | */ 32 | toJSON() { 33 | return this._value; 34 | } 35 | 36 | /** 37 | * The type of the Metric Impl. {@link MetricTypes}. 38 | * @return {string} The type of the Metric Impl. 39 | */ 40 | getType() { 41 | return MetricTypes.GAUGE; 42 | } 43 | } 44 | 45 | module.exports = SettableGauge; 46 | 47 | /** 48 | * Properties that can be supplied to the constructor of a {@link Counter} 49 | * 50 | * @interface SettableGaugeProperties 51 | * @typedef SettableGaugeProperties 52 | * @type {Object} 53 | * @property {number} initialValue An initial value to use for this settable gauge. Defaults to 0. 54 | * @example 55 | * // Creates a Gauge that with an initial value of 500. 56 | * const settableGauge = new SettableGauge({ initialValue: 500 }) 57 | * 58 | */ 59 | -------------------------------------------------------------------------------- /packages/measured-core/lib/metrics/Timer.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('./Metric'); 2 | const Histogram = require('./Histogram'); 3 | const Meter = require('./Meter'); 4 | const Stopwatch = require('../util/Stopwatch'); 5 | 6 | /** 7 | * 8 | * Timers are a combination of Meters and Histograms. They measure the rate as well as distribution of scalar events. 9 | *

    10 | * Since they are frequently used for tracking how long certain things take, they expose an API for that: See example 1. 11 | *

    12 | * But you can also use them as generic histograms that also track the rate of events: See example 2. 13 | * 14 | * @example 15 | * var Measured = require('measured') 16 | * var timer = new Measured.Timer(); 17 | * http.createServer(function(req, res) { 18 | * var stopwatch = timer.start(); 19 | * req.on('end', function() { 20 | * stopwatch.end(); 21 | * }); 22 | * }); 23 | * 24 | * 25 | * @example 26 | * var Measured = require('measured') 27 | * var timer = new Measured.Timer(); 28 | * http.createServer(function(req, res) { 29 | * if (req.headers['content-length']) { 30 | * timer.update(parseInt(req.headers['content-length'], 10)); 31 | * } 32 | * }); 33 | * 34 | * @implements {Metric} 35 | */ 36 | class Timer { 37 | /** 38 | * @param {TimerProperties} [properties] See {@link TimerProperties}. 39 | */ 40 | constructor(properties) { 41 | properties = properties || {}; 42 | 43 | this._meter = properties.meter || new Meter({}); 44 | this._histogram = properties.histogram || new Histogram({}); 45 | this._getTime = properties.getTime; 46 | this._keepAlive = !!properties.keepAlive; 47 | 48 | if (!properties.keepAlive) { 49 | this.unref(); 50 | } 51 | } 52 | 53 | /** 54 | * @return {Stopwatch} Returns a Stopwatch that has been started. 55 | */ 56 | start() { 57 | const self = this; 58 | const watch = new Stopwatch({ getTime: this._getTime }); 59 | 60 | watch.once('end', elapsed => { 61 | self.update(elapsed); 62 | }); 63 | 64 | return watch; 65 | } 66 | 67 | /** 68 | * Updates the internal histogram with value and marks one event on the internal meter. 69 | * @param {number} value 70 | */ 71 | update(value) { 72 | this._meter.mark(); 73 | this._histogram.update(value); 74 | } 75 | 76 | /** 77 | * Resets all values. Timers initialized with custom options will be reset to the default settings. 78 | */ 79 | reset() { 80 | this._meter.reset(); 81 | this._histogram.reset(); 82 | } 83 | 84 | end() { 85 | this._meter.end(); 86 | } 87 | 88 | /** 89 | * Refs the backing timer again. Idempotent. 90 | */ 91 | ref() { 92 | this._meter.ref(); 93 | } 94 | 95 | /** 96 | * Unrefs the backing timer. The meter will not keep the event loop alive. Idempotent. 97 | */ 98 | unref() { 99 | this._meter.unref(); 100 | } 101 | 102 | /** 103 | * toJSON output: 104 | * 105 | *

  • meter: See Meter#toJSON output docs above.
  • 106 | *
  • histogram: See Histogram#toJSON output docs above.
  • 107 | * 108 | * @return {any} 109 | */ 110 | toJSON() { 111 | return { 112 | meter: this._meter.toJSON(), 113 | histogram: this._histogram.toJSON() 114 | }; 115 | } 116 | 117 | /** 118 | * The type of the Metric Impl. {@link MetricTypes}. 119 | * @return {string} The type of the Metric Impl. 120 | */ 121 | getType() { 122 | return MetricTypes.TIMER; 123 | } 124 | } 125 | 126 | module.exports = Timer; 127 | 128 | /** 129 | * @interface TimerProperties 130 | * @typedef TimerProperties 131 | * @type {Object} 132 | * @property {Meter} meter The internal meter to use. Defaults to a new {@link Meter}. 133 | * @property {Histogram} histogram The internal histogram to use. Defaults to a new {@link Histogram}. 134 | * @property {function} getTime optional function override for supplying time to the {@link Stopwatch} 135 | * @property {boolean} keepAlive Optional flag to unref the associated timer. Defaults to `false`. 136 | */ 137 | -------------------------------------------------------------------------------- /packages/measured-core/lib/util/BinaryHeap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Based on http://en.wikipedia.org/wiki/Binary_Heap 3 | * as well as http://eloquentjavascript.net/appendix2.html 4 | */ 5 | class BinaryHeap { 6 | constructor(options) { 7 | options = options || {}; 8 | 9 | this._elements = options.elements || []; 10 | this._score = options.score || this._score; 11 | } 12 | 13 | /** 14 | * Add elements to the binary heap. 15 | * @param {any[]} elements 16 | */ 17 | add(...elements) { 18 | elements.forEach(element => { 19 | this._elements.push(element); 20 | this._bubble(this._elements.length - 1); 21 | }); 22 | } 23 | 24 | first() { 25 | return this._elements[0]; 26 | } 27 | 28 | removeFirst() { 29 | const root = this._elements[0]; 30 | const last = this._elements.pop(); 31 | 32 | if (this._elements.length > 0) { 33 | this._elements[0] = last; 34 | this._sink(0); 35 | } 36 | 37 | return root; 38 | } 39 | 40 | clone() { 41 | return new BinaryHeap({ 42 | elements: this.toArray(), 43 | score: this._score 44 | }); 45 | } 46 | 47 | toSortedArray() { 48 | const array = []; 49 | const clone = this.clone(); 50 | let element; 51 | 52 | while (true) { 53 | element = clone.removeFirst(); 54 | if (element === undefined) { 55 | break; 56 | } 57 | 58 | array.push(element); 59 | } 60 | 61 | return array; 62 | } 63 | 64 | toArray() { 65 | return [].concat(this._elements); 66 | } 67 | 68 | size() { 69 | return this._elements.length; 70 | } 71 | 72 | _bubble(bubbleIndex) { 73 | const bubbleElement = this._elements[bubbleIndex]; 74 | const bubbleScore = this._score(bubbleElement); 75 | let parentIndex; 76 | let parentElement; 77 | let parentScore; 78 | 79 | while (bubbleIndex > 0) { 80 | parentIndex = this._parentIndex(bubbleIndex); 81 | parentElement = this._elements[parentIndex]; 82 | parentScore = this._score(parentElement); 83 | 84 | if (bubbleScore <= parentScore) { 85 | break; 86 | } 87 | 88 | this._elements[parentIndex] = bubbleElement; 89 | this._elements[bubbleIndex] = parentElement; 90 | bubbleIndex = parentIndex; 91 | } 92 | } 93 | 94 | _sink(sinkIndex) { 95 | const sinkElement = this._elements[sinkIndex]; 96 | const sinkScore = this._score(sinkElement); 97 | const { length } = this._elements; 98 | let swapIndex; 99 | let swapScore; 100 | let swapElement; 101 | let childIndexes; 102 | let i; 103 | let childIndex; 104 | let childElement; 105 | let childScore; 106 | 107 | while (true) { 108 | swapIndex = null; 109 | swapScore = null; 110 | swapElement = null; 111 | childIndexes = this._childIndexes(sinkIndex); 112 | 113 | for (i = 0; i < childIndexes.length; i++) { 114 | childIndex = childIndexes[i]; 115 | 116 | if (childIndex >= length) { 117 | break; 118 | } 119 | 120 | childElement = this._elements[childIndex]; 121 | childScore = this._score(childElement); 122 | 123 | if (childScore > sinkScore) { 124 | if (swapScore === null || swapScore < childScore) { 125 | swapIndex = childIndex; 126 | swapScore = childScore; 127 | swapElement = childElement; 128 | } 129 | } 130 | } 131 | 132 | if (swapIndex === null) { 133 | break; 134 | } 135 | 136 | this._elements[swapIndex] = sinkElement; 137 | this._elements[sinkIndex] = swapElement; 138 | sinkIndex = swapIndex; 139 | } 140 | } 141 | 142 | _parentIndex(index) { 143 | return Math.floor((index - 1) / 2); 144 | } 145 | 146 | _childIndexes(index) { 147 | return [2 * index + 1, 2 * index + 2]; 148 | } 149 | 150 | _score(element) { 151 | return element.valueOf(); 152 | } 153 | } 154 | 155 | module.exports = BinaryHeap; 156 | -------------------------------------------------------------------------------- /packages/measured-core/lib/util/ExponentiallyDecayingSample.js: -------------------------------------------------------------------------------- 1 | const BinaryHeap = require('./BinaryHeap'); 2 | const units = require('./units'); 3 | 4 | const RESCALE_INTERVAL = units.HOURS; 5 | const ALPHA = 0.015; 6 | const SIZE = 1028; 7 | 8 | /** 9 | * ExponentiallyDecayingSample 10 | */ 11 | class ExponentiallyDecayingSample { 12 | constructor(options) { 13 | options = options || {}; 14 | 15 | this._elements = new BinaryHeap({ 16 | score: element => -element.priority 17 | }); 18 | 19 | this._rescaleInterval = options.rescaleInterval || RESCALE_INTERVAL; 20 | this._alpha = options.alpha || ALPHA; 21 | this._size = options.size || SIZE; 22 | this._random = options.random || this._random; 23 | this._landmark = null; 24 | this._nextRescale = null; 25 | } 26 | 27 | update(value, timestamp) { 28 | const now = Date.now(); 29 | if (!this._landmark) { 30 | this._landmark = now; 31 | this._nextRescale = this._landmark + this._rescaleInterval; 32 | } 33 | 34 | timestamp = timestamp || now; 35 | 36 | const newSize = this._elements.size() + 1; 37 | 38 | const element = { 39 | priority: this._priority(timestamp - this._landmark), 40 | value: value 41 | }; 42 | 43 | if (newSize <= this._size) { 44 | this._elements.add(element); 45 | } else if (element.priority > this._elements.first().priority) { 46 | this._elements.removeFirst(); 47 | this._elements.add(element); 48 | } 49 | 50 | if (now >= this._nextRescale) { 51 | this._rescale(now); 52 | } 53 | } 54 | 55 | toSortedArray() { 56 | return this._elements.toSortedArray().map(element => element.value); 57 | } 58 | 59 | toArray() { 60 | return this._elements.toArray().map(element => element.value); 61 | } 62 | 63 | toArrayWithWeights() { 64 | return this._elements.toArray(); 65 | } 66 | 67 | _weight(age) { 68 | // We divide by 1000 to not run into huge numbers before reaching a 69 | // rescale event. 70 | return Math.exp(this._alpha * (age / 1000)); 71 | } 72 | 73 | _priority(age) { 74 | return this._weight(age) / this._random(); 75 | } 76 | 77 | _random() { 78 | return Math.random(); 79 | } 80 | 81 | _rescale(now) { 82 | now = now || Date.now(); 83 | 84 | const self = this; 85 | const oldLandmark = this._landmark; 86 | this._landmark = now || Date.now(); 87 | this._nextRescale = now + this._rescaleInterval; 88 | 89 | const factor = self._priority(-(self._landmark - oldLandmark)); 90 | 91 | this._elements.toArray().forEach(element => { 92 | element.priority *= factor; 93 | }); 94 | } 95 | } 96 | 97 | module.exports = ExponentiallyDecayingSample; 98 | -------------------------------------------------------------------------------- /packages/measured-core/lib/util/ExponentiallyMovingWeightedAverage.js: -------------------------------------------------------------------------------- 1 | const units = require('./units'); 2 | 3 | const TICK_INTERVAL = 5 * units.SECONDS; 4 | 5 | /** 6 | * ExponentiallyMovingWeightedAverage 7 | */ 8 | class ExponentiallyMovingWeightedAverage { 9 | constructor(timePeriod, tickInterval) { 10 | this._timePeriod = timePeriod || units.MINUTE; 11 | this._tickInterval = tickInterval || TICK_INTERVAL; 12 | this._alpha = 1 - Math.exp(-this._tickInterval / this._timePeriod); 13 | this._count = 0; 14 | this._rate = 0; 15 | } 16 | 17 | update(n) { 18 | this._count += n; 19 | } 20 | 21 | tick() { 22 | const instantRate = this._count / this._tickInterval; 23 | this._count = 0; 24 | 25 | this._rate += this._alpha * (instantRate - this._rate); 26 | } 27 | 28 | rate(timeUnit) { 29 | return (this._rate || 0) * timeUnit; 30 | } 31 | } 32 | 33 | module.exports = ExponentiallyMovingWeightedAverage; 34 | -------------------------------------------------------------------------------- /packages/measured-core/lib/util/Stopwatch.js: -------------------------------------------------------------------------------- 1 | const { EventEmitter } = require('events'); 2 | 3 | /** 4 | * A simple object for tracking elapsed time 5 | * 6 | * @extends {EventEmitter} 7 | */ 8 | class Stopwatch extends EventEmitter { 9 | /** 10 | * Creates a started Stopwatch 11 | * @param {StopwatchProperties} [options] See {@link StopwatchProperties} 12 | */ 13 | constructor(options) { 14 | super(); 15 | options = options || {}; 16 | EventEmitter.call(this); 17 | 18 | if (options.getTime) { 19 | this._getTime = options.getTime; 20 | } 21 | this._start = this._getTime(); 22 | this._ended = false; 23 | } 24 | 25 | /** 26 | * Called to mark the end of the timer task 27 | * @return {number} the total execution time 28 | */ 29 | end() { 30 | if (this._ended) { 31 | return null; 32 | } 33 | 34 | this._ended = true; 35 | const elapsed = this._getTime() - this._start; 36 | 37 | this.emit('end', elapsed); 38 | return elapsed; 39 | } 40 | 41 | _getTime() { 42 | if (!process.hrtime) { 43 | return Date.now(); 44 | } 45 | 46 | const hrtime = process.hrtime(); 47 | return hrtime[0] * 1000 + hrtime[1] / (1000 * 1000); 48 | } 49 | } 50 | 51 | module.exports = Stopwatch; 52 | 53 | /** 54 | * @interface StopwatchProperties 55 | * @typedef StopwatchProperties 56 | * @type {Object} 57 | * @property {function} getTime optional function override for supplying time., defaults to new Date() / process.hrt() 58 | */ 59 | -------------------------------------------------------------------------------- /packages/measured-core/lib/util/units.js: -------------------------------------------------------------------------------- 1 | const NANOSECONDS = 1 / (1000 * 1000); 2 | const MICROSECONDS = 1 / 1000; 3 | const MILLISECONDS = 1; 4 | const SECONDS = 1000 * MILLISECONDS; 5 | const MINUTES = 60 * SECONDS; 6 | const HOURS = 60 * MINUTES; 7 | const DAYS = 24 * HOURS; 8 | 9 | /** 10 | * Time units, as found in Java: {@link http://download.oracle.com/javase/6/docs/api/java/util/concurrent/TimeUnit.html} 11 | * @module timeUnits 12 | * @example 13 | * const timeUnit = require('measured-core').unit 14 | * setTimeout(() => {}, 5 * timeUnit.MINUTES) 15 | */ 16 | module.exports = { 17 | /** 18 | * nanoseconds in milliseconds 19 | * @type {number} 20 | */ 21 | NANOSECONDS, 22 | /** 23 | * microseconds in milliseconds 24 | * @type {number} 25 | */ 26 | MICROSECONDS, 27 | /** 28 | * milliseconds in milliseconds 29 | * @type {number} 30 | */ 31 | MILLISECONDS, 32 | /** 33 | * seconds in milliseconds 34 | * @type {number} 35 | */ 36 | SECONDS, 37 | /** 38 | * minutes in milliseconds 39 | * @type {number} 40 | */ 41 | MINUTES, 42 | /** 43 | * hours in milliseconds 44 | * @type {number} 45 | */ 46 | HOURS, 47 | /** 48 | * days in milliseconds 49 | * @type {number} 50 | */ 51 | DAYS 52 | }; 53 | -------------------------------------------------------------------------------- /packages/measured-core/lib/validators/metricValidators.js: -------------------------------------------------------------------------------- 1 | const { MetricTypes } = require('../metrics/Metric'); 2 | 3 | // TODO: Object.values(...) does not exist in Node.js 6.x, switch after LTS period ends. 4 | // const metricTypeValues = Object.values(MetricTypes); 5 | const metricTypeValues = Object.keys(MetricTypes).map(key => MetricTypes[key]); 6 | 7 | /** 8 | * This module contains various validators to validate publicly exposed input. 9 | * 10 | * @module metricValidators 11 | */ 12 | module.exports = { 13 | /** 14 | * Validates that a metric implements the metric interface. 15 | * 16 | * @param {Metric} metric The object that is supposed to be a metric. 17 | */ 18 | validateMetric: metric => { 19 | if (!metric) { 20 | throw new TypeError('The metric was undefined, when it was required'); 21 | } 22 | if (typeof metric.toJSON !== 'function') { 23 | throw new TypeError('Metrics must implement toJSON(), see the Metric interface in the docs.'); 24 | } 25 | if (typeof metric.getType !== 'function') { 26 | throw new TypeError('Metrics must implement getType(), see the Metric interface in the docs.'); 27 | } 28 | const type = metric.getType(); 29 | 30 | if (!metricTypeValues.includes(type)) { 31 | throw new TypeError( 32 | `Metric#getType(), must return a type defined in MetricsTypes. Found: ${type}, Valid values: ${metricTypeValues.join( 33 | ', ' 34 | )}` 35 | ); 36 | } 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /packages/measured-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "measured-core", 3 | "description": "A Node library for measuring and reporting application-level metrics.", 4 | "version": "2.0.0", 5 | "homepage": "https://yaorg.github.io/node-measured/", 6 | "engines": { 7 | "node": ">= 5.12" 8 | }, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "./lib/index.js", 13 | "scripts": { 14 | "clean": "rm -fr build", 15 | "format": "prettier --write './lib/**/*.{ts,js}'", 16 | "lint": "eslint lib --ext .js", 17 | "test:node": "mocha './test/**/test-*.js'", 18 | "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'", 19 | "test:browser": "mochify './test/**/test-*.js'", 20 | "test": "yarn test:node:coverage && yarn test:browser", 21 | "coverage": "nyc report --reporter=text-lcov | coveralls" 22 | }, 23 | "dependencies": { 24 | "binary-search": "^1.3.3", 25 | "optional-js": "^2.0.0" 26 | }, 27 | "repository": { 28 | "url": "git://github.com/yaorg/node-measured.git" 29 | }, 30 | "files": [ 31 | "lib", 32 | "README.md" 33 | ], 34 | "license": "MIT", 35 | "devDependencies": { 36 | "jsdoc": "^3.5.5" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/measured-core/test/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | var common = exports; 5 | var path = require('path'); 6 | 7 | common.dir = {}; 8 | common.dir.root = path.dirname(__dirname); 9 | common.dir.lib = path.join(common.dir.root, 'lib'); 10 | 11 | common.measured = require(common.dir.root); 12 | */ 13 | exports.measured = require('../lib/index'); 14 | -------------------------------------------------------------------------------- /packages/measured-core/test/integration/test-Collection_end.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var common = require('../common'); 4 | 5 | var collection = new common.measured.Collection(); 6 | 7 | collection.timer('a').start(); 8 | collection.meter('b').start(); 9 | collection.counter('c'); 10 | 11 | collection.end(); 12 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/metrics/test-CachedGauge.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const TimeUnits = require('../../../lib/util/units'); 3 | const CachedGauge = require('../../../lib/metrics/CachedGauge'); 4 | const assert = require('assert'); 5 | 6 | describe('CachedGauge', () => { 7 | let cachedGauge; 8 | it('A cachedGauge immediately calls the callback to set its initial value', () => { 9 | cachedGauge = new CachedGauge( 10 | () => { 11 | return new Promise(resolve => { 12 | resolve(10); 13 | }); 14 | }, 15 | 1, 16 | TimeUnits.MINUTES 17 | ); // Shouldn't update in the unit test. 18 | 19 | return wait(5 * TimeUnits.MILLISECONDS).then(() => { 20 | assert.equal(cachedGauge.toJSON(), 10); 21 | }); 22 | }); 23 | 24 | it('A cachedGauge calls the callback at the interval provided', () => { 25 | const values = [1, 2]; 26 | cachedGauge = new CachedGauge( 27 | () => { 28 | return new Promise(resolve => { 29 | resolve(values.shift()); 30 | }); 31 | }, 32 | 5, 33 | TimeUnits.MILLISECONDS 34 | ); 35 | 36 | return wait(7 * TimeUnits.MILLISECONDS).then(() => { 37 | assert.equal(cachedGauge.toJSON(), 2); 38 | assert.equal(values.length, 0, 'the callback should have been called 2x, emptying the values array'); 39 | }); 40 | }); 41 | 42 | afterEach(() => { 43 | if (cachedGauge) { 44 | cachedGauge.end(); 45 | } 46 | }); 47 | }); 48 | 49 | const wait = waitInterval => { 50 | return new Promise(resolve => { 51 | setTimeout(() => { 52 | resolve(); 53 | }, waitInterval); 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/metrics/test-Counter.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | var Counter = common.measured.Counter; 7 | 8 | describe('Counter', function() { 9 | var counter; 10 | beforeEach(function() { 11 | counter = new Counter(); 12 | }); 13 | 14 | it('has initial value of 0', function() { 15 | var json = counter.toJSON(); 16 | assert.deepEqual(json, 0); 17 | }); 18 | 19 | it('can be initialized with a given count', function() { 20 | counter = new Counter({ count: 5 }); 21 | assert.equal(counter.toJSON(), 5); 22 | }); 23 | 24 | it('#inc works incrementally', function() { 25 | counter.inc(5); 26 | assert.equal(counter.toJSON(), 5); 27 | 28 | counter.inc(3); 29 | assert.equal(counter.toJSON(), 8); 30 | }); 31 | 32 | it('#inc defaults to 1', function() { 33 | counter.inc(); 34 | assert.equal(counter.toJSON(), 1); 35 | 36 | counter.inc(); 37 | assert.equal(counter.toJSON(), 2); 38 | }); 39 | 40 | it('#inc adds zero', function() { 41 | counter.inc(0); 42 | assert.equal(counter.toJSON(), 0); 43 | }); 44 | 45 | it('#dec works incrementally', function() { 46 | counter.dec(3); 47 | assert.equal(counter.toJSON(), -3); 48 | 49 | counter.dec(2); 50 | assert.equal(counter.toJSON(), -5); 51 | }); 52 | 53 | it('#dec defaults to 1', function() { 54 | counter.dec(); 55 | assert.equal(counter.toJSON(), -1); 56 | 57 | counter.dec(); 58 | assert.equal(counter.toJSON(), -2); 59 | }); 60 | 61 | it('#dec substracts zero', function() { 62 | counter.dec(0); 63 | assert.equal(counter.toJSON(), 0); 64 | }); 65 | 66 | it('#reset works', function() { 67 | counter.inc(23); 68 | assert.equal(counter.toJSON(), 23); 69 | 70 | counter.reset(); 71 | assert.equal(counter.toJSON(), 0); 72 | 73 | counter.reset(50); 74 | assert.equal(counter.toJSON(), 50); 75 | }); 76 | 77 | it('returns the expected type', () => { 78 | assert.equal(counter.getType(), 'Counter'); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/metrics/test-Gauge.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | 7 | describe('Gauge', function() { 8 | it('reads value from function', function() { 9 | var i = 0; 10 | 11 | var gauge = new common.measured.Gauge(function() { 12 | return i++; 13 | }); 14 | 15 | assert.equal(gauge.toJSON(), 0); 16 | assert.equal(gauge.toJSON(), 1); 17 | }); 18 | 19 | it('returns the expected type', () => { 20 | const gauge = new common.measured.SettableGauge(); 21 | assert.equal(gauge.getType(), 'Gauge'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/metrics/test-Histogram.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | var sinon = require('sinon'); 7 | var Histogram = common.measured.Histogram; 8 | var EDS = common.measured.ExponentiallyDecayingSample; 9 | 10 | describe('Histogram', function() { 11 | var histogram; 12 | beforeEach(function() { 13 | histogram = new Histogram(); 14 | }); 15 | 16 | it('all values are null in the beginning', function() { 17 | var json = histogram.toJSON(); 18 | assert.strictEqual(json.min, null); 19 | assert.strictEqual(json.max, null); 20 | assert.strictEqual(json.sum, 0); 21 | assert.strictEqual(json.variance, null); 22 | assert.strictEqual(json.mean, 0); 23 | assert.strictEqual(json.stddev, null); 24 | assert.strictEqual(json.count, 0); 25 | assert.strictEqual(json.median, null); 26 | assert.strictEqual(json.p75, null); 27 | assert.strictEqual(json.p95, null); 28 | assert.strictEqual(json.p99, null); 29 | assert.strictEqual(json.p999, null); 30 | }); 31 | 32 | it('returns the expected type', () => { 33 | assert.equal(histogram.getType(), 'Histogram'); 34 | }); 35 | }); 36 | 37 | describe('Histogram#update', function() { 38 | var sample; 39 | var histogram; 40 | beforeEach(function() { 41 | sample = sinon.stub(new EDS()); 42 | histogram = new Histogram({ sample: sample }); 43 | 44 | sample.toArray.returns([]); 45 | }); 46 | 47 | it('updates underlaying sample', function() { 48 | histogram.update(5); 49 | assert.ok(sample.update.calledWith(5)); 50 | }); 51 | 52 | it('keeps track of min', function() { 53 | histogram.update(5); 54 | histogram.update(3); 55 | histogram.update(6); 56 | 57 | assert.equal(histogram.toJSON().min, 3); 58 | }); 59 | 60 | it('keeps track of max', function() { 61 | histogram.update(5); 62 | histogram.update(9); 63 | histogram.update(3); 64 | 65 | assert.equal(histogram.toJSON().max, 9); 66 | }); 67 | 68 | it('keeps track of sum', function() { 69 | histogram.update(5); 70 | histogram.update(1); 71 | histogram.update(12); 72 | 73 | assert.equal(histogram.toJSON().sum, 18); 74 | }); 75 | 76 | it('keeps track of count', function() { 77 | histogram.update(5); 78 | histogram.update(1); 79 | histogram.update(12); 80 | 81 | assert.equal(histogram.toJSON().count, 3); 82 | }); 83 | 84 | it('keeps track of mean', function() { 85 | histogram.update(5); 86 | histogram.update(1); 87 | histogram.update(12); 88 | 89 | assert.equal(histogram.toJSON().mean, 6); 90 | }); 91 | 92 | it('keeps track of variance (example without variance)', function() { 93 | histogram.update(5); 94 | histogram.update(5); 95 | histogram.update(5); 96 | 97 | assert.equal(histogram.toJSON().variance, 0); 98 | }); 99 | 100 | it('keeps track of variance (example with variance)', function() { 101 | histogram.update(1); 102 | histogram.update(2); 103 | histogram.update(3); 104 | histogram.update(4); 105 | 106 | assert.equal(histogram.toJSON().variance.toFixed(3), '1.667'); 107 | }); 108 | 109 | it('keeps track of stddev', function() { 110 | histogram.update(1); 111 | histogram.update(2); 112 | histogram.update(3); 113 | histogram.update(4); 114 | 115 | assert.equal(histogram.toJSON().stddev.toFixed(3), '1.291'); 116 | }); 117 | 118 | it('keeps track of percentiles', function() { 119 | var values = []; 120 | var i; 121 | for (i = 1; i <= 100; i++) { 122 | values.push(i); 123 | } 124 | sample.toArray.returns(values); 125 | 126 | var json = histogram.toJSON(); 127 | assert.equal(json.median.toFixed(3), '50.500'); 128 | assert.equal(json.p75.toFixed(3), '75.750'); 129 | assert.equal(json.p95.toFixed(3), '95.950'); 130 | assert.equal(json.p99.toFixed(3), '99.990'); 131 | assert.equal(json.p999.toFixed(3), '100.000'); 132 | }); 133 | }); 134 | 135 | describe('Histogram#percentiles', function() { 136 | var sample; 137 | var histogram; 138 | beforeEach(function() { 139 | sample = sinon.stub(new EDS()); 140 | histogram = new Histogram({ sample: sample }); 141 | 142 | var values = []; 143 | var i; 144 | for (i = 1; i <= 100; i++) { 145 | values.push(i); 146 | } 147 | 148 | var swapWith; 149 | var value; 150 | for (i = 0; i < 100; i++) { 151 | swapWith = Math.floor(Math.random() * 100); 152 | value = values[i]; 153 | 154 | values[i] = values[swapWith]; 155 | values[swapWith] = value; 156 | } 157 | 158 | sample.toArray.returns(values); 159 | }); 160 | 161 | it('calculates single percentile correctly', function() { 162 | var percentiles = histogram._percentiles([0.5]); 163 | assert.equal(percentiles[0.5], 50.5); 164 | 165 | percentiles = histogram._percentiles([0.99]); 166 | assert.equal(percentiles[0.99], 99.99); 167 | }); 168 | }); 169 | 170 | describe('Histogram#weightedPercentiles', function() { 171 | var sample; 172 | var histogram; 173 | beforeEach(function() { 174 | sample = sinon.stub(new EDS()); 175 | histogram = new Histogram({ 176 | sample: sample, 177 | percentilesMethod: Histogram.weightedPercentiles 178 | }); 179 | 180 | var values = []; 181 | var i; 182 | for (i = 1; i <= 100; i++) { 183 | values.push({ value: i, priority: 1 }); 184 | } 185 | 186 | var swapWith; 187 | var value; 188 | for (i = 0; i < 100; i++) { 189 | swapWith = Math.floor(Math.random() * 100); 190 | value = values[i]; 191 | 192 | values[i] = values[swapWith]; 193 | values[swapWith] = value; 194 | } 195 | 196 | sample.toArrayWithWeights.returns(values); 197 | sample.toArray.returns( 198 | values.map(function(item) { 199 | return item.value; 200 | }) 201 | ); 202 | }); 203 | 204 | it('calculates single percentile correctly', function() { 205 | var percentiles = histogram._percentiles([0.5]); 206 | assert.equal(percentiles[0.5], 50.5); 207 | 208 | percentiles = histogram._percentiles([0.99]); 209 | assert.equal(percentiles[0.99], 99.99); 210 | }); 211 | }); 212 | 213 | describe('Histogram#reset', function() { 214 | var sample; 215 | var histogram; 216 | beforeEach(function() { 217 | sample = new EDS(); 218 | histogram = new Histogram({ sample: sample }); 219 | }); 220 | 221 | it('resets all values', function() { 222 | histogram.update(5); 223 | histogram.update(2); 224 | var json = histogram.toJSON(); 225 | 226 | var key; 227 | for (key in json) { 228 | if (json.hasOwnProperty(key)) { 229 | assert.ok(typeof json[key] === 'number'); 230 | } 231 | } 232 | 233 | histogram.reset(); 234 | json = histogram.toJSON(); 235 | 236 | for (key in json) { 237 | if (json.hasOwnProperty(key)) { 238 | assert.ok(json[key] === 0 || json[key] === null); 239 | } 240 | } 241 | }); 242 | }); 243 | 244 | describe('Histogram#hasValues', function() { 245 | var histogram; 246 | beforeEach(function() { 247 | histogram = new Histogram(); 248 | }); 249 | 250 | it('has values', function() { 251 | histogram.update(5); 252 | assert.ok(histogram.hasValues()); 253 | }); 254 | 255 | it('has no values', function() { 256 | assert.equal(histogram.hasValues(), false); 257 | }); 258 | }); 259 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/metrics/test-Meter.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | var sinon = require('sinon'); 7 | var units = common.measured.units; 8 | 9 | describe('Meter', function() { 10 | var meter; 11 | var clock; 12 | beforeEach(function() { 13 | clock = sinon.useFakeTimers(); 14 | meter = new common.measured.Meter({ 15 | getTime: function() { 16 | return new Date().getTime(); 17 | } 18 | }); 19 | }); 20 | 21 | afterEach(function() { 22 | clock.restore(); 23 | }); 24 | 25 | it('all values are correctly initialized', function() { 26 | assert.deepEqual(meter.toJSON(), { 27 | mean: 0, 28 | count: 0, 29 | currentRate: 0, 30 | '1MinuteRate': 0, 31 | '5MinuteRate': 0, 32 | '15MinuteRate': 0 33 | }); 34 | }); 35 | 36 | it('supports rates override from opts', function() { 37 | var rate = sinon.stub().returns(666); 38 | var properties = { 39 | m1Rate: { rate: rate }, 40 | m5Rate: { rate: rate }, 41 | m15Rate: { rate: rate } 42 | }; 43 | var json = new common.measured.Meter(properties).toJSON(); 44 | 45 | assert.equal(json['1MinuteRate'].toFixed(0), '666'); 46 | assert.equal(json['5MinuteRate'].toFixed(0), '666'); 47 | assert.equal(json['15MinuteRate'].toFixed(0), '666'); 48 | }); 49 | 50 | it('decay over two marks and ticks', function() { 51 | meter.mark(5); 52 | meter._tick(); 53 | 54 | var json = meter.toJSON(); 55 | assert.equal(json.count, 5); 56 | assert.equal(json['1MinuteRate'].toFixed(4), '0.0800'); 57 | assert.equal(json['5MinuteRate'].toFixed(4), '0.0165'); 58 | assert.equal(json['15MinuteRate'].toFixed(4), '0.0055'); 59 | 60 | meter.mark(10); 61 | meter._tick(); 62 | 63 | json = meter.toJSON(); 64 | assert.equal(json.count, 15); 65 | assert.equal(json['1MinuteRate'].toFixed(3), '0.233'); 66 | assert.equal(json['5MinuteRate'].toFixed(3), '0.049'); 67 | assert.equal(json['15MinuteRate'].toFixed(3), '0.017'); 68 | }); 69 | 70 | it('mean rate', function() { 71 | meter.mark(5); 72 | clock.tick(5000); 73 | 74 | var json = meter.toJSON(); 75 | assert.equal(json.mean, 1); 76 | 77 | clock.tick(5000); 78 | 79 | json = meter.toJSON(); 80 | assert.equal(json.mean, 0.5); 81 | }); 82 | 83 | it('currentRate is the observed rate since the last toJSON call', function() { 84 | meter.mark(1); 85 | meter.mark(2); 86 | meter.mark(3); 87 | 88 | clock.tick(3000); 89 | 90 | assert.equal(meter.toJSON().currentRate, 2); 91 | }); 92 | 93 | it('currentRate resets by reading it', function() { 94 | meter.mark(1); 95 | meter.mark(2); 96 | meter.mark(3); 97 | 98 | meter.toJSON(); 99 | assert.strictEqual(meter.toJSON().currentRate, 0); 100 | }); 101 | 102 | it('currentRate also resets internal duration timer by reading it', function() { 103 | meter.mark(1); 104 | meter.mark(2); 105 | meter.mark(3); 106 | clock.tick(1000); 107 | meter.toJSON(); 108 | 109 | clock.tick(1000); 110 | meter.toJSON(); 111 | 112 | meter.mark(1); 113 | clock.tick(1000); 114 | assert.strictEqual(meter.toJSON().currentRate, 1); 115 | }); 116 | 117 | it('#reset resets all values', function() { 118 | meter.mark(1); 119 | var json = meter.toJSON(); 120 | 121 | var key, value; 122 | for (key in json) { 123 | if (json.hasOwnProperty(key)) { 124 | value = json[key]; 125 | assert.ok(typeof value === 'number'); 126 | } 127 | } 128 | 129 | meter.reset(); 130 | json = meter.toJSON(); 131 | 132 | for (key in json) { 133 | if (json.hasOwnProperty(key)) { 134 | value = json[key]; 135 | assert.ok(value === 0 || value === null); 136 | } 137 | } 138 | }); 139 | 140 | it('returns the expected type', () => { 141 | assert.equal(meter.getType(), 'Meter'); 142 | }); 143 | }); 144 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/metrics/test-NoOpMeter.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 3 | var common = require('../../common'); 4 | var assert = require('assert'); 5 | 6 | describe('NoOpMeter', () => { 7 | let meter; 8 | 9 | beforeEach(() => { 10 | meter = new common.measured.NoOpMeter(); 11 | }); 12 | 13 | it('always returns empty object', () => { 14 | assert.deepEqual(meter.toJSON(), {}); 15 | }); 16 | 17 | it('returns the expected type', () => { 18 | assert.equal(meter.getType(), 'Meter'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/metrics/test-SettableGauge.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | 7 | describe('SettableGauge', function() { 8 | it('can be set with an initial value', () => { 9 | const gauge = new common.measured.SettableGauge({ initialValue: 5 }); 10 | assert.equal(gauge.toJSON(), 5); 11 | gauge.setValue(11); 12 | assert.equal(gauge.toJSON(), 11); 13 | }); 14 | 15 | it('reads value from internal state', () => { 16 | const gauge = new common.measured.SettableGauge(); 17 | assert.equal(gauge.toJSON(), 0); 18 | gauge.setValue(5); 19 | assert.equal(gauge.toJSON(), 5); 20 | }); 21 | 22 | it('returns the expected type', () => { 23 | const gauge = new common.measured.SettableGauge(); 24 | assert.equal(gauge.getType(), 'Gauge'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/metrics/test-Timer.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | var sinon = require('sinon'); 7 | var Timer = common.measured.Timer; 8 | var Histogram = common.measured.Histogram; 9 | var Meter = common.measured.Meter; 10 | 11 | describe('Timer', function() { 12 | var timer; 13 | var meter; 14 | var histogram; 15 | var clock; 16 | beforeEach(function() { 17 | clock = sinon.useFakeTimers(); 18 | meter = sinon.stub(new Meter()); 19 | histogram = sinon.stub(new Histogram()); 20 | 21 | timer = new Timer({ 22 | meter: meter, 23 | histogram: histogram, 24 | getTime: function() { 25 | return new Date().getTime(); 26 | } 27 | }); 28 | }); 29 | 30 | afterEach(function() { 31 | clock.restore(); 32 | }); 33 | 34 | it('can be initialized without options', function() { 35 | timer = new Timer(); 36 | }); 37 | 38 | it('#update() marks the meter', function() { 39 | timer.update(5); 40 | 41 | assert.ok(meter.mark.calledOnce); 42 | }); 43 | 44 | it('#update() updates the histogram', function() { 45 | timer.update(5); 46 | 47 | assert.ok(histogram.update.calledWith(5)); 48 | }); 49 | 50 | it('#toJSON() contains meter info', function() { 51 | meter.toJSON.returns({ a: 1, b: 2 }); 52 | var json = timer.toJSON(); 53 | 54 | assert.deepEqual(json.meter, { a: 1, b: 2 }); 55 | }); 56 | 57 | it('#toJSON() contains histogram info', function() { 58 | histogram.toJSON.returns({ c: 3, d: 4 }); 59 | var json = timer.toJSON(); 60 | 61 | assert.deepEqual(json.histogram, { c: 3, d: 4 }); 62 | }); 63 | 64 | it('#start returns a Stopwatch which updates the timer', function() { 65 | clock.tick(10); 66 | 67 | var watch = timer.start(); 68 | clock.tick(50); 69 | watch.end(); 70 | 71 | assert.ok(meter.mark.calledOnce); 72 | assert.equal(histogram.update.args[0][0], 50); 73 | }); 74 | 75 | it('#reset is delegated to histogram and meter', function() { 76 | timer.reset(); 77 | 78 | assert.ok(meter.reset.calledOnce); 79 | assert.ok(histogram.reset.calledOnce); 80 | }); 81 | 82 | it('returns the expected type', () => { 83 | assert.equal(timer.getType(), 'Timer'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/test-Collection.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../common'); 5 | var assert = require('assert'); 6 | 7 | describe('Collection', function() { 8 | var collection; 9 | beforeEach(function() { 10 | collection = common.measured.createCollection(); 11 | }); 12 | 13 | it('with two counters', function() { 14 | collection = new common.measured.Collection('counters'); 15 | var a = collection.counter('a'); 16 | var b = collection.counter('b'); 17 | 18 | a.inc(3); 19 | b.inc(5); 20 | 21 | assert.deepEqual(collection.toJSON(), { 22 | counters: { 23 | a: 3, 24 | b: 5 25 | } 26 | }); 27 | }); 28 | 29 | it('returns same metric object when given the same name', function() { 30 | var a1 = collection.counter('a'); 31 | var a2 = collection.counter('a'); 32 | 33 | assert.strictEqual(a1, a2); 34 | }); 35 | 36 | it('throws exception when creating a metric without name', function() { 37 | assert.throws(function() { 38 | collection.counter(); 39 | }, /You must supply a metric name/); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/util/test-BinaryHeap.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | var BinaryHeap = common.measured.BinaryHeap; 7 | 8 | describe('BinaryHeap#toArray', function() { 9 | it('is empty in the beginning', function() { 10 | var heap = new BinaryHeap(); 11 | assert.deepEqual(heap.toArray(), []); 12 | }); 13 | 14 | it('does not leak internal references', function() { 15 | var heap = new BinaryHeap(); 16 | var array = heap.toArray(); 17 | array.push(1); 18 | 19 | assert.deepEqual(heap.toArray(), []); 20 | }); 21 | }); 22 | 23 | describe('BinaryHeap#toSortedArray', function() { 24 | it('is empty in the beginning', function() { 25 | var heap = new BinaryHeap(); 26 | assert.deepEqual(heap.toSortedArray(), []); 27 | }); 28 | 29 | it('does not leak internal references', function() { 30 | var heap = new BinaryHeap(); 31 | var array = heap.toSortedArray(); 32 | array.push(1); 33 | 34 | assert.deepEqual(heap.toSortedArray(), []); 35 | }); 36 | 37 | it('returns a sorted array', function() { 38 | var heap = new BinaryHeap(); 39 | heap.add(1, 2, 3, 4, 5, 6, 7, 8); 40 | 41 | assert.deepEqual(heap.toSortedArray(), [8, 7, 6, 5, 4, 3, 2, 1]); 42 | }); 43 | }); 44 | 45 | describe('BinaryHeap#add', function() { 46 | var heap; 47 | beforeEach(function() { 48 | heap = new BinaryHeap(); 49 | }); 50 | 51 | it('lets you add one element', function() { 52 | heap.add(1); 53 | 54 | assert.deepEqual(heap.toArray(), [1]); 55 | }); 56 | 57 | it('lets you add two elements', function() { 58 | heap.add(1); 59 | heap.add(2); 60 | 61 | assert.deepEqual(heap.toArray(), [2, 1]); 62 | }); 63 | 64 | it('lets you add two elements at once', function() { 65 | heap.add(1, 2); 66 | 67 | assert.deepEqual(heap.toArray(), [2, 1]); 68 | }); 69 | 70 | it('places elements according to their valueOf()', function() { 71 | heap.add(2); 72 | heap.add(1); 73 | heap.add(3); 74 | 75 | assert.deepEqual(heap.toArray(), [3, 1, 2]); 76 | }); 77 | }); 78 | 79 | describe('BinaryHeap#removeFirst', function() { 80 | var heap; 81 | beforeEach(function() { 82 | heap = new BinaryHeap(); 83 | heap.add(1, 2, 3, 4, 5, 6, 7, 8); 84 | }); 85 | 86 | it('removeFirst returns the last element', function() { 87 | var element = heap.removeFirst(); 88 | assert.equal(element, 8); 89 | }); 90 | 91 | it('removeFirst removes the last element', function() { 92 | heap.removeFirst(); 93 | assert.equal(heap.toArray().length, 7); 94 | }); 95 | 96 | it('removeFirst works multiple times', function() { 97 | assert.equal(heap.removeFirst(), 8); 98 | assert.equal(heap.removeFirst(), 7); 99 | assert.equal(heap.removeFirst(), 6); 100 | assert.equal(heap.removeFirst(), 5); 101 | assert.equal(heap.removeFirst(), 4); 102 | assert.equal(heap.removeFirst(), 3); 103 | assert.equal(heap.removeFirst(), 2); 104 | assert.equal(heap.removeFirst(), 1); 105 | assert.equal(heap.removeFirst(), undefined); 106 | }); 107 | }); 108 | 109 | describe('BinaryHeap#first', function() { 110 | var heap; 111 | beforeEach(function() { 112 | heap = new BinaryHeap(); 113 | heap.add(1, 2, 3); 114 | }); 115 | 116 | it('returns the first element but does not remove it', function() { 117 | var element = heap.first(); 118 | assert.equal(element, 3); 119 | 120 | assert.equal(heap.toArray().length, 3); 121 | }); 122 | }); 123 | 124 | describe('BinaryHeap#size', function() { 125 | it('takes custom score function', function() { 126 | var heap = new BinaryHeap({ elements: [1, 2, 3] }); 127 | assert.equal(heap.size(), 3); 128 | }); 129 | }); 130 | 131 | describe('BinaryHeap', function() { 132 | it('takes custom score function', function() { 133 | var heap = new BinaryHeap({ 134 | score: function(obj) { 135 | return -obj; 136 | } 137 | }); 138 | 139 | heap.add(8, 7, 6, 5, 4, 3, 2, 1); 140 | assert.deepEqual(heap.toSortedArray(), [1, 2, 3, 4, 5, 6, 7, 8]); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/util/test-ExponentiallyDecayingSample.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | var EDS = common.measured.ExponentiallyDecayingSample; 7 | var units = common.measured.units; 8 | 9 | describe('ExponentiallyDecayingSample#toSortedArray', function() { 10 | var sample; 11 | beforeEach(function() { 12 | sample = new EDS({ 13 | size: 3, 14 | random: function() { 15 | return 1; 16 | } 17 | }); 18 | }); 19 | 20 | it('returns an empty array by default', function() { 21 | assert.deepEqual(sample.toSortedArray(), []); 22 | }); 23 | 24 | it('is always sorted by priority', function() { 25 | sample.update('a', Date.now() + 3000); 26 | sample.update('b', Date.now() + 2000); 27 | sample.update('c', Date.now()); 28 | 29 | assert.deepEqual(sample.toSortedArray(), ['c', 'b', 'a']); 30 | }); 31 | }); 32 | 33 | describe('ExponentiallyDecayingSample#toArray', function() { 34 | var sample; 35 | beforeEach(function() { 36 | sample = new EDS({ 37 | size: 3, 38 | random: function() { 39 | return 1; 40 | } 41 | }); 42 | }); 43 | 44 | it('returns an empty array by default', function() { 45 | assert.deepEqual(sample.toArray(), []); 46 | }); 47 | 48 | it('may return an unsorted array', function() { 49 | sample.update('a', Date.now() + 3000); 50 | sample.update('b', Date.now() + 2000); 51 | sample.update('c', Date.now()); 52 | 53 | assert.deepEqual(sample.toArray(), ['c', 'a', 'b']); 54 | }); 55 | }); 56 | 57 | describe('ExponentiallyDecayingSample#update', function() { 58 | var sample; 59 | beforeEach(function() { 60 | sample = new EDS({ 61 | size: 2, 62 | random: function() { 63 | return 1; 64 | } 65 | }); 66 | }); 67 | 68 | it('can add one item', function() { 69 | sample.update('a'); 70 | 71 | assert.deepEqual(sample.toSortedArray(), ['a']); 72 | }); 73 | 74 | it('sorts items according to priority ascending', function() { 75 | sample.update('a', Date.now()); 76 | sample.update('b', Date.now() + 1000); 77 | 78 | assert.deepEqual(sample.toSortedArray(), ['a', 'b']); 79 | }); 80 | 81 | it('pops items with lowest priority', function() { 82 | sample.update('a', Date.now()); 83 | sample.update('b', Date.now() + 1000); 84 | sample.update('c', Date.now() + 2000); 85 | 86 | assert.deepEqual(sample.toSortedArray(), ['b', 'c']); 87 | }); 88 | 89 | it('items with too low of a priority do not make it in', function() { 90 | sample.update('a', Date.now() + 1000); 91 | sample.update('b', Date.now() + 2000); 92 | sample.update('c', Date.now()); 93 | 94 | assert.deepEqual(sample.toSortedArray(), ['a', 'b']); 95 | }); 96 | }); 97 | 98 | describe('ExponentiallyDecayingSample#_rescale', function() { 99 | var sample; 100 | beforeEach(function() { 101 | sample = new EDS({ 102 | size: 2, 103 | random: function() { 104 | return 1; 105 | } 106 | }); 107 | }); 108 | 109 | it('works as expected', function() { 110 | sample.update('a', Date.now() + 50 * units.MINUTES); 111 | sample.update('b', Date.now() + 55 * units.MINUTES); 112 | 113 | var elements = sample._elements.toSortedArray(); 114 | assert.ok(elements[0].priority > 1000); 115 | assert.ok(elements[1].priority > 1000); 116 | 117 | sample._rescale(Date.now() + 60 * units.MINUTES); 118 | 119 | elements = sample._elements.toSortedArray(); 120 | assert.ok(elements[0].priority < 1); 121 | assert.ok(elements[0].priority > 0); 122 | assert.ok(elements[1].priority < 1); 123 | assert.ok(elements[1].priority > 0); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/util/test-ExponentiallyMovingWeightedAverage.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | var units = common.measured.units; 7 | var EMWA = common.measured.ExponentiallyMovingWeightedAverage; 8 | 9 | describe('ExponentiallyMovingWeightedAverage', function() { 10 | it('decay over several updates and ticks', function() { 11 | var ewma = new EMWA(units.MINUTES, 5 * units.SECONDS); 12 | 13 | ewma.update(5); 14 | ewma.tick(); 15 | 16 | assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.080'); 17 | 18 | ewma.update(5); 19 | ewma.update(5); 20 | ewma.tick(); 21 | 22 | assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.233'); 23 | 24 | ewma.update(15); 25 | ewma.tick(); 26 | 27 | assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.455'); 28 | 29 | var i; 30 | for (i = 0; i < 200; i++) { 31 | ewma.update(15); 32 | ewma.tick(); 33 | } 34 | 35 | assert.equal(ewma.rate(units.SECONDS).toFixed(3), '3.000'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/measured-core/test/unit/util/test-Stopwatch.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | 'use strict'; 3 | 4 | var common = require('../../common'); 5 | var assert = require('assert'); 6 | var Stopwatch = common.measured.Stopwatch; 7 | var sinon = require('sinon'); 8 | 9 | describe('Stopwatch', function() { 10 | var watch; 11 | var clock; 12 | beforeEach(function() { 13 | clock = sinon.useFakeTimers(); 14 | watch = new Stopwatch({ 15 | getTime: function() { 16 | return new Date().getTime(); 17 | } 18 | }); 19 | }); 20 | 21 | afterEach(function() { 22 | clock.restore(); 23 | }); 24 | 25 | it('returns time on end', function() { 26 | clock.tick(100); 27 | 28 | var elapsed = watch.end(); 29 | assert.equal(elapsed, 100); 30 | }); 31 | 32 | it('emits time on end', function() { 33 | clock.tick(20); 34 | 35 | var time; 36 | watch.on('end', function(_time) { 37 | time = _time; 38 | }); 39 | 40 | watch.end(); 41 | 42 | assert.equal(time, 20); 43 | }); 44 | 45 | it('becomes useless after being ended once', function() { 46 | clock.tick(20); 47 | 48 | var time; 49 | watch.on('end', function(_time) { 50 | time = _time; 51 | }); 52 | 53 | assert.equal(watch.end(), 20); 54 | assert.equal(time, 20); 55 | 56 | time = null; 57 | assert.equal(watch.end(), undefined); 58 | assert.equal(time, null); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/README.md: -------------------------------------------------------------------------------- 1 | # Measured Node Metrics 2 | 3 | Various metrics generators and http framework middlewares that can be used with a self reporting metrics registry to easily instrument metrics for a node app. 4 | 5 | [![npm](https://img.shields.io/npm/v/measured-node-metrics.svg)](https://www.npmjs.com/package/measured-node-metrics) 6 | 7 | ## Install 8 | 9 | ``` 10 | npm install measured-node-metrics 11 | ``` 12 | 13 | ## What is in this package 14 | 15 | ### [Measured Node Metrics Module](https://yaorg.github.io/node-measured/module-measured-node-metrics.html) 16 | See the docs for the main module to see the exported helper functions and maps of metric generators for various system and os metrics. 17 | 18 | ## Example usage 19 | 20 | ```javascript 21 | const express = require('express'); 22 | const { createProcessMetrics, createOSMetrics, createExpressMiddleware } = require('measured-node-metrics'); 23 | 24 | const registry = new SelfReportingMetricsRegistry(new SomeReporterImple()); 25 | 26 | // Create and register default OS metrics 27 | createOSMetrics(registry); 28 | // Create and register default process metrics 29 | createProcessMetrics(registry); 30 | // Use the express middleware 31 | const app = express(); 32 | app.use(createExpressMiddleware(registry)); 33 | 34 | // Implement the rest of app 35 | ``` 36 | 37 | You can also create your own middleware if your not using express, (please contribute it) 38 | ```javascript 39 | const { onRequestStart, onRequestEnd } = require('measured-node-metrics'); 40 | 41 | /** 42 | * Creates an Express middleware that reports a timer on request data. 43 | * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. 44 | * 45 | * @param {SelfReportingMetricsRegistry} metricsRegistry 46 | * @param {number} [reportingIntervalInSeconds] 47 | * @return {Function} 48 | */ 49 | createExpressMiddleware: (metricsRegistry, reportingIntervalInSeconds) => { 50 | return (req, res, next) => { 51 | const stopwatch = onRequestStart(); 52 | 53 | req.on('end', () => { 54 | const { method } = req; 55 | const { statusCode } = res; 56 | // path variables should be stripped in order to avoid runaway time series creation, 57 | // /v1/cars/:id should be one dimension rather than n, one for each id. 58 | const uri = req.route ? req.route.path : '_unknown'; 59 | onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds); 60 | }); 61 | 62 | next(); 63 | }; 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/lib/index.js: -------------------------------------------------------------------------------- 1 | const { nodeProcessMetrics, createProcessMetrics } = require('./nodeProcessMetrics'); 2 | const { nodeOsMetrics, createOSMetrics } = require('./nodeOsMetrics'); 3 | const { createExpressMiddleware, createKoaMiddleware, onRequestStart, onRequestEnd } = require('./nodeHttpRequestMetrics'); 4 | 5 | /** 6 | * The main module for the measured-node-metrics lib. 7 | * 8 | * Various functions to help create node metrics and http framework middlewares 9 | * that can be used with a self reporting metrics registry to easily instrument metrics for a node app. 10 | * 11 | * @module measured-node-metrics 12 | */ 13 | module.exports = { 14 | /** 15 | * Map of metric names and a functions that can be used to generate that metric object that can be registered with a 16 | * self reporting metrics registry or used as seen fit. 17 | * 18 | * See {@link nodeProcessMetrics}. 19 | * 20 | * @type {Object.} 21 | */ 22 | nodeProcessMetrics, 23 | 24 | /** 25 | * Method that can be used to add a set of default node process metrics to your node app. 26 | * 27 | * registers all metrics defined in the {@link nodeProcessMetrics} map. 28 | * 29 | * @function 30 | * @name createProcessMetrics 31 | * @param {SelfReportingMetricsRegistry} metricsRegistry 32 | * @param {Dimensions} customDimensions 33 | * @param {number} reportingIntervalInSeconds 34 | */ 35 | createProcessMetrics, 36 | 37 | /** 38 | * Map of metric names and a functions that can be used to generate that metric object that can be registered with a 39 | * self reporting metrics registry or used as seen fit. 40 | * 41 | * See {@link nodeOsMetrics}. 42 | * 43 | * @type {Object.} 44 | */ 45 | nodeOsMetrics, 46 | 47 | /** 48 | * Method that can be used to add a set of default node process metrics to your app. 49 | * 50 | * registers all metrics defined in the {@link nodeOsMetrics} map. 51 | * 52 | * @function 53 | * @name createOSMetrics 54 | * @param {SelfReportingMetricsRegistry} metricsRegistry 55 | * @param {Dimensions} customDimensions 56 | * @param {number} reportingIntervalInSeconds 57 | */ 58 | createOSMetrics, 59 | 60 | /** 61 | * Creates an Express middleware that reports a timer on request data. 62 | * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. 63 | * 64 | * @function 65 | * @name createExpressMiddleware 66 | * @param {SelfReportingMetricsRegistry} metricsRegistry 67 | * @param {number} [reportingIntervalInSeconds] 68 | * @return {Function} 69 | */ 70 | createExpressMiddleware, 71 | 72 | /** 73 | * Creates a Koa middleware that reports a timer on request data. 74 | * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. 75 | * 76 | * @function 77 | * @name createExpressMiddleware 78 | * @param {SelfReportingMetricsRegistry} metricsRegistry 79 | * @param {number} [reportingIntervalInSeconds] 80 | * @return {Function} 81 | */ 82 | createKoaMiddleware, 83 | 84 | /** 85 | * At the start of the request, create a stopwatch, that starts tracking how long the request is taking. 86 | * @function 87 | * @name onRequestStart 88 | * @return {Stopwatch} 89 | */ 90 | onRequestStart, 91 | 92 | /** 93 | * When the request ends stop the stop watch and create or update the timer for requests that tracked by method, statuscode, path. 94 | * The timers (meters and histograms) that get reported will be filterable by status codes, http method, the uri path. 95 | * You will be able to create dash boards such as success percentage, latency percentiles by path and method, etc. 96 | * 97 | * @function 98 | * @name onRequestEnd 99 | * @param metricsRegistry The Self Reporting Metrics Registry 100 | * @param stopwatch The stopwatch created by onRequestStart 101 | * @param method The Http Method for the request 102 | * @param statusCode The status code for the response 103 | * @param [uri] The uri for the request. Please note to avoid out of control time series dimension creation spread, 104 | * you would want to strip out ids and or other variables from the uri path. 105 | * @param [reportingIntervalInSeconds] override the reporting interval defaults to every 10 seconds. 106 | */ 107 | onRequestEnd 108 | }; 109 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/lib/nodeHttpRequestMetrics.js: -------------------------------------------------------------------------------- 1 | const { Stopwatch } = require('measured-core'); 2 | 3 | /** 4 | * The default reporting interval for requests 5 | * @type {number} 6 | */ 7 | const DEFAULT_REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10; 8 | 9 | /** 10 | * This module has functions needed to create middlewares for frameworks such as express and koa. 11 | * It also exports the 2 functions needed to implement your own middleware. 12 | * If you implement a middleware for a framework not implemented here, please contribute it back. 13 | * 14 | * @module node-http-request-metrics 15 | */ 16 | module.exports = { 17 | /** 18 | * Creates an Express middleware that reports a timer on request data. 19 | * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. 20 | * 21 | * @param {SelfReportingMetricsRegistry} metricsRegistry 22 | * @param {number} [reportingIntervalInSeconds] 23 | * @return {Function} 24 | */ 25 | createExpressMiddleware: (metricsRegistry, reportingIntervalInSeconds) => { 26 | return (req, res, next) => { 27 | const stopwatch = module.exports.onRequestStart(); 28 | 29 | res.on('finish', () => { 30 | const { method } = req; 31 | const { statusCode } = res; 32 | const uri = req.route ? req.route.path : '_unknown'; 33 | module.exports.onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds); 34 | }); 35 | 36 | next(); 37 | }; 38 | }, 39 | 40 | /** 41 | * Creates a Koa middleware that reports a timer on request data. 42 | * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. 43 | * 44 | * @param {SelfReportingMetricsRegistry} metricsRegistry 45 | * @param {number} [reportingIntervalInSeconds] 46 | * @return {Function} 47 | */ 48 | createKoaMiddleware: (metricsRegistry, reportingIntervalInSeconds) => async (ctx, next) => { 49 | const stopwatch = module.exports.onRequestStart(); 50 | const { req, res } = ctx; 51 | 52 | res.once('finish', () => { 53 | const { method } = req; 54 | const { statusCode } = res; 55 | const uri = ctx._matchedRoute || '_unknown'; 56 | module.exports.onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds); 57 | }); 58 | 59 | await next(); 60 | }, 61 | 62 | /** 63 | * At the start of the request, create a stopwatch, that starts tracking how long the request is taking. 64 | * @return {Stopwatch} 65 | */ 66 | onRequestStart: () => { 67 | return new Stopwatch(); 68 | }, 69 | 70 | /** 71 | * When the request ends stop the stop watch and create or update the timer for requests that tracked by method, status code, path. 72 | * The timers (meters and histograms) that get reported will be filterable by status codes, http method, the uri path. 73 | * You will be able to create dash boards such as success percentage, latency percentiles by uri path and method, etc. 74 | * 75 | * @param {SelfReportingMetricsRegistry} metricsRegistry The Self Reporting Metrics Registry 76 | * @param {Stopwatch} stopwatch The stopwatch created by onRequestStart 77 | * @param {string} method The Http Method for the request 78 | * @param {string|number} statusCode The status code for the response 79 | * @param {string} [uri] The uri path for the request. Please note to avoid out of control time series dimension creation spread, 80 | * you would want to strip out ids and or other variables from the uri path. 81 | * @param {number} [reportingIntervalInSeconds] override the reporting interval defaults to every 10 seconds. 82 | */ 83 | onRequestEnd: (metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds) => { 84 | reportingIntervalInSeconds = reportingIntervalInSeconds || DEFAULT_REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS; 85 | 86 | const customDimensions = { 87 | statusCode: `${statusCode}`, 88 | method: `${method}` 89 | }; 90 | 91 | if (uri) { 92 | customDimensions.uri = uri; 93 | } 94 | 95 | // get or create the timer for the request count/latency timer 96 | const requestTimer = metricsRegistry.getOrCreateTimer('requests', customDimensions, reportingIntervalInSeconds); 97 | 98 | // stop the request latency counter 99 | const time = stopwatch.end(); 100 | requestTimer.update(time); 101 | } 102 | }; 103 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/lib/nodeOsMetrics.js: -------------------------------------------------------------------------------- 1 | const { Gauge, CachedGauge } = require('measured-core'); 2 | const { cpuAverage, calculateCpuUsagePercent } = require('./utils/CpuUtils'); 3 | const os = require('os'); 4 | 5 | /** 6 | * The default reporting interval for node os metrics is 30 seconds. 7 | * 8 | * @type {number} 9 | */ 10 | const DEFAULT_NODE_OS_METRICS_REPORTING_INTERVAL_IN_SECONDS = 30; 11 | 12 | /** 13 | * A map of Metric generating functions, that create Metrics to measure node os stats. 14 | */ 15 | const nodeOsMetrics = { 16 | /** 17 | * https://nodejs.org/api/os.html#os_os_loadavg 18 | * @return {Gauge} 19 | */ 20 | 'node.os.loadavg.1m': () => { 21 | return new Gauge(() => { 22 | return os.loadavg()[0]; 23 | }); 24 | }, 25 | /** 26 | * https://nodejs.org/api/os.html#os_os_loadavg 27 | * @return {Gauge} 28 | */ 29 | 'node.os.loadavg.5m': () => { 30 | return new Gauge(() => { 31 | return os.loadavg()[1]; 32 | }); 33 | }, 34 | /** 35 | * https://nodejs.org/api/os.html#os_os_loadavg 36 | * @return {Gauge} 37 | */ 38 | 'node.os.loadavg.15m': () => { 39 | return new Gauge(() => { 40 | return os.loadavg()[2]; 41 | }); 42 | }, 43 | 'node.os.freemem': () => { 44 | return new Gauge(() => { 45 | return os.freemem(); 46 | }); 47 | }, 48 | 'node.os.totalmem': () => { 49 | return new Gauge(() => { 50 | return os.totalmem(); 51 | }); 52 | }, 53 | 54 | /** 55 | * Gauge to track how long the os has been running. 56 | *\ 57 | *]=- 58 | * See {@link https://nodejs.org/api/os.html#os_os_uptime} for more information. 59 | * @return {Gauge} 60 | */ 61 | 'node.os.uptime': () => { 62 | return new Gauge(() => { 63 | // The os.uptime() method returns the system uptime in number of seconds. 64 | return os.uptime(); 65 | }); 66 | }, 67 | 68 | /** 69 | * Creates a {@link CachedGauge} that will self update every updateIntervalInSeconds and sample the 70 | * cpu usage across all cores for sampleTimeInSeconds. 71 | * 72 | * @param {number} [updateIntervalInSeconds] How often to update and cache the cpu usage average, defaults to 30 seconds. 73 | * @param {number} [sampleTimeInSeconds] How long to sample the cpu usage over, defaults to 5 seconds. 74 | */ 75 | 'node.os.cpu.all-cores-avg': (updateIntervalInSeconds, sampleTimeInSeconds) => { 76 | updateIntervalInSeconds = updateIntervalInSeconds || 30; 77 | sampleTimeInSeconds = sampleTimeInSeconds || 5; 78 | 79 | return new CachedGauge(() => { 80 | return new Promise(resolve => { 81 | //Grab first CPU Measure 82 | const startMeasure = cpuAverage(); 83 | setTimeout(() => { 84 | //Grab second Measure 85 | const endMeasure = cpuAverage(); 86 | const percentageCPU = calculateCpuUsagePercent(startMeasure, endMeasure); 87 | resolve(percentageCPU); 88 | }, sampleTimeInSeconds); 89 | }); 90 | }, updateIntervalInSeconds); 91 | } 92 | }; 93 | 94 | /** 95 | * This module contains the methods to create and register default node os metrics to a metrics registry. 96 | * 97 | * @module node-os-metrics 98 | */ 99 | module.exports = { 100 | /** 101 | * Method that can be used to add a set of default node process metrics to your app. 102 | * 103 | * @param {SelfReportingMetricsRegistry} metricsRegistry 104 | * @param {Dimensions} customDimensions 105 | * @param {number} reportingIntervalInSeconds 106 | */ 107 | createOSMetrics: (metricsRegistry, customDimensions, reportingIntervalInSeconds) => { 108 | customDimensions = customDimensions || {}; 109 | reportingIntervalInSeconds = reportingIntervalInSeconds || DEFAULT_NODE_OS_METRICS_REPORTING_INTERVAL_IN_SECONDS; 110 | 111 | Object.keys(nodeOsMetrics).forEach(metricName => { 112 | metricsRegistry.register(metricName, nodeOsMetrics[metricName](), customDimensions, reportingIntervalInSeconds); 113 | }); 114 | }, 115 | 116 | /** 117 | * Map of metric names to a corresponding function that creates and returns a Metric that tracks it. 118 | * See {@link nodeOsMetrics} 119 | */ 120 | nodeOsMetrics 121 | }; 122 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/lib/nodeProcessMetrics.js: -------------------------------------------------------------------------------- 1 | const { Gauge } = require('measured-core'); 2 | const process = require('process'); 3 | 4 | /** 5 | * The default reporting interval for node process metrics is 30 seconds. 6 | * 7 | * @type {number} 8 | */ 9 | const DEFAULT_NODE_PROCESS_METRICS_REPORTING_INTERVAL_IN_SECONDS = 30; 10 | 11 | /** 12 | * A map of Metric generating functions, that create Metrics to measure node process stats. 13 | * @type {Object.} 14 | */ 15 | const nodeProcessMetrics = { 16 | /** 17 | * Creates a gauge that reports the rss from the node memory usage api. 18 | * See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information. 19 | * 20 | * @return {Gauge} 21 | */ 22 | 'node.process.memory-usage.rss': () => { 23 | return new Gauge(() => { 24 | return process.memoryUsage().rss; 25 | }); 26 | }, 27 | /** 28 | * See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information. 29 | * 30 | * @return {Gauge} 31 | */ 32 | 'node.process.memory-usage.heap-total': () => { 33 | return new Gauge(() => { 34 | return process.memoryUsage().heapTotal; 35 | }); 36 | }, 37 | /** 38 | * See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information. 39 | * 40 | * @return {Gauge} 41 | */ 42 | 'node.process.memory-usage.heap-used': () => { 43 | return new Gauge(() => { 44 | return process.memoryUsage().heapUsed; 45 | }); 46 | }, 47 | /** 48 | * See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information. 49 | * 50 | * @return {Gauge} 51 | */ 52 | 'node.process.memory-usage.external': () => { 53 | return new Gauge(() => { 54 | const mem = process.memoryUsage(); 55 | return Object.prototype.hasOwnProperty.call(mem, 'external') ? mem.external : 0; 56 | }); 57 | }, 58 | /** 59 | * Gauge to track how long the node process has been running. 60 | * 61 | * See {@link https://nodejs.org/api/process.html#process_process_uptime} for more information. 62 | * @return {Gauge} 63 | */ 64 | 'node.process.uptime': () => { 65 | return new Gauge(() => { 66 | return Math.floor(process.uptime()); 67 | }); 68 | } 69 | }; 70 | 71 | /** 72 | * This module contains the methods to create and register default node process metrics to a metrics registry. 73 | * 74 | * @module node-process-metrics 75 | */ 76 | module.exports = { 77 | /** 78 | * Method that can be used to add a set of default node process metrics to your app. 79 | * 80 | * @param {SelfReportingMetricsRegistry} metricsRegistry 81 | * @param {Dimensions} [customDimensions] 82 | * @param {number} [reportingIntervalInSeconds] 83 | */ 84 | createProcessMetrics: (metricsRegistry, customDimensions, reportingIntervalInSeconds) => { 85 | customDimensions = customDimensions || {}; 86 | reportingIntervalInSeconds = 87 | reportingIntervalInSeconds || DEFAULT_NODE_PROCESS_METRICS_REPORTING_INTERVAL_IN_SECONDS; 88 | 89 | Object.keys(nodeProcessMetrics).forEach(metricName => { 90 | metricsRegistry.register( 91 | metricName, 92 | nodeProcessMetrics[metricName](), 93 | customDimensions, 94 | reportingIntervalInSeconds 95 | ); 96 | }); 97 | }, 98 | 99 | /** 100 | * Map of metric names to a corresponding function that creates and returns a Metric that tracks it. 101 | * See {@link nodeProcessMetrics} 102 | */ 103 | nodeProcessMetrics 104 | }; 105 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/lib/utils/CpuUtils.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | 3 | /** 4 | * @module CpuUtils 5 | */ 6 | module.exports = { 7 | /** 8 | * 9 | * @return {{idle: number, total: number}} 10 | */ 11 | cpuAverage: () => { 12 | //Initialise sum of idle and time of cores and fetch CPU info 13 | let totalIdle = 0, 14 | totalTick = 0; 15 | const cpus = os.cpus(); 16 | 17 | cpus.forEach(cpu => { 18 | //Total up the time in the cores tick 19 | Object.keys(cpu.times).forEach(type => { 20 | totalTick += cpu.times[type]; 21 | }); 22 | //Total up the idle time of the core 23 | totalIdle += cpu.times.idle; 24 | }); 25 | 26 | //Return the average Idle and Tick times 27 | return { idle: totalIdle / cpus.length, total: totalTick / cpus.length }; 28 | }, 29 | 30 | /** 31 | * 32 | * @param {{idle: number, total: number}} startMeasure 33 | * @param {{idle: number, total: number}} endMeasure 34 | */ 35 | calculateCpuUsagePercent: (startMeasure, endMeasure) => { 36 | //Calculate the difference in idle and total time between the measures 37 | const idleDifference = endMeasure.idle - startMeasure.idle; 38 | const totalDifference = endMeasure.total - startMeasure.total; 39 | //Calculate the average percentage CPU usage 40 | return Math.ceil(100 - 100 * idleDifference / totalDifference); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "measured-node-metrics", 3 | "description": "Various metrics generators and http framework middlewares that can be used with a self reporting metrics registry to easily instrument metrics for a node app.", 4 | "version": "2.0.0", 5 | "homepage": "https://yaorg.github.io/node-measured/", 6 | "engines": { 7 | "node": ">= 5.12" 8 | }, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "./lib/index.js", 13 | "scripts": { 14 | "clean": "rm -fr build", 15 | "format": "prettier --write './lib/**/*.{ts,js}'", 16 | "lint": "eslint lib --ext .js", 17 | "test:node": "mocha './test/**/test-*.js'", 18 | "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'", 19 | "test:browser": "exit 0", 20 | "test": "yarn test:node:coverage", 21 | "coverage": "nyc report --reporter=text-lcov | coveralls" 22 | }, 23 | "repository": { 24 | "url": "git://github.com/yaorg/node-measured.git" 25 | }, 26 | "dependencies": { 27 | "measured-core": "^2.0.0" 28 | }, 29 | "files": [ 30 | "lib", 31 | "README.md" 32 | ], 33 | "license": "MIT", 34 | "devDependencies": { 35 | "express": "^4.16.3", 36 | "find-free-port": "^1.2.0", 37 | "jsdoc": "^3.5.5", 38 | "measured-reporting": "^2.0.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/test/integration/test-express-middleware.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const express = require('express'); 3 | const Registry = require('measured-reporting').SelfReportingMetricsRegistry; 4 | const TestReporter = require('../unit/TestReporter'); 5 | const { createExpressMiddleware } = require('../../lib'); 6 | const findFreePort = require('find-free-port'); 7 | const assert = require('assert'); 8 | const http = require('http'); 9 | 10 | describe('express-middleware', () => { 11 | let port; 12 | let reporter; 13 | let registry; 14 | let middleware; 15 | let app; 16 | let httpServer; 17 | beforeEach(() => { 18 | return new Promise(resolve => { 19 | reporter = new TestReporter(); 20 | registry = new Registry(reporter); 21 | middleware = createExpressMiddleware(registry, 1); 22 | app = express(); 23 | app.use(middleware); 24 | app.use(express.json()); 25 | 26 | app.get('/hello', (req, res) => res.send('Hello World!')); 27 | app.post('/world', (req, res) => res.status(201).send('Hello World!')); 28 | app.get('/users/:userId', (req, res) => { 29 | res.send(`id: ${req.params.userId}`); 30 | }); 31 | 32 | findFreePort(3000).then(portArr => { 33 | port = portArr.shift(); 34 | 35 | httpServer = http.createServer(app); 36 | httpServer.listen(port); 37 | resolve(); 38 | }); 39 | }); 40 | }); 41 | 42 | afterEach(() => { 43 | httpServer.close(); 44 | registry.shutdown(); 45 | }); 46 | 47 | it('creates a single timer that has 1 count for requests, when an http call is made once', () => { 48 | return callLocalHost(port, 'hello').then(() => { 49 | const registeredKeys = registry._registry.allKeys(); 50 | assert(registeredKeys.length === 1); 51 | assert.equal(registeredKeys[0], 'requests-GET-200-/hello'); 52 | const metricWrapper = registry._registry.getMetricWrapperByKey('requests-GET-200-/hello'); 53 | const { name, dimensions } = metricWrapper; 54 | assert.equal(name, 'requests'); 55 | assert.deepEqual(dimensions, { statusCode: '200', method: 'GET', uri: '/hello' }); 56 | }); 57 | }); 58 | 59 | it('creates a single timer that has 1 count for requests, when an http POST call is made once', () => { 60 | const options = { method: 'POST', headers: { 'Content-Type': 'application/json' } }; 61 | return callLocalHost(port, 'world', options).then(() => { 62 | const registeredKeys = registry._registry.allKeys(); 63 | assert(registeredKeys.length === 1); 64 | assert.equal(registeredKeys[0], 'requests-POST-201-/world'); 65 | const metricWrapper = registry._registry.getMetricWrapperByKey('requests-POST-201-/world'); 66 | const { name, dimensions } = metricWrapper; 67 | assert.equal(name, 'requests'); 68 | assert.deepEqual(dimensions, { statusCode: '201', method: 'POST', uri: '/world' }); 69 | }); 70 | }); 71 | 72 | it('does not create runaway n metrics in the registry for n ids in the path', () => { 73 | return Promise.all([ 74 | callLocalHost(port, 'users/foo'), 75 | callLocalHost(port, 'users/bar'), 76 | callLocalHost(port, 'users/bop') 77 | ]).then(() => { 78 | assert.equal(registry._registry.allKeys().length, 1, 'There should only be one metric for /users and GET'); 79 | }); 80 | }); 81 | }); 82 | 83 | const callLocalHost = (port, endpoint, options) => { 84 | return new Promise((resolve, reject) => { 85 | const req = Object.assign({ protocol: 'http:', 86 | host: '127.0.0.1', 87 | port: `${port}`, 88 | path: `/${endpoint}`, 89 | method: 'GET' }, 90 | options || {}); 91 | http 92 | .request(req, resp => { 93 | let data = ''; 94 | resp.on('data', chunk => { 95 | data += chunk; 96 | }); 97 | 98 | resp.on('end', () => { 99 | console.log(JSON.stringify(data)); 100 | resolve(); 101 | }); 102 | }) 103 | .on('error', err => { 104 | console.log('Error: ', JSON.stringify(err)); 105 | reject(); 106 | }) 107 | .end(); 108 | }); 109 | }; 110 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/test/integration/test-koa-middleware.js: -------------------------------------------------------------------------------- 1 | const Koa = require('koa'); 2 | const KoaBodyParser = require('koa-bodyparser'); 3 | const Router = require('koa-router'); 4 | const Registry = require('measured-reporting').SelfReportingMetricsRegistry; 5 | const TestReporter = require('../unit/TestReporter'); 6 | const { createKoaMiddleware } = require('../../lib'); 7 | const findFreePort = require('find-free-port'); 8 | const assert = require('assert'); 9 | const http = require('http'); 10 | 11 | describe('koa-middleware', () => { 12 | let port; 13 | let reporter; 14 | let registry; 15 | let middleware; 16 | let app; 17 | let httpServer; 18 | let router; 19 | beforeEach(() => { 20 | return new Promise(resolve => { 21 | reporter = new TestReporter(); 22 | registry = new Registry(reporter); 23 | middleware = createKoaMiddleware(registry, 1); 24 | app = new Koa(); 25 | router = new Router(); 26 | 27 | router.get('/hello', ({ response }) => { 28 | response.body = 'Hello World!'; 29 | return response; 30 | }); 31 | router.post('/world', ({ response }) => { 32 | response.body = 'Hello World!'; 33 | response.status = 201; 34 | return response; 35 | }); 36 | router.get('/users/:userId', ({ params, response }) => { 37 | response.body = `id: ${params.userId}`; 38 | return response; 39 | }); 40 | 41 | app.use(middleware); 42 | app.use(KoaBodyParser()); 43 | app.use(router.routes()); 44 | app.use(router.allowedMethods()); 45 | 46 | app.on('error', (err) => console.error(err)); 47 | 48 | findFreePort(3000).then(portArr => { 49 | port = portArr.shift(); 50 | 51 | httpServer = app.listen(port); 52 | resolve(); 53 | }); 54 | }); 55 | }); 56 | 57 | afterEach(() => { 58 | httpServer.close(); 59 | registry.shutdown(); 60 | }); 61 | 62 | it('creates a single timer that has 1 count for requests, when an http call is made once', () => { 63 | return callLocalHost(port, 'hello').then(() => { 64 | const registeredKeys = registry._registry.allKeys(); 65 | assert(registeredKeys.length === 1); 66 | assert.equal(registeredKeys[0], 'requests-GET-200-/hello'); 67 | const metricWrapper = registry._registry.getMetricWrapperByKey('requests-GET-200-/hello'); 68 | const { name, dimensions } = metricWrapper; 69 | assert.equal(name, 'requests'); 70 | assert.deepEqual(dimensions, { statusCode: '200', method: 'GET', uri: '/hello' }); 71 | }); 72 | }); 73 | 74 | it('creates a single timer that has 1 count for requests, when an http POST call is made once', () => { 75 | const options = { method: 'POST', headers: { 'Content-Type': 'application/json' } }; 76 | return callLocalHost(port, 'world', options).then(() => { 77 | const registeredKeys = registry._registry.allKeys(); 78 | assert(registeredKeys.length === 1); 79 | assert.equal(registeredKeys[0], 'requests-POST-201-/world'); 80 | const metricWrapper = registry._registry.getMetricWrapperByKey('requests-POST-201-/world'); 81 | const { name, dimensions } = metricWrapper; 82 | assert.equal(name, 'requests'); 83 | assert.deepEqual(dimensions, { statusCode: '201', method: 'POST', uri: '/world' }); 84 | }); 85 | }); 86 | 87 | it('does not create runaway n metrics in the registry for n ids in the path', () => { 88 | return Promise.all([ 89 | callLocalHost(port, 'users/foo'), 90 | callLocalHost(port, 'users/bar'), 91 | callLocalHost(port, 'users/bop') 92 | ]).then(() => { 93 | assert.equal(registry._registry.allKeys().length, 1, 'There should only be one metric for /users and GET'); 94 | }); 95 | }); 96 | }); 97 | 98 | const callLocalHost = (port, endpoint, options) => { 99 | return new Promise((resolve, reject) => { 100 | const req = Object.assign({ protocol: 'http:', 101 | host: '127.0.0.1', 102 | port: `${port}`, 103 | path: `/${endpoint}`, 104 | method: 'GET' }, 105 | options || {}); 106 | http 107 | .request(req, resp => { 108 | let data = ''; 109 | resp.on('data', chunk => { 110 | data += chunk; 111 | }); 112 | 113 | resp.on('end', () => { 114 | console.log(JSON.stringify(data)); 115 | resolve(); 116 | }); 117 | }) 118 | .on('error', err => { 119 | console.log('Error: ', JSON.stringify(err)); 120 | reject(); 121 | }) 122 | .end(); 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/test/unit/TestReporter.js: -------------------------------------------------------------------------------- 1 | const { Reporter } = require('measured-reporting'); 2 | 3 | /** 4 | * @extends Reporter 5 | */ 6 | class TestReporter extends Reporter { 7 | constructor(options) { 8 | super(options); 9 | 10 | this._reportedMetrics = []; 11 | } 12 | 13 | getReportedMetrics() { 14 | return this._reportedMetrics; 15 | } 16 | 17 | _reportMetrics(metrics) { 18 | this._reportedMetrics.push(metrics); 19 | } 20 | } 21 | 22 | module.exports = TestReporter; 23 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/test/unit/test-nodeHttpRequestMetrics.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const assert = require('assert'); 3 | const EventEmitter = require('events'); 4 | const { Stopwatch } = require('measured-core'); 5 | const { createExpressMiddleware, createKoaMiddleware, onRequestStart, onRequestEnd } = require('../../lib'); 6 | const TestReporter = require('./TestReporter'); 7 | const Registry = require('measured-reporting').SelfReportingMetricsRegistry; 8 | 9 | class MockResponse extends EventEmitter { 10 | constructor() { 11 | super(); 12 | this.statusCode = 200; 13 | } 14 | 15 | finish() { 16 | this.emit('finish'); 17 | } 18 | } 19 | 20 | describe('onRequestStart', () => { 21 | it('returns a stopwatch', () => { 22 | const stopwatch = onRequestStart(); 23 | assert(stopwatch.constructor.name === 'Stopwatch'); 24 | }); 25 | }); 26 | 27 | describe('onRequestEnd', () => { 28 | it('stops the stopwatch and gets or creates a timer and then updates it with the elapsed time with the appropriate dimensions', () => { 29 | const stopwatch = new Stopwatch(); 30 | const registry = new Registry(new TestReporter()); 31 | 32 | onRequestEnd(registry, stopwatch, 'POST', 201, '/some/path'); 33 | 34 | const registeredKeys = registry._registry.allKeys(); 35 | assert(registeredKeys.length === 1); 36 | const expectedKey = 'requests-POST-201-/some/path'; 37 | assert.equal(registeredKeys[0], expectedKey); 38 | const metricWrapper = registry._registry.getMetricWrapperByKey(expectedKey); 39 | assert.equal(metricWrapper.name, 'requests'); 40 | assert.deepEqual(metricWrapper.dimensions, { statusCode: '201', method: 'POST', uri: '/some/path' }); 41 | assert.equal(metricWrapper.metricImpl.getType(), 'Timer'); 42 | assert.equal(metricWrapper.metricImpl._histogram._count, 1); 43 | registry.shutdown(); 44 | }); 45 | }); 46 | 47 | describe('createExpressMiddleware', () => { 48 | it('creates and registers a metric called request that is a timer', () => { 49 | const reporter = new TestReporter(); 50 | const registry = new Registry(reporter); 51 | 52 | const middleware = createExpressMiddleware(registry); 53 | 54 | const res = new MockResponse(); 55 | middleware( 56 | { 57 | method: 'GET', 58 | routine: { path: '/v1/rest/some-end-point' } 59 | }, 60 | res, 61 | () => {} 62 | ); 63 | res.finish(); 64 | 65 | const registeredKeys = registry._registry.allKeys(); 66 | assert(registeredKeys.length === 1); 67 | assert(registeredKeys[0].includes('requests-GET')); 68 | registry.shutdown(); 69 | }); 70 | }); 71 | 72 | describe('createKoaMiddleware', () => { 73 | it('creates and registers a metric called request that is a timer', async () => { 74 | const reporter = new TestReporter(); 75 | const registry = new Registry(reporter); 76 | 77 | const middleware = createKoaMiddleware(registry); 78 | 79 | const res = new MockResponse(); 80 | middleware( 81 | { 82 | req: { 83 | method: 'GET', 84 | url: '/v1/rest/some-end-point', 85 | }, 86 | res, 87 | }, 88 | () => Promise.resolve() 89 | ).then(() => { 90 | const registeredKeys = registry._registry.allKeys(); 91 | assert(registeredKeys.length === 1); 92 | assert(registeredKeys[0].includes('requests-GET')); 93 | registry.shutdown(); 94 | }); 95 | res.finish(); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/test/unit/test-nodeOsMetrics.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const assert = require('assert'); 3 | const { validateMetric } = require('measured-core').metricValidators; 4 | const { nodeOsMetrics, createOSMetrics } = require('../../lib'); 5 | const TestReporter = require('./TestReporter'); 6 | const Registry = require('measured-reporting').SelfReportingMetricsRegistry; 7 | const { MetricTypes } = require('measured-core'); 8 | 9 | describe('nodeOsMetrics', () => { 10 | it('contains a map of string to functions that generate a valid metric object', () => { 11 | Object.keys(nodeOsMetrics).forEach(metricName => { 12 | assert(typeof metricName === 'string', 'The key should be a string'); 13 | 14 | const metricGeneratingFunction = nodeOsMetrics[metricName]; 15 | assert(typeof metricGeneratingFunction === 'function', 'metric generating function should be a function'); 16 | 17 | const metric = metricGeneratingFunction(); 18 | validateMetric(metric); 19 | 20 | const value = metric.toJSON(); 21 | const type = metric.getType(); 22 | if ([MetricTypes.COUNTER, MetricTypes.GAUGE].includes(type)) { 23 | assert(typeof value === 'number'); 24 | } else { 25 | assert(typeof value === 'object'); 26 | } 27 | 28 | if (metric.end) { 29 | metric.end(); 30 | } 31 | }); 32 | }); 33 | }); 34 | 35 | describe('createOSMetrics', () => { 36 | it('creates and registers a metric for every metric defined in nodeOsMetrics', () => { 37 | const reporter = new TestReporter(); 38 | const registry = new Registry(reporter); 39 | 40 | createOSMetrics(registry); 41 | 42 | const registeredKeys = registry._registry.allKeys(); 43 | const expectedKeys = Object.keys(nodeOsMetrics); 44 | assert(registeredKeys.length > 1); 45 | assert.deepEqual(registeredKeys, expectedKeys); 46 | 47 | registry.shutdown(); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/test/unit/test-nodeProcessMetrics.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const assert = require('assert'); 3 | const { validateMetric } = require('measured-core').metricValidators; 4 | const { nodeProcessMetrics, createProcessMetrics } = require('../../lib'); 5 | const TestReporter = require('./TestReporter'); 6 | const Registry = require('measured-reporting').SelfReportingMetricsRegistry; 7 | const { MetricTypes } = require('measured-core'); 8 | 9 | describe('nodeProcessMetrics', () => { 10 | it('contains a map of string to functions that generate a valid metric object', () => { 11 | Object.keys(nodeProcessMetrics).forEach(metricName => { 12 | assert(typeof metricName === 'string', 'The key should be a string'); 13 | 14 | const metricGeneratingFunction = nodeProcessMetrics[metricName]; 15 | assert(typeof metricGeneratingFunction === 'function', 'metric generating function should be a function'); 16 | 17 | const metric = metricGeneratingFunction(); 18 | validateMetric(metric); 19 | 20 | const value = metric.toJSON(); 21 | const type = metric.getType(); 22 | if ([MetricTypes.COUNTER, MetricTypes.GAUGE].includes(type)) { 23 | assert(typeof value === 'number'); 24 | } else { 25 | assert(typeof value === 'object'); 26 | } 27 | }); 28 | }); 29 | }); 30 | 31 | describe('createProcessMetrics', () => { 32 | it('creates and registers a metric for every metric defined in nodeProcessMetrics', () => { 33 | const reporter = new TestReporter(); 34 | const registry = new Registry(reporter); 35 | 36 | createProcessMetrics(registry); 37 | 38 | const registeredKeys = registry._registry.allKeys(); 39 | const expectedKeys = Object.keys(nodeProcessMetrics); 40 | assert(registeredKeys.length > 1); 41 | assert.deepEqual(registeredKeys, expectedKeys); 42 | 43 | registry.shutdown(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/measured-node-metrics/test/unit/utils/test-CpuUtils.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const assert = require('assert'); 3 | const CpuUtils = require('../../../lib/utils/CpuUtils'); 4 | 5 | describe('CpuUtils', () => { 6 | it('#cpuAverage ', () => { 7 | const measure = CpuUtils.cpuAverage(); 8 | assert(typeof measure.idle === 'number'); 9 | assert(measure.idle > 0); 10 | 11 | assert(typeof measure.total === 'number'); 12 | assert(measure.total > 0); 13 | }); 14 | 15 | it('#calculateCpuUsagePercent calculates a percent', () => { 16 | const start = CpuUtils.cpuAverage(); 17 | 18 | for (let i = 0; i < 10000000; i++) { 19 | Math.floor(Math.random() * Math.floor(10000000)); 20 | } 21 | 22 | const end = CpuUtils.cpuAverage(); 23 | const percent = CpuUtils.calculateCpuUsagePercent(start, end); 24 | assert(typeof percent === 'number'); 25 | assert(percent > 0); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/measured-reporting/README.md: -------------------------------------------------------------------------------- 1 | # Measured Reporting 2 | 3 | The registry and reporting library that has the classes needed to create a dimension aware, self reporting metrics registry. 4 | 5 | [![npm](https://img.shields.io/npm/v/measured-reporting.svg)](https://www.npmjs.com/package/measured-reporting) 6 | 7 | ## Install 8 | 9 | ``` 10 | npm install measured-reporting 11 | ``` 12 | 13 | ## What is in this package 14 | 15 | ### [Self Reporting Metrics Registry](https://yaorg.github.io/node-measured/SelfReportingMetricsRegistry.html) 16 | A dimensional aware self-reporting metrics registry, just supply this class with a reporter implementation at instantiation and this is all you need to instrument application level metrics in your app. 17 | 18 | See the [SelfReportingMetricsRegistryOptions](https://yaorg.github.io/node-measured/global.html#SelfReportingMetricsRegistryOptions) for advanced configuration. 19 | 20 | ```javascript 21 | const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting'); 22 | const registry = new SelfReportingMetricsRegistry(new LoggingReporter({ 23 | defaultDimensions: { 24 | hostname: os.hostname() 25 | } 26 | })); 27 | 28 | // The metric will flow through LoggingReporter#_reportMetrics(metrics) every 10 seconds by default 29 | const myCounter = registry.getOrCreateCounter('my-counter'); 30 | 31 | ``` 32 | 33 | ### [Reporter Abstract Class](https://yaorg.github.io/node-measured/Reporter.html) 34 | Extend this class and override the [_reportMetrics(metrics)](https://yaorg.github.io/node-measured/Reporter.html#_reportMetrics__anchor) method to create a vendor specific reporter implementation. 35 | 36 | See the [ReporterOptions](https://yaorg.github.io/node-measured/global.html#ReporterOptions) for advanced configuration. 37 | 38 | #### Current Implementations 39 | - [SignalFx Reporter](https://yaorg.github.io/node-measured/SignalFxMetricsReporter.html) in the `measured-signalfx-reporter` package. 40 | - reports metrics to SignalFx. 41 | - [Logging Reporter](https://yaorg.github.io/node-measured/LoggingReporter.html) in the `measured-reporting` package. 42 | - A reporter impl that simply logs the metrics via the Logger 43 | 44 | #### Creating an anonymous Implementation 45 | You can technically create an anonymous instance of this, see the following example. 46 | ```javascript 47 | const os = require('os'); 48 | const process = require('process'); 49 | const { SelfReportingMetricsRegistry, Reporter } = require('measured-reporting'); 50 | 51 | // Create a self reporting registry with an anonymous Reporter instance; 52 | const registry = new SelfReportingMetricsRegistry( 53 | new class extends Reporter { 54 | constructor() { 55 | super({ 56 | defaultDimensions: { 57 | hostname: os.hostname(), 58 | env: process.env['NODE_ENV'] ? process.env['NODE_ENV'] : 'unset' 59 | } 60 | }) 61 | } 62 | 63 | _reportMetrics(metrics) { 64 | metrics.forEach(metric => { 65 | console.log(JSON.stringify({ 66 | metricName: metric.name, 67 | dimensions: this._getDimensions(metric), 68 | data: metric.metricImpl.toJSON() 69 | })) 70 | }); 71 | } 72 | }() 73 | ); 74 | 75 | // create a gauge that reports the process uptime every second 76 | const processUptimeGauge = registry.getOrCreateGauge('node.process.uptime', () => process.uptime(), {}, 1); 77 | ``` 78 | 79 | Example output: 80 | ```bash 81 | APP5HTD6ACCD8C:foo jfiel2$ NODE_ENV=development node index.js 82 | {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":0.092} 83 | {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":1.099} 84 | {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":2.104} 85 | {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":3.105} 86 | {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":4.106} 87 | ``` 88 | 89 | 90 | Consider creating a proper class and contributing it back to Measured if it is generic and sharable. 91 | 92 | ### [Logging Reporter Class](https://yaorg.github.io/node-measured/LoggingReporter.html) 93 | A simple reporter that logs the metrics via the Logger. 94 | 95 | See the [ReporterOptions](http://yaorg.github.io/node-measured/build/docs/packages/measured-reporting/global.html#ReporterOptions) for advanced configuration. 96 | 97 | ```javascript 98 | const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting'); 99 | const registry = new SelfReportingMetricsRegistry(new LoggingReporter({ 100 | logger: myLogerImpl, // defaults to new console logger if not supplied 101 | defaultDimensions: { 102 | hostname: require('os').hostname() 103 | } 104 | })); 105 | ``` 106 | 107 | ## What are dimensions? 108 | As described by Signal Fx: 109 | 110 | *A dimension is a key/value pair that, along with the metric name, is part of the identity of a time series. 111 | You can filter and aggregate time series by those dimensions across SignalFx.* 112 | 113 | DataDog has a [nice blog post](https://www.datadoghq.com/blog/the-power-of-tagged-metrics/) about how they are used in their aggregator api. 114 | 115 | Graphite also supports the concept via [tags](http://graphite.readthedocs.io/en/latest/tags.html). 116 | 117 | -------------------------------------------------------------------------------- /packages/measured-reporting/lib/@types/types.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A wrapper object around a {@link Metric}, {@link Dimensions} and the metric name 3 | * 4 | * @interface MetricWrapper 5 | * @typedef MetricWrapper 6 | * @type {Object} 7 | * @property {string} name The supplied name of the Metric 8 | * @property {Metric} metricImpl The {@link Metric} object 9 | * @property {Dimensions} dimensions The {@link Dimensions} for the given {@link Metric} 10 | */ 11 | 12 | /** 13 | * A Dictionary of string, string key value pairs 14 | * 15 | * @interface Dimensions 16 | * @typedef Dimensions 17 | * @type {Object.} 18 | * 19 | * @example 20 | * { 21 | * path: "/api/foo" 22 | * method: "GET" 23 | * statusCode: "200" 24 | * } 25 | */ 26 | -------------------------------------------------------------------------------- /packages/measured-reporting/lib/index.js: -------------------------------------------------------------------------------- 1 | const SelfReportingMetricsRegistry = require('./registries/SelfReportingMetricsRegistry'); 2 | const Reporter = require('./reporters/Reporter'); 3 | const LoggingReporter = require('./reporters/LoggingReporter'); 4 | const inputValidators = require('./validators/inputValidators'); 5 | 6 | /** 7 | * The main measured module that is referenced when require('measured-reporting') is used. 8 | * @module measured-reporting 9 | */ 10 | module.exports = { 11 | /** 12 | * The Self Reporting Metrics Registry Class. 13 | * 14 | * @type {SelfReportingMetricsRegistry} 15 | */ 16 | SelfReportingMetricsRegistry, 17 | 18 | /** 19 | * The abstract / base Reporter class. 20 | * 21 | * @type {Reporter} 22 | */ 23 | Reporter, 24 | 25 | /** 26 | * The basic included reference reporter, simply logs the metrics. 27 | * See {ReporterOptions} for options. 28 | * 29 | * @type {LoggingReporter} 30 | */ 31 | LoggingReporter, 32 | 33 | /** 34 | * Various Input Validation functions. 35 | * 36 | * @type {inputValidators} 37 | */ 38 | inputValidators 39 | }; 40 | -------------------------------------------------------------------------------- /packages/measured-reporting/lib/registries/DimensionAwareMetricsRegistry.js: -------------------------------------------------------------------------------- 1 | const mapcap = require('mapcap'); 2 | 3 | /** 4 | * Simple registry that stores Metrics by name and dimensions. 5 | */ 6 | class DimensionAwareMetricsRegistry { 7 | /** 8 | * @param {DimensionAwareMetricsRegistryOptions} [options] Configurable options for the Dimension Aware Metrics Registry 9 | */ 10 | constructor(options) { 11 | options = options || {}; 12 | 13 | let metrics = new Map(); 14 | if (options.metricLimit) { 15 | metrics = mapcap(metrics, options.metricLimit, options.lru); 16 | } 17 | 18 | this._metrics = metrics; 19 | } 20 | 21 | /** 22 | * Checks to see if a metric with the given name and dimensions is present. 23 | * 24 | * @param {string} name The metric name 25 | * @param {Dimensions} dimensions The dimensions for the metric 26 | * @returns {boolean} true if the metric with given dimensions is present 27 | */ 28 | hasMetric(name, dimensions) { 29 | const key = this._generateStorageKey(name, dimensions); 30 | return this._metrics.has(key); 31 | } 32 | 33 | /** 34 | * Retrieves a metric with a given name and dimensions is present. 35 | * 36 | * @param {string} name The metric name 37 | * @param {Dimensions} dimensions The dimensions for the metric 38 | * @returns {Metric} a wrapper object around name, dimension and {@link Metric} 39 | */ 40 | getMetric(name, dimensions) { 41 | const key = this._generateStorageKey(name, dimensions); 42 | return this._metrics.get(key).metricImpl; 43 | } 44 | 45 | /** 46 | * Retrieves a metric by the calculated key (name / dimension combo). 47 | * 48 | * @param {string} key The registered key for the given registered {@link MetricWrapper} 49 | * @returns {MetricWrapper} a wrapper object around name, dimension and {@link Metric} 50 | */ 51 | getMetricWrapperByKey(key) { 52 | return this._metrics.get(key); 53 | } 54 | 55 | /** 56 | * Upserts a {@link Metric} in the internal storage map for a given name, dimension combo 57 | * 58 | * @param {string} name The metric name 59 | * @param {Metric} metric The {@link Metric} impl 60 | * @param {Dimensions} dimensions The dimensions for the metric 61 | * @return {string} The registry key for the metric, dimension combo 62 | */ 63 | putMetric(name, metric, dimensions) { 64 | const key = this._generateStorageKey(name, dimensions); 65 | this._metrics.set(key, { 66 | name: name, 67 | metricImpl: metric, 68 | dimensions: dimensions || {} 69 | }); 70 | return key; 71 | } 72 | 73 | /** 74 | * Returns an array of all keys of metrics stored in this registry. 75 | * @return {string[]} all keys of metrics stored in this registry. 76 | */ 77 | allKeys() { 78 | return Array.from(this._metrics.keys()); 79 | } 80 | 81 | /** 82 | * Generates a unique key off of the metric name and custom dimensions for internal use in the registry maps. 83 | * 84 | * @param {string} name The metric name 85 | * @param {Dimensions} dimensions The dimensions for the metric 86 | * @return {string} a unique key based off of the metric nae and dimensions 87 | * @private 88 | */ 89 | _generateStorageKey(name, dimensions) { 90 | let key = name; 91 | if (dimensions) { 92 | Object.keys(dimensions) 93 | .sort() 94 | .forEach(dimensionKey => { 95 | key = `${key}-${dimensions[dimensionKey]}`; 96 | }); 97 | } 98 | return key; 99 | } 100 | } 101 | 102 | module.exports = DimensionAwareMetricsRegistry; 103 | 104 | /** 105 | * Configurable options for the Dimension Aware Metrics Registry 106 | * 107 | * @interface DimensionAwareMetricsRegistryOptions 108 | * @typedef DimensionAwareMetricsRegistryOptions 109 | * @property {Number} metricLimit the maximum number of metrics the registry may hold before dropping metrics 110 | * @property {Boolean} lru switch dropping strategy from "least recently added" to "least recently used" 111 | */ 112 | -------------------------------------------------------------------------------- /packages/measured-reporting/lib/reporters/LoggingReporter.js: -------------------------------------------------------------------------------- 1 | const Reporter = require('./Reporter'); 2 | 3 | /** 4 | * A reporter impl that simply logs the metrics via the Logger. 5 | * 6 | * @example 7 | * const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting'); 8 | * const registry = new SelfReportingMetricsRegistry(new LoggingReporter()); 9 | * 10 | * @extends {Reporter} 11 | */ 12 | class LoggingReporter extends Reporter { 13 | /** 14 | * @param {LoggingReporterOptions} [options] 15 | */ 16 | constructor(options) { 17 | super(options); 18 | const level = (options || {}).logLevelToLogAt; 19 | this._logLevel = (level || 'info').toLowerCase(); 20 | } 21 | 22 | /** 23 | * Logs the metrics via the inherited logger instance. 24 | * @param {MetricWrapper[]} metrics 25 | * @protected 26 | */ 27 | _reportMetrics(metrics) { 28 | metrics.forEach(metric => { 29 | this._log[this._logLevel]( 30 | JSON.stringify({ 31 | metricName: metric.name, 32 | dimensions: this._getDimensions(metric), 33 | data: metric.metricImpl.toJSON() 34 | }) 35 | ); 36 | }); 37 | } 38 | } 39 | 40 | module.exports = LoggingReporter; 41 | 42 | /** 43 | * @interface LoggingReporterOptions 44 | * @typedef LoggingReporterOptions 45 | * @type {Object} 46 | * @property {Dimensions} defaultDimensions A dictionary of dimensions to include with every metric reported 47 | * @property {Logger} [logger] The logger to use, if not supplied a new Buynan logger will be created 48 | * @property {string} [logLevel] The log level to use with the created console logger if you didn't supply your own logger. 49 | * @property {number} [defaultReportingIntervalInSeconds] The default reporting interval to use if non is supplied when registering a metric, defaults to 10 seconds. 50 | * @property {string} [logLevelToLogAt] You can specify the log level ['debug', 'info', 'warn', 'error'] that this reporter will use when logging the metrics via the logger. 51 | */ 52 | -------------------------------------------------------------------------------- /packages/measured-reporting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "measured-reporting", 3 | "description": "The classes needed to create self reporting dimension aware metrics registries", 4 | "version": "2.0.0", 5 | "homepage": "https://yaorg.github.io/node-measured/", 6 | "engines": { 7 | "node": ">= 5.12" 8 | }, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "./lib/index.js", 13 | "scripts": { 14 | "clean": "rm -fr build", 15 | "format": "prettier --write './lib/**/*.{ts,js}'", 16 | "lint": "eslint lib --ext .js", 17 | "test:node": "mocha './test/**/test-*.js'", 18 | "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'", 19 | "test:browser": "exit 0", 20 | "test": "yarn test:node:coverage", 21 | "coverage": "nyc report --reporter=text-lcov | coveralls" 22 | }, 23 | "repository": { 24 | "url": "git://github.com/yaorg/node-measured.git" 25 | }, 26 | "dependencies": { 27 | "console-log-level": "^1.4.1", 28 | "mapcap": "^1.0.0", 29 | "measured-core": "^2.0.0", 30 | "optional-js": "^2.0.0" 31 | }, 32 | "files": [ 33 | "lib", 34 | "README.md" 35 | ], 36 | "license": "MIT", 37 | "devDependencies": { 38 | "jsdoc": "^3.5.5", 39 | "loglevel": "^1.6.1", 40 | "winston": "^2.4.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/measured-reporting/test/unit/registries/test-DimensionAwareMetricsRegistry.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const assert = require('assert'); 3 | const { Counter } = require('measured-core'); 4 | const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry'); 5 | 6 | describe('DimensionAwareMetricsRegistry', () => { 7 | let registry; 8 | beforeEach(() => { 9 | registry = new DimensionAwareMetricsRegistry(); 10 | }); 11 | 12 | it('hasMetric() returns true after putMetric() and getMetric() retrieves it, and it has the expected value', () => { 13 | const counter = new Counter({ 14 | count: 10 15 | }); 16 | 17 | const metricName = 'counter'; 18 | 19 | const dimensions = { 20 | foo: 'bar' 21 | }; 22 | 23 | assert(!registry.hasMetric(metricName, dimensions)); 24 | registry.putMetric(metricName, counter, dimensions); 25 | assert(registry.hasMetric(metricName, dimensions)); 26 | assert(counter === registry.getMetric(metricName, dimensions)); 27 | assert.equal(10, registry.getMetric(metricName, dimensions).toJSON()); 28 | }); 29 | 30 | it('getMetricByKey() returns the proper metric wrapper', () => { 31 | const counter = new Counter({ 32 | count: 10 33 | }); 34 | 35 | const metricName = 'counter'; 36 | 37 | const dimensions = { 38 | foo: 'bar' 39 | }; 40 | 41 | const key = registry.putMetric(metricName, counter, dimensions); 42 | assert(key.includes('counter-bar')); 43 | const wrapper = registry.getMetricWrapperByKey(key); 44 | assert.deepEqual(counter, wrapper.metricImpl); 45 | assert.deepEqual(dimensions, wrapper.dimensions); 46 | assert.equal(metricName, wrapper.name); 47 | }); 48 | 49 | it('#_generateStorageKey generates the same key for a metric name and dimensions with different ordering', () => { 50 | const metricName = 'the-metric-name'; 51 | const demensions1 = { 52 | foo: 'bar', 53 | bam: 'boo' 54 | }; 55 | const demensions2 = { 56 | bam: 'boo', 57 | foo: 'bar' 58 | }; 59 | 60 | const key1 = registry._generateStorageKey(metricName, demensions1); 61 | const key2 = registry._generateStorageKey(metricName, demensions2); 62 | 63 | assert.equal(key1, key2); 64 | }); 65 | 66 | it('#_generateStorageKey generates the same key for a metric name and dimensions when called 2x', () => { 67 | const metricName = 'the-metric-name'; 68 | const demensions1 = { 69 | foo: 'bar', 70 | bam: 'boo' 71 | }; 72 | 73 | const key1 = registry._generateStorageKey(metricName, demensions1); 74 | const key2 = registry._generateStorageKey(metricName, demensions1); 75 | 76 | assert.equal(key1, key2); 77 | }); 78 | 79 | it('#_generateStorageKey generates the same key for a metric name and no dimensions when called 2x', () => { 80 | const metricName = 'the-metric-name'; 81 | const demensions1 = {}; 82 | 83 | const key1 = registry._generateStorageKey(metricName, demensions1); 84 | const key2 = registry._generateStorageKey(metricName, demensions1); 85 | 86 | assert.equal(key1, key2); 87 | }); 88 | 89 | it('metricLimit limits metric count', () => { 90 | const limitedRegistry = new DimensionAwareMetricsRegistry({ 91 | metricLimit: 10 92 | }); 93 | 94 | const counter = new Counter({ 95 | count: 10 96 | }); 97 | 98 | const dimensions = { 99 | foo: 'bar' 100 | }; 101 | 102 | for (let i = 0; i < 20; i++) { 103 | limitedRegistry.putMetric(`metric #${i}`, counter, dimensions); 104 | } 105 | 106 | assert.equal(10, limitedRegistry._metrics.size); 107 | assert(!limitedRegistry.hasMetric('metric #0', dimensions)); 108 | }); 109 | 110 | it('lru changes metric dropping strategy', () => { 111 | const limitedRegistry = new DimensionAwareMetricsRegistry({ 112 | metricLimit: 10, 113 | lru: true 114 | }); 115 | 116 | const counter = new Counter({ 117 | count: 10 118 | }); 119 | 120 | const dimensions = { 121 | foo: 'bar' 122 | }; 123 | 124 | for (let i = 0; i < 10; i++) { 125 | limitedRegistry.putMetric(`metric #${i}`, counter, dimensions); 126 | } 127 | 128 | // Touch the first added metric 129 | limitedRegistry.getMetric('metric #0', dimensions); 130 | 131 | // Put a new metric in to trigger a drop 132 | limitedRegistry.putMetric('metric #11', counter, dimensions); 133 | 134 | // Verify that it dropped metric #1, not metric #0 135 | assert(limitedRegistry.hasMetric('metric #0', dimensions)); 136 | assert(!limitedRegistry.hasMetric('metric #1', dimensions)); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /packages/measured-reporting/test/unit/registries/test-SelfReportingMetricsRegistry.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const assert = require('assert'); 3 | const sinon = require('sinon'); 4 | const { Counter } = require('measured-core'); 5 | const { SelfReportingMetricsRegistry } = require('../../../lib'); 6 | const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry'); 7 | 8 | describe('SelfReportingMetricsRegistry', () => { 9 | let selfReportingRegistry; 10 | let reporter; 11 | let mockReporter; 12 | let registry; 13 | 14 | beforeEach(() => { 15 | registry = new DimensionAwareMetricsRegistry(); 16 | reporter = { 17 | reportMetricOnInterval() {}, 18 | setRegistry() {} 19 | }; 20 | mockReporter = sinon.mock(reporter); 21 | selfReportingRegistry = new SelfReportingMetricsRegistry(reporter, { registry }); 22 | }); 23 | 24 | it('throws an error if a metric has already been registered', () => { 25 | registry.putMetric('my-metric', new Counter(), {}); 26 | assert.throws(() => { 27 | selfReportingRegistry.register('my-metric', new Counter(), {}); 28 | }); 29 | }); 30 | 31 | it('#register registers the metric and informs the reporter to report', () => { 32 | const metricName = 'foo'; 33 | const reportInterval = 1; 34 | const metricKey = metricName; 35 | 36 | mockReporter 37 | .expects('reportMetricOnInterval') 38 | .once() 39 | .withArgs(metricKey, reportInterval); 40 | 41 | selfReportingRegistry.register(metricKey, new Counter(), {}, reportInterval); 42 | 43 | assert.equal(1, registry._metrics.size); 44 | 45 | mockReporter.restore(); 46 | mockReporter.verify(); 47 | }); 48 | 49 | it('#getOrCreateGauge creates a gauge and when called a second time returns the same gauge', () => { 50 | mockReporter.expects('reportMetricOnInterval').once(); 51 | 52 | const gauge = selfReportingRegistry.getOrCreateGauge('the-metric-name', () => 10, {}, 1); 53 | const theSameGauge = selfReportingRegistry.getOrCreateGauge('the-metric-name', () => 10, {}, 1); 54 | 55 | mockReporter.restore(); 56 | mockReporter.verify(); 57 | 58 | assert.deepEqual(gauge, theSameGauge); 59 | }); 60 | 61 | it('#getOrCreateHistogram creates and registers the metric and when called a second time returns the same metric', () => { 62 | mockReporter.expects('reportMetricOnInterval').once(); 63 | 64 | const metric = selfReportingRegistry.getOrCreateHistogram('the-metric-name', {}, 1); 65 | const theSameMetric = selfReportingRegistry.getOrCreateHistogram('the-metric-name', {}, 1); 66 | 67 | mockReporter.restore(); 68 | mockReporter.verify(); 69 | 70 | assert.deepEqual(metric, theSameMetric); 71 | }); 72 | 73 | it('#getOrCreateMeter creates and registers the metric and when called a second time returns the same metric', () => { 74 | mockReporter.expects('reportMetricOnInterval').once(); 75 | 76 | const metric = selfReportingRegistry.getOrCreateMeter('the-metric-name', {}, 1); 77 | const theSameMetric = selfReportingRegistry.getOrCreateMeter('the-metric-name', {}, 1); 78 | 79 | mockReporter.restore(); 80 | mockReporter.verify(); 81 | 82 | assert.deepEqual(metric, theSameMetric); 83 | metric.end(); 84 | }); 85 | 86 | it('#getOrCreateCounter creates and registers the metric and when called a second time returns the same metric', () => { 87 | mockReporter.expects('reportMetricOnInterval').once(); 88 | 89 | const metric = selfReportingRegistry.getOrCreateCounter('the-metric-name', {}, 1); 90 | const theSameMetric = selfReportingRegistry.getOrCreateCounter('the-metric-name', {}, 1); 91 | 92 | mockReporter.restore(); 93 | mockReporter.verify(); 94 | 95 | assert.deepEqual(metric, theSameMetric); 96 | }); 97 | 98 | it('#getOrCreateTimer creates and registers the metric and when called a second time returns the same metric', () => { 99 | mockReporter.expects('reportMetricOnInterval').once(); 100 | 101 | const metric = selfReportingRegistry.getOrCreateTimer('the-metric-name', {}, 1); 102 | const theSameMetric = selfReportingRegistry.getOrCreateTimer('the-metric-name', {}, 1); 103 | 104 | mockReporter.restore(); 105 | mockReporter.verify(); 106 | 107 | assert.deepEqual(metric, theSameMetric); 108 | metric.end(); 109 | }); 110 | 111 | it('#getOrCreateSettableGauge creates and registers the metric and when called a second time returns the same metric', () => { 112 | mockReporter.expects('reportMetricOnInterval').once(); 113 | 114 | const metric = selfReportingRegistry.getOrCreateSettableGauge('the-metric-name', {}, 1); 115 | const theSameMetric = selfReportingRegistry.getOrCreateSettableGauge('the-metric-name', {}, 1); 116 | 117 | mockReporter.restore(); 118 | mockReporter.verify(); 119 | 120 | assert.deepEqual(metric, theSameMetric); 121 | }); 122 | 123 | it('#getOrCreateCachedGauge creates and registers the metric and when called a second time returns the same metric', () => { 124 | mockReporter.expects('reportMetricOnInterval').once(); 125 | 126 | const metric = selfReportingRegistry.getOrCreateCachedGauge('the-metric-name', () => { 127 | return new Promise((r) => { r(10); }); 128 | }, 1, {}, 1); 129 | 130 | const theSameMetric = selfReportingRegistry.getOrCreateCachedGauge('the-metric-name', () => { 131 | return new Promise((r) => { r(10); }); 132 | }, 1, {}, 1); 133 | 134 | // clear the interval 135 | metric.end(); 136 | 137 | mockReporter.restore(); 138 | mockReporter.verify(); 139 | 140 | assert.deepEqual(metric, theSameMetric); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /packages/measured-reporting/test/unit/reporters/test-LoggingReporter.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const assert = require('assert'); 3 | const { LoggingReporter } = require('../../../lib'); 4 | 5 | describe('LoggingReporter', () => { 6 | let loggedMessages = []; 7 | let logger; 8 | beforeEach(() => { 9 | logger = { 10 | debug: (...msgs) => { 11 | loggedMessages.push('debug: ', ...msgs); 12 | }, 13 | info: (...msgs) => { 14 | loggedMessages.push('info: ', ...msgs); 15 | }, 16 | warn: (...msgs) => { 17 | loggedMessages.push('warn: ', ...msgs); 18 | }, 19 | error: (...msgs) => { 20 | loggedMessages.push('error: ', ...msgs); 21 | } 22 | }; 23 | }); 24 | 25 | it('uses the supplied log level', () => { 26 | let reporter = new LoggingReporter({ 27 | logger: logger, 28 | logLevelToLogAt: 'debug' 29 | }); 30 | 31 | reporter._reportMetrics([{ 32 | name: 'test', 33 | dimensions: {}, 34 | metricImpl: {toJSON: () => 5} 35 | }]); 36 | 37 | assert.equal(loggedMessages.shift(), "debug: "); 38 | assert.equal(loggedMessages.shift(), "{\"metricName\":\"test\",\"dimensions\":{},\"data\":5}") 39 | }); 40 | 41 | it('defaults to info level, if no override supplied', () => { 42 | it('uses the supplied log level', () => { 43 | let reporter = new LoggingReporter({ 44 | logger: logger, 45 | }); 46 | 47 | reporter._reportMetrics([{ 48 | name: 'test', 49 | dimensions: {}, 50 | metricImpl: {toJSON: () => 5} 51 | }]); 52 | 53 | assert.equal(loggedMessages.shift(), "info: "); 54 | assert.equal(loggedMessages.shift(), "{\"metricName\":\"test\",\"dimensions\":{},\"data\":5}") 55 | }); 56 | }); 57 | 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /packages/measured-reporting/test/unit/reporters/test-Reporter.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const assert = require('assert'); 3 | const TimeUnits = require('measured-core').units; 4 | const { Counter } = require('measured-core'); 5 | const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry'); 6 | const { Reporter } = require('../../../lib'); 7 | const { validateReporterInstance } = require('../../../lib/validators/inputValidators'); 8 | 9 | /** 10 | * @extends Reporter 11 | */ 12 | class TestReporter extends Reporter { 13 | constructor(options) { 14 | super(options); 15 | 16 | this._reportedMetrics = []; 17 | } 18 | 19 | getReportedMetrics() { 20 | return this._reportedMetrics; 21 | } 22 | 23 | _reportMetrics(metrics) { 24 | this._reportedMetrics.push(metrics); 25 | } 26 | } 27 | 28 | describe('Reporter', () => { 29 | let reporter; 30 | let counter = new Counter({ count: 5 }); 31 | let metricName = 'my-test-metric-key'; 32 | let metricKey; 33 | let metricInterval = 1; 34 | let metricDimensions = { 35 | hostname: 'instance-hostname', 36 | foo: 'bar' 37 | }; 38 | let registry; 39 | 40 | beforeEach(() => { 41 | registry = new DimensionAwareMetricsRegistry(); 42 | metricKey = registry.putMetric(metricName, counter, metricDimensions); 43 | reporter = new TestReporter(); 44 | reporter.setRegistry(registry); 45 | }); 46 | 47 | it('throws an error if you try to instantiate the abstract class', () => { 48 | assert.throws(() => { 49 | new Reporter(); 50 | }, /^TypeError: Can\'t instantiate abstract class\!$/); 51 | }); 52 | 53 | it('throws an error if _reportMetrics is not implemented', () => { 54 | class BadImpl extends Reporter {} 55 | assert.throws(() => { 56 | const badImpl = new BadImpl(); 57 | badImpl._reportMetrics([]); 58 | }, /method _reportMetrics\(metrics\) must be implemented/); 59 | }); 60 | 61 | it('throws an error if reportMetricOnInterval is called before setRegistry', () => { 62 | assert.throws(() => { 63 | let unsetReporter = new TestReporter(); 64 | unsetReporter.reportMetricOnInterval(metricKey, metricInterval); 65 | }, /must call setRegistry/); 66 | }); 67 | 68 | it('_reportMetricsWithInterval reports the test metric wrapper', () => { 69 | reporter._intervalToMetric[metricInterval] = new Set([metricKey]); 70 | reporter._reportMetricsWithInterval(metricInterval); 71 | assert.equal(reporter.getReportedMetrics().length, 1); 72 | assert.deepEqual(reporter.getReportedMetrics().shift(), [registry.getMetricWrapperByKey(metricKey)]); 73 | }); 74 | 75 | it('should report 5 times in a 5 second window with a metric set to be reporting every 1 second', (done, fail) => { 76 | reportAndWait(reporter, metricKey, metricInterval) 77 | .then(() => { 78 | reporter.shutdown(); 79 | const numberOfReports = reporter.getReportedMetrics().length; 80 | assert.equal(numberOfReports, 5); 81 | done(); 82 | }) 83 | .catch(() => { 84 | reporter.shutdown(); 85 | assert.fail('', '', ''); 86 | }); 87 | }).timeout(10000); 88 | 89 | it('should only create 1 interval for 2 metrics with the same reporting interval', () => { 90 | reporter.reportMetricOnInterval(metricKey, metricInterval); 91 | metricKey = registry.putMetric('foo', counter, metricDimensions); 92 | reporter.reportMetricOnInterval('foo', metricInterval); 93 | 94 | const intervalCount = reporter._intervals.length; 95 | assert.equal(1, intervalCount); 96 | reporter.shutdown(); 97 | }); 98 | 99 | it('should left merge dimensions with the metric dimensions taking precedence when _getDimensions is called', () => { 100 | let defaultDimensions = { 101 | hostname: 'instance-hostname', 102 | foo: 'bar' 103 | }; 104 | reporter = new TestReporter({ defaultDimensions }); 105 | const customDimensions = { 106 | foo: 'bam', 107 | region: 'us-west-2' 108 | }; 109 | const merged = reporter._getDimensions({ dimensions: customDimensions }); 110 | const expected = { 111 | hostname: 'instance-hostname', 112 | foo: 'bam', 113 | region: 'us-west-2' 114 | }; 115 | assert.deepEqual(expected, merged); 116 | }); 117 | 118 | it('Can be used to create an anonymous instance of a reporter', () => { 119 | const anonymousReporter = new class extends Reporter { 120 | _reportMetrics(metrics) { 121 | metrics.forEach(metric => console.log(JSON.stringify(metric))); 122 | } 123 | }(); 124 | 125 | validateReporterInstance(anonymousReporter); 126 | }); 127 | 128 | it('unrefs timers, when configured to', () => { 129 | let calledUnref = false; 130 | 131 | const timer = setTimeout(() => {}, 100); 132 | clearTimeout(timer); 133 | const proto = timer.constructor.prototype; 134 | const { unref } = proto; 135 | proto.unref = function wrappedUnref() { 136 | calledUnref = true; 137 | return unref.call(this); 138 | }; 139 | 140 | reporter = new TestReporter({ unrefTimers: true }); 141 | reporter.setRegistry(registry); 142 | reporter.reportMetricOnInterval(metricKey, metricInterval); 143 | 144 | proto.unref = unref; 145 | 146 | assert.ok(calledUnref); 147 | }); 148 | 149 | it('resets metrics, when configured to', () => { 150 | reporter = new TestReporter({ resetMetricsOnInterval: true }); 151 | reporter.setRegistry(registry); 152 | reporter._intervalToMetric[metricInterval] = new Set([metricKey]); 153 | reporter._reportMetricsWithInterval(metricInterval); 154 | 155 | const [[{ metricImpl }]] = reporter.getReportedMetrics(); 156 | 157 | assert.equal(metricImpl.toJSON(), 0); 158 | }); 159 | }); 160 | 161 | const reportAndWait = (reporter, metricKey, metricInterval) => { 162 | return new Promise(resolve => { 163 | reporter.reportMetricOnInterval(metricKey, metricInterval); 164 | setTimeout(() => { 165 | resolve(); 166 | }, 5 * TimeUnits.SECONDS); 167 | }); 168 | }; 169 | -------------------------------------------------------------------------------- /packages/measured-reporting/test/unit/validators/test-inputValidators.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const consoleLogLevel = require('console-log-level'); 3 | const loglevel = require('loglevel'); 4 | const winston = require('winston'); 5 | const assert = require('assert'); 6 | const { Counter } = require('measured-core'); 7 | const { 8 | validateGaugeOptions, 9 | validateOptionalLogger, 10 | validateMetric, 11 | validateReporterInstance, 12 | validateSelfReportingMetricsRegistryParameters, 13 | validateOptionalPublishingInterval, 14 | validateOptionalDimensions, 15 | validateMetricName, 16 | validateNumberReturningCallback 17 | } = require('../../../lib/validators/inputValidators'); 18 | 19 | describe('validateGaugeOptions', () => { 20 | it('it does nothing for the happy path', () => { 21 | validateGaugeOptions('foo', () => 10, { foo: 'bar' }, 1); 22 | }); 23 | 24 | it('throws an error if the call back returns an object', () => { 25 | assert.throws(() => { 26 | validateGaugeOptions( 27 | 'foo', 28 | () => { 29 | return { value: 10, anotherValue: 10 }; 30 | }, 31 | { foo: 'bar' }, 32 | 1 33 | ); 34 | }, /options.callback must return a number, actual return type: object/); 35 | }); 36 | }); 37 | 38 | describe('validateNumberReturningCallback', () => { 39 | it('throws an error if a non function is supplied', () => { 40 | assert.throws(() => { 41 | validateNumberReturningCallback({}); 42 | }, /must be function/); 43 | }); 44 | }); 45 | 46 | describe('validateOptionalLogger', () => { 47 | it('validates a Buynan logger', () => { 48 | const logger = consoleLogLevel({ name: 'consoleLogLevel-logger' }); 49 | validateOptionalLogger(logger); 50 | }); 51 | 52 | it('validates a Winston logger', () => { 53 | validateOptionalLogger(winston); 54 | }); 55 | 56 | it('validates a Loglevel logger', () => { 57 | validateOptionalLogger(loglevel); 58 | }); 59 | 60 | it('validates an artisanal logger', () => { 61 | validateOptionalLogger({ 62 | debug: (...msgs) => { 63 | console.log('debug: ', ...msgs); 64 | }, 65 | info: (...msgs) => { 66 | console.log('info: ', ...msgs); 67 | }, 68 | warn: (...msgs) => { 69 | console.log('warn: ', ...msgs); 70 | }, 71 | error: (...msgs) => { 72 | console.log('error: ', ...msgs); 73 | } 74 | }); 75 | }); 76 | 77 | it('throws an error when a logger is missing an expected method', () => { 78 | assert.throws(() => { 79 | validateOptionalLogger({}); 80 | }, /The logger that was passed in does not support/); 81 | }); 82 | 83 | it('does not throw an error if a logger is not passed in as an arg', () => { 84 | validateOptionalLogger(null); 85 | }); 86 | }); 87 | 88 | describe('validateMetric', () => { 89 | it('throws an error if the metric is undefined', () => { 90 | assert.throws(() => { 91 | validateMetric(undefined); 92 | }, /^TypeError: The metric was undefined, when it was required$/); 93 | }); 94 | 95 | it('throws an error if the metric is null', () => { 96 | assert.throws(() => { 97 | validateMetric(null); 98 | }, /^TypeError: The metric was undefined, when it was required$/); 99 | }); 100 | 101 | it('throws an error if toJSON is not a function', () => { 102 | assert.throws(() => { 103 | validateMetric({}); 104 | }, /must implement toJSON()/); 105 | }); 106 | 107 | it('throws an error if getType is not a function', () => { 108 | assert.throws(() => { 109 | validateMetric({ toJSON: () => {} }); 110 | }, /must implement getType()/); 111 | }); 112 | 113 | it('throws an error if #getType() does not return an expected value', () => { 114 | assert.throws(() => { 115 | validateMetric({ 116 | toJSON: () => {}, 117 | getType: () => { 118 | return 'foo'; 119 | } 120 | }); 121 | }, /Metric#getType\(\), must return a type defined in MetricsTypes/); 122 | }); 123 | 124 | it('does nothing when a valid metric is supplied', () => { 125 | validateMetric(new Counter()); 126 | }); 127 | }); 128 | 129 | describe('validateReporterInstance', () => { 130 | it('throws an error if undefined was passed in', () => { 131 | assert.throws(() => { 132 | validateReporterInstance(null); 133 | }, /The reporter was undefined/); 134 | }); 135 | 136 | it('throws an error if setRegistry is not a function', () => { 137 | assert.throws(() => { 138 | validateReporterInstance({}); 139 | }, /must implement setRegistry/); 140 | }); 141 | 142 | it('throws an error if reportMetricOnInterval is not a function', () => { 143 | assert.throws(() => { 144 | validateReporterInstance({ setRegistry: () => {} }); 145 | }, /must implement reportMetricOnInterval/); 146 | }); 147 | 148 | it('does nothing for a valid reporter instance', () => { 149 | validateReporterInstance({ setRegistry: () => {}, reportMetricOnInterval: () => {} }); 150 | }); 151 | }); 152 | 153 | describe('validateSelfReportingMetricsRegistryParameters', () => { 154 | it('does nothing when a reporter is passed in', () => { 155 | validateSelfReportingMetricsRegistryParameters([{ setRegistry: () => {}, reportMetricOnInterval: () => {} }]); 156 | }); 157 | }); 158 | 159 | describe('validateOptionalPublishingInterval', () => { 160 | it('throws an error if validateOptionalPublishingInterval is not a number', () => { 161 | assert.throws(() => { 162 | validateOptionalPublishingInterval('1'); 163 | }, /must be of type number/); 164 | }); 165 | }); 166 | 167 | describe('validateOptionalDimensions', () => { 168 | it('throws an error if passed dimensions is not an object', () => { 169 | assert.throws(() => { 170 | validateOptionalDimensions(1); 171 | }, /options.dimensions should be an object/); 172 | }); 173 | 174 | it('throws an error if passed dimensions is not an object.', () => { 175 | assert.throws(() => { 176 | validateOptionalDimensions(['thing', 'otherthing']); 177 | }, /dimensions where detected to be an array/); 178 | }); 179 | 180 | it('throws an error if passed dimensions is not an object has non string values for demension keys', () => { 181 | assert.throws(() => { 182 | validateOptionalDimensions({ someKeyThatIsANumber: 1 }); 183 | }, /should be of type string/); 184 | }); 185 | }); 186 | 187 | describe('validateMetricName', () => { 188 | it('throw an error if a non-string is passed', () => { 189 | assert.throws(() => { 190 | validateMetricName({}); 191 | }, /options.name is a required option and must be of type string/); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/README.md: -------------------------------------------------------------------------------- 1 | # Measured SignalFx Reporter 2 | 3 | This package ties together [measured-core](../measured-core) and [measured-reporting](../measured-reporting) to create a dimensional aware self reporting metrics registry that reports metrics to [SignalFx](https://signalfx.com/). 4 | 5 | ## Install 6 | 7 | ``` 8 | npm install measured-signalfx-reporter 9 | ``` 10 | 11 | ## What is in this package 12 | 13 | ### [SignalFxMetricsReporter](https://yaorg.github.io/node-measured/SignalFxMetricsReporter.html) 14 | A SignalFx specific implementation of the [Reporter Abstract Class](https://yaorg.github.io/node-measured/Reporter.html). 15 | 16 | ### [SignalFxSelfReportingMetricsRegistry](https://yaorg.github.io/node-measured/SignalFxSelfReportingMetricsRegistry.html) 17 | Extends [Self Reporting Metrics Registry](https://yaorg.github.io/node-measured/SelfReportingMetricsRegistry.html) but overrides methods that generate Meters to use the NoOpMeter. 18 | 19 | ### NoOpMeters 20 | 21 | Please note that this package ignores Meters by default. Meters do not make sense to use with SignalFx because the same 22 | values can be calculated using simple counters and the aggregation functions available within SignalFx itself. 23 | Additionally, this saves you money because SignalFx charges based on your DPM (Datapoints per Minute) consumption. 24 | 25 | This can be changed if anyone has a good argument for using Meters. Please file an issue. 26 | 27 | ### Usage 28 | 29 | See the full end to end example here: [SignalFx Express Full End to End Example](https://yaorg.github.io/node-measured/packages/measured-signalfx-reporter/tutorial-SignalFx%20Express%20Full%20End%20to%20End%20Example.html) 30 | 31 | ### Dev 32 | 33 | There is a user acceptance test server to test this library end-to-end with [SignalFx](https://signalfx.com/). 34 | 35 | ```bash 36 | SIGNALFX_API_KEY=xxxxx yarn uat:server 37 | ``` -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/lib/SignalFxEventCategories.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Different categories of events supported, within the SignalFx Event API. 3 | * 4 | * @example 5 | * const registry = new SignalFxSelfReportingMetricsRegistry(...); 6 | * registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT); 7 | * 8 | * @module SignalFxEventCategories 9 | */ 10 | module.exports = { 11 | /** 12 | * Created by user via UI or API, e.g. a deployment event 13 | * @type {SignalFxEventCategoryId} 14 | */ 15 | USER_DEFINED: 'USER_DEFINED', 16 | /** 17 | * Output by anomaly detectors 18 | * @type {SignalFxEventCategoryId} 19 | */ 20 | ALERT: 'ALERT', 21 | /** 22 | * Audit trail events 23 | * @type {SignalFxEventCategoryId} 24 | */ 25 | AUDIT: 'AUDIT', 26 | /** 27 | * Generated by analytics server 28 | * @type {SignalFxEventCategoryId} 29 | */ 30 | JOB: 'JOB', 31 | /** 32 | * Event originated within collectd 33 | * @type {SignalFxEventCategoryId} 34 | */ 35 | COLLECTD: 'COLLECTD', 36 | /** 37 | * Service discovery event 38 | * @type {SignalFxEventCategoryId} 39 | */ 40 | SERVICE_DISCOVERY: 'SERVICE_DISCOVERY', 41 | /** 42 | * Created by exception appenders to denote exceptional events 43 | * @type {SignalFxEventCategoryId} 44 | */ 45 | EXCEPTION: 'EXCEPTION' 46 | }; 47 | 48 | /** 49 | * @interface SignalFxEventCategoryId 50 | * @typedef SignalFxEventCategoryId 51 | * @type {string} 52 | * @example 53 | * const registry = new SignalFxSelfReportingMetricsRegistry(...); 54 | * registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT); 55 | */ 56 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/lib/index.js: -------------------------------------------------------------------------------- 1 | const SignalFxMetricsReporter = require('./reporters/SignalFxMetricsReporter'); 2 | const SignalFxSelfReportingMetricsRegistry = require('./registries/SignalFxSelfReportingMetricsRegistry'); 3 | const SignalFxEventCategories = require('./SignalFxEventCategories'); 4 | 5 | /** 6 | * The main measured module that is referenced when require('measured-signalfx-reporter') is used. 7 | * @module measured-signalfx-reporter 8 | */ 9 | module.exports = { 10 | /** 11 | * {@type SignalFxMetricsReporter} 12 | */ 13 | SignalFxMetricsReporter, 14 | /** 15 | * {@type SignalFxSelfReportingMetricsRegistry} 16 | */ 17 | SignalFxSelfReportingMetricsRegistry, 18 | /** 19 | * {@type SignalFxEventCategories} 20 | */ 21 | SignalFxEventCategories 22 | }; 23 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/lib/registries/SignalFxSelfReportingMetricsRegistry.js: -------------------------------------------------------------------------------- 1 | const { NoOpMeter, Timer } = require('measured-core'); 2 | const { SelfReportingMetricsRegistry } = require('measured-reporting'); 3 | const { validateTimerOptions } = require('measured-reporting').inputValidators; 4 | 5 | /** 6 | * A SignalFx Self Reporting Metrics Registry that disallows the use of meters. 7 | * Meters don't make sense to use with SignalFx because the rate aggregations can be done within SignalFx itself. 8 | * Meters simply waste DPM (Datapoints per Minute). 9 | * 10 | * @extends {SelfReportingMetricsRegistry} 11 | */ 12 | class SignalFxSelfReportingMetricsRegistry extends SelfReportingMetricsRegistry { 13 | /** 14 | * Creates a {@link Timer} or get the existing Timer for a given name and dimension combo with a NoOpMeter. 15 | * 16 | * @param {string} name The Metric name 17 | * @param {Dimensions} dimensions any custom {@link Dimensions} for the Metric 18 | * @param {number} publishingIntervalInSeconds a optional custom publishing interval 19 | * @return {Timer} 20 | */ 21 | getOrCreateTimer(name, dimensions, publishingIntervalInSeconds) { 22 | validateTimerOptions(name, dimensions, publishingIntervalInSeconds); 23 | 24 | let timer; 25 | if (this._registry.hasMetric(name, dimensions)) { 26 | timer = this._registry.getMetric(name, dimensions); 27 | } else { 28 | timer = new Timer({ meter: new NoOpMeter() }); 29 | const key = this._registry.putMetric(name, timer, dimensions); 30 | this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); 31 | } 32 | 33 | return timer; 34 | } 35 | 36 | /** 37 | * Meters are not reported to SignalFx. 38 | * Meters do not make sense to use with SignalFx because the same values can be calculated 39 | * using simple counters and aggregations within SignalFx itself. 40 | * 41 | * @param {string} name The Metric name 42 | * @param {Dimensions} dimensions any custom {@link Dimensions} for the Metric 43 | * @param {number} publishingIntervalInSeconds a optional custom publishing interval 44 | * @return {NoOpMeter|*} 45 | */ 46 | getOrCreateMeter(name, dimensions, publishingIntervalInSeconds) { 47 | this._log.error( 48 | 'Meters will not get reported using the SignalFx reporter as they waste DPM, please use a counter instead' 49 | ); 50 | return new NoOpMeter(); 51 | } 52 | 53 | /** 54 | * Function exposes the event API of Signal Fx. 55 | * See {@link https://github.com/signalfx/signalfx-nodejs#sending-events} for more details. 56 | * 57 | * @param {string} eventType The event type (name of the event time series). 58 | * @param {SignalFxEventCategoryId} [category] the category of event. See {@link module:SignalFxEventCategories}. Value by default is USER_DEFINED. 59 | * @param {Dimensions} [dimensions] a map of event dimensions, empty dictionary by default 60 | * @param {Object.} [properties] a map of extra properties on that event, empty dictionary by default 61 | * @param {number} [timestamp] a timestamp, by default is current time. 62 | * 63 | * @example 64 | * const { 65 | * SignalFxSelfReportingMetricsRegistry, 66 | * SignalFxMetricsReporter, 67 | * SignalFxEventCategories 68 | * } = require('measured-signalfx-reporter'); 69 | * const registry = new SignalFxSelfReportingMetricsRegistry(new SignalFxMetricsReporter(signalFxClient)); 70 | * registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT); 71 | */ 72 | sendEvent(eventType, category, dimensions, properties, timestamp) { 73 | return Promise.all( 74 | this._reporters.filter(reporter => typeof reporter.sendEvent === 'function').map(reporter => 75 | reporter.sendEvent(eventType, category, dimensions, properties, timestamp).catch(error => { 76 | return error; 77 | }) 78 | ) 79 | ); 80 | } 81 | } 82 | 83 | module.exports = SignalFxSelfReportingMetricsRegistry; 84 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/lib/validators/inputValidators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Validation functions for validating public input 3 | * @module SignalFxReporterInputValidators 4 | * @private 5 | */ 6 | module.exports = { 7 | /** 8 | * Validates that the object supplied for the sfx client at least has a send function 9 | * @param signalFxClient 10 | */ 11 | validateSignalFxClient: signalFxClient => { 12 | if (signalFxClient === undefined) { 13 | throw new Error('signalFxClient was undefined when it is required'); 14 | } 15 | 16 | if (typeof signalFxClient.send !== 'function' || signalFxClient.length < 1) { 17 | throw new Error('signalFxClient must implement send(data: any)'); 18 | } 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "measured-signalfx-reporter", 3 | "description": "A Registry Reporter that knows how to report core metrics to SignalFx", 4 | "version": "2.0.0", 5 | "homepage": "https://yaorg.github.io/node-measured/", 6 | "engines": { 7 | "node": ">= 5.12" 8 | }, 9 | "publishConfig": { 10 | "access": "public" 11 | }, 12 | "main": "./lib/index.js", 13 | "scripts": { 14 | "clean": "rm -fr build", 15 | "format": "prettier --write './lib/**/*.{ts,js}'", 16 | "lint": "eslint lib --ext .js", 17 | "test:node": "mocha './test/**/test-*.js'", 18 | "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'", 19 | "test:browser": "exit 0", 20 | "test": "yarn test:node:coverage", 21 | "coverage": "nyc report --reporter=text-lcov | coveralls", 22 | "uat:server": "node --inspect test/user-acceptance-test/index.js" 23 | }, 24 | "repository": { 25 | "url": "git://github.com/yaorg/node-measured.git" 26 | }, 27 | "dependencies": { 28 | "console-log-level": "^1.4.1", 29 | "measured-core": "^2.0.0", 30 | "measured-reporting": "^2.0.0", 31 | "optional-js": "^2.0.0" 32 | }, 33 | "files": [ 34 | "lib", 35 | "README.md" 36 | ], 37 | "license": "MIT", 38 | "devDependencies": { 39 | "express": "^4.16.3", 40 | "jsdoc": "^3.5.5", 41 | "signalfx": "^6.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/test/unit/registries/test-SignalFxSelfReportingMetricsRegistry.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const { SignalFxSelfReportingMetricsRegistry } = require('../../../lib'); 3 | const { Reporter } = require('measured-reporting'); 4 | const sinon = require('sinon'); 5 | const assert = require('assert'); 6 | 7 | /** 8 | * @extends Reporter 9 | */ 10 | class TestReporter extends Reporter { 11 | reportMetricOnInterval(metricKey, intervalInSeconds) {} 12 | _reportMetrics(metrics) {} 13 | } 14 | 15 | describe('SignalFxSelfReportingMetricsRegistry', () => { 16 | let registry; 17 | let reporter; 18 | let mockReporter; 19 | beforeEach(() => { 20 | reporter = new TestReporter(); 21 | mockReporter = sinon.mock(reporter); 22 | registry = new SignalFxSelfReportingMetricsRegistry(reporter); 23 | }); 24 | 25 | it('#getOrCreateTimer uses a no-op meter', () => { 26 | mockReporter.expects('reportMetricOnInterval').once(); 27 | const timer = registry.getOrCreateTimer('my-timer'); 28 | assert.equal(timer._meter.constructor.name, 'NoOpMeter'); 29 | const theSameTimer = registry.getOrCreateTimer('my-timer'); 30 | assert(timer === theSameTimer); 31 | }); 32 | 33 | it('#getOrCreateMeter uses a no-op meter', () => { 34 | mockReporter.expects('reportMetricOnInterval').never(); 35 | const meter = registry.getOrCreateMeter('my-meter'); 36 | assert.equal(meter.constructor.name, 'NoOpMeter'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/test/unit/reporters/test-SignalFxMetricsReporter.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const { SignalFxMetricsReporter } = require('../../../lib'); 3 | const { Histogram, MetricTypes, Gauge, Timer, Meter, Counter } = require('measured-core'); 4 | const assert = require('assert'); 5 | const sinon = require('sinon'); 6 | 7 | describe('SignalFxMetricsReporter', () => { 8 | const name = 'request'; 9 | const dimensions = { 10 | foo: 'bar' 11 | }; 12 | let reporter; 13 | let signalFxClient; 14 | let clientSpy; 15 | beforeEach(() => { 16 | signalFxClient = { 17 | send: data => { 18 | return new Promise(resolve => { 19 | resolve(); 20 | }); 21 | } 22 | }; 23 | clientSpy = sinon.spy(signalFxClient, 'send'); 24 | // noinspection JSCheckFunctionSignatures 25 | reporter = new SignalFxMetricsReporter(signalFxClient); 26 | }); 27 | 28 | it('#_reportMetrics sends the expected data to signal fx for a histogram', () => { 29 | const metric = new Histogram(); 30 | const metricMock = sinon.mock(metric); 31 | metricMock 32 | .expects('getType') 33 | .once() 34 | .returns(MetricTypes.HISTOGRAM); 35 | metricMock 36 | .expects('toJSON') 37 | .once() 38 | .returns({ 39 | min: 1, 40 | max: 10, 41 | sum: 100, 42 | variance: 55, 43 | mean: 5, 44 | stddev: 54, 45 | count: 20, 46 | median: 50, 47 | p75: 4, 48 | p95: 6, 49 | p99: 7, 50 | p999: 9 51 | }); 52 | 53 | const metricWrapper = { 54 | name: name, 55 | metricImpl: metric, 56 | dimensions: dimensions 57 | }; 58 | 59 | const expected = { 60 | gauges: [ 61 | { 62 | metric: `${name}.max`, 63 | value: '10', 64 | dimensions: dimensions 65 | }, 66 | { 67 | metric: `${name}.min`, 68 | value: '1', 69 | dimensions: dimensions 70 | }, 71 | { 72 | metric: `${name}.mean`, 73 | value: '5', 74 | dimensions: dimensions 75 | }, 76 | { 77 | metric: `${name}.p95`, 78 | value: '6', 79 | dimensions: dimensions 80 | }, 81 | { 82 | metric: `${name}.p99`, 83 | value: '7', 84 | dimensions: dimensions 85 | } 86 | ], 87 | cumulative_counters: [ 88 | { 89 | metric: `${name}.count`, 90 | value: '20', 91 | dimensions: dimensions 92 | } 93 | ] 94 | }; 95 | 96 | reporter._reportMetrics([metricWrapper]); 97 | 98 | assert( 99 | clientSpy.withArgs( 100 | sinon.match(actual => { 101 | assert.deepEqual(expected, actual); 102 | return true; 103 | }) 104 | ).calledOnce 105 | ); 106 | }); 107 | 108 | it('#_reportMetrics sends the expected data to signal fx for a gauge', () => { 109 | const metric = new Gauge(() => 10); 110 | 111 | const metricWrapper = { 112 | name: name, 113 | metricImpl: metric, 114 | dimensions: dimensions 115 | }; 116 | 117 | const expected = { 118 | gauges: [ 119 | { 120 | metric: `${name}`, 121 | value: '10', 122 | dimensions: dimensions 123 | } 124 | ] 125 | }; 126 | 127 | reporter._reportMetrics([metricWrapper]); 128 | 129 | assert( 130 | clientSpy.withArgs( 131 | sinon.match(actual => { 132 | assert.deepEqual(expected, actual); 133 | return true; 134 | }) 135 | ).calledOnce 136 | ); 137 | }); 138 | 139 | it('#_reportMetrics sends the expected data to signal fx for a counter', () => { 140 | const metric = new Counter({ count: 5 }); 141 | 142 | const metricWrapper = { 143 | name: name, 144 | metricImpl: metric, 145 | dimensions: dimensions 146 | }; 147 | 148 | const expected = { 149 | cumulative_counters: [ 150 | { 151 | metric: `${name}.count`, 152 | value: '5', 153 | dimensions: dimensions 154 | } 155 | ] 156 | }; 157 | 158 | reporter._reportMetrics([metricWrapper]); 159 | 160 | assert( 161 | clientSpy.withArgs( 162 | sinon.match(actual => { 163 | assert.deepEqual(expected, actual); 164 | return true; 165 | }) 166 | ).calledOnce 167 | ); 168 | }); 169 | 170 | it('#_reportMetrics sends the expected data to signal fx for a meter', () => { 171 | const metric = new Meter(); 172 | 173 | const metricWrapper = { 174 | name: name, 175 | metricImpl: metric, 176 | dimensions: dimensions 177 | }; 178 | 179 | reporter._reportMetrics([metricWrapper]); 180 | 181 | assert(clientSpy.withArgs({}).calledOnce); 182 | 183 | metric.end(); 184 | }); 185 | it('#_reportMetrics sends the expected data to signal fx for an array multiple metrics', () => { 186 | const metric = new Histogram(); 187 | const metricMock = sinon.mock(metric); 188 | metricMock 189 | .expects('getType') 190 | .once() 191 | .returns(MetricTypes.HISTOGRAM); 192 | metricMock 193 | .expects('toJSON') 194 | .once() 195 | .returns({ 196 | min: 1, 197 | max: 10, 198 | sum: 100, 199 | variance: 55, 200 | mean: 5, 201 | stddev: 54, 202 | count: 20, 203 | median: 50, 204 | p75: 4, 205 | p95: 6, 206 | p99: 7, 207 | p999: 9 208 | }); 209 | 210 | const histogramWRapper = { 211 | name: name, 212 | metricImpl: metric, 213 | dimensions: dimensions 214 | }; 215 | 216 | const counterWrapper = { 217 | name: 'my-counter', 218 | metricImpl: new Counter({ count: 6 }) 219 | }; 220 | 221 | const gaugeWrapper = { 222 | name: 'my-gauge', 223 | metricImpl: new Gauge(() => 8) 224 | }; 225 | 226 | const expected = { 227 | gauges: [ 228 | { 229 | metric: `${name}.max`, 230 | value: '10', 231 | dimensions: dimensions 232 | }, 233 | { 234 | metric: `${name}.min`, 235 | value: '1', 236 | dimensions: dimensions 237 | }, 238 | { 239 | metric: `${name}.mean`, 240 | value: '5', 241 | dimensions: dimensions 242 | }, 243 | { 244 | metric: `${name}.p95`, 245 | value: '6', 246 | dimensions: dimensions 247 | }, 248 | { 249 | metric: `${name}.p99`, 250 | value: '7', 251 | dimensions: dimensions 252 | }, 253 | { 254 | metric: 'my-gauge', 255 | value: '8', 256 | dimensions: {} 257 | } 258 | ], 259 | cumulative_counters: [ 260 | { 261 | metric: `${name}.count`, 262 | value: '20', 263 | dimensions: dimensions 264 | }, 265 | { 266 | metric: 'my-counter.count', 267 | value: '6', 268 | dimensions: {} 269 | } 270 | ] 271 | }; 272 | 273 | reporter._reportMetrics([histogramWRapper, counterWrapper, gaugeWrapper]); 274 | 275 | assert( 276 | clientSpy.withArgs( 277 | sinon.match(actual => { 278 | assert.deepEqual(expected, actual); 279 | return true; 280 | }) 281 | ).calledOnce 282 | ); 283 | }); 284 | 285 | it('#_reportMetrics doesnt add metrics tp]o send if a bad metric was supplied', () => { 286 | reporter._reportMetrics([ 287 | { 288 | name: 'something', 289 | metricImpl: { getType: () => 'something random' } 290 | } 291 | ]); 292 | 293 | assert(clientSpy.withArgs({}).calledOnce); 294 | }); 295 | 296 | it('#_reportMetrics sends the expected data to signal fx for a timer', () => {}); 297 | }); 298 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/test/unit/validators/test-inputValidators.js: -------------------------------------------------------------------------------- 1 | /*global describe, it, beforeEach, afterEach*/ 2 | const { validateSignalFxClient } = require('../../../lib/validators/inputValidators'); 3 | const assert = require('assert'); 4 | 5 | describe('validateRequiredSignalFxMetricsReporterParameters', () => { 6 | it('does nothing for the happy path', () => { 7 | validateSignalFxClient({ send: () => {} }); 8 | }); 9 | 10 | it('throws an error when a bad signal fx client is supplied', () => { 11 | assert.throws(() => { 12 | validateSignalFxClient({}); 13 | }, /signalFxClient must implement send\(data: any\)/); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/measured-signalfx-reporter/test/user-acceptance-test/index.js: -------------------------------------------------------------------------------- 1 | const signalfx = require('signalfx'); 2 | const express = require('express'); 3 | const consoleLogLevel = require('console-log-level'); 4 | const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry, SignalFxEventCategories } = require('../../lib'); 5 | const { createOSMetrics, createProcessMetrics, createExpressMiddleware } = require('../../../measured-node-metrics/lib'); 6 | const libraryMetadata = require('../../package'); 7 | 8 | const log = consoleLogLevel({ name: 'SelfReportingMetricsRegistry', level: 'info' }); 9 | 10 | const library = libraryMetadata.name; 11 | const version = libraryMetadata.version; 12 | 13 | const defaultDimensions = { 14 | app: library, 15 | app_version: version, 16 | env: 'test' 17 | }; 18 | 19 | /** 20 | * Get your api key from a secrets provider of some kind. 21 | * 22 | * Good examples: 23 | * 24 | *
  • S3 with KMS 25 | *
  • Cerberus 26 | *
  • AWS Secrets Manager 27 | *
  • Vault 28 | *
  • Confidant 29 | * 30 | * Bad examples: 31 | * 32 | *
  • Checked into SCM in plaintext as a property 33 | *
  • Set as a plaintext environment variable 34 | * 35 | * @return {string} Returns the resolved Signal Fx Api Key 36 | */ 37 | const apiKeyResolver = () => { 38 | // https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/ 39 | return process.env.SIGNALFX_API_KEY; 40 | }; 41 | 42 | // Create the signal fx client 43 | const signalFxClient = new signalfx.Ingest(apiKeyResolver(), { 44 | userAgents: library 45 | }); 46 | 47 | // Create the signal fx reporter with the client 48 | const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, { 49 | defaultDimensions: defaultDimensions, 50 | defaultReportingIntervalInSeconds: 10, 51 | logLevel: 'debug' 52 | }); 53 | 54 | // Create the self reporting metrics registry with the signal fx reporter 55 | const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' }); 56 | metricsRegistry.sendEvent('events.app.starting'); 57 | 58 | createOSMetrics(metricsRegistry); 59 | createProcessMetrics(metricsRegistry); 60 | 61 | const app = express(); 62 | // wire up the metrics middleware 63 | app.use(createExpressMiddleware(metricsRegistry)); 64 | 65 | app.get('/hello', (req, res) => { 66 | res.send('hello world'); 67 | }); 68 | 69 | app.get('/path2', (req, res) => { 70 | res.send('path2'); 71 | }); 72 | 73 | app.listen(8080, () => log.info('Example app listening on port 8080!')); 74 | 75 | process.on('SIGINT', async () => { 76 | log.info('SIG INT, exiting'); 77 | await metricsRegistry.sendEvent('events.app.exiting'); 78 | process.exit(0); 79 | }); 80 | 81 | 82 | process.on('uncaughtException', async (err) => { 83 | log.error('There was an uncaught error', err); 84 | await metricsRegistry.sendEvent('events.app.uncaught-exception', SignalFxEventCategories.ALERT, {err: JSON.stringify(err)}); 85 | }); 86 | -------------------------------------------------------------------------------- /scripts/generate-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | ROOT_DIR="${SCRIPT_DIR}/.." 5 | DOCSTRAP_PATH="${ROOT_DIR}/node_modules/ink-docstrap/template/" 6 | 7 | # Clear out old docs 8 | rm -fr ${ROOT_DIR}/build/docs 9 | 10 | # create the directory structure 11 | mkdir -p ${ROOT_DIR}/build/docs/{img,packages/{measured-core,measured-reporting,measured-signalfx-reporter}} 12 | 13 | # Copy the image assets 14 | cp ${ROOT_DIR}/documentation/assets/measured.* ${ROOT_DIR}/build/docs/img/ 15 | 16 | # Copy our docpath customizations into the docstrap template dir 17 | cp ${ROOT_DIR}/documentation/docstrap_customized/template/* ${DOCSTRAP_PATH} 18 | 19 | # Generate the complete API docs for all packages 20 | export PACKAGE_NAME=root 21 | jsdoc --recurse --configure ./.jsdoc.json \ 22 | --tutorials ${ROOT_DIR}/tutorials \ 23 | --template ${DOCSTRAP_PATH} \ 24 | --readme ${ROOT_DIR}/Readme.md \ 25 | --destination build/docs/ \ 26 | ${ROOT_DIR}/packages/**/lib/ 27 | 28 | # Create the docs for measured-core 29 | export PACKAGE_NAME=measured-core 30 | jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \ 31 | --tutorials ${ROOT_DIR}/tutorials \ 32 | --template ${DOCSTRAP_PATH} \ 33 | --readme ${ROOT_DIR}/packages/measured-core/README.md \ 34 | --destination build/docs/packages/measured-core/ \ 35 | ${ROOT_DIR}/packages/measured-core/lib/ 36 | 37 | # Create the docs for measured-reporting 38 | export PACKAGE_NAME=measured-reporting 39 | jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \ 40 | --tutorials ${ROOT_DIR}/tutorials \ 41 | --template ${DOCSTRAP_PATH} \ 42 | --readme ${ROOT_DIR}/packages/measured-reporting/README.md \ 43 | --destination build/docs/packages/measured-reporting/ \ 44 | ${ROOT_DIR}/packages/measured-reporting/lib/ 45 | 46 | # Create the docs for measured-signalfx-reporter 47 | export PACKAGE_NAME=measured-signalfx-reporter 48 | jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \ 49 | --tutorials ${ROOT_DIR}/tutorials \ 50 | --template ${DOCSTRAP_PATH} \ 51 | --readme ${ROOT_DIR}/packages/measured-signalfx-reporter/README.md \ 52 | --destination build/docs/packages/measured-signalfx-reporter/ \ 53 | ${ROOT_DIR}/packages/measured-signalfx-reporter/lib/ 54 | 55 | # Create the docs for measured-node-metrics 56 | export PACKAGE_NAME=measured-node-metrics 57 | jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \ 58 | --tutorials ${ROOT_DIR}/tutorials \ 59 | --template ${DOCSTRAP_PATH} \ 60 | --readme ${ROOT_DIR}/packages/measured-node-metrics/README.md \ 61 | --destination build/docs/packages/measured-node-metrics/ \ 62 | ${ROOT_DIR}/packages/measured-node-metrics/lib/ -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ###################################################################### 4 | # 5 | # This script is intended to be used in a Travis CI/CD env. 6 | # It assumes Travis has been set up with jq, and awk and the following secure env vars. 7 | # 8 | # GH_TOKEN: A github oath token with perms to create and push tags and call the releases API. 9 | # NPM_TOKEN: A npm auth token that can publish the packages. 10 | # 11 | ###################################################################### 12 | 13 | set -e 14 | 15 | LATEST_RELEASE_DATA=$(curl -s --header "Accept: application/json" -L https://github.com/yaorg/node-measured/releases/latest) 16 | LATEST_GITHUB_RELEASE=$(echo $LATEST_RELEASE_DATA | jq --raw-output ".tag_name" | sed 's/v\(.*\)/\1/') 17 | CURRENT_VERSION=$(cat lerna.json | jq --raw-output ".version") 18 | 19 | echo "Processing tag information to determine if release is major, minor or patch." 20 | echo "The current version tag is: ${CURRENT_VERSION}" 21 | echo "The new version tag is: ${LATEST_GITHUB_RELEASE}" 22 | 23 | if [ -z ${GH_TOKEN} ] 24 | then 25 | echo "GH_TOKEN is null, you must supply oath token for github. Aborting!" 26 | exit 1 27 | fi 28 | 29 | if [ -z ${NPM_TOKEN} ] 30 | then 31 | echo "NPM_TOKEN is null, you must supply auth token for npm. Aborting!" 32 | exit 1 33 | fi 34 | 35 | if [ -z ${LATEST_GITHUB_RELEASE} ] 36 | then 37 | echo "NEW_VERSION is null, aborting!" 38 | exit 1 39 | fi 40 | 41 | if [ -z ${CURRENT_VERSION} ] 42 | then 43 | echo "CURRENT_VERSION is null, aborting!" 44 | exit 1 45 | fi 46 | 47 | if [ ${CURRENT_VERSION} == ${LATEST_GITHUB_RELEASE} ] 48 | then 49 | echo "The current version and the new version are the same, aborting!" 50 | exit 1 51 | fi 52 | 53 | CD_VERSION=$(awk -v NEW_VERSION=${LATEST_GITHUB_RELEASE} -v CURRENT_VERSION=${CURRENT_VERSION} 'BEGIN{ 54 | split(NEW_VERSION,newVersionParts,/\./) 55 | split(CURRENT_VERSION,currentVersionParts,/\./) 56 | 57 | for (i=1;i in currentVersionParts;i++) { 58 | if (newVersionParts[i] != currentVersionParts[i]) { 59 | if (i == 1) { 60 | printf "major\n" 61 | } 62 | 63 | if (i == 2) { 64 | printf "minor\n" 65 | } 66 | 67 | if (i == 3) { 68 | printf "patch\n" 69 | } 70 | break 71 | } 72 | } 73 | }') 74 | 75 | echo 76 | echo "determined to use semver: '${CD_VERSION}' flag for lerna publish --cd-version" 77 | echo 78 | 79 | echo "Re-wireing origin remote to use GH_TOKEN" 80 | git remote rm origin 81 | git remote add origin https://fieldju:${GH_TOKEN}@github.com/yaorg/node-measured.git 82 | git fetch --all 83 | git checkout master 84 | 85 | echo "Deleting tag created by github to allow lerna to create it" 86 | RELEASE="v${LATEST_GITHUB_RELEASE}" 87 | git tag -d ${RELEASE} 88 | git push origin :refs/tags/${RELEASE} 89 | 90 | echo "Preparing .npmrc" 91 | echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc 92 | echo 'registry=http://registry.npmjs.org' >> .npmrc 93 | 94 | lerna publish --cd-version ${CD_VERSION} --yes --force-publish 95 | 96 | echo "Re-binding orphaned github release to tag, so that it shows up as latest release rather than draft release" 97 | RELEASE_ID=$(curl -s --header "Accept: application/vnd.github.v3+json" -H "Authorization: token ${GH_TOKEN}" -L https://api.github.com/repos/yaorg/node-measured/releases | jq --arg RELEASE ${RELEASE} -r '.[] | select(.name==$RELEASE) | .id') 98 | curl --request PATCH --data '{"draft":"false"}' -s --header "Accept: application/vnd.github.v3+json" -H "Authorization: token ${GH_TOKEN}" -L https://api.github.com/repos/yaorg/node-measured/releases/${RELEASE_ID} -------------------------------------------------------------------------------- /tutorials/SignalFx Express Full End to End Example.md: -------------------------------------------------------------------------------- 1 | ### Using Measured to instrument OS, Process and Express Metrics. 2 | 3 | This tutorial shows how to use the measured libraries to fully instrument OS and Node Process metrics as well as create an express middleware. 4 | 5 | The middleware will measure request count, latency distributions (req/res time histogram) and add dimensions to make it filterable by request method, response status code, request uri path. 6 | 7 | **NOTE:** You must add `app.use(createExpressMiddleware(...))` **before** the use of any express bodyParsers like `app.use(express.json())` because requests that are first handled by a bodyParser will not get measured. 8 | 9 | ```javascript 10 | const os = require('os'); 11 | const signalfx = require('signalfx'); 12 | const express = require('express'); 13 | const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry } = require('measured-signalfx-reporter'); 14 | const { createProcessMetrics, createOSMetrics, createExpressMiddleware } = require('measured-node-metrics'); 15 | const libraryMetadata = require('./package'); // get metadata from package.json 16 | 17 | const library = libraryMetadata.name; 18 | const version = libraryMetadata.version; 19 | 20 | // Report process and os stats 1x per minute 21 | const PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS = 60; 22 | // Report the request count and histogram stats every 10 seconds 23 | const REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10; 24 | 25 | const defaultDimensions = { 26 | app: library, 27 | app_version: version, 28 | env: 'test' 29 | }; 30 | 31 | /** 32 | * Get your api key from a secrets provider of some kind. 33 | * 34 | * Good examples: 35 | * 36 | *
  • S3 with KMS 37 | *
  • Cerberus 38 | *
  • AWS Secrets Manager 39 | *
  • Vault 40 | *
  • Confidant 41 | * 42 | * Bad examples: 43 | * 44 | *
  • Checked into SCM in plaintext as a property 45 | *
  • Set as a plaintext environment variable 46 | * 47 | * @return {string} Returns the resolved Signal Fx Api Key 48 | */ 49 | const apiKeyResolver = () => { 50 | // https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/ 51 | return process.env.SIGNALFX_API_KEY; 52 | }; 53 | 54 | // Create the signal fx client 55 | const signalFxClient = new signalfx.Ingest(apiKeyResolver(), { 56 | userAgents: library 57 | }); 58 | 59 | // Create the signal fx reporter with the client 60 | const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, { 61 | defaultDimensions: defaultDimensions, 62 | defaultReportingIntervalInSeconds: 10, 63 | logLevel: 'debug' 64 | }); 65 | 66 | // Create the self reporting metrics registry with the signal fx reporter 67 | const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' }); 68 | 69 | createOSMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS); 70 | createProcessMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS); 71 | 72 | const app = express(); 73 | // wire up the metrics middleware 74 | app.use(createExpressMiddleware(metricsRegistry, REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS)); 75 | 76 | app.get('/hello', (req, res) => { 77 | res.send('hello world'); 78 | }); 79 | 80 | app.get('/path2', (req, res) => { 81 | res.send('path2'); 82 | }); 83 | 84 | app.listen(8080, () => log.info('Example app listening on port 8080!')); 85 | ``` 86 | -------------------------------------------------------------------------------- /tutorials/SignalFx Koa Full End to End Example.md: -------------------------------------------------------------------------------- 1 | ### Using Measured to instrument OS, Process and Koa Metrics. 2 | 3 | This tutorial shows how to use the measured libraries to fully instrument OS and Node Process metrics as well as create a Koa middleware. 4 | 5 | The middleware will measure request count, latency distributions (req/res time histogram) and add dimensions to make it filterable by request method, response status code, request uri path. 6 | 7 | **NOTE:** You must add `app.use(createKoaMiddleware(...))` **before** the use of any Koa bodyParsers like `app.use(KoaBodyParser())` because requests that are first handled by a bodyParser will not get measured. 8 | 9 | ```javascript 10 | const os = require('os'); 11 | const signalfx = require('signalfx'); 12 | const Koa = require('koa'); 13 | const KoaBodyParser = require('koa-bodyparser'); 14 | const Router = require('koa-router'); 15 | const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry } = require('measured-signalfx-reporter'); 16 | const { createProcessMetrics, createOSMetrics, createKoaMiddleware } = require('measured-node-metrics'); 17 | const libraryMetadata = require('./package'); // get metadata from package.json 18 | 19 | const library = libraryMetadata.name; 20 | const version = libraryMetadata.version; 21 | 22 | // Report process and os stats 1x per minute 23 | const PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS = 60; 24 | // Report the request count and histogram stats every 10 seconds 25 | const REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10; 26 | 27 | const defaultDimensions = { 28 | app: library, 29 | app_version: version, 30 | env: 'test' 31 | }; 32 | 33 | /** 34 | * Get your api key from a secrets provider of some kind. 35 | * 36 | * Good examples: 37 | * 38 | *
  • S3 with KMS 39 | *
  • Cerberus 40 | *
  • AWS Secrets Manager 41 | *
  • Vault 42 | *
  • Confidant 43 | * 44 | * Bad examples: 45 | * 46 | *
  • Checked into SCM in plaintext as a property 47 | *
  • Set as a plaintext environment variable 48 | * 49 | * @return {string} Returns the resolved Signal Fx Api Key 50 | */ 51 | const apiKeyResolver = () => { 52 | // https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/ 53 | return process.env.SIGNALFX_API_KEY; 54 | }; 55 | 56 | // Create the signal fx client 57 | const signalFxClient = new signalfx.Ingest(apiKeyResolver(), { 58 | userAgents: library 59 | }); 60 | 61 | // Create the signal fx reporter with the client 62 | const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, { 63 | defaultDimensions: defaultDimensions, 64 | defaultReportingIntervalInSeconds: 10, 65 | logLevel: 'debug' 66 | }); 67 | 68 | // Create the self reporting metrics registry with the signal fx reporter 69 | const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' }); 70 | 71 | createOSMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS); 72 | createProcessMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS); 73 | 74 | const app = new Koa(); 75 | router = new Router(); 76 | 77 | router.get('/hello', (req, res) => { 78 | res.send('hello world'); 79 | }); 80 | 81 | router.get('/path2', (req, res) => { 82 | res.send('path2'); 83 | }); 84 | 85 | // wire up the metrics middleware 86 | app.use(createKoaMiddleware(metricsRegistry, REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS)); 87 | app.use(KoaBodyParser()); 88 | app.use(router.routes()); 89 | 90 | app.listen(8080, () => log.info('Example app listening on port 8080!')); 91 | ``` 92 | --------------------------------------------------------------------------------