├── .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 | [](http://travis-ci.org/yaorg/node-measured) [](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 | [](https://www.npmjs.com/package/measured-core) [](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 | [](https://www.npmjs.com/package/measured-reporting) [](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 | [](https://www.npmjs.com/package/measured-node-metrics) [](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 | [](https://www.npmjs.com/package/measured-signalfx-reporter) [](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 | [](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 | [](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 | [](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 |
--------------------------------------------------------------------------------