Repository: felixge/node-measured Branch: master Commit: 916cb3ae8aa7 Files: 90 Total size: 248.9 KB Directory structure: gitextract_rxmrbihn/ ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .jsdoc.json ├── .prettierrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Readme.md ├── documentation/ │ ├── assets/ │ │ └── measured.license.md │ └── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ node_modules/ ================================================ FILE: .eslintrc.json ================================================ { "extends": "airbnb/base", "env": { "jest": true }, "rules": { "max-len": 0, "no-underscore-dangle": 0, "no-use-before-define": 0, "object-shorthand": 0, "comma-dangle": 0, "class-methods-use-this": 0, "no-param-reassign": 0, "no-constant-condition": 0, "no-plusplus": 0, "one-var-declaration-per-line": 0, "one-var": 0, "prefer-destructuring": ["error", { "array": false, "object": true }], "arrow-body-style": 0, "no-mixed-operators": 0, "arrow-parens": 0, "function-paren-newline": 0, "no-unused-vars": 0, "object-curly-newline": 0, "spaced-comment": 0 } } ================================================ FILE: .gitignore ================================================ # Created by .ignore support plugin (hsz.mobi) ### Node template # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next # Idea .idea **build/ *.un~ **/.DS_Store ================================================ FILE: .jsdoc.json ================================================ { "tags": { "allowUnknownTags": true }, "plugins": ["plugins/markdown"], "templates": { "logoFile": "img/measured.png", "cleverLinks": false, "monospaceLinks": false, "dateFormat": "ddd MMM Do YYYY", "outputSourceFiles": true, "outputSourcePath": true, "systemName": "Measured", "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", "navType": "vertical", "theme": "cosmo", "linenums": true, "collapseSymbols": false, "inverseNav": true, "protocol": "html://", "methodHeadingReturns": false, "index": { "root": "./", "measured-core": "../../", "measured-reporting": "../../", "measured-signalfx-reporter": "../../" }, "analytics":{"ua":"UA-119781742-1", "domain":"yaorg.github.io"} }, "markdown": { "parser": "gfm", "hardwrap": true } } ================================================ FILE: .prettierrc ================================================ printWidth: 120 tabWidth: 2 semi: true singleQuote: true bracketSpacing: true ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 11 - 10 - 8 before_install: - sudo apt-get -qq update - sudo apt-get install -y gawk jq jobs: include: - stage: publish code coverage node_js: "8" script: - npm run test:node:coverage - yarn coverage - stage: release docs if: type != pull_request AND branch = master node_js: "8" script: - yarn generate-docs - yarn travis-deploy-github-pages - stage: lerna publish and npm release if: type != pull_request AND tag IS present node_js: "8" script: - scripts/publish.sh sudo: required dist: xenial addons: chrome: stable env: global: - secure: ZKLEofNJCHSzJzfmayPgRdDWqD5N5g7OPs+gJELmwopAKCUaoOH4H5iH0eg1TZzcAoPL/qwU+gheEW1V7Io4PmVB2WUXUdBP9vaqZlrIw/sa6RD/51jpx4MkXd6ciXl1vmiWMnqPBj+S0NuYjKOt1tGMPDiOtK96UUVNA8uRMrs= - secure: sAfy698oA7zJdVEYt+MrBFlmbXIC0Xg+PSIA6bc6klp11yfgXS3vIXM5cW4MuijLJhxvPDmGzhsgVHB+pIQXffeVYKQypCp6JcoDG6OyUxUglvHoLaIfpMD+jQzu2I05EFCxyRZWrq+i5tMWZjkUPUfRJrXtnXEYqm2A5cWPOHs= ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Pull requests are welcome, please keep conversations and commit messages civil. 1. Fork, then clone the repo. 1. Set up your machine. ```bash yarn && yarn bootstrap ``` 1. Make sure the tests pass. ```bash yarn test ``` 1. Make your change. Add tests for your change. Make the linter and tests pass. 1. Document changes using the [jsdoc standards](http://usejsdoc.org/) to ensure that our code self-generates good documentation. build the jsdoc site, the files will be outputted to `./build/docs/` ```bash yarn generate-docs ``` Validate that your generated documentation is what you intended, as documentation gets autogenerated from master. 1. Run prettier to format code to comply with code style (Modified AirBnB Rule set: [ES Lint Config](.eslintrc.json)) ```bash yarn format ``` 1. Make sure sure the linter and unit tests pass and the docs can be generated. ```bash yarn test ``` 1. Push to your fork and submit a pull request and wait for peer review. We may suggest some changes or improvements or alternatives. Some things that will increase the chance that your pull request is accepted: 1. Your code must be documented inline via jsdoc annotations, tested, and pass the linter. 1. Your changes must not break the existing library API without good justification. 1. Your commit messages should be reasonable. (`git rebase -i head~n` choose the r option to reword you commits). 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/) ## Releasing a new version 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). 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/): 1. MAJOR version when you make incompatible API changes (e.g. `1.15.0` -> `2.0.0`) 2. MINOR version when you add functionality in a backwards-compatible manner (e.g. `1.15.0` -> `1.16.0`) 3. PATCH version when you make backwards-compatible bug fixes (e.g. `1.15.0` -> `1.15.1` 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). 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). 2. The tag version and release title is expected to be the same and in the following format: `v1.43.0` 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). ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Yet Another Org and Contributors Copyright (c) 2012-2017 Felix Geisendörfer and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Readme.md ================================================ # Measured Node libraries for measuring and reporting application-level metrics. Measured is heavily inspired by Coda Hale, Yammer Inc's [Dropwizard Metrics Libraries](https://github.com/dropwizard/metrics) [![Build Status](https://secure.travis-ci.org/yaorg/node-measured.png?branch=master)](http://travis-ci.org/yaorg/node-measured) [![Coverage Status](https://coveralls.io/repos/github/yaorg/node-measured/badge.svg?branch=master)](https://coveralls.io/github/yaorg/node-measured?branch=master) ## Available packages ### [Measured Core](packages/measured-core) **The core measured library that has the Metric interfaces and implementations.** [![npm](https://img.shields.io/npm/v/measured-core.svg)](https://www.npmjs.com/package/measured-core) [![downloads](https://img.shields.io/npm/dm/measured-core.svg)](https://www.npmjs.com/package/measured-core) ### [Measured Reporting](packages/measured-reporting) **The registry and reporting library that has the classes needed to create a dimension aware, self reporting metrics registry.** [![npm](https://img.shields.io/npm/v/measured-reporting.svg)](https://www.npmjs.com/package/measured-reporting) [![downloads](https://img.shields.io/npm/dm/measured-reporting.svg)](https://www.npmjs.com/package/measured-reporting) ### [Measured Node Metrics](packages/measured-node-metrics) **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.** [![npm](https://img.shields.io/npm/v/measured-node-metrics.svg)](https://www.npmjs.com/package/measured-node-metrics) [![downloads](https://img.shields.io/npm/dm/measured-node-metrics.svg)](https://www.npmjs.com/package/measured-node-metrics) ### [Measured SignalFx Reporter](packages/measured-signalfx-reporter) **A reporter that can be used with measured-reporting to send metrics to [SignalFx](https://signalfx.com/).** [![npm](https://img.shields.io/npm/v/measured-signalfx-reporter.svg)](https://www.npmjs.com/package/measured-signalfx-reporter) [![downloads](https://img.shields.io/npm/dm/measured-signalfx-reporter.svg)](https://www.npmjs.com/package/measured-signalfx-reporter) ### Measured Datadog reporter **Not implemented, community contribution wanted.** ### Measured Graphite reporter **Not implemented, community contribution wanted.** ## Development and Contributing See [Development and Contributing](https://github.com/yaorg/node-measured/blob/master/CONTRIBUTING.md) ## License This project Measured and all of its modules are licensed under the [MIT license](https://github.com/yaorg/node-measured/blob/master/LICENSE). ================================================ FILE: documentation/assets/measured.license.md ================================================ measured.png and measured.svg by [SimpleIcon](http://www.simpleicon.com/) available at [flaticon](http://www.flaticon.com/packs/simpleicon-business) Licenced by [CC BY 3.0](https://creativecommons.org/licenses/by/3.0), via Wikimedia Commons ================================================ FILE: documentation/docstrap_customized/template/publish.js ================================================ "use strict"; /** * @module template/publish * @type {*} */ /*global env: true */ var template = require('jsdoc/template'), doop = require('jsdoc/util/doop'), fs = require('jsdoc/fs'), _ = require('underscore'), path = require('jsdoc/path'), taffy = require('taffydb').taffy, handle = require('jsdoc/util/error').handle, helper = require('jsdoc/util/templateHelper'), moment = require("moment"), htmlsafe = helper.htmlsafe, sanitizeHtml = require('sanitize-html'), linkto = helper.linkto, resolveAuthorLinks = helper.resolveAuthorLinks, scopeToPunc = helper.scopeToPunc, hasOwnProp = Object.prototype.hasOwnProperty, conf = env.conf.templates || {}, data, view, outdir = env.opts.destination, searchEnabled = conf.search !== false; var globalUrl = helper.getUniqueFilename('global'); var indexUrl = helper.getUniqueFilename('index'); var packageName = process.env.PACKAGE_NAME; var rootPath = !conf.index ? indexUrl : conf.index[packageName]; var navOptions = { includeDate: conf.includeDate !== false, logoFile: conf.logoFile ? rootPath + conf.logoFile : "", systemName: conf.systemName || "Documentation", navType: conf.navType || "vertical", footer: conf.footer || "", copyright: conf.copyright || "", theme: conf.theme || "simplex", syntaxTheme: conf.syntaxTheme || "default", linenums: conf.linenums, collapseSymbols: conf.collapseSymbols || false, inverseNav: conf.inverseNav, outputSourceFiles: conf.outputSourceFiles === true, sourceRootPath: conf.sourceRootPath, disablePackagePath: conf.disablePackagePath, outputSourcePath: conf.outputSourcePath, dateFormat: conf.dateFormat, analytics: conf.analytics || null, methodHeadingReturns: conf.methodHeadingReturns === true, sort: conf.sort, search: searchEnabled, index: rootPath }; var searchableDocuments = {}; var navigationMaster = { index: { title: navOptions.systemName, link: navOptions.index, members: [] }, namespace: { title: "Namespaces", link: helper.getUniqueFilename("namespaces.list"), members: [] }, module: { title: "Modules", link: helper.getUniqueFilename("modules.list"), members: [] }, class: { title: "Classes", link: helper.getUniqueFilename('classes.list'), members: [] }, mixin: { title: "Mixins", link: helper.getUniqueFilename("mixins.list"), members: [] }, event: { title: "Events", link: helper.getUniqueFilename("events.list"), members: [] }, interface: { title: "Interfaces", link: helper.getUniqueFilename("interfaces.list"), members: [] }, tutorial: { title: "Tutorials", link: helper.getUniqueFilename("tutorials.list"), members: [] }, global: { title: "Global", link: globalUrl, members: [] }, external: { title: "Externals", link: helper.getUniqueFilename("externals.list"), members: [] } }; function find(spec) { return helper.find(data, spec); } function tutoriallink(tutorial) { return helper.toTutorial(tutorial, null, { tag: 'em', classname: 'disabled', prefix: 'Tutorial: ' }); } function getAncestorLinks(doclet) { return helper.getAncestorLinks(data, doclet); } function hashToLink(doclet, hash) { if (!/^(#.+)/.test(hash)) { return hash; } var url = helper.createLink(doclet); url = url.replace(/(#.+|$)/, hash); return '' + hash + ''; } function needsSignature(doclet) { var needsSig = false; // function and class definitions always get a signature if (doclet.kind === 'function' || doclet.kind === 'class') { needsSig = true; } // typedefs that contain functions get a signature, too else if (doclet.kind === 'typedef' && doclet.type && doclet.type.names && doclet.type.names.length) { for (var i = 0, l = doclet.type.names.length; i < l; i++) { if (doclet.type.names[i].toLowerCase() === 'function') { needsSig = true; break; } } } return needsSig; } function addSignatureParams(f) { var optionalClass = 'optional'; var params = helper.getSignatureParams(f, optionalClass); f.signature = (f.signature || '') + '('; for (var i = 0, l = params.length; i < l; i++) { var element = params[i]; var seperator = (i > 0) ? ', ' : ''; if (!new RegExp("class=[\"|']"+optionalClass+"[\"|']").test(element)) { f.signature += seperator + element; } else { var regExp = new RegExp("(.*?)<\\/span>", "i"); f.signature += element.replace(regExp, " $`["+seperator+"$1$']"); } } f.signature += ')'; } function addSignatureReturns(f) { if (navOptions.methodHeadingReturns) { var returnTypes = helper.getSignatureReturns(f); f.signature = '' + (f.signature || '') + '' + '' + (returnTypes.length ? ' → {' + returnTypes.join('|') + '}' : '') + ''; } else { f.signature = f.signature || ''; } } function addSignatureTypes(f) { var types = helper.getSignatureTypes(f); f.signature = (f.signature || '') + '' + (types.length ? ' :' + types.join('|') : '') + ''; } function addAttribs(f) { var attribs = helper.getAttribs(f); f.attribs = '' + htmlsafe(attribs.length ? '<' + attribs.join(', ') + '> ' : '') + ''; } function shortenPaths(files, commonPrefix) { // // always use forward slashes // var regexp = new RegExp( '\\\\', 'g' ); // // var prefix = commonPrefix.toLowerCase().replace( regexp, "/" ); // // Object.keys( files ).forEach( function ( file ) { // files[file].shortened = files[file] // .resolved // .toLowerCase() // .replace( regexp, '/' ) // .replace( prefix, '' ); // } ); Object.keys(files).forEach(function(file) { files[file].shortened = files[file].resolved.replace(commonPrefix, '') // always use forward slashes .replace(/\\/g, '/'); }); return files; } function getPathFromDoclet(doclet) { if (!doclet.meta) { return; } return path.normalize(doclet.meta.path && doclet.meta.path !== 'null' ? doclet.meta.path + '/' + doclet.meta.filename : doclet.meta.filename); } function searchData(html) { var startOfContent = html.indexOf("
"); if (startOfContent > 0) { var startOfSecondContent = html.indexOf("
", startOfContent + 2); if (startOfSecondContent > 0) { startOfContent = startOfSecondContent; } html = html.slice(startOfContent); } var endOfContent = html.indexOf(""); if (endOfContent > 0) { html = html.substring(0, endOfContent); } var stripped = sanitizeHtml(html, {allowedTags: [], allowedAttributes: []}); stripped = stripped.replace(/\s+/g, ' '); return stripped; } function generate(docType, title, docs, filename, resolveLinks) { resolveLinks = resolveLinks === false ? false : true; var docData = { title: title, docs: docs, docType: docType }; var outpath = path.join(outdir, filename), html = view.render('container.tmpl', docData); if (resolveLinks) { html = helper.resolveLinks(html); // turn {@link foo} into foo } if (searchEnabled) { searchableDocuments[filename] = { "id": filename, "title": title, "body": searchData(html) }; } fs.writeFileSync(outpath, html, 'utf8'); } function generateSourceFiles(sourceFiles) { Object.keys(sourceFiles).forEach(function(file) { var source; // links are keyed to the shortened path in each doclet's `meta.shortpath` property var sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened); helper.registerLink(sourceFiles[file].shortened, sourceOutfile); try { source = { kind: 'source', code: helper.htmlsafe(fs.readFileSync(sourceFiles[file].resolved, 'utf8')) }; } catch (e) { handle(e); } generate('source', 'Source: ' + sourceFiles[file].shortened, [source], sourceOutfile, false); }); } /** * Look for classes or functions with the same name as modules (which indicates that the module * exports only that class or function), then attach the classes or functions to the `module` * property of the appropriate module doclets. The name of each class or function is also updated * for display purposes. This function mutates the original arrays. * * @private * @param {Array.} doclets - The array of classes and functions to * check. * @param {Array.} modules - The array of module doclets to search. */ function attachModuleSymbols(doclets, modules) { var symbols = {}; // build a lookup table doclets.forEach(function(symbol) { symbols[symbol.longname] = symbols[symbol.longname] || []; symbols[symbol.longname].push(symbol); }); return modules.map(function(module) { if (symbols[module.longname]) { module.modules = symbols[module.longname] // Only show symbols that have a description. Make an exception for classes, because // we want to show the constructor-signature heading no matter what. .filter(function(symbol) { return symbol.description || symbol.kind === 'class'; }) .map(function(symbol) { symbol = doop(symbol); if (symbol.kind === 'class' || symbol.kind === 'function') { symbol.name = symbol.name.replace('module:', '(require("') + '"))'; } return symbol; }); } }); } /** * Create the navigation sidebar. * @param {object} members The members that will be used to create the sidebar. * @param {array} members.classes * @param {array} members.externals * @param {array} members.globals * @param {array} members.mixins * @param {array} members.interfaces * @param {array} members.modules * @param {array} members.namespaces * @param {array} members.tutorials * @param {array} members.events * @return {string} The HTML for the navigation sidebar. */ function buildNav(members) { var seen = {}; var nav = navigationMaster; if (members.modules.length) { members.modules.forEach(function(m) { if (!hasOwnProp.call(seen, m.longname)) { nav.module.members.push(linkto(m.longname, m.longname.replace("module:", ""))); } seen[m.longname] = true; }); } if (members.externals.length) { members.externals.forEach(function(e) { if (!hasOwnProp.call(seen, e.longname)) { nav.external.members.push(linkto(e.longname, e.name.replace(/(^"|"$)/g, ''))); } seen[e.longname] = true; }); } if (members.classes.length) { members.classes.forEach(function(c) { if (!hasOwnProp.call(seen, c.longname)) { nav.class.members.push(linkto(c.longname, c.longname.replace("module:", ""))); } seen[c.longname] = true; }); } if (members.events.length) { members.events.forEach(function(e) { if (!hasOwnProp.call(seen, e.longname)) { nav.event.members.push(linkto(e.longname, e.longname.replace("module:", ""))); } seen[e.longname] = true; }); } if (members.namespaces.length) { members.namespaces.forEach(function(n) { if (!hasOwnProp.call(seen, n.longname)) { nav.namespace.members.push(linkto(n.longname, n.longname.replace("module:", ""))); } seen[n.longname] = true; }); } if (members.mixins.length) { members.mixins.forEach(function(m) { if (!hasOwnProp.call(seen, m.longname)) { nav.mixin.members.push(linkto(m.longname, m.longname.replace("module:", ""))); } seen[m.longname] = true; }); } if (members.interfaces && members.interfaces.length) { members.interfaces.forEach(function(m) { if (!hasOwnProp.call(seen, m.longname)) { nav.interface.members.push(linkto(m.longname, m.longname.replace("module:", ""))); } seen[m.longname] = true; }); } if (members.tutorials.length) { members.tutorials.forEach(function(t) { nav.tutorial.members.push(tutoriallink(t.name)); }); } if (members.globals.length) { members.globals.forEach(function(g) { if (g.kind !== 'typedef' && !hasOwnProp.call(seen, g.longname)) { nav.global.members.push(linkto(g.longname, g.longname.replace("module:", ""))); } seen[g.longname] = true; }); // even if there are no links, provide a link to the global page. if (nav.global.members.length === 0) { nav.global.members.push(linkto("global", "Global")); } } var topLevelNav = []; _.each(nav, function(entry, name) { if (entry.members.length > 0 && name !== "index") { topLevelNav.push({ title: entry.title, link: entry.link, members: entry.members }); } }); nav.topLevelNav = topLevelNav; } /** @param {TAFFY} taffyData See . @param {object} opts @param {Tutorial} tutorials */ exports.publish = function(taffyData, opts, tutorials) { data = taffyData; conf['default'] = conf['default'] || {}; var templatePath = opts.template; view = new template.Template(templatePath + '/tmpl'); // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness // doesn't try to hand them out later // var indexUrl = helper.getUniqueFilename( 'index' ); // don't call registerLink() on this one! 'index' is also a valid longname // var globalUrl = helper.getUniqueFilename( 'global' ); helper.registerLink('global', globalUrl); // set up templating // set up templating view.layout = conf['default'].layoutFile ? path.getResourcePath(path.dirname(conf['default'].layoutFile), path.basename(conf['default'].layoutFile) ) : 'layout.tmpl'; // set up tutorials for helper helper.setTutorials(tutorials); data = helper.prune(data); var sortOption = navOptions.sort === undefined ? opts.sort : navOptions.sort; sortOption = sortOption === undefined ? true : sortOption; sortOption = sortOption === true ? 'longname, version, since' : sortOption; if (sortOption) { data.sort(sortOption); } helper.addEventListeners(data); var sourceFiles = {}; var sourceFilePaths = []; data().each(function(doclet) { doclet.attribs = ''; if (doclet.examples) { doclet.examples = doclet.examples.map(function(example) { var caption, lang; // allow using a markdown parser on the examples captions (surrounded by useless HTML p tags) if (example.match(/^\s*(

)?([\s\S]+?)<\/caption>(\s*)([\s\S]+?)(<\/p>)?$/i)) { caption = RegExp.$2; example = RegExp.$4 + (RegExp.$1 ? '' : RegExp.$5); } var lang = /{@lang (.*?)}/.exec(example); if (lang && lang[1]) { example = example.replace(lang[0], ""); lang = lang[1]; } else { lang = null; } return { caption: caption || '', code: example, lang: lang || "javascript" }; }); } if (doclet.see) { doclet.see.forEach(function(seeItem, i) { doclet.see[i] = hashToLink(doclet, seeItem); }); } // build a list of source files var sourcePath; if (doclet.meta) { sourcePath = getPathFromDoclet(doclet); sourceFiles[sourcePath] = { resolved: sourcePath, shortened: null }; //Check to see if the array of source file paths already contains // the source path, if not then add it if (sourceFilePaths.indexOf(sourcePath) === -1) { sourceFilePaths.push(sourcePath) } } }); // update outdir if necessary, then create outdir var packageInfo = (find({ kind: 'package' }) || [])[0]; if (navOptions.disablePackagePath !== true && packageInfo && packageInfo.name) { if (packageInfo.version) { outdir = path.join(outdir, packageInfo.name, packageInfo.version); } else { outdir = path.join(outdir, packageInfo.name); } } fs.mkPath(outdir); // copy the template's static files to outdir var fromDir = path.join( templatePath, 'static' ); var staticFiles = fs.ls( fromDir, 3 ); staticFiles.forEach( function ( fileName ) { var toFile = fileName.replace( fromDir, outdir ); var toDir = fs.toDir( toFile ); fs.mkPath( toDir ); fs.copyFileSync( fileName, '', toFile ); } ); // copy user-specified static files to outdir var staticFilePaths; var staticFileFilter; var staticFileScanner; if (conf.default.staticFiles) { // The canonical property name is `include`. We accept `paths` for backwards compatibility // with a bug in JSDoc 3.2.x. staticFilePaths = conf.default.staticFiles.include || conf.default.staticFiles.paths || []; staticFileFilter = new (require('jsdoc/src/filter')).Filter(conf.default.staticFiles); staticFileScanner = new (require('jsdoc/src/scanner')).Scanner(); staticFilePaths.forEach(function(filePath) { var extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter); extraStaticFiles.forEach(function(fileName) { var sourcePath = fs.toDir(filePath); var toDir = fs.toDir( fileName.replace(sourcePath, outdir) ); fs.mkPath(toDir); fs.copyFileSync(fileName, toDir); }); }); } if (sourceFilePaths.length) { var payload = navOptions.sourceRootPath; if (!payload) { payload = path.commonPrefix(sourceFilePaths); } sourceFiles = shortenPaths(sourceFiles, payload); } data().each(function(doclet) { var url = helper.createLink(doclet); helper.registerLink(doclet.longname, url); // add a shortened version of the full path var docletPath; if (doclet.meta) { docletPath = getPathFromDoclet(doclet); if (!_.isEmpty(sourceFiles[docletPath])) { docletPath = sourceFiles[docletPath].shortened; if (docletPath) { doclet.meta.shortpath = docletPath; } } } }); data().each(function(doclet) { var url = helper.longnameToUrl[doclet.longname]; if (url.indexOf('#') > -1) { doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop(); } else { doclet.id = doclet.name; } if (needsSignature(doclet)) { addSignatureParams(doclet); addSignatureReturns(doclet); addAttribs(doclet); } }); // do this after the urls have all been generated data().each(function(doclet) { doclet.ancestors = getAncestorLinks(doclet); if (doclet.kind === 'member') { addSignatureTypes(doclet); addAttribs(doclet); } if (doclet.kind === 'constant') { addSignatureTypes(doclet); addAttribs(doclet); doclet.kind = 'member'; } }); var members = helper.getMembers(data); members.tutorials = tutorials.children; // add template helpers view.find = find; view.linkto = linkto; view.resolveAuthorLinks = resolveAuthorLinks; view.tutoriallink = tutoriallink; view.htmlsafe = htmlsafe; view.moment = moment; // once for all buildNav(members); view.nav = navigationMaster; view.navOptions = navOptions; attachModuleSymbols(find({ kind: ['class', 'function'], longname: { left: 'module:' } }), members.modules); // only output pretty-printed source files if requested; do this before generating any other // pages, so the other pages can link to the source files if (navOptions.outputSourceFiles) { generateSourceFiles(sourceFiles); } if (members.globals.length) { generate('global', 'Global', [{ kind: 'globalobj' }], globalUrl); } // some browsers can't make the dropdown work if (view.nav.module && view.nav.module.members.length) { generate('module', view.nav.module.title, [{ kind: 'sectionIndex', contents: view.nav.module }], navigationMaster.module.link); } if (view.nav.class && view.nav.class.members.length) { generate('class', view.nav.class.title, [{ kind: 'sectionIndex', contents: view.nav.class }], navigationMaster.class.link); } if (view.nav.namespace && view.nav.namespace.members.length) { generate('namespace', view.nav.namespace.title, [{ kind: 'sectionIndex', contents: view.nav.namespace }], navigationMaster.namespace.link); } if (view.nav.mixin && view.nav.mixin.members.length) { generate('mixin', view.nav.mixin.title, [{ kind: 'sectionIndex', contents: view.nav.mixin }], navigationMaster.mixin.link); } if (view.nav.interface && view.nav.interface.members.length) { generate('interface', view.nav.interface.title, [{ kind: 'sectionIndex', contents: view.nav.interface }], navigationMaster.interface.link); } if (view.nav.external && view.nav.external.members.length) { generate('external', view.nav.external.title, [{ kind: 'sectionIndex', contents: view.nav.external }], navigationMaster.external.link); } if (view.nav.tutorial && view.nav.tutorial.members.length) { generate('tutorial', view.nav.tutorial.title, [{ kind: 'sectionIndex', contents: view.nav.tutorial }], navigationMaster.tutorial.link); } // index page displays information from package.json and lists files var files = find({ kind: 'file' }), packages = find({ kind: 'package' }); generate('index', 'Index', packages.concat( [{ kind: 'mainpage', readme: opts.readme, longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page' }] ).concat(files), indexUrl); // set up the lists that we'll use to generate pages var classes = taffy(members.classes); var modules = taffy(members.modules); var namespaces = taffy(members.namespaces); var mixins = taffy(members.mixins); var interfaces = taffy(members.interfaces); var externals = taffy(members.externals); for (var longname in helper.longnameToUrl) { if (hasOwnProp.call(helper.longnameToUrl, longname)) { var myClasses = helper.find(classes, { longname: longname }); if (myClasses.length) { generate('class', 'Class: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname]); } var myModules = helper.find(modules, { longname: longname }); if (myModules.length) { generate('module', 'Module: ' + myModules[0].name, myModules, helper.longnameToUrl[longname]); } var myNamespaces = helper.find(namespaces, { longname: longname }); if (myNamespaces.length) { generate('namespace', 'Namespace: ' + myNamespaces[0].name, myNamespaces, helper.longnameToUrl[longname]); } var myMixins = helper.find(mixins, { longname: longname }); if (myMixins.length) { generate('mixin', 'Mixin: ' + myMixins[0].name, myMixins, helper.longnameToUrl[longname]); } var myInterfaces = helper.find(interfaces, { longname: longname }); if (myInterfaces.length) { generate('interface', 'Interface: ' + myInterfaces[0].name, myInterfaces, helper.longnameToUrl[longname]); } var myExternals = helper.find(externals, { longname: longname }); if (myExternals.length) { generate('external', 'External: ' + myExternals[0].name, myExternals, helper.longnameToUrl[longname]); } } } // TODO: move the tutorial functions to templateHelper.js function generateTutorial(title, tutorial, filename) { var tutorialData = { title: title, header: tutorial.title, content: tutorial.parse(), children: tutorial.children, docs: null }; var tutorialPath = path.join(outdir, filename), html = view.render('tutorial.tmpl', tutorialData); // yes, you can use {@link} in tutorials too! html = helper.resolveLinks(html); // turn {@link foo} into foo if (searchEnabled) { searchableDocuments[filename] = { "id": filename, "title": title, "body": searchData(html) }; } fs.writeFileSync(tutorialPath, html, 'utf8'); } // tutorials can have only one parent so there is no risk for loops function saveChildren(node) { node.children.forEach(function(child) { generateTutorial('Tutorial: ' + child.title, child, helper.tutorialToUrl(child.name)); saveChildren(child); }); } function generateQuickTextSearch(templatePath, searchableDocuments, navOptions) { var data = { searchableDocuments: JSON.stringify(searchableDocuments), navOptions: navOptions }; var tmplString = fs.readFileSync(templatePath + "/quicksearch.tmpl").toString(), tmpl = _.template(tmplString); var html = tmpl(data), outpath = path.join(outdir, "quicksearch.html"); fs.writeFileSync(outpath, html, "utf8"); } saveChildren(tutorials); if (searchEnabled) { generateQuickTextSearch(templatePath + '/tmpl', searchableDocuments, navOptions); } }; ================================================ FILE: lerna.json ================================================ { "lerna": "2.11.0", "npmClient": "yarn", "useWorkspaces": true, "packages": [ "packages/*" ], "version": "2.0.0" } ================================================ FILE: package.json ================================================ { "private": true, "devDependencies": { "coveralls": "^3.0.1", "eslint": "^4.19.1", "eslint-config-airbnb": "^16.1.0", "eslint-config-prettier": "^2.9.0", "eslint-plugin-import": "^2.11.0", "eslint-plugin-jsx-a11y": "^6.0.3", "eslint-plugin-prettier": "^2.6.0", "eslint-plugin-react": "^7.8.1", "gh-pages": "^1.1.0", "ink-docstrap": "^1.3.2", "jsdoc": "^3.5.5", "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", "koa-router": "^9.1.0", "lerna": "^2.11.0", "mocha": "^5.1.1", "mocha-lcov-reporter": "^1.3.0", "mochify": "^5.6.1", "nyc": "^11.8.0", "prettier": "^1.12.1", "showdown": "^1.8.6", "sinon": "^5.0.7" }, "scripts": { "bootstrap": "lerna bootstrap", "lint": "lerna exec yarn lint", "pretest": "yarn lint", "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text lerna exec yarn test:node", "test:browser": "lerna exec yarn test:browser", "test": "yarn test:node:coverage && yarn test:browser", "format": "lerna exec yarn format", "clean": "rm -fr build && lerna exec yarn clean", "generate-docs": "./scripts/generate-docs.sh", "deploy-docs": "gh-pages -d build/docs", "travis-deploy-github-pages": "gh-pages -r \"https://${GH_TOKEN}@github.com/yaorg/node-measured.git\" -d build/docs", "coverage": "nyc report --reporter=text-lcov | coveralls" }, "repository": { "url": "git://github.com/yaorg/node-measured.git" }, "homepage": "https://yaorg.github.io/node-measured/", "license": "MIT", "workspaces": [ "packages/*" ] } ================================================ FILE: packages/measured-core/README.md ================================================ # Measured Core The core measured library that has the Metric interfaces and implementations. [![npm](https://img.shields.io/npm/v/measured-core.svg)](https://www.npmjs.com/package/measured-core) ## Install ``` npm install measured-core ``` ## What is in this package ### Metric Implemenations The core library has the following metrics classes: #### [Gauge](https://yaorg.github.io/node-measured/packages/measured-core/Gauge.html) Values that can be read instantly via a supplied call back. #### [SettableGauge](https://yaorg.github.io/node-measured/packages/measured-core/SettableGauge.html) Just like a Gauge but its value is set directly rather than supplied by a callback. #### [CachedGauge](https://yaorg.github.io/node-measured/packages/measured-core/CachedGauge.html) 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. #### [Counter](https://yaorg.github.io/node-measured/packages/measured-core/Counter.html) Counters are things that increment or decrement. #### [Timer](https://yaorg.github.io/node-measured/packages/measured-core/Timer.html) Timers are a combination of Meters and Histograms. They measure the rate as well as distribution of scalar events. #### [Histogram](https://yaorg.github.io/node-measured/packages/measured-core/Histogram.html) Keeps a reservoir of statistically relevant values to explore their distribution. #### [Meter](https://yaorg.github.io/node-measured/packages/measured-core/Meter.html) Things that are measured as events / interval. ### Registry The core library comes with a basic registry class #### [Collection](https://yaorg.github.io/node-measured/packages/measured-core/Collection.html) that is not aware of dimensions / tags and leaves reporting up to you. #### See the [measured-reporting](../measured-reporting/) module for more advanced and featured registries. ### Other 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'). ## Usage **Step 1:** Add measurements to your code. For example, lets track the requests/sec of a http server: ```js var http = require('http'); var stats = require('measured').createCollection(); http.createServer(function(req, res) { stats.meter('requestsPerSecond').mark(); res.end('Thanks'); }).listen(3000); ``` **Step 2:** Show the collected measurements (more advanced examples follow later): ```js setInterval(function() { console.log(stats.toJSON()); }, 1000); ``` This will output something like this every second: ``` { requestsPerSecond: { mean: 1710.2180279856818, count: 10511, 'currentRate': 1941.4893498239829, '1MinuteRate': 168.08263156623656, '5MinuteRate': 34.74630977619571, '15MinuteRate': 11.646507524106095 } } ``` **Step 3:** Aggregate the data into your backend of choice. Here are a few time series data aggregators. - [Graphite](http://graphite.wikidot.com/) - A free and open source, self hosted and managed solution for time series data. - [SignalFx](https://signalfx.com/) - An enterprise SASS offering for time series data. - [Datadog](https://www.datadoghq.com/) - An enterprise SASS offering for time series data. ================================================ FILE: packages/measured-core/lib/Collection.js ================================================ const Optional = require('optional-js'); const Counter = require('./metrics/Counter'); const Gauge = require('./metrics/Gauge'); const SettableGauge = require('./metrics/SettableGauge'); const CachedGauge = require('./metrics/CachedGauge'); const Histogram = require('./metrics/Histogram'); const Meter = require('./metrics/Meter'); const Timer = require('./metrics/Timer'); const { MetricTypes } = require('./metrics/Metric'); /** * A Simple collection that stores names and a {@link Metric} instances with a few convenience methods for * creating / registering and then gathering all data the registered metrics. * @example * var { Collection } = require('measured'); * const collection = new Collection('node-process-metrics'); * const gauge = collection.gauge('node.process.heap_used', () => { * return process.memoryUsage().heapUsed; * }); */ class Collection { /** * Creates a named collection of metrics * @param {string} [name] The name to use for this collection. */ constructor(name) { this.name = name; /** * internal map of metric name to {@link Metric} * @type {Object.} * @private */ this._metrics = {}; } /** * register a metric that was created outside the provided convenience methods of this collection * @param name The metric name * @param metric The {@link Metric} implementation * @example * var { Collection, Gauge } = require('measured'); * const collection = new Collection('node-process-metrics'); * const gauge = new Gauge(() => { * return process.memoryUsage().heapUsed; * }); * collection.register('node.process.heap_used', gauge); */ register(name, metric) { this._metrics[name] = metric; } /** * Fetches the data/values from all registered metrics * @return {Object} The combined JSON object */ toJSON() { const json = {}; Object.keys(this._metrics).forEach(metric => { if (Object.prototype.hasOwnProperty.call(this._metrics, metric)) { json[metric] = this._metrics[metric].toJSON(); } }); if (!this.name) { return json; } const wrapper = {}; wrapper[this.name] = json; return wrapper; } /** * Gets or creates and registers a {@link Gauge} * @param {string} name The metric name * @param {function} readFn See {@link Gauge} * @return {Gauge} */ gauge(name, readFn) { this._validateName(name); let gauge; this._getMetricForNameAndType(name, MetricTypes.GAUGE).ifPresentOrElse( registeredMetric => { gauge = registeredMetric; }, () => { gauge = new Gauge(readFn); this.register(name, gauge); } ); return gauge; } /** * Gets or creates and registers a {@link Counter} * @param {string} name The metric name * @param {CounterProperties} [properties] See {@link CounterProperties} * @return {Counter} */ counter(name, properties) { this._validateName(name); let counter; this._getMetricForNameAndType(name, MetricTypes.COUNTER).ifPresentOrElse( registeredMetric => { counter = registeredMetric; }, () => { counter = new Counter(properties); this.register(name, counter); } ); return counter; } /** * Gets or creates and registers a {@link Histogram} * @param {string} name The metric name * @param {HistogramProperties} [properties] See {@link HistogramProperties} * @return {Histogram} */ histogram(name, properties) { this._validateName(name); let histogram; this._getMetricForNameAndType(name, MetricTypes.HISTOGRAM).ifPresentOrElse( registeredMetric => { histogram = registeredMetric; }, () => { histogram = new Histogram(properties); this.register(name, histogram); } ); return histogram; } /** * Gets or creates and registers a {@link Timer} * @param {string} name The metric name * @param {TimerProperties} [properties] See {@link TimerProperties} * @return {Timer} */ timer(name, properties) { this._validateName(name); let timer; this._getMetricForNameAndType(name, MetricTypes.TIMER).ifPresentOrElse( registeredMetric => { timer = registeredMetric; }, () => { timer = new Timer(properties); this.register(name, timer); } ); return timer; } /** * Gets or creates and registers a {@link Meter} * @param {string} name The metric name * @param {MeterProperties} [properties] See {@link MeterProperties} * @return {Meter} */ meter(name, properties) { this._validateName(name); let meter; this._getMetricForNameAndType(name, MetricTypes.METER).ifPresentOrElse( registeredMetric => { meter = registeredMetric; }, () => { meter = new Meter(properties); this.register(name, meter); } ); return meter; } /** * Gets or creates and registers a {@link SettableGauge} * @param {string} name The metric name * @param {SettableGaugeProperties} [properties] See {@link SettableGaugeProperties} * @return {SettableGauge} */ settableGauge(name, properties) { this._validateName(name); let settableGauge; this._getMetricForNameAndType(name, MetricTypes.GAUGE).ifPresentOrElse( registeredMetric => { settableGauge = registeredMetric; }, () => { settableGauge = new SettableGauge(properties); this.register(name, settableGauge); } ); return settableGauge; } /** * Gets or creates and registers a {@link SettableGauge} * @param {string} name The metric name * @param {function} valueProducingPromiseCallback A function that returns a promise than when * resolved supplies the value that should be cached in this gauge. * @param {number} updateIntervalInSeconds How often the cached gauge should update it's value. * @return {CachedGauge} */ cachedGauge(name, valueProducingPromiseCallback, updateIntervalInSeconds) { this._validateName(name); let cachedGauge; this._getMetricForNameAndType(name, MetricTypes.GAUGE).ifPresentOrElse( registeredMetric => { cachedGauge = registeredMetric; }, () => { cachedGauge = new CachedGauge(valueProducingPromiseCallback, updateIntervalInSeconds); this.register(name, cachedGauge); } ); return cachedGauge; } /** * Checks the registry for a metric with a given name and type, if it exists in the registry as a * different type an error is thrown. * @param {string} name The metric name * @param {string} requestedType The metric type * @return {Optional} * @private */ _getMetricForNameAndType(name, requestedType) { if (this._metrics[name]) { const metric = this._metrics[name]; const actualType = metric.getType(); if (requestedType !== actualType) { throw new Error( `You requested a metric of type: ${requestedType} with name: ${name}, but it exists in the registry as type: ${actualType}` ); } return Optional.of(metric); } return Optional.empty(); } /** * Validates that the provided name is valid. * * @param name The provided metric name param. * @private */ _validateName(name) { if (!name || typeof name !== 'string') { throw new Error('You must supply a metric name'); } } /** * Calls end on all metrics in the registry that support end() */ end() { const metrics = this._metrics; Object.keys(metrics).forEach(name => { const metric = metrics[name]; if (metric.end) { metric.end(); } }); } } module.exports = Collection; ================================================ FILE: packages/measured-core/lib/index.js ================================================ const Collection = require('./Collection'); const Counter = require('./metrics/Counter'); const Gauge = require('./metrics/Gauge'); const SettableGauge = require('./metrics/SettableGauge'); const CachedGauge = require('./metrics/CachedGauge'); const Histogram = require('./metrics/Histogram'); const Meter = require('./metrics/Meter'); const NoOpMeter = require('./metrics/NoOpMeter'); const Timer = require('./metrics/Timer'); const BinaryHeap = require('./util/BinaryHeap'); const ExponentiallyDecayingSample = require('./util/ExponentiallyDecayingSample'); const ExponentiallyMovingWeightedAverage = require('./util/ExponentiallyMovingWeightedAverage'); const Stopwatch = require('./util/Stopwatch'); const units = require('./util/units'); const { MetricTypes } = require('./metrics/Metric'); const metricValidators = require('./validators/metricValidators'); /** * The main measured-core module that is referenced when require('measured-core') is used. * @module measured-core */ module.exports = { /** * See {@link Collection} * @type {Collection} */ Collection, /** * See {@link Counter} * @type {Counter} */ Counter, /** * See {@link Gauge} * @type {Gauge} */ Gauge, /** * See {@link SettableGauge} * @type {SettableGauge} */ SettableGauge, /** * See {@link CachedGauge} * @type {CachedGauge} */ CachedGauge, /** * See {@link Histogram} * @type {Histogram} */ Histogram, /** * See {@link Meter} * @type {Meter} */ Meter, /** * See {@link NoOpMeter} * @type {NoOpMeter} */ NoOpMeter, /** * See {@link Timer} * @type {Timer} */ Timer, /** * See {@link BinaryHeap} * @type {BinaryHeap} */ BinaryHeap, /** * See {@link ExponentiallyDecayingSample} * @type {ExponentiallyDecayingSample} */ ExponentiallyDecayingSample, /** * See {@link ExponentiallyMovingWeightedAverage} * @type {ExponentiallyMovingWeightedAverage} */ ExponentiallyMovingWeightedAverage, /** * See {@link Stopwatch} * @type {Stopwatch} */ Stopwatch, /** * See {@link MetricTypes} * @type {MetricTypes} */ MetricTypes, /** * See {@link units} * @type {units} */ units, /** * See {@link units} * @type {units} */ TimeUnits: units, /** * See {@link module:metricValidators} * @type {Object.} */ metricValidators, /** * Creates a named collection. See {@link Collection} for more details * * @param name The name for the collection * @return {Collection} */ createCollection: name => { return new Collection(name); } }; ================================================ FILE: packages/measured-core/lib/metrics/CachedGauge.js ================================================ const { MetricTypes } = require('./Metric'); const TimeUnits = require('../util/units'); /** * A Cached Gauge takes a function that returns a promise that resolves a * value that should be cached and updated on a given interval. * * toJSON() will return the currently cached value. * * @example * const cpuAverageCachedGauge = new CachedGauge(() => { * return new Promise(resolve => { * //Grab first CPU Measure * const startMeasure = cpuAverage(); * setTimeout(() => { * //Grab second Measure * const endMeasure = cpuAverage(); * const percentageCPU = calculateCpuUsagePercent(startMeasure, endMeasure); * resolve(percentageCPU); * }, sampleTimeInSeconds); * }); * }, updateIntervalInSeconds); * * @implements {Metric} */ class CachedGauge { /** * @param {function} valueProducingPromiseCallback A function that returns a promise than when * resolved supplies the value that should be cached in this gauge. * @param {number} updateIntervalInSeconds How often the cached gauge should update it's value. * @param {number} [timeUnitOverride] by default this function takes updateIntervalInSeconds and multiplies it by TimeUnits.SECONDS (1000), * You can override it here. */ constructor(valueProducingPromiseCallback, updateIntervalInSeconds, timeUnitOverride) { const timeUnit = timeUnitOverride || TimeUnits.SECONDS; this._valueProducingPromiseCallback = valueProducingPromiseCallback; this._value = 0; this._updateValue(); this._interval = setInterval(() => { this._updateValue(); }, updateIntervalInSeconds * timeUnit); } /** * Calls the promise producing callback and sets the value when it gets resolved. * @private */ _updateValue() { this._valueProducingPromiseCallback().then(value => { this._value = value; }); } /** * @return {number} Gauges directly return the value which should be a number. */ toJSON() { return this._value; } /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() { return MetricTypes.GAUGE; } /** * Clears the interval, so that it doesn't keep any processes alive. */ end() { clearInterval(this._interval); this._interval = null; } } module.exports = CachedGauge; ================================================ FILE: packages/measured-core/lib/metrics/Counter.js ================================================ const { MetricTypes } = require('./Metric'); /** * Counters are things that increment or decrement * @implements {Metric} * @example * var Measured = require('measured') * var activeUploads = new Measured.Counter(); * http.createServer(function(req, res) { * activeUploads.inc(); * req.on('end', function() { * activeUploads.dec(); * }); * }); */ class Counter { /** * @param {CounterProperties} [properties] see {@link CounterProperties} */ constructor(properties) { properties = properties || {}; this._count = properties.count || 0; } /** * Counters directly return their currently value. * @return {number} */ toJSON() { return this._count; } /** * Increments the counter. * @param {number} n Increment the counter by n. Defaults to 1. */ inc(n) { this._count += arguments.length ? n : 1; } /** * Decrements the counter * @param {number} n Decrement the counter by n. Defaults to 1. */ dec(n) { this._count -= arguments.length ? n : 1; } /** * Resets the counter back to count Defaults to 0. * @param {number} count Resets the counter back to count Defaults to 0. */ reset(count) { this._count = count || 0; } /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() { return MetricTypes.COUNTER; } } module.exports = Counter; /** * Properties that can be supplied to the constructor of a {@link Counter} * * @interface CounterProperties * @typedef CounterProperties * @type {Object} * @property {number} count An initial count for the counter. Defaults to 0. * @example * // Creates a counter that starts at 5. * const counter = new Counter({ count: 5 }) */ ================================================ FILE: packages/measured-core/lib/metrics/Gauge.js ================================================ const { MetricTypes } = require('./Metric'); /** * Values that can be read instantly * @implements {Metric} * @example * var Measured = require('measured') * var gauge = new Measured.Gauge(function() { * return process.memoryUsage().rss; * }); */ class Gauge { /** * @param {function} readFn A function that returns the numeric value for this gauge. */ constructor(readFn) { this._readFn = readFn; } /** * @return {number} Gauges directly return the value from the callback which should be a number. */ toJSON() { return this._readFn(); } /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() { return MetricTypes.GAUGE; } } module.exports = Gauge; ================================================ FILE: packages/measured-core/lib/metrics/Histogram.js ================================================ const { MetricTypes } = require('./Metric'); const binarySearch = require('binary-search'); const EDS = require('../util/ExponentiallyDecayingSample'); /** * Keeps a reservoir of statistically relevant values biased towards the last 5 minutes to explore their distribution. * @implements {Metric} * @example * var Measured = require('measured') * var histogram = new Measured.Histogram(); * http.createServer(function(req, res) { * if (req.headers['content-length']) { * histogram.update(parseInt(req.headers['content-length'], 10)); * } * }); */ class Histogram { /** @param {HistogramProperties} [properties] see {@link HistogramProperties}. */ constructor(properties) { this._properties = properties || {}; this._initializeState(); } _initializeState() { this._sample = this._properties.sample || new EDS(); this._percentilesMethod = this._properties.percentilesMethod || this._percentiles; this._min = null; this._max = null; this._count = 0; this._sum = 0; // These are for the Welford algorithm for calculating running constiance // without floating-point doom. this._constianceM = 0; this._constianceS = 0; } /** * Pushes value into the sample. timestamp defaults to Date.now(). * @param {number} value */ update(value) { this._count++; this._sum += value; this._sample.update(value); this._updateMin(value); this._updateMax(value); this._updateVariance(value); } _percentiles(percentiles) { const values = this._sample.toArray().sort((a, b) => { return a === b ? 0 : a - b; }); const results = {}; let i, percentile, pos, lower, upper; for (i = 0; i < percentiles.length; i++) { percentile = percentiles[i]; if (values.length) { pos = percentile * (values.length + 1); if (pos < 1) { results[percentile] = values[0]; } else if (pos >= values.length) { results[percentile] = values[values.length - 1]; } else { lower = values[Math.floor(pos) - 1]; upper = values[Math.ceil(pos) - 1]; results[percentile] = lower + (pos - Math.floor(pos)) * (upper - lower); } } else { results[percentile] = null; } } return results; } weightedPercentiles(percentiles) { const values = this._sample.toArrayWithWeights().sort((a, b) => { return a.value === b.value ? 0 : a.value - b.value; }); const sumWeight = values.reduce((sum, sample) => { return sum + sample.priority; }, 0); const normWeights = values.map(value => { return value.priority / sumWeight; }); const quantiles = [0]; let i; for (i = 1; i < values.length; i++) { quantiles[i] = quantiles[i - 1] + normWeights[i - 1]; } function gt(a, b) { return a - b; } const results = {}; let percentile, pos; for (i = 0; i < percentiles.length; i++) { percentile = percentiles[i]; if (values.length) { pos = binarySearch(quantiles, percentile, gt); if (pos < 0) { results[percentile] = values[-pos - 1 - 1].value; } else if (pos < 1) { results[percentile] = values[0].value; } else if (pos >= values.length) { results[percentile] = values[values.length - 1].value; } } else { results[percentile] = null; } } return results; } /** * Resets all values. Histograms initialized with custom options will be reset to the default settings (patch welcome). */ reset() { // while this is technically a bug?, copying existing logic to maintain current api, // TODO reset should reset the sample, not override it with a new EDS() this._properties.sample = new EDS(); this._initializeState(); } /** * Checks whether the histogram contains values. * @return {boolean} Whether the histogram contains values. */ hasValues() { return this._count > 0; } /** * @return {HistogramData} */ toJSON() { const percentiles = this._percentilesMethod([0.5, 0.75, 0.95, 0.99, 0.999]); return { min: this._min, max: this._max, sum: this._sum, variance: this._calculateVariance(), mean: this._calculateMean(), stddev: this._calculateStddev(), count: this._count, median: percentiles[0.5], p75: percentiles[0.75], p95: percentiles[0.95], p99: percentiles[0.99], p999: percentiles[0.999] }; } _updateMin(value) { if (this._min === null || value < this._min) { this._min = value; } } _updateMax(value) { if (this._max === null || value > this._max) { this._max = value; } } _updateVariance(value) { if (this._count === 1) { this._constianceM = value; return value; } const oldM = this._constianceM; this._constianceM += (value - oldM) / this._count; this._constianceS += (value - oldM) * (value - this._constianceM); // TODO is this right, above it returns in the if statement but does nothing but update internal state for the else case? return undefined; } /** * * @return {number|null} * @private */ _calculateMean() { return this._count === 0 ? 0 : this._sum / this._count; } /** * @return {number|null} * @private */ _calculateVariance() { return this._count <= 1 ? null : this._constianceS / (this._count - 1); } /** * @return {number|null} * @private */ _calculateStddev() { return this._count < 1 ? null : Math.sqrt(this._calculateVariance()); } /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() { return MetricTypes.HISTOGRAM; } } module.exports = Histogram; /** * Properties to create a {@link Histogram} with. * * @interface HistogramProperties * @typedef HistogramProperties * @type {Object} * @property {object} sample The sample reservoir to use. Defaults to an ExponentiallyDecayingSample. */ /** * The data returned from Histogram::toJSON() * @interface HistogramData * @typedef HistogramData * @typedef {object} * @property {number|null} min The lowest observed value. * @property {number|null} max The highest observed value. * @property {number|null} sum The sum of all observed values. * @property {number|null} variance The variance of all observed values. * @property {number|null} mean The average of all observed values. * @property {number|null} stddev The stddev of all observed values. * @property {number} count The number of observed values. * @property {number} median 50% of all values in the resevoir are at or below this value. * @property {number} p75 See median, 75% percentile. * @property {number} p95 See median, 95% percentile. * @property {number} p99 See median, 99% percentile. * @property {number} p999 See median, 99.9% percentile. */ ================================================ FILE: packages/measured-core/lib/metrics/Meter.js ================================================ const { MetricTypes } = require('./Metric'); const units = require('../util/units'); const EWMA = require('../util/ExponentiallyMovingWeightedAverage'); const RATE_UNIT = units.SECONDS; const TICK_INTERVAL = 5 * units.SECONDS; /** * Things that are measured as events / interval. * @implements {Metric} * @example * var Measured = require('measured') * var meter = new Measured.Meter(); * http.createServer(function(req, res) { * meter.mark(); * }); */ class Meter { /** * @param {MeterProperties} [properties] see {@link MeterProperties}. */ constructor(properties) { this._properties = properties || {}; this._initializeState(); if (!this._properties.keepAlive) { this.unref(); } } /** * Initializes the state of this Metric * @private */ _initializeState() { this._rateUnit = this._properties.rateUnit || RATE_UNIT; this._tickInterval = this._properties.tickInterval || TICK_INTERVAL; if (this._properties.getTime) { this._getTime = this._properties.getTime; } this._m1Rate = this._properties.m1Rate || new EWMA(units.MINUTES, this._tickInterval); this._m5Rate = this._properties.m5Rate || new EWMA(5 * units.MINUTES, this._tickInterval); this._m15Rate = this._properties.m15Rate || new EWMA(15 * units.MINUTES, this._tickInterval); this._count = 0; this._currentSum = 0; this._startTime = this._getTime(); this._lastToJSON = this._getTime(); this._interval = setInterval(this._tick.bind(this), TICK_INTERVAL); } /** * Register n events as having just occured. Defaults to 1. * @param {number} [n] */ mark(n) { if (!this._interval) { this.start(); } n = n || 1; this._count += n; this._currentSum += n; this._m1Rate.update(n); this._m5Rate.update(n); this._m15Rate.update(n); } start() {} end() { clearInterval(this._interval); this._interval = null; } /** * Refs the backing timer again. Idempotent. */ ref() { if (this._interval && this._interval.ref) { this._interval.ref(); } } /** * Unrefs the backing timer. The meter will not keep the event loop alive. Idempotent. */ unref() { if (this._interval && this._interval.unref) { this._interval.unref(); } } _tick() { this._m1Rate.tick(); this._m5Rate.tick(); this._m15Rate.tick(); } /** * Resets all values. Meters initialized with custom options will be reset to the default settings (patch welcome). */ reset() { this.end(); this._initializeState(); } meanRate() { if (this._count === 0) { return 0; } const elapsed = this._getTime() - this._startTime; return this._count / elapsed * this._rateUnit; } currentRate() { const currentSum = this._currentSum; const duration = this._getTime() - this._lastToJSON; const currentRate = currentSum / duration * this._rateUnit; this._currentSum = 0; this._lastToJSON = this._getTime(); // currentRate could be NaN if duration was 0, so fix that return currentRate || 0; } /** * @return {MeterData} */ toJSON() { return { mean: this.meanRate(), count: this._count, currentRate: this.currentRate(), '1MinuteRate': this._m1Rate.rate(this._rateUnit), '5MinuteRate': this._m5Rate.rate(this._rateUnit), '15MinuteRate': this._m15Rate.rate(this._rateUnit) }; } _getTime() { if (!process.hrtime) { return new Date().getTime(); } const hrtime = process.hrtime(); return hrtime[0] * 1000 + hrtime[1] / (1000 * 1000); } /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() { return MetricTypes.METER; } } module.exports = Meter; /** * * @interface MeterProperties * @typedef MeterProperties * @type {Object} * @property {number} rateUnit The rate unit. Defaults to 1000 (1 sec). * @property {number} tickInterval The interval in which the averages are updated. Defaults to 5000 (5 sec). * @property {boolean} keepAlive Optional flag to unref the associated timer. Defaults to `false`. * @example * const meter = new Meter({ rateUnit: 1000, tickInterval: 5000}) */ /** * The data returned from Meter::toJSON() * @interface MeterData * @typedef MeterData * @typedef {object} * @property {number} mean The average rate since the meter was started. * @property {number} count The total of all values added to the meter. * @property {number} currentRate The rate of the meter since the last toJSON() call. * @property {number} 1MinuteRate The rate of the meter biased towards the last 1 minute. * @property {number} 5MinuteRate The rate of the meter biased towards the last 5 minutes. * @property {number} 15MinuteRate The rate of the meter biased towards the last 15 minutes. */ ================================================ FILE: packages/measured-core/lib/metrics/Metric.js ================================================ /** * Interface for Metric types. * * Implementations *

*

  • Counter, things that increment or decrement.
  • *
  • Gauge, values that can be read instantly via a supplied call back.
  • *
  • Histogram, keeps a reservoir of statistically relevant values to explore their distribution.
  • *
  • Meter, things that are measured as events / interval.
  • *
  • NoOpMeter, an empty impl of meter, useful for supplying to a Timer, when you only care about the Histogram.
  • *
  • SettableGauge, just like a Gauge but its value is set directly rather than supplied by a callback.
  • *
  • 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.
  • *
  • Timer, timers are a combination of Meters and Histograms. They measure the rate as well as distribution of scalar events.
  • *

    * * @interface Metric */ // eslint-disable-next-line no-unused-vars class Metric { /** * Please note that dispite its name, this method can return raw numbers on * certain implementations such as counters and gauges. * * @return {any} Returns the data from the Metric */ toJSON() {} /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() {} } /** * An enum like object that is the set of core metric types that all implementors of {@link Metric} are. * * @typedef MetricTypes * @interface MetricTypes * @type {Object.} * @property {COUNTER} The type for Counters. * @property {GAUGE} The type for Gauges. * @property {HISTOGRAM} The type for Histograms. * @property {METER} The type for Meters. * @property {TIMER} The type for Timers. */ const MetricTypes = { COUNTER: 'Counter', GAUGE: 'Gauge', HISTOGRAM: 'Histogram', METER: 'Meter', TIMER: 'Timer' }; module.exports = { MetricTypes }; ================================================ FILE: packages/measured-core/lib/metrics/NoOpMeter.js ================================================ const { MetricTypes } = require('./Metric'); /** * A No-Op Impl of Meter that can be used with a timer, to only create histogram data. * This is useful for some time series aggregators that can calculate rates for you just off of sent count. * * @implements {Metric} * @example * const { NoOpMeter, Timer } = require('measured') * const meter = new NoOpMeter(); * const timer = new Timer({meter: meter}); * ... * // do some stuff with the timer and stopwatch api * ... */ // eslint-disable-next-line padded-blocks class NoOpMeter { /** * No-Op impl * @param {number} n Number of events to mark. */ // eslint-disable-next-line no-unused-vars mark(n) {} /** * No-Op impl */ start() {} /** * No-Op impl */ end() {} /** * No-Op impl */ ref() {} /** * No-Op impl */ unref() {} /** * No-Op impl */ reset() {} /** * No-Op impl */ meanRate() {} /** * No-Op impl */ currentRate() {} /** * Returns an empty object * @return {{}} */ toJSON() { return {}; } /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() { return MetricTypes.METER; } } module.exports = NoOpMeter; ================================================ FILE: packages/measured-core/lib/metrics/SettableGauge.js ================================================ const { MetricTypes } = require('./Metric'); /** * Works like a {@link Gauge}, but rather than getting its value from a callback, the value * is set when needed. This can be useful for setting a gauges value for asynchronous operations. * @implements {Metric} * @example * const settableGauge = new SettableGauge(); * // Update the settable gauge ever 10'ish seconds * setInterval(() => { * calculateSomethingAsync().then((value) => { * settableGauge.setValue(value); * }); * }, 10000); */ class SettableGauge { /** * @param {SettableGaugeProperties} [options] See {@link SettableGaugeProperties}. */ constructor(options) { options = options || {}; this._value = options.initialValue || 0; } setValue(value) { this._value = value; } /** * @return {number} Settable Gauges directly return there current value. */ toJSON() { return this._value; } /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() { return MetricTypes.GAUGE; } } module.exports = SettableGauge; /** * Properties that can be supplied to the constructor of a {@link Counter} * * @interface SettableGaugeProperties * @typedef SettableGaugeProperties * @type {Object} * @property {number} initialValue An initial value to use for this settable gauge. Defaults to 0. * @example * // Creates a Gauge that with an initial value of 500. * const settableGauge = new SettableGauge({ initialValue: 500 }) * */ ================================================ FILE: packages/measured-core/lib/metrics/Timer.js ================================================ const { MetricTypes } = require('./Metric'); const Histogram = require('./Histogram'); const Meter = require('./Meter'); const Stopwatch = require('../util/Stopwatch'); /** * * Timers are a combination of Meters and Histograms. They measure the rate as well as distribution of scalar events. *

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

    * But you can also use them as generic histograms that also track the rate of events: See example 2. * * @example * var Measured = require('measured') * var timer = new Measured.Timer(); * http.createServer(function(req, res) { * var stopwatch = timer.start(); * req.on('end', function() { * stopwatch.end(); * }); * }); * * * @example * var Measured = require('measured') * var timer = new Measured.Timer(); * http.createServer(function(req, res) { * if (req.headers['content-length']) { * timer.update(parseInt(req.headers['content-length'], 10)); * } * }); * * @implements {Metric} */ class Timer { /** * @param {TimerProperties} [properties] See {@link TimerProperties}. */ constructor(properties) { properties = properties || {}; this._meter = properties.meter || new Meter({}); this._histogram = properties.histogram || new Histogram({}); this._getTime = properties.getTime; this._keepAlive = !!properties.keepAlive; if (!properties.keepAlive) { this.unref(); } } /** * @return {Stopwatch} Returns a Stopwatch that has been started. */ start() { const self = this; const watch = new Stopwatch({ getTime: this._getTime }); watch.once('end', elapsed => { self.update(elapsed); }); return watch; } /** * Updates the internal histogram with value and marks one event on the internal meter. * @param {number} value */ update(value) { this._meter.mark(); this._histogram.update(value); } /** * Resets all values. Timers initialized with custom options will be reset to the default settings. */ reset() { this._meter.reset(); this._histogram.reset(); } end() { this._meter.end(); } /** * Refs the backing timer again. Idempotent. */ ref() { this._meter.ref(); } /** * Unrefs the backing timer. The meter will not keep the event loop alive. Idempotent. */ unref() { this._meter.unref(); } /** * toJSON output: * *

  • meter: See Meter#toJSON output docs above.
  • *
  • histogram: See Histogram#toJSON output docs above.
  • * * @return {any} */ toJSON() { return { meter: this._meter.toJSON(), histogram: this._histogram.toJSON() }; } /** * The type of the Metric Impl. {@link MetricTypes}. * @return {string} The type of the Metric Impl. */ getType() { return MetricTypes.TIMER; } } module.exports = Timer; /** * @interface TimerProperties * @typedef TimerProperties * @type {Object} * @property {Meter} meter The internal meter to use. Defaults to a new {@link Meter}. * @property {Histogram} histogram The internal histogram to use. Defaults to a new {@link Histogram}. * @property {function} getTime optional function override for supplying time to the {@link Stopwatch} * @property {boolean} keepAlive Optional flag to unref the associated timer. Defaults to `false`. */ ================================================ FILE: packages/measured-core/lib/util/BinaryHeap.js ================================================ /** * Based on http://en.wikipedia.org/wiki/Binary_Heap * as well as http://eloquentjavascript.net/appendix2.html */ class BinaryHeap { constructor(options) { options = options || {}; this._elements = options.elements || []; this._score = options.score || this._score; } /** * Add elements to the binary heap. * @param {any[]} elements */ add(...elements) { elements.forEach(element => { this._elements.push(element); this._bubble(this._elements.length - 1); }); } first() { return this._elements[0]; } removeFirst() { const root = this._elements[0]; const last = this._elements.pop(); if (this._elements.length > 0) { this._elements[0] = last; this._sink(0); } return root; } clone() { return new BinaryHeap({ elements: this.toArray(), score: this._score }); } toSortedArray() { const array = []; const clone = this.clone(); let element; while (true) { element = clone.removeFirst(); if (element === undefined) { break; } array.push(element); } return array; } toArray() { return [].concat(this._elements); } size() { return this._elements.length; } _bubble(bubbleIndex) { const bubbleElement = this._elements[bubbleIndex]; const bubbleScore = this._score(bubbleElement); let parentIndex; let parentElement; let parentScore; while (bubbleIndex > 0) { parentIndex = this._parentIndex(bubbleIndex); parentElement = this._elements[parentIndex]; parentScore = this._score(parentElement); if (bubbleScore <= parentScore) { break; } this._elements[parentIndex] = bubbleElement; this._elements[bubbleIndex] = parentElement; bubbleIndex = parentIndex; } } _sink(sinkIndex) { const sinkElement = this._elements[sinkIndex]; const sinkScore = this._score(sinkElement); const { length } = this._elements; let swapIndex; let swapScore; let swapElement; let childIndexes; let i; let childIndex; let childElement; let childScore; while (true) { swapIndex = null; swapScore = null; swapElement = null; childIndexes = this._childIndexes(sinkIndex); for (i = 0; i < childIndexes.length; i++) { childIndex = childIndexes[i]; if (childIndex >= length) { break; } childElement = this._elements[childIndex]; childScore = this._score(childElement); if (childScore > sinkScore) { if (swapScore === null || swapScore < childScore) { swapIndex = childIndex; swapScore = childScore; swapElement = childElement; } } } if (swapIndex === null) { break; } this._elements[swapIndex] = sinkElement; this._elements[sinkIndex] = swapElement; sinkIndex = swapIndex; } } _parentIndex(index) { return Math.floor((index - 1) / 2); } _childIndexes(index) { return [2 * index + 1, 2 * index + 2]; } _score(element) { return element.valueOf(); } } module.exports = BinaryHeap; ================================================ FILE: packages/measured-core/lib/util/ExponentiallyDecayingSample.js ================================================ const BinaryHeap = require('./BinaryHeap'); const units = require('./units'); const RESCALE_INTERVAL = units.HOURS; const ALPHA = 0.015; const SIZE = 1028; /** * ExponentiallyDecayingSample */ class ExponentiallyDecayingSample { constructor(options) { options = options || {}; this._elements = new BinaryHeap({ score: element => -element.priority }); this._rescaleInterval = options.rescaleInterval || RESCALE_INTERVAL; this._alpha = options.alpha || ALPHA; this._size = options.size || SIZE; this._random = options.random || this._random; this._landmark = null; this._nextRescale = null; } update(value, timestamp) { const now = Date.now(); if (!this._landmark) { this._landmark = now; this._nextRescale = this._landmark + this._rescaleInterval; } timestamp = timestamp || now; const newSize = this._elements.size() + 1; const element = { priority: this._priority(timestamp - this._landmark), value: value }; if (newSize <= this._size) { this._elements.add(element); } else if (element.priority > this._elements.first().priority) { this._elements.removeFirst(); this._elements.add(element); } if (now >= this._nextRescale) { this._rescale(now); } } toSortedArray() { return this._elements.toSortedArray().map(element => element.value); } toArray() { return this._elements.toArray().map(element => element.value); } toArrayWithWeights() { return this._elements.toArray(); } _weight(age) { // We divide by 1000 to not run into huge numbers before reaching a // rescale event. return Math.exp(this._alpha * (age / 1000)); } _priority(age) { return this._weight(age) / this._random(); } _random() { return Math.random(); } _rescale(now) { now = now || Date.now(); const self = this; const oldLandmark = this._landmark; this._landmark = now || Date.now(); this._nextRescale = now + this._rescaleInterval; const factor = self._priority(-(self._landmark - oldLandmark)); this._elements.toArray().forEach(element => { element.priority *= factor; }); } } module.exports = ExponentiallyDecayingSample; ================================================ FILE: packages/measured-core/lib/util/ExponentiallyMovingWeightedAverage.js ================================================ const units = require('./units'); const TICK_INTERVAL = 5 * units.SECONDS; /** * ExponentiallyMovingWeightedAverage */ class ExponentiallyMovingWeightedAverage { constructor(timePeriod, tickInterval) { this._timePeriod = timePeriod || units.MINUTE; this._tickInterval = tickInterval || TICK_INTERVAL; this._alpha = 1 - Math.exp(-this._tickInterval / this._timePeriod); this._count = 0; this._rate = 0; } update(n) { this._count += n; } tick() { const instantRate = this._count / this._tickInterval; this._count = 0; this._rate += this._alpha * (instantRate - this._rate); } rate(timeUnit) { return (this._rate || 0) * timeUnit; } } module.exports = ExponentiallyMovingWeightedAverage; ================================================ FILE: packages/measured-core/lib/util/Stopwatch.js ================================================ const { EventEmitter } = require('events'); /** * A simple object for tracking elapsed time * * @extends {EventEmitter} */ class Stopwatch extends EventEmitter { /** * Creates a started Stopwatch * @param {StopwatchProperties} [options] See {@link StopwatchProperties} */ constructor(options) { super(); options = options || {}; EventEmitter.call(this); if (options.getTime) { this._getTime = options.getTime; } this._start = this._getTime(); this._ended = false; } /** * Called to mark the end of the timer task * @return {number} the total execution time */ end() { if (this._ended) { return null; } this._ended = true; const elapsed = this._getTime() - this._start; this.emit('end', elapsed); return elapsed; } _getTime() { if (!process.hrtime) { return Date.now(); } const hrtime = process.hrtime(); return hrtime[0] * 1000 + hrtime[1] / (1000 * 1000); } } module.exports = Stopwatch; /** * @interface StopwatchProperties * @typedef StopwatchProperties * @type {Object} * @property {function} getTime optional function override for supplying time., defaults to new Date() / process.hrt() */ ================================================ FILE: packages/measured-core/lib/util/units.js ================================================ const NANOSECONDS = 1 / (1000 * 1000); const MICROSECONDS = 1 / 1000; const MILLISECONDS = 1; const SECONDS = 1000 * MILLISECONDS; const MINUTES = 60 * SECONDS; const HOURS = 60 * MINUTES; const DAYS = 24 * HOURS; /** * Time units, as found in Java: {@link http://download.oracle.com/javase/6/docs/api/java/util/concurrent/TimeUnit.html} * @module timeUnits * @example * const timeUnit = require('measured-core').unit * setTimeout(() => {}, 5 * timeUnit.MINUTES) */ module.exports = { /** * nanoseconds in milliseconds * @type {number} */ NANOSECONDS, /** * microseconds in milliseconds * @type {number} */ MICROSECONDS, /** * milliseconds in milliseconds * @type {number} */ MILLISECONDS, /** * seconds in milliseconds * @type {number} */ SECONDS, /** * minutes in milliseconds * @type {number} */ MINUTES, /** * hours in milliseconds * @type {number} */ HOURS, /** * days in milliseconds * @type {number} */ DAYS }; ================================================ FILE: packages/measured-core/lib/validators/metricValidators.js ================================================ const { MetricTypes } = require('../metrics/Metric'); // TODO: Object.values(...) does not exist in Node.js 6.x, switch after LTS period ends. // const metricTypeValues = Object.values(MetricTypes); const metricTypeValues = Object.keys(MetricTypes).map(key => MetricTypes[key]); /** * This module contains various validators to validate publicly exposed input. * * @module metricValidators */ module.exports = { /** * Validates that a metric implements the metric interface. * * @param {Metric} metric The object that is supposed to be a metric. */ validateMetric: metric => { if (!metric) { throw new TypeError('The metric was undefined, when it was required'); } if (typeof metric.toJSON !== 'function') { throw new TypeError('Metrics must implement toJSON(), see the Metric interface in the docs.'); } if (typeof metric.getType !== 'function') { throw new TypeError('Metrics must implement getType(), see the Metric interface in the docs.'); } const type = metric.getType(); if (!metricTypeValues.includes(type)) { throw new TypeError( `Metric#getType(), must return a type defined in MetricsTypes. Found: ${type}, Valid values: ${metricTypeValues.join( ', ' )}` ); } } }; ================================================ FILE: packages/measured-core/package.json ================================================ { "name": "measured-core", "description": "A Node library for measuring and reporting application-level metrics.", "version": "2.0.0", "homepage": "https://yaorg.github.io/node-measured/", "engines": { "node": ">= 5.12" }, "publishConfig": { "access": "public" }, "main": "./lib/index.js", "scripts": { "clean": "rm -fr build", "format": "prettier --write './lib/**/*.{ts,js}'", "lint": "eslint lib --ext .js", "test:node": "mocha './test/**/test-*.js'", "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'", "test:browser": "mochify './test/**/test-*.js'", "test": "yarn test:node:coverage && yarn test:browser", "coverage": "nyc report --reporter=text-lcov | coveralls" }, "dependencies": { "binary-search": "^1.3.3", "optional-js": "^2.0.0" }, "repository": { "url": "git://github.com/yaorg/node-measured.git" }, "files": [ "lib", "README.md" ], "license": "MIT", "devDependencies": { "jsdoc": "^3.5.5" } } ================================================ FILE: packages/measured-core/test/common.js ================================================ 'use strict'; /* var common = exports; var path = require('path'); common.dir = {}; common.dir.root = path.dirname(__dirname); common.dir.lib = path.join(common.dir.root, 'lib'); common.measured = require(common.dir.root); */ exports.measured = require('../lib/index'); ================================================ FILE: packages/measured-core/test/integration/test-Collection_end.js ================================================ 'use strict'; var common = require('../common'); var collection = new common.measured.Collection(); collection.timer('a').start(); collection.meter('b').start(); collection.counter('c'); collection.end(); ================================================ FILE: packages/measured-core/test/unit/metrics/test-CachedGauge.js ================================================ /*global describe, it, beforeEach, afterEach*/ const TimeUnits = require('../../../lib/util/units'); const CachedGauge = require('../../../lib/metrics/CachedGauge'); const assert = require('assert'); describe('CachedGauge', () => { let cachedGauge; it('A cachedGauge immediately calls the callback to set its initial value', () => { cachedGauge = new CachedGauge( () => { return new Promise(resolve => { resolve(10); }); }, 1, TimeUnits.MINUTES ); // Shouldn't update in the unit test. return wait(5 * TimeUnits.MILLISECONDS).then(() => { assert.equal(cachedGauge.toJSON(), 10); }); }); it('A cachedGauge calls the callback at the interval provided', () => { const values = [1, 2]; cachedGauge = new CachedGauge( () => { return new Promise(resolve => { resolve(values.shift()); }); }, 5, TimeUnits.MILLISECONDS ); return wait(7 * TimeUnits.MILLISECONDS).then(() => { assert.equal(cachedGauge.toJSON(), 2); assert.equal(values.length, 0, 'the callback should have been called 2x, emptying the values array'); }); }); afterEach(() => { if (cachedGauge) { cachedGauge.end(); } }); }); const wait = waitInterval => { return new Promise(resolve => { setTimeout(() => { resolve(); }, waitInterval); }); }; ================================================ FILE: packages/measured-core/test/unit/metrics/test-Counter.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); var Counter = common.measured.Counter; describe('Counter', function() { var counter; beforeEach(function() { counter = new Counter(); }); it('has initial value of 0', function() { var json = counter.toJSON(); assert.deepEqual(json, 0); }); it('can be initialized with a given count', function() { counter = new Counter({ count: 5 }); assert.equal(counter.toJSON(), 5); }); it('#inc works incrementally', function() { counter.inc(5); assert.equal(counter.toJSON(), 5); counter.inc(3); assert.equal(counter.toJSON(), 8); }); it('#inc defaults to 1', function() { counter.inc(); assert.equal(counter.toJSON(), 1); counter.inc(); assert.equal(counter.toJSON(), 2); }); it('#inc adds zero', function() { counter.inc(0); assert.equal(counter.toJSON(), 0); }); it('#dec works incrementally', function() { counter.dec(3); assert.equal(counter.toJSON(), -3); counter.dec(2); assert.equal(counter.toJSON(), -5); }); it('#dec defaults to 1', function() { counter.dec(); assert.equal(counter.toJSON(), -1); counter.dec(); assert.equal(counter.toJSON(), -2); }); it('#dec substracts zero', function() { counter.dec(0); assert.equal(counter.toJSON(), 0); }); it('#reset works', function() { counter.inc(23); assert.equal(counter.toJSON(), 23); counter.reset(); assert.equal(counter.toJSON(), 0); counter.reset(50); assert.equal(counter.toJSON(), 50); }); it('returns the expected type', () => { assert.equal(counter.getType(), 'Counter'); }); }); ================================================ FILE: packages/measured-core/test/unit/metrics/test-Gauge.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); describe('Gauge', function() { it('reads value from function', function() { var i = 0; var gauge = new common.measured.Gauge(function() { return i++; }); assert.equal(gauge.toJSON(), 0); assert.equal(gauge.toJSON(), 1); }); it('returns the expected type', () => { const gauge = new common.measured.SettableGauge(); assert.equal(gauge.getType(), 'Gauge'); }); }); ================================================ FILE: packages/measured-core/test/unit/metrics/test-Histogram.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); var sinon = require('sinon'); var Histogram = common.measured.Histogram; var EDS = common.measured.ExponentiallyDecayingSample; describe('Histogram', function() { var histogram; beforeEach(function() { histogram = new Histogram(); }); it('all values are null in the beginning', function() { var json = histogram.toJSON(); assert.strictEqual(json.min, null); assert.strictEqual(json.max, null); assert.strictEqual(json.sum, 0); assert.strictEqual(json.variance, null); assert.strictEqual(json.mean, 0); assert.strictEqual(json.stddev, null); assert.strictEqual(json.count, 0); assert.strictEqual(json.median, null); assert.strictEqual(json.p75, null); assert.strictEqual(json.p95, null); assert.strictEqual(json.p99, null); assert.strictEqual(json.p999, null); }); it('returns the expected type', () => { assert.equal(histogram.getType(), 'Histogram'); }); }); describe('Histogram#update', function() { var sample; var histogram; beforeEach(function() { sample = sinon.stub(new EDS()); histogram = new Histogram({ sample: sample }); sample.toArray.returns([]); }); it('updates underlaying sample', function() { histogram.update(5); assert.ok(sample.update.calledWith(5)); }); it('keeps track of min', function() { histogram.update(5); histogram.update(3); histogram.update(6); assert.equal(histogram.toJSON().min, 3); }); it('keeps track of max', function() { histogram.update(5); histogram.update(9); histogram.update(3); assert.equal(histogram.toJSON().max, 9); }); it('keeps track of sum', function() { histogram.update(5); histogram.update(1); histogram.update(12); assert.equal(histogram.toJSON().sum, 18); }); it('keeps track of count', function() { histogram.update(5); histogram.update(1); histogram.update(12); assert.equal(histogram.toJSON().count, 3); }); it('keeps track of mean', function() { histogram.update(5); histogram.update(1); histogram.update(12); assert.equal(histogram.toJSON().mean, 6); }); it('keeps track of variance (example without variance)', function() { histogram.update(5); histogram.update(5); histogram.update(5); assert.equal(histogram.toJSON().variance, 0); }); it('keeps track of variance (example with variance)', function() { histogram.update(1); histogram.update(2); histogram.update(3); histogram.update(4); assert.equal(histogram.toJSON().variance.toFixed(3), '1.667'); }); it('keeps track of stddev', function() { histogram.update(1); histogram.update(2); histogram.update(3); histogram.update(4); assert.equal(histogram.toJSON().stddev.toFixed(3), '1.291'); }); it('keeps track of percentiles', function() { var values = []; var i; for (i = 1; i <= 100; i++) { values.push(i); } sample.toArray.returns(values); var json = histogram.toJSON(); assert.equal(json.median.toFixed(3), '50.500'); assert.equal(json.p75.toFixed(3), '75.750'); assert.equal(json.p95.toFixed(3), '95.950'); assert.equal(json.p99.toFixed(3), '99.990'); assert.equal(json.p999.toFixed(3), '100.000'); }); }); describe('Histogram#percentiles', function() { var sample; var histogram; beforeEach(function() { sample = sinon.stub(new EDS()); histogram = new Histogram({ sample: sample }); var values = []; var i; for (i = 1; i <= 100; i++) { values.push(i); } var swapWith; var value; for (i = 0; i < 100; i++) { swapWith = Math.floor(Math.random() * 100); value = values[i]; values[i] = values[swapWith]; values[swapWith] = value; } sample.toArray.returns(values); }); it('calculates single percentile correctly', function() { var percentiles = histogram._percentiles([0.5]); assert.equal(percentiles[0.5], 50.5); percentiles = histogram._percentiles([0.99]); assert.equal(percentiles[0.99], 99.99); }); }); describe('Histogram#weightedPercentiles', function() { var sample; var histogram; beforeEach(function() { sample = sinon.stub(new EDS()); histogram = new Histogram({ sample: sample, percentilesMethod: Histogram.weightedPercentiles }); var values = []; var i; for (i = 1; i <= 100; i++) { values.push({ value: i, priority: 1 }); } var swapWith; var value; for (i = 0; i < 100; i++) { swapWith = Math.floor(Math.random() * 100); value = values[i]; values[i] = values[swapWith]; values[swapWith] = value; } sample.toArrayWithWeights.returns(values); sample.toArray.returns( values.map(function(item) { return item.value; }) ); }); it('calculates single percentile correctly', function() { var percentiles = histogram._percentiles([0.5]); assert.equal(percentiles[0.5], 50.5); percentiles = histogram._percentiles([0.99]); assert.equal(percentiles[0.99], 99.99); }); }); describe('Histogram#reset', function() { var sample; var histogram; beforeEach(function() { sample = new EDS(); histogram = new Histogram({ sample: sample }); }); it('resets all values', function() { histogram.update(5); histogram.update(2); var json = histogram.toJSON(); var key; for (key in json) { if (json.hasOwnProperty(key)) { assert.ok(typeof json[key] === 'number'); } } histogram.reset(); json = histogram.toJSON(); for (key in json) { if (json.hasOwnProperty(key)) { assert.ok(json[key] === 0 || json[key] === null); } } }); }); describe('Histogram#hasValues', function() { var histogram; beforeEach(function() { histogram = new Histogram(); }); it('has values', function() { histogram.update(5); assert.ok(histogram.hasValues()); }); it('has no values', function() { assert.equal(histogram.hasValues(), false); }); }); ================================================ FILE: packages/measured-core/test/unit/metrics/test-Meter.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); var sinon = require('sinon'); var units = common.measured.units; describe('Meter', function() { var meter; var clock; beforeEach(function() { clock = sinon.useFakeTimers(); meter = new common.measured.Meter({ getTime: function() { return new Date().getTime(); } }); }); afterEach(function() { clock.restore(); }); it('all values are correctly initialized', function() { assert.deepEqual(meter.toJSON(), { mean: 0, count: 0, currentRate: 0, '1MinuteRate': 0, '5MinuteRate': 0, '15MinuteRate': 0 }); }); it('supports rates override from opts', function() { var rate = sinon.stub().returns(666); var properties = { m1Rate: { rate: rate }, m5Rate: { rate: rate }, m15Rate: { rate: rate } }; var json = new common.measured.Meter(properties).toJSON(); assert.equal(json['1MinuteRate'].toFixed(0), '666'); assert.equal(json['5MinuteRate'].toFixed(0), '666'); assert.equal(json['15MinuteRate'].toFixed(0), '666'); }); it('decay over two marks and ticks', function() { meter.mark(5); meter._tick(); var json = meter.toJSON(); assert.equal(json.count, 5); assert.equal(json['1MinuteRate'].toFixed(4), '0.0800'); assert.equal(json['5MinuteRate'].toFixed(4), '0.0165'); assert.equal(json['15MinuteRate'].toFixed(4), '0.0055'); meter.mark(10); meter._tick(); json = meter.toJSON(); assert.equal(json.count, 15); assert.equal(json['1MinuteRate'].toFixed(3), '0.233'); assert.equal(json['5MinuteRate'].toFixed(3), '0.049'); assert.equal(json['15MinuteRate'].toFixed(3), '0.017'); }); it('mean rate', function() { meter.mark(5); clock.tick(5000); var json = meter.toJSON(); assert.equal(json.mean, 1); clock.tick(5000); json = meter.toJSON(); assert.equal(json.mean, 0.5); }); it('currentRate is the observed rate since the last toJSON call', function() { meter.mark(1); meter.mark(2); meter.mark(3); clock.tick(3000); assert.equal(meter.toJSON().currentRate, 2); }); it('currentRate resets by reading it', function() { meter.mark(1); meter.mark(2); meter.mark(3); meter.toJSON(); assert.strictEqual(meter.toJSON().currentRate, 0); }); it('currentRate also resets internal duration timer by reading it', function() { meter.mark(1); meter.mark(2); meter.mark(3); clock.tick(1000); meter.toJSON(); clock.tick(1000); meter.toJSON(); meter.mark(1); clock.tick(1000); assert.strictEqual(meter.toJSON().currentRate, 1); }); it('#reset resets all values', function() { meter.mark(1); var json = meter.toJSON(); var key, value; for (key in json) { if (json.hasOwnProperty(key)) { value = json[key]; assert.ok(typeof value === 'number'); } } meter.reset(); json = meter.toJSON(); for (key in json) { if (json.hasOwnProperty(key)) { value = json[key]; assert.ok(value === 0 || value === null); } } }); it('returns the expected type', () => { assert.equal(meter.getType(), 'Meter'); }); }); ================================================ FILE: packages/measured-core/test/unit/metrics/test-NoOpMeter.js ================================================ /*global describe, it, beforeEach, afterEach*/ var common = require('../../common'); var assert = require('assert'); describe('NoOpMeter', () => { let meter; beforeEach(() => { meter = new common.measured.NoOpMeter(); }); it('always returns empty object', () => { assert.deepEqual(meter.toJSON(), {}); }); it('returns the expected type', () => { assert.equal(meter.getType(), 'Meter'); }); }); ================================================ FILE: packages/measured-core/test/unit/metrics/test-SettableGauge.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); describe('SettableGauge', function() { it('can be set with an initial value', () => { const gauge = new common.measured.SettableGauge({ initialValue: 5 }); assert.equal(gauge.toJSON(), 5); gauge.setValue(11); assert.equal(gauge.toJSON(), 11); }); it('reads value from internal state', () => { const gauge = new common.measured.SettableGauge(); assert.equal(gauge.toJSON(), 0); gauge.setValue(5); assert.equal(gauge.toJSON(), 5); }); it('returns the expected type', () => { const gauge = new common.measured.SettableGauge(); assert.equal(gauge.getType(), 'Gauge'); }); }); ================================================ FILE: packages/measured-core/test/unit/metrics/test-Timer.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); var sinon = require('sinon'); var Timer = common.measured.Timer; var Histogram = common.measured.Histogram; var Meter = common.measured.Meter; describe('Timer', function() { var timer; var meter; var histogram; var clock; beforeEach(function() { clock = sinon.useFakeTimers(); meter = sinon.stub(new Meter()); histogram = sinon.stub(new Histogram()); timer = new Timer({ meter: meter, histogram: histogram, getTime: function() { return new Date().getTime(); } }); }); afterEach(function() { clock.restore(); }); it('can be initialized without options', function() { timer = new Timer(); }); it('#update() marks the meter', function() { timer.update(5); assert.ok(meter.mark.calledOnce); }); it('#update() updates the histogram', function() { timer.update(5); assert.ok(histogram.update.calledWith(5)); }); it('#toJSON() contains meter info', function() { meter.toJSON.returns({ a: 1, b: 2 }); var json = timer.toJSON(); assert.deepEqual(json.meter, { a: 1, b: 2 }); }); it('#toJSON() contains histogram info', function() { histogram.toJSON.returns({ c: 3, d: 4 }); var json = timer.toJSON(); assert.deepEqual(json.histogram, { c: 3, d: 4 }); }); it('#start returns a Stopwatch which updates the timer', function() { clock.tick(10); var watch = timer.start(); clock.tick(50); watch.end(); assert.ok(meter.mark.calledOnce); assert.equal(histogram.update.args[0][0], 50); }); it('#reset is delegated to histogram and meter', function() { timer.reset(); assert.ok(meter.reset.calledOnce); assert.ok(histogram.reset.calledOnce); }); it('returns the expected type', () => { assert.equal(timer.getType(), 'Timer'); }); }); ================================================ FILE: packages/measured-core/test/unit/test-Collection.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../common'); var assert = require('assert'); describe('Collection', function() { var collection; beforeEach(function() { collection = common.measured.createCollection(); }); it('with two counters', function() { collection = new common.measured.Collection('counters'); var a = collection.counter('a'); var b = collection.counter('b'); a.inc(3); b.inc(5); assert.deepEqual(collection.toJSON(), { counters: { a: 3, b: 5 } }); }); it('returns same metric object when given the same name', function() { var a1 = collection.counter('a'); var a2 = collection.counter('a'); assert.strictEqual(a1, a2); }); it('throws exception when creating a metric without name', function() { assert.throws(function() { collection.counter(); }, /You must supply a metric name/); }); }); ================================================ FILE: packages/measured-core/test/unit/util/test-BinaryHeap.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); var BinaryHeap = common.measured.BinaryHeap; describe('BinaryHeap#toArray', function() { it('is empty in the beginning', function() { var heap = new BinaryHeap(); assert.deepEqual(heap.toArray(), []); }); it('does not leak internal references', function() { var heap = new BinaryHeap(); var array = heap.toArray(); array.push(1); assert.deepEqual(heap.toArray(), []); }); }); describe('BinaryHeap#toSortedArray', function() { it('is empty in the beginning', function() { var heap = new BinaryHeap(); assert.deepEqual(heap.toSortedArray(), []); }); it('does not leak internal references', function() { var heap = new BinaryHeap(); var array = heap.toSortedArray(); array.push(1); assert.deepEqual(heap.toSortedArray(), []); }); it('returns a sorted array', function() { var heap = new BinaryHeap(); heap.add(1, 2, 3, 4, 5, 6, 7, 8); assert.deepEqual(heap.toSortedArray(), [8, 7, 6, 5, 4, 3, 2, 1]); }); }); describe('BinaryHeap#add', function() { var heap; beforeEach(function() { heap = new BinaryHeap(); }); it('lets you add one element', function() { heap.add(1); assert.deepEqual(heap.toArray(), [1]); }); it('lets you add two elements', function() { heap.add(1); heap.add(2); assert.deepEqual(heap.toArray(), [2, 1]); }); it('lets you add two elements at once', function() { heap.add(1, 2); assert.deepEqual(heap.toArray(), [2, 1]); }); it('places elements according to their valueOf()', function() { heap.add(2); heap.add(1); heap.add(3); assert.deepEqual(heap.toArray(), [3, 1, 2]); }); }); describe('BinaryHeap#removeFirst', function() { var heap; beforeEach(function() { heap = new BinaryHeap(); heap.add(1, 2, 3, 4, 5, 6, 7, 8); }); it('removeFirst returns the last element', function() { var element = heap.removeFirst(); assert.equal(element, 8); }); it('removeFirst removes the last element', function() { heap.removeFirst(); assert.equal(heap.toArray().length, 7); }); it('removeFirst works multiple times', function() { assert.equal(heap.removeFirst(), 8); assert.equal(heap.removeFirst(), 7); assert.equal(heap.removeFirst(), 6); assert.equal(heap.removeFirst(), 5); assert.equal(heap.removeFirst(), 4); assert.equal(heap.removeFirst(), 3); assert.equal(heap.removeFirst(), 2); assert.equal(heap.removeFirst(), 1); assert.equal(heap.removeFirst(), undefined); }); }); describe('BinaryHeap#first', function() { var heap; beforeEach(function() { heap = new BinaryHeap(); heap.add(1, 2, 3); }); it('returns the first element but does not remove it', function() { var element = heap.first(); assert.equal(element, 3); assert.equal(heap.toArray().length, 3); }); }); describe('BinaryHeap#size', function() { it('takes custom score function', function() { var heap = new BinaryHeap({ elements: [1, 2, 3] }); assert.equal(heap.size(), 3); }); }); describe('BinaryHeap', function() { it('takes custom score function', function() { var heap = new BinaryHeap({ score: function(obj) { return -obj; } }); heap.add(8, 7, 6, 5, 4, 3, 2, 1); assert.deepEqual(heap.toSortedArray(), [1, 2, 3, 4, 5, 6, 7, 8]); }); }); ================================================ FILE: packages/measured-core/test/unit/util/test-ExponentiallyDecayingSample.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); var EDS = common.measured.ExponentiallyDecayingSample; var units = common.measured.units; describe('ExponentiallyDecayingSample#toSortedArray', function() { var sample; beforeEach(function() { sample = new EDS({ size: 3, random: function() { return 1; } }); }); it('returns an empty array by default', function() { assert.deepEqual(sample.toSortedArray(), []); }); it('is always sorted by priority', function() { sample.update('a', Date.now() + 3000); sample.update('b', Date.now() + 2000); sample.update('c', Date.now()); assert.deepEqual(sample.toSortedArray(), ['c', 'b', 'a']); }); }); describe('ExponentiallyDecayingSample#toArray', function() { var sample; beforeEach(function() { sample = new EDS({ size: 3, random: function() { return 1; } }); }); it('returns an empty array by default', function() { assert.deepEqual(sample.toArray(), []); }); it('may return an unsorted array', function() { sample.update('a', Date.now() + 3000); sample.update('b', Date.now() + 2000); sample.update('c', Date.now()); assert.deepEqual(sample.toArray(), ['c', 'a', 'b']); }); }); describe('ExponentiallyDecayingSample#update', function() { var sample; beforeEach(function() { sample = new EDS({ size: 2, random: function() { return 1; } }); }); it('can add one item', function() { sample.update('a'); assert.deepEqual(sample.toSortedArray(), ['a']); }); it('sorts items according to priority ascending', function() { sample.update('a', Date.now()); sample.update('b', Date.now() + 1000); assert.deepEqual(sample.toSortedArray(), ['a', 'b']); }); it('pops items with lowest priority', function() { sample.update('a', Date.now()); sample.update('b', Date.now() + 1000); sample.update('c', Date.now() + 2000); assert.deepEqual(sample.toSortedArray(), ['b', 'c']); }); it('items with too low of a priority do not make it in', function() { sample.update('a', Date.now() + 1000); sample.update('b', Date.now() + 2000); sample.update('c', Date.now()); assert.deepEqual(sample.toSortedArray(), ['a', 'b']); }); }); describe('ExponentiallyDecayingSample#_rescale', function() { var sample; beforeEach(function() { sample = new EDS({ size: 2, random: function() { return 1; } }); }); it('works as expected', function() { sample.update('a', Date.now() + 50 * units.MINUTES); sample.update('b', Date.now() + 55 * units.MINUTES); var elements = sample._elements.toSortedArray(); assert.ok(elements[0].priority > 1000); assert.ok(elements[1].priority > 1000); sample._rescale(Date.now() + 60 * units.MINUTES); elements = sample._elements.toSortedArray(); assert.ok(elements[0].priority < 1); assert.ok(elements[0].priority > 0); assert.ok(elements[1].priority < 1); assert.ok(elements[1].priority > 0); }); }); ================================================ FILE: packages/measured-core/test/unit/util/test-ExponentiallyMovingWeightedAverage.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); var units = common.measured.units; var EMWA = common.measured.ExponentiallyMovingWeightedAverage; describe('ExponentiallyMovingWeightedAverage', function() { it('decay over several updates and ticks', function() { var ewma = new EMWA(units.MINUTES, 5 * units.SECONDS); ewma.update(5); ewma.tick(); assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.080'); ewma.update(5); ewma.update(5); ewma.tick(); assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.233'); ewma.update(15); ewma.tick(); assert.equal(ewma.rate(units.SECONDS).toFixed(3), '0.455'); var i; for (i = 0; i < 200; i++) { ewma.update(15); ewma.tick(); } assert.equal(ewma.rate(units.SECONDS).toFixed(3), '3.000'); }); }); ================================================ FILE: packages/measured-core/test/unit/util/test-Stopwatch.js ================================================ /*global describe, it, beforeEach, afterEach*/ 'use strict'; var common = require('../../common'); var assert = require('assert'); var Stopwatch = common.measured.Stopwatch; var sinon = require('sinon'); describe('Stopwatch', function() { var watch; var clock; beforeEach(function() { clock = sinon.useFakeTimers(); watch = new Stopwatch({ getTime: function() { return new Date().getTime(); } }); }); afterEach(function() { clock.restore(); }); it('returns time on end', function() { clock.tick(100); var elapsed = watch.end(); assert.equal(elapsed, 100); }); it('emits time on end', function() { clock.tick(20); var time; watch.on('end', function(_time) { time = _time; }); watch.end(); assert.equal(time, 20); }); it('becomes useless after being ended once', function() { clock.tick(20); var time; watch.on('end', function(_time) { time = _time; }); assert.equal(watch.end(), 20); assert.equal(time, 20); time = null; assert.equal(watch.end(), undefined); assert.equal(time, null); }); }); ================================================ FILE: packages/measured-node-metrics/README.md ================================================ # Measured Node Metrics 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. [![npm](https://img.shields.io/npm/v/measured-node-metrics.svg)](https://www.npmjs.com/package/measured-node-metrics) ## Install ``` npm install measured-node-metrics ``` ## What is in this package ### [Measured Node Metrics Module](https://yaorg.github.io/node-measured/module-measured-node-metrics.html) See the docs for the main module to see the exported helper functions and maps of metric generators for various system and os metrics. ## Example usage ```javascript const express = require('express'); const { createProcessMetrics, createOSMetrics, createExpressMiddleware } = require('measured-node-metrics'); const registry = new SelfReportingMetricsRegistry(new SomeReporterImple()); // Create and register default OS metrics createOSMetrics(registry); // Create and register default process metrics createProcessMetrics(registry); // Use the express middleware const app = express(); app.use(createExpressMiddleware(registry)); // Implement the rest of app ``` You can also create your own middleware if your not using express, (please contribute it) ```javascript const { onRequestStart, onRequestEnd } = require('measured-node-metrics'); /** * Creates an Express middleware that reports a timer on request data. * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. * * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {number} [reportingIntervalInSeconds] * @return {Function} */ createExpressMiddleware: (metricsRegistry, reportingIntervalInSeconds) => { return (req, res, next) => { const stopwatch = onRequestStart(); req.on('end', () => { const { method } = req; const { statusCode } = res; // path variables should be stripped in order to avoid runaway time series creation, // /v1/cars/:id should be one dimension rather than n, one for each id. const uri = req.route ? req.route.path : '_unknown'; onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds); }); next(); }; } ``` ================================================ FILE: packages/measured-node-metrics/lib/index.js ================================================ const { nodeProcessMetrics, createProcessMetrics } = require('./nodeProcessMetrics'); const { nodeOsMetrics, createOSMetrics } = require('./nodeOsMetrics'); const { createExpressMiddleware, createKoaMiddleware, onRequestStart, onRequestEnd } = require('./nodeHttpRequestMetrics'); /** * The main module for the measured-node-metrics lib. * * Various functions to help create node metrics and http framework middlewares * that can be used with a self reporting metrics registry to easily instrument metrics for a node app. * * @module measured-node-metrics */ module.exports = { /** * Map of metric names and a functions that can be used to generate that metric object that can be registered with a * self reporting metrics registry or used as seen fit. * * See {@link nodeProcessMetrics}. * * @type {Object.} */ nodeProcessMetrics, /** * Method that can be used to add a set of default node process metrics to your node app. * * registers all metrics defined in the {@link nodeProcessMetrics} map. * * @function * @name createProcessMetrics * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {Dimensions} customDimensions * @param {number} reportingIntervalInSeconds */ createProcessMetrics, /** * Map of metric names and a functions that can be used to generate that metric object that can be registered with a * self reporting metrics registry or used as seen fit. * * See {@link nodeOsMetrics}. * * @type {Object.} */ nodeOsMetrics, /** * Method that can be used to add a set of default node process metrics to your app. * * registers all metrics defined in the {@link nodeOsMetrics} map. * * @function * @name createOSMetrics * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {Dimensions} customDimensions * @param {number} reportingIntervalInSeconds */ createOSMetrics, /** * Creates an Express middleware that reports a timer on request data. * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. * * @function * @name createExpressMiddleware * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {number} [reportingIntervalInSeconds] * @return {Function} */ createExpressMiddleware, /** * Creates a Koa middleware that reports a timer on request data. * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. * * @function * @name createExpressMiddleware * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {number} [reportingIntervalInSeconds] * @return {Function} */ createKoaMiddleware, /** * At the start of the request, create a stopwatch, that starts tracking how long the request is taking. * @function * @name onRequestStart * @return {Stopwatch} */ onRequestStart, /** * When the request ends stop the stop watch and create or update the timer for requests that tracked by method, statuscode, path. * The timers (meters and histograms) that get reported will be filterable by status codes, http method, the uri path. * You will be able to create dash boards such as success percentage, latency percentiles by path and method, etc. * * @function * @name onRequestEnd * @param metricsRegistry The Self Reporting Metrics Registry * @param stopwatch The stopwatch created by onRequestStart * @param method The Http Method for the request * @param statusCode The status code for the response * @param [uri] The uri for the request. Please note to avoid out of control time series dimension creation spread, * you would want to strip out ids and or other variables from the uri path. * @param [reportingIntervalInSeconds] override the reporting interval defaults to every 10 seconds. */ onRequestEnd }; ================================================ FILE: packages/measured-node-metrics/lib/nodeHttpRequestMetrics.js ================================================ const { Stopwatch } = require('measured-core'); /** * The default reporting interval for requests * @type {number} */ const DEFAULT_REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10; /** * This module has functions needed to create middlewares for frameworks such as express and koa. * It also exports the 2 functions needed to implement your own middleware. * If you implement a middleware for a framework not implemented here, please contribute it back. * * @module node-http-request-metrics */ module.exports = { /** * Creates an Express middleware that reports a timer on request data. * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. * * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {number} [reportingIntervalInSeconds] * @return {Function} */ createExpressMiddleware: (metricsRegistry, reportingIntervalInSeconds) => { return (req, res, next) => { const stopwatch = module.exports.onRequestStart(); res.on('finish', () => { const { method } = req; const { statusCode } = res; const uri = req.route ? req.route.path : '_unknown'; module.exports.onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds); }); next(); }; }, /** * Creates a Koa middleware that reports a timer on request data. * With this middleware you will get requests counts and latency percentiles all filterable by status codes, http method, and uri paths. * * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {number} [reportingIntervalInSeconds] * @return {Function} */ createKoaMiddleware: (metricsRegistry, reportingIntervalInSeconds) => async (ctx, next) => { const stopwatch = module.exports.onRequestStart(); const { req, res } = ctx; res.once('finish', () => { const { method } = req; const { statusCode } = res; const uri = ctx._matchedRoute || '_unknown'; module.exports.onRequestEnd(metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds); }); await next(); }, /** * At the start of the request, create a stopwatch, that starts tracking how long the request is taking. * @return {Stopwatch} */ onRequestStart: () => { return new Stopwatch(); }, /** * When the request ends stop the stop watch and create or update the timer for requests that tracked by method, status code, path. * The timers (meters and histograms) that get reported will be filterable by status codes, http method, the uri path. * You will be able to create dash boards such as success percentage, latency percentiles by uri path and method, etc. * * @param {SelfReportingMetricsRegistry} metricsRegistry The Self Reporting Metrics Registry * @param {Stopwatch} stopwatch The stopwatch created by onRequestStart * @param {string} method The Http Method for the request * @param {string|number} statusCode The status code for the response * @param {string} [uri] The uri path for the request. Please note to avoid out of control time series dimension creation spread, * you would want to strip out ids and or other variables from the uri path. * @param {number} [reportingIntervalInSeconds] override the reporting interval defaults to every 10 seconds. */ onRequestEnd: (metricsRegistry, stopwatch, method, statusCode, uri, reportingIntervalInSeconds) => { reportingIntervalInSeconds = reportingIntervalInSeconds || DEFAULT_REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS; const customDimensions = { statusCode: `${statusCode}`, method: `${method}` }; if (uri) { customDimensions.uri = uri; } // get or create the timer for the request count/latency timer const requestTimer = metricsRegistry.getOrCreateTimer('requests', customDimensions, reportingIntervalInSeconds); // stop the request latency counter const time = stopwatch.end(); requestTimer.update(time); } }; ================================================ FILE: packages/measured-node-metrics/lib/nodeOsMetrics.js ================================================ const { Gauge, CachedGauge } = require('measured-core'); const { cpuAverage, calculateCpuUsagePercent } = require('./utils/CpuUtils'); const os = require('os'); /** * The default reporting interval for node os metrics is 30 seconds. * * @type {number} */ const DEFAULT_NODE_OS_METRICS_REPORTING_INTERVAL_IN_SECONDS = 30; /** * A map of Metric generating functions, that create Metrics to measure node os stats. */ const nodeOsMetrics = { /** * https://nodejs.org/api/os.html#os_os_loadavg * @return {Gauge} */ 'node.os.loadavg.1m': () => { return new Gauge(() => { return os.loadavg()[0]; }); }, /** * https://nodejs.org/api/os.html#os_os_loadavg * @return {Gauge} */ 'node.os.loadavg.5m': () => { return new Gauge(() => { return os.loadavg()[1]; }); }, /** * https://nodejs.org/api/os.html#os_os_loadavg * @return {Gauge} */ 'node.os.loadavg.15m': () => { return new Gauge(() => { return os.loadavg()[2]; }); }, 'node.os.freemem': () => { return new Gauge(() => { return os.freemem(); }); }, 'node.os.totalmem': () => { return new Gauge(() => { return os.totalmem(); }); }, /** * Gauge to track how long the os has been running. *\ *]=- * See {@link https://nodejs.org/api/os.html#os_os_uptime} for more information. * @return {Gauge} */ 'node.os.uptime': () => { return new Gauge(() => { // The os.uptime() method returns the system uptime in number of seconds. return os.uptime(); }); }, /** * Creates a {@link CachedGauge} that will self update every updateIntervalInSeconds and sample the * cpu usage across all cores for sampleTimeInSeconds. * * @param {number} [updateIntervalInSeconds] How often to update and cache the cpu usage average, defaults to 30 seconds. * @param {number} [sampleTimeInSeconds] How long to sample the cpu usage over, defaults to 5 seconds. */ 'node.os.cpu.all-cores-avg': (updateIntervalInSeconds, sampleTimeInSeconds) => { updateIntervalInSeconds = updateIntervalInSeconds || 30; sampleTimeInSeconds = sampleTimeInSeconds || 5; return new CachedGauge(() => { return new Promise(resolve => { //Grab first CPU Measure const startMeasure = cpuAverage(); setTimeout(() => { //Grab second Measure const endMeasure = cpuAverage(); const percentageCPU = calculateCpuUsagePercent(startMeasure, endMeasure); resolve(percentageCPU); }, sampleTimeInSeconds); }); }, updateIntervalInSeconds); } }; /** * This module contains the methods to create and register default node os metrics to a metrics registry. * * @module node-os-metrics */ module.exports = { /** * Method that can be used to add a set of default node process metrics to your app. * * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {Dimensions} customDimensions * @param {number} reportingIntervalInSeconds */ createOSMetrics: (metricsRegistry, customDimensions, reportingIntervalInSeconds) => { customDimensions = customDimensions || {}; reportingIntervalInSeconds = reportingIntervalInSeconds || DEFAULT_NODE_OS_METRICS_REPORTING_INTERVAL_IN_SECONDS; Object.keys(nodeOsMetrics).forEach(metricName => { metricsRegistry.register(metricName, nodeOsMetrics[metricName](), customDimensions, reportingIntervalInSeconds); }); }, /** * Map of metric names to a corresponding function that creates and returns a Metric that tracks it. * See {@link nodeOsMetrics} */ nodeOsMetrics }; ================================================ FILE: packages/measured-node-metrics/lib/nodeProcessMetrics.js ================================================ const { Gauge } = require('measured-core'); const process = require('process'); /** * The default reporting interval for node process metrics is 30 seconds. * * @type {number} */ const DEFAULT_NODE_PROCESS_METRICS_REPORTING_INTERVAL_IN_SECONDS = 30; /** * A map of Metric generating functions, that create Metrics to measure node process stats. * @type {Object.} */ const nodeProcessMetrics = { /** * Creates a gauge that reports the rss from the node memory usage api. * See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information. * * @return {Gauge} */ 'node.process.memory-usage.rss': () => { return new Gauge(() => { return process.memoryUsage().rss; }); }, /** * See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information. * * @return {Gauge} */ 'node.process.memory-usage.heap-total': () => { return new Gauge(() => { return process.memoryUsage().heapTotal; }); }, /** * See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information. * * @return {Gauge} */ 'node.process.memory-usage.heap-used': () => { return new Gauge(() => { return process.memoryUsage().heapUsed; }); }, /** * See {@link https://nodejs.org/api/process.html#process_process_memoryusage} for more information. * * @return {Gauge} */ 'node.process.memory-usage.external': () => { return new Gauge(() => { const mem = process.memoryUsage(); return Object.prototype.hasOwnProperty.call(mem, 'external') ? mem.external : 0; }); }, /** * Gauge to track how long the node process has been running. * * See {@link https://nodejs.org/api/process.html#process_process_uptime} for more information. * @return {Gauge} */ 'node.process.uptime': () => { return new Gauge(() => { return Math.floor(process.uptime()); }); } }; /** * This module contains the methods to create and register default node process metrics to a metrics registry. * * @module node-process-metrics */ module.exports = { /** * Method that can be used to add a set of default node process metrics to your app. * * @param {SelfReportingMetricsRegistry} metricsRegistry * @param {Dimensions} [customDimensions] * @param {number} [reportingIntervalInSeconds] */ createProcessMetrics: (metricsRegistry, customDimensions, reportingIntervalInSeconds) => { customDimensions = customDimensions || {}; reportingIntervalInSeconds = reportingIntervalInSeconds || DEFAULT_NODE_PROCESS_METRICS_REPORTING_INTERVAL_IN_SECONDS; Object.keys(nodeProcessMetrics).forEach(metricName => { metricsRegistry.register( metricName, nodeProcessMetrics[metricName](), customDimensions, reportingIntervalInSeconds ); }); }, /** * Map of metric names to a corresponding function that creates and returns a Metric that tracks it. * See {@link nodeProcessMetrics} */ nodeProcessMetrics }; ================================================ FILE: packages/measured-node-metrics/lib/utils/CpuUtils.js ================================================ const os = require('os'); /** * @module CpuUtils */ module.exports = { /** * * @return {{idle: number, total: number}} */ cpuAverage: () => { //Initialise sum of idle and time of cores and fetch CPU info let totalIdle = 0, totalTick = 0; const cpus = os.cpus(); cpus.forEach(cpu => { //Total up the time in the cores tick Object.keys(cpu.times).forEach(type => { totalTick += cpu.times[type]; }); //Total up the idle time of the core totalIdle += cpu.times.idle; }); //Return the average Idle and Tick times return { idle: totalIdle / cpus.length, total: totalTick / cpus.length }; }, /** * * @param {{idle: number, total: number}} startMeasure * @param {{idle: number, total: number}} endMeasure */ calculateCpuUsagePercent: (startMeasure, endMeasure) => { //Calculate the difference in idle and total time between the measures const idleDifference = endMeasure.idle - startMeasure.idle; const totalDifference = endMeasure.total - startMeasure.total; //Calculate the average percentage CPU usage return Math.ceil(100 - 100 * idleDifference / totalDifference); } }; ================================================ FILE: packages/measured-node-metrics/package.json ================================================ { "name": "measured-node-metrics", "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.", "version": "2.0.0", "homepage": "https://yaorg.github.io/node-measured/", "engines": { "node": ">= 5.12" }, "publishConfig": { "access": "public" }, "main": "./lib/index.js", "scripts": { "clean": "rm -fr build", "format": "prettier --write './lib/**/*.{ts,js}'", "lint": "eslint lib --ext .js", "test:node": "mocha './test/**/test-*.js'", "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'", "test:browser": "exit 0", "test": "yarn test:node:coverage", "coverage": "nyc report --reporter=text-lcov | coveralls" }, "repository": { "url": "git://github.com/yaorg/node-measured.git" }, "dependencies": { "measured-core": "^2.0.0" }, "files": [ "lib", "README.md" ], "license": "MIT", "devDependencies": { "express": "^4.16.3", "find-free-port": "^1.2.0", "jsdoc": "^3.5.5", "measured-reporting": "^2.0.0" } } ================================================ FILE: packages/measured-node-metrics/test/integration/test-express-middleware.js ================================================ /*global describe, it, beforeEach, afterEach*/ const express = require('express'); const Registry = require('measured-reporting').SelfReportingMetricsRegistry; const TestReporter = require('../unit/TestReporter'); const { createExpressMiddleware } = require('../../lib'); const findFreePort = require('find-free-port'); const assert = require('assert'); const http = require('http'); describe('express-middleware', () => { let port; let reporter; let registry; let middleware; let app; let httpServer; beforeEach(() => { return new Promise(resolve => { reporter = new TestReporter(); registry = new Registry(reporter); middleware = createExpressMiddleware(registry, 1); app = express(); app.use(middleware); app.use(express.json()); app.get('/hello', (req, res) => res.send('Hello World!')); app.post('/world', (req, res) => res.status(201).send('Hello World!')); app.get('/users/:userId', (req, res) => { res.send(`id: ${req.params.userId}`); }); findFreePort(3000).then(portArr => { port = portArr.shift(); httpServer = http.createServer(app); httpServer.listen(port); resolve(); }); }); }); afterEach(() => { httpServer.close(); registry.shutdown(); }); it('creates a single timer that has 1 count for requests, when an http call is made once', () => { return callLocalHost(port, 'hello').then(() => { const registeredKeys = registry._registry.allKeys(); assert(registeredKeys.length === 1); assert.equal(registeredKeys[0], 'requests-GET-200-/hello'); const metricWrapper = registry._registry.getMetricWrapperByKey('requests-GET-200-/hello'); const { name, dimensions } = metricWrapper; assert.equal(name, 'requests'); assert.deepEqual(dimensions, { statusCode: '200', method: 'GET', uri: '/hello' }); }); }); it('creates a single timer that has 1 count for requests, when an http POST call is made once', () => { const options = { method: 'POST', headers: { 'Content-Type': 'application/json' } }; return callLocalHost(port, 'world', options).then(() => { const registeredKeys = registry._registry.allKeys(); assert(registeredKeys.length === 1); assert.equal(registeredKeys[0], 'requests-POST-201-/world'); const metricWrapper = registry._registry.getMetricWrapperByKey('requests-POST-201-/world'); const { name, dimensions } = metricWrapper; assert.equal(name, 'requests'); assert.deepEqual(dimensions, { statusCode: '201', method: 'POST', uri: '/world' }); }); }); it('does not create runaway n metrics in the registry for n ids in the path', () => { return Promise.all([ callLocalHost(port, 'users/foo'), callLocalHost(port, 'users/bar'), callLocalHost(port, 'users/bop') ]).then(() => { assert.equal(registry._registry.allKeys().length, 1, 'There should only be one metric for /users and GET'); }); }); }); const callLocalHost = (port, endpoint, options) => { return new Promise((resolve, reject) => { const req = Object.assign({ protocol: 'http:', host: '127.0.0.1', port: `${port}`, path: `/${endpoint}`, method: 'GET' }, options || {}); http .request(req, resp => { let data = ''; resp.on('data', chunk => { data += chunk; }); resp.on('end', () => { console.log(JSON.stringify(data)); resolve(); }); }) .on('error', err => { console.log('Error: ', JSON.stringify(err)); reject(); }) .end(); }); }; ================================================ FILE: packages/measured-node-metrics/test/integration/test-koa-middleware.js ================================================ const Koa = require('koa'); const KoaBodyParser = require('koa-bodyparser'); const Router = require('koa-router'); const Registry = require('measured-reporting').SelfReportingMetricsRegistry; const TestReporter = require('../unit/TestReporter'); const { createKoaMiddleware } = require('../../lib'); const findFreePort = require('find-free-port'); const assert = require('assert'); const http = require('http'); describe('koa-middleware', () => { let port; let reporter; let registry; let middleware; let app; let httpServer; let router; beforeEach(() => { return new Promise(resolve => { reporter = new TestReporter(); registry = new Registry(reporter); middleware = createKoaMiddleware(registry, 1); app = new Koa(); router = new Router(); router.get('/hello', ({ response }) => { response.body = 'Hello World!'; return response; }); router.post('/world', ({ response }) => { response.body = 'Hello World!'; response.status = 201; return response; }); router.get('/users/:userId', ({ params, response }) => { response.body = `id: ${params.userId}`; return response; }); app.use(middleware); app.use(KoaBodyParser()); app.use(router.routes()); app.use(router.allowedMethods()); app.on('error', (err) => console.error(err)); findFreePort(3000).then(portArr => { port = portArr.shift(); httpServer = app.listen(port); resolve(); }); }); }); afterEach(() => { httpServer.close(); registry.shutdown(); }); it('creates a single timer that has 1 count for requests, when an http call is made once', () => { return callLocalHost(port, 'hello').then(() => { const registeredKeys = registry._registry.allKeys(); assert(registeredKeys.length === 1); assert.equal(registeredKeys[0], 'requests-GET-200-/hello'); const metricWrapper = registry._registry.getMetricWrapperByKey('requests-GET-200-/hello'); const { name, dimensions } = metricWrapper; assert.equal(name, 'requests'); assert.deepEqual(dimensions, { statusCode: '200', method: 'GET', uri: '/hello' }); }); }); it('creates a single timer that has 1 count for requests, when an http POST call is made once', () => { const options = { method: 'POST', headers: { 'Content-Type': 'application/json' } }; return callLocalHost(port, 'world', options).then(() => { const registeredKeys = registry._registry.allKeys(); assert(registeredKeys.length === 1); assert.equal(registeredKeys[0], 'requests-POST-201-/world'); const metricWrapper = registry._registry.getMetricWrapperByKey('requests-POST-201-/world'); const { name, dimensions } = metricWrapper; assert.equal(name, 'requests'); assert.deepEqual(dimensions, { statusCode: '201', method: 'POST', uri: '/world' }); }); }); it('does not create runaway n metrics in the registry for n ids in the path', () => { return Promise.all([ callLocalHost(port, 'users/foo'), callLocalHost(port, 'users/bar'), callLocalHost(port, 'users/bop') ]).then(() => { assert.equal(registry._registry.allKeys().length, 1, 'There should only be one metric for /users and GET'); }); }); }); const callLocalHost = (port, endpoint, options) => { return new Promise((resolve, reject) => { const req = Object.assign({ protocol: 'http:', host: '127.0.0.1', port: `${port}`, path: `/${endpoint}`, method: 'GET' }, options || {}); http .request(req, resp => { let data = ''; resp.on('data', chunk => { data += chunk; }); resp.on('end', () => { console.log(JSON.stringify(data)); resolve(); }); }) .on('error', err => { console.log('Error: ', JSON.stringify(err)); reject(); }) .end(); }); }; ================================================ FILE: packages/measured-node-metrics/test/unit/TestReporter.js ================================================ const { Reporter } = require('measured-reporting'); /** * @extends Reporter */ class TestReporter extends Reporter { constructor(options) { super(options); this._reportedMetrics = []; } getReportedMetrics() { return this._reportedMetrics; } _reportMetrics(metrics) { this._reportedMetrics.push(metrics); } } module.exports = TestReporter; ================================================ FILE: packages/measured-node-metrics/test/unit/test-nodeHttpRequestMetrics.js ================================================ /*global describe, it, beforeEach, afterEach*/ const assert = require('assert'); const EventEmitter = require('events'); const { Stopwatch } = require('measured-core'); const { createExpressMiddleware, createKoaMiddleware, onRequestStart, onRequestEnd } = require('../../lib'); const TestReporter = require('./TestReporter'); const Registry = require('measured-reporting').SelfReportingMetricsRegistry; class MockResponse extends EventEmitter { constructor() { super(); this.statusCode = 200; } finish() { this.emit('finish'); } } describe('onRequestStart', () => { it('returns a stopwatch', () => { const stopwatch = onRequestStart(); assert(stopwatch.constructor.name === 'Stopwatch'); }); }); describe('onRequestEnd', () => { it('stops the stopwatch and gets or creates a timer and then updates it with the elapsed time with the appropriate dimensions', () => { const stopwatch = new Stopwatch(); const registry = new Registry(new TestReporter()); onRequestEnd(registry, stopwatch, 'POST', 201, '/some/path'); const registeredKeys = registry._registry.allKeys(); assert(registeredKeys.length === 1); const expectedKey = 'requests-POST-201-/some/path'; assert.equal(registeredKeys[0], expectedKey); const metricWrapper = registry._registry.getMetricWrapperByKey(expectedKey); assert.equal(metricWrapper.name, 'requests'); assert.deepEqual(metricWrapper.dimensions, { statusCode: '201', method: 'POST', uri: '/some/path' }); assert.equal(metricWrapper.metricImpl.getType(), 'Timer'); assert.equal(metricWrapper.metricImpl._histogram._count, 1); registry.shutdown(); }); }); describe('createExpressMiddleware', () => { it('creates and registers a metric called request that is a timer', () => { const reporter = new TestReporter(); const registry = new Registry(reporter); const middleware = createExpressMiddleware(registry); const res = new MockResponse(); middleware( { method: 'GET', routine: { path: '/v1/rest/some-end-point' } }, res, () => {} ); res.finish(); const registeredKeys = registry._registry.allKeys(); assert(registeredKeys.length === 1); assert(registeredKeys[0].includes('requests-GET')); registry.shutdown(); }); }); describe('createKoaMiddleware', () => { it('creates and registers a metric called request that is a timer', async () => { const reporter = new TestReporter(); const registry = new Registry(reporter); const middleware = createKoaMiddleware(registry); const res = new MockResponse(); middleware( { req: { method: 'GET', url: '/v1/rest/some-end-point', }, res, }, () => Promise.resolve() ).then(() => { const registeredKeys = registry._registry.allKeys(); assert(registeredKeys.length === 1); assert(registeredKeys[0].includes('requests-GET')); registry.shutdown(); }); res.finish(); }); }); ================================================ FILE: packages/measured-node-metrics/test/unit/test-nodeOsMetrics.js ================================================ /*global describe, it, beforeEach, afterEach*/ const assert = require('assert'); const { validateMetric } = require('measured-core').metricValidators; const { nodeOsMetrics, createOSMetrics } = require('../../lib'); const TestReporter = require('./TestReporter'); const Registry = require('measured-reporting').SelfReportingMetricsRegistry; const { MetricTypes } = require('measured-core'); describe('nodeOsMetrics', () => { it('contains a map of string to functions that generate a valid metric object', () => { Object.keys(nodeOsMetrics).forEach(metricName => { assert(typeof metricName === 'string', 'The key should be a string'); const metricGeneratingFunction = nodeOsMetrics[metricName]; assert(typeof metricGeneratingFunction === 'function', 'metric generating function should be a function'); const metric = metricGeneratingFunction(); validateMetric(metric); const value = metric.toJSON(); const type = metric.getType(); if ([MetricTypes.COUNTER, MetricTypes.GAUGE].includes(type)) { assert(typeof value === 'number'); } else { assert(typeof value === 'object'); } if (metric.end) { metric.end(); } }); }); }); describe('createOSMetrics', () => { it('creates and registers a metric for every metric defined in nodeOsMetrics', () => { const reporter = new TestReporter(); const registry = new Registry(reporter); createOSMetrics(registry); const registeredKeys = registry._registry.allKeys(); const expectedKeys = Object.keys(nodeOsMetrics); assert(registeredKeys.length > 1); assert.deepEqual(registeredKeys, expectedKeys); registry.shutdown(); }); }); ================================================ FILE: packages/measured-node-metrics/test/unit/test-nodeProcessMetrics.js ================================================ /*global describe, it, beforeEach, afterEach*/ const assert = require('assert'); const { validateMetric } = require('measured-core').metricValidators; const { nodeProcessMetrics, createProcessMetrics } = require('../../lib'); const TestReporter = require('./TestReporter'); const Registry = require('measured-reporting').SelfReportingMetricsRegistry; const { MetricTypes } = require('measured-core'); describe('nodeProcessMetrics', () => { it('contains a map of string to functions that generate a valid metric object', () => { Object.keys(nodeProcessMetrics).forEach(metricName => { assert(typeof metricName === 'string', 'The key should be a string'); const metricGeneratingFunction = nodeProcessMetrics[metricName]; assert(typeof metricGeneratingFunction === 'function', 'metric generating function should be a function'); const metric = metricGeneratingFunction(); validateMetric(metric); const value = metric.toJSON(); const type = metric.getType(); if ([MetricTypes.COUNTER, MetricTypes.GAUGE].includes(type)) { assert(typeof value === 'number'); } else { assert(typeof value === 'object'); } }); }); }); describe('createProcessMetrics', () => { it('creates and registers a metric for every metric defined in nodeProcessMetrics', () => { const reporter = new TestReporter(); const registry = new Registry(reporter); createProcessMetrics(registry); const registeredKeys = registry._registry.allKeys(); const expectedKeys = Object.keys(nodeProcessMetrics); assert(registeredKeys.length > 1); assert.deepEqual(registeredKeys, expectedKeys); registry.shutdown(); }); }); ================================================ FILE: packages/measured-node-metrics/test/unit/utils/test-CpuUtils.js ================================================ /*global describe, it, beforeEach, afterEach*/ const assert = require('assert'); const CpuUtils = require('../../../lib/utils/CpuUtils'); describe('CpuUtils', () => { it('#cpuAverage ', () => { const measure = CpuUtils.cpuAverage(); assert(typeof measure.idle === 'number'); assert(measure.idle > 0); assert(typeof measure.total === 'number'); assert(measure.total > 0); }); it('#calculateCpuUsagePercent calculates a percent', () => { const start = CpuUtils.cpuAverage(); for (let i = 0; i < 10000000; i++) { Math.floor(Math.random() * Math.floor(10000000)); } const end = CpuUtils.cpuAverage(); const percent = CpuUtils.calculateCpuUsagePercent(start, end); assert(typeof percent === 'number'); assert(percent > 0); }); }); ================================================ FILE: packages/measured-reporting/README.md ================================================ # Measured Reporting The registry and reporting library that has the classes needed to create a dimension aware, self reporting metrics registry. [![npm](https://img.shields.io/npm/v/measured-reporting.svg)](https://www.npmjs.com/package/measured-reporting) ## Install ``` npm install measured-reporting ``` ## What is in this package ### [Self Reporting Metrics Registry](https://yaorg.github.io/node-measured/SelfReportingMetricsRegistry.html) 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. See the [SelfReportingMetricsRegistryOptions](https://yaorg.github.io/node-measured/global.html#SelfReportingMetricsRegistryOptions) for advanced configuration. ```javascript const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting'); const registry = new SelfReportingMetricsRegistry(new LoggingReporter({ defaultDimensions: { hostname: os.hostname() } })); // The metric will flow through LoggingReporter#_reportMetrics(metrics) every 10 seconds by default const myCounter = registry.getOrCreateCounter('my-counter'); ``` ### [Reporter Abstract Class](https://yaorg.github.io/node-measured/Reporter.html) 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. See the [ReporterOptions](https://yaorg.github.io/node-measured/global.html#ReporterOptions) for advanced configuration. #### Current Implementations - [SignalFx Reporter](https://yaorg.github.io/node-measured/SignalFxMetricsReporter.html) in the `measured-signalfx-reporter` package. - reports metrics to SignalFx. - [Logging Reporter](https://yaorg.github.io/node-measured/LoggingReporter.html) in the `measured-reporting` package. - A reporter impl that simply logs the metrics via the Logger #### Creating an anonymous Implementation You can technically create an anonymous instance of this, see the following example. ```javascript const os = require('os'); const process = require('process'); const { SelfReportingMetricsRegistry, Reporter } = require('measured-reporting'); // Create a self reporting registry with an anonymous Reporter instance; const registry = new SelfReportingMetricsRegistry( new class extends Reporter { constructor() { super({ defaultDimensions: { hostname: os.hostname(), env: process.env['NODE_ENV'] ? process.env['NODE_ENV'] : 'unset' } }) } _reportMetrics(metrics) { metrics.forEach(metric => { console.log(JSON.stringify({ metricName: metric.name, dimensions: this._getDimensions(metric), data: metric.metricImpl.toJSON() })) }); } }() ); // create a gauge that reports the process uptime every second const processUptimeGauge = registry.getOrCreateGauge('node.process.uptime', () => process.uptime(), {}, 1); ``` Example output: ```bash APP5HTD6ACCD8C:foo jfiel2$ NODE_ENV=development node index.js {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":0.092} {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":1.099} {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":2.104} {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":3.105} {"metricName":"node.process.uptime","dimensions":{"hostname":"APP5HTD6ACCD8C","env":"development"},"data":4.106} ``` Consider creating a proper class and contributing it back to Measured if it is generic and sharable. ### [Logging Reporter Class](https://yaorg.github.io/node-measured/LoggingReporter.html) A simple reporter that logs the metrics via the Logger. See the [ReporterOptions](http://yaorg.github.io/node-measured/build/docs/packages/measured-reporting/global.html#ReporterOptions) for advanced configuration. ```javascript const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting'); const registry = new SelfReportingMetricsRegistry(new LoggingReporter({ logger: myLogerImpl, // defaults to new console logger if not supplied defaultDimensions: { hostname: require('os').hostname() } })); ``` ## What are dimensions? As described by Signal Fx: *A dimension is a key/value pair that, along with the metric name, is part of the identity of a time series. You can filter and aggregate time series by those dimensions across SignalFx.* 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. Graphite also supports the concept via [tags](http://graphite.readthedocs.io/en/latest/tags.html). ================================================ FILE: packages/measured-reporting/lib/@types/types.js ================================================ /** * A wrapper object around a {@link Metric}, {@link Dimensions} and the metric name * * @interface MetricWrapper * @typedef MetricWrapper * @type {Object} * @property {string} name The supplied name of the Metric * @property {Metric} metricImpl The {@link Metric} object * @property {Dimensions} dimensions The {@link Dimensions} for the given {@link Metric} */ /** * A Dictionary of string, string key value pairs * * @interface Dimensions * @typedef Dimensions * @type {Object.} * * @example * { * path: "/api/foo" * method: "GET" * statusCode: "200" * } */ ================================================ FILE: packages/measured-reporting/lib/index.js ================================================ const SelfReportingMetricsRegistry = require('./registries/SelfReportingMetricsRegistry'); const Reporter = require('./reporters/Reporter'); const LoggingReporter = require('./reporters/LoggingReporter'); const inputValidators = require('./validators/inputValidators'); /** * The main measured module that is referenced when require('measured-reporting') is used. * @module measured-reporting */ module.exports = { /** * The Self Reporting Metrics Registry Class. * * @type {SelfReportingMetricsRegistry} */ SelfReportingMetricsRegistry, /** * The abstract / base Reporter class. * * @type {Reporter} */ Reporter, /** * The basic included reference reporter, simply logs the metrics. * See {ReporterOptions} for options. * * @type {LoggingReporter} */ LoggingReporter, /** * Various Input Validation functions. * * @type {inputValidators} */ inputValidators }; ================================================ FILE: packages/measured-reporting/lib/registries/DimensionAwareMetricsRegistry.js ================================================ const mapcap = require('mapcap'); /** * Simple registry that stores Metrics by name and dimensions. */ class DimensionAwareMetricsRegistry { /** * @param {DimensionAwareMetricsRegistryOptions} [options] Configurable options for the Dimension Aware Metrics Registry */ constructor(options) { options = options || {}; let metrics = new Map(); if (options.metricLimit) { metrics = mapcap(metrics, options.metricLimit, options.lru); } this._metrics = metrics; } /** * Checks to see if a metric with the given name and dimensions is present. * * @param {string} name The metric name * @param {Dimensions} dimensions The dimensions for the metric * @returns {boolean} true if the metric with given dimensions is present */ hasMetric(name, dimensions) { const key = this._generateStorageKey(name, dimensions); return this._metrics.has(key); } /** * Retrieves a metric with a given name and dimensions is present. * * @param {string} name The metric name * @param {Dimensions} dimensions The dimensions for the metric * @returns {Metric} a wrapper object around name, dimension and {@link Metric} */ getMetric(name, dimensions) { const key = this._generateStorageKey(name, dimensions); return this._metrics.get(key).metricImpl; } /** * Retrieves a metric by the calculated key (name / dimension combo). * * @param {string} key The registered key for the given registered {@link MetricWrapper} * @returns {MetricWrapper} a wrapper object around name, dimension and {@link Metric} */ getMetricWrapperByKey(key) { return this._metrics.get(key); } /** * Upserts a {@link Metric} in the internal storage map for a given name, dimension combo * * @param {string} name The metric name * @param {Metric} metric The {@link Metric} impl * @param {Dimensions} dimensions The dimensions for the metric * @return {string} The registry key for the metric, dimension combo */ putMetric(name, metric, dimensions) { const key = this._generateStorageKey(name, dimensions); this._metrics.set(key, { name: name, metricImpl: metric, dimensions: dimensions || {} }); return key; } /** * Returns an array of all keys of metrics stored in this registry. * @return {string[]} all keys of metrics stored in this registry. */ allKeys() { return Array.from(this._metrics.keys()); } /** * Generates a unique key off of the metric name and custom dimensions for internal use in the registry maps. * * @param {string} name The metric name * @param {Dimensions} dimensions The dimensions for the metric * @return {string} a unique key based off of the metric nae and dimensions * @private */ _generateStorageKey(name, dimensions) { let key = name; if (dimensions) { Object.keys(dimensions) .sort() .forEach(dimensionKey => { key = `${key}-${dimensions[dimensionKey]}`; }); } return key; } } module.exports = DimensionAwareMetricsRegistry; /** * Configurable options for the Dimension Aware Metrics Registry * * @interface DimensionAwareMetricsRegistryOptions * @typedef DimensionAwareMetricsRegistryOptions * @property {Number} metricLimit the maximum number of metrics the registry may hold before dropping metrics * @property {Boolean} lru switch dropping strategy from "least recently added" to "least recently used" */ ================================================ FILE: packages/measured-reporting/lib/registries/SelfReportingMetricsRegistry.js ================================================ const consoleLogLevel = require('console-log-level'); const { CachedGauge, SettableGauge, Gauge, Timer, Counter, Meter, Histogram } = require('measured-core'); const DimensionAwareMetricsRegistry = require('./DimensionAwareMetricsRegistry'); const { validateSelfReportingMetricsRegistryParameters, validateRegisterOptions, validateGaugeOptions, validateCounterOptions, validateHistogramOptions, validateTimerOptions, validateSettableGaugeOptions, validateCachedGaugeOptions } = require('../validators/inputValidators'); function prefix() { return `${new Date().toISOString()}: `; } /** * A dimensional aware self-reporting metrics registry */ class SelfReportingMetricsRegistry { /** * @param {Reporter|Reporter[]} reporters A single {@link Reporter} or an array of reporters that will be used to report metrics on an interval. * @param {SelfReportingMetricsRegistryOptions} [options] Configurable options for the Self Reporting Metrics Registry */ constructor(reporters, options) { options = options || {}; if (!Array.isArray(reporters)) { reporters = [reporters]; } validateSelfReportingMetricsRegistryParameters(reporters, options); /** * @type {Reporter} * @protected */ this._reporters = reporters; /** * @type {DimensionAwareMetricsRegistry} * @protected */ this._registry = options.registry || new DimensionAwareMetricsRegistry(); this._reporters.forEach(reporter => reporter.setRegistry(this._registry)); /** * Loggers to use, defaults to a new console logger if nothing is supplied in options * @type {Logger} * @protected */ this._log = options.logger || consoleLogLevel({ name: 'SelfReportingMetricsRegistry', level: options.logLevel || 'info', prefix: prefix }); } /** * Registers a manually created Metric. * * @param {string} name The Metric name * @param {Metric} metric The {@link Metric} to register * @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric * @param {number} [publishingIntervalInSeconds] a optional custom publishing interval * @example * const settableGauge = new SettableGauge(5); * // register the gauge and have it report to every 10 seconds * registry.register('my-gauge', settableGauge, {}, 10); * interval(() => { * // such as cpu % used * determineAValueThatCannotBeSync((value) => { * settableGauge.update(value); * }) * }, 10000) */ register(name, metric, dimensions, publishingIntervalInSeconds) { validateRegisterOptions(name, metric, dimensions, publishingIntervalInSeconds); if (this._registry.hasMetric(name, dimensions)) { throw new Error( `Metric with name: ${name} and dimensions: ${JSON.stringify(dimensions)} has already been registered` ); } else { const key = this._registry.putMetric(name, metric, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return metric; } /** * Creates a {@link Gauge} or gets the existing Gauge for a given name and dimension combo * * @param {string} name The Metric name * @param {function} callback The callback that will return a value to report to signal fx * @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric * @param {number} [publishingIntervalInSeconds] a optional custom publishing interval * @return {Gauge} * @example * // https://nodejs.org/api/process.html#process_process_memoryusage * // Report heap total and heap used at the default interval * registry.getOrCreateGauge( * 'process-memory-heap-total', * () => { * return process.memoryUsage().heapTotal * } * ); * registry.getOrCreateGauge( * 'process-memory-heap-used', * () => { * return process.memoryUsage().heapUsed * } * ) */ getOrCreateGauge(name, callback, dimensions, publishingIntervalInSeconds) { validateGaugeOptions(name, callback, dimensions, publishingIntervalInSeconds); let gauge; if (this._registry.hasMetric(name, dimensions)) { gauge = this._registry.getMetric(name, dimensions); } else { gauge = new Gauge(callback); const key = this._registry.putMetric(name, gauge, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return gauge; } /** * Creates a {@link Histogram} or gets the existing Histogram for a given name and dimension combo * * @param {string} name The Metric name * @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric * @param {number} [publishingIntervalInSeconds] a optional custom publishing interval * @return {Histogram} */ getOrCreateHistogram(name, dimensions, publishingIntervalInSeconds) { validateHistogramOptions(name, dimensions, publishingIntervalInSeconds); let histogram; if (this._registry.hasMetric(name, dimensions)) { histogram = this._registry.getMetric(name, dimensions); } else { histogram = new Histogram(); const key = this._registry.putMetric(name, histogram, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return histogram; } /** * Creates a {@link Meter} or gets the existing Meter for a given name and dimension combo * * @param {string} name The Metric name * @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric * @param {number} [publishingIntervalInSeconds] a optional custom publishing interval * @return {Meter} */ getOrCreateMeter(name, dimensions, publishingIntervalInSeconds) { // todo validate options let meter; if (this._registry.hasMetric(name, dimensions)) { meter = this._registry.getMetric(name, dimensions); } else { meter = new Meter(); const key = this._registry.putMetric(name, meter, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return meter; } /** * Creates a {@link Counter} or gets the existing Counter for a given name and dimension combo * * @param {string} name The Metric name * @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric * @param {number} [publishingIntervalInSeconds] a optional custom publishing interval * @return {Counter} */ getOrCreateCounter(name, dimensions, publishingIntervalInSeconds) { validateCounterOptions(name, dimensions, publishingIntervalInSeconds); let counter; if (this._registry.hasMetric(name, dimensions)) { counter = this._registry.getMetric(name, dimensions); } else { counter = new Counter(); const key = this._registry.putMetric(name, counter, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return counter; } /** * Creates a {@link Timer} or gets the existing Timer for a given name and dimension combo. * * @param {string} name The Metric name * @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric * @param {number} [publishingIntervalInSeconds] a optional custom publishing interval * @return {Timer} */ getOrCreateTimer(name, dimensions, publishingIntervalInSeconds) { validateTimerOptions(name, dimensions, publishingIntervalInSeconds); let timer; if (this._registry.hasMetric(name, dimensions)) { timer = this._registry.getMetric(name, dimensions); } else { timer = new Timer(); const key = this._registry.putMetric(name, timer, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return timer; } /** * Creates a {@link SettableGauge} or gets the existing SettableGauge for a given name and dimension combo. * * @param {string} name The Metric name * @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric * @param {number} [publishingIntervalInSeconds] a optional custom publishing interval * @return {SettableGauge} */ getOrCreateSettableGauge(name, dimensions, publishingIntervalInSeconds) { validateSettableGaugeOptions(name, dimensions, publishingIntervalInSeconds); let settableGauge; if (this._registry.hasMetric(name, dimensions)) { settableGauge = this._registry.getMetric(name, dimensions); } else { settableGauge = new SettableGauge(); const key = this._registry.putMetric(name, settableGauge, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return settableGauge; } /** * Creates a {@link CachedGauge} or gets the existing CachedGauge for a given name and dimension combo. * * @param {string} name The Metric name. * @param {function} valueProducingPromiseCallback. * @param {number} cachedGaugeUpdateIntervalInSeconds. * @param {Dimensions} [dimensions] any custom {@link Dimensions} for the Metric. * @param {number} [publishingIntervalInSeconds] a optional custom publishing interval. * @return {CachedGauge} */ getOrCreateCachedGauge( name, valueProducingPromiseCallback, cachedGaugeUpdateIntervalInSeconds, dimensions, publishingIntervalInSeconds ) { validateCachedGaugeOptions(name, valueProducingPromiseCallback, dimensions, publishingIntervalInSeconds); let cachedGauge; if (this._registry.hasMetric(name, dimensions)) { cachedGauge = this._registry.getMetric(name, dimensions); } else { cachedGauge = new CachedGauge(valueProducingPromiseCallback, cachedGaugeUpdateIntervalInSeconds); const key = this._registry.putMetric(name, cachedGauge, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return cachedGauge; } /** * Calls end on all metrics in the registry that support end() and calls end on the reporter */ shutdown() { // shutdown the reporter this._reporters.forEach(reporter => reporter.shutdown()); // shutdown any metrics that have an end method this._registry.allKeys().forEach(key => { const metricWrapper = this._registry.getMetricWrapperByKey(key); if (metricWrapper.metricImpl.end) { metricWrapper.metricImpl.end(); } }); } } module.exports = SelfReportingMetricsRegistry; /** * Configurable options for the Self Reporting Metrics Registry * * @interface SelfReportingMetricsRegistryOptions * @typedef SelfReportingMetricsRegistryOptions * @property {Logger} logger the Logger to use * @property {string} logLevel The Log level to use if defaulting to included logger * @property {DimensionAwareMetricsRegistry} registry The registry to use, defaults to new DimensionAwareMetricsRegistry */ ================================================ FILE: packages/measured-reporting/lib/reporters/LoggingReporter.js ================================================ const Reporter = require('./Reporter'); /** * A reporter impl that simply logs the metrics via the Logger. * * @example * const { SelfReportingMetricsRegistry, LoggingReporter } = require('measured-reporting'); * const registry = new SelfReportingMetricsRegistry(new LoggingReporter()); * * @extends {Reporter} */ class LoggingReporter extends Reporter { /** * @param {LoggingReporterOptions} [options] */ constructor(options) { super(options); const level = (options || {}).logLevelToLogAt; this._logLevel = (level || 'info').toLowerCase(); } /** * Logs the metrics via the inherited logger instance. * @param {MetricWrapper[]} metrics * @protected */ _reportMetrics(metrics) { metrics.forEach(metric => { this._log[this._logLevel]( JSON.stringify({ metricName: metric.name, dimensions: this._getDimensions(metric), data: metric.metricImpl.toJSON() }) ); }); } } module.exports = LoggingReporter; /** * @interface LoggingReporterOptions * @typedef LoggingReporterOptions * @type {Object} * @property {Dimensions} defaultDimensions A dictionary of dimensions to include with every metric reported * @property {Logger} [logger] The logger to use, if not supplied a new Buynan logger will be created * @property {string} [logLevel] The log level to use with the created console logger if you didn't supply your own logger. * @property {number} [defaultReportingIntervalInSeconds] The default reporting interval to use if non is supplied when registering a metric, defaults to 10 seconds. * @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. */ ================================================ FILE: packages/measured-reporting/lib/reporters/Reporter.js ================================================ const consoleLogLevel = require('console-log-level'); const Optional = require('optional-js'); const { validateReporterParameters } = require('../validators/inputValidators'); const DEFAULT_REPORTING_INTERVAL_IN_SECONDS = 10; function prefix() { return `${new Date().toISOString()}: `; } /** * The abstract reporter that specific implementations can extend to create a Self Reporting Metrics Registry Reporter. * * {@link SelfReportingMetricsRegistry} * * @example * const os = require('os'); * const process = require('process'); * const { SelfReportingMetricsRegistry, Reporter } = require('measured-reporting'); * * // Create a self reporting registry with a named anonymous reporter instance; * const registry = new SelfReportingMetricsRegistry( * new class ConsoleReporter extends Reporter { * constructor() { * super({ * defaultDimensions: { * hostname: os.hostname(), * env: process.env['NODE_ENV'] ? process.env['NODE_ENV'] : 'unset' * } * }) * } * * _reportMetrics(metrics) { * metrics.forEach(metric => { * console.log(JSON.stringify({ * metricName: metric.name, * dimensions: this._getDimensions(metric), * data: metric.metricImpl.toJSON() * })) * }); * } * }() * ); * * @example * // Create a regular class that extends Reporter * class LoggingReporter extends Reporter { * _reportMetrics(metrics) { * metrics.forEach(metric => { * this._log.info(JSON.stringify({ * metricName: metric.name, * dimensions: this._getDimensions(metric), * data: metric.metricImpl.toJSON() * })) * }); * } * } * * @abstract */ class Reporter { /** * @param {ReporterOptions} [options] The optional params to supply when creating a reporter. */ constructor(options) { if (this.constructor === Reporter) { throw new TypeError("Can't instantiate abstract class!"); } options = options || {}; validateReporterParameters(options); /** * Map of intervals to metric keys, this will be used to look up what metrics should be reported at a given interval. * * @type {Object.>} * @private */ this._intervalToMetric = {}; this._intervals = []; /** * Map of default dimensions, that should be sent with every metric. * * @type {Dimensions} * @protected */ this._defaultDimensions = options.defaultDimensions || {}; /** * Loggers to use, defaults to a new console logger if nothing is supplied in options * @type {Logger} * @protected */ this._log = options.logger || consoleLogLevel({ name: 'Reporter', level: options.logLevel || 'info', prefix: prefix }); /** * The default reporting interval, a number in seconds. * If not overridden via the {@see ReporterOptions}, defaults to 10 seconds. * * @type {number} * @protected */ this._defaultReportingIntervalInSeconds = options.defaultReportingIntervalInSeconds || DEFAULT_REPORTING_INTERVAL_IN_SECONDS; /** * Flag to indicate if reporting timers should be unref'd. * If not overridden via the {@see ReporterOptions}, defaults to false. * * @type {boolean} * @protected */ this._unrefTimers = !!options.unrefTimers; /** * Flag to indicate if metrics should be reset on each reporting interval. * If not overridden via the {@see ReporterOptions}, defaults to false. * * @type {boolean} * @protected */ this._resetMetricsOnInterval = !!options.resetMetricsOnInterval; } /** * Sets the registry, this must be called before reportMetricOnInterval. * * @param {DimensionAwareMetricsRegistry} registry */ setRegistry(registry) { this._registry = registry; } /** * Informs the reporter to report a metric on a given interval in seconds. * * @param {string} metricKey The metric key for the metric in the metric registry. * @param {number} intervalInSeconds The interval in seconds to report the metric on. */ reportMetricOnInterval(metricKey, intervalInSeconds) { intervalInSeconds = intervalInSeconds || this._defaultReportingIntervalInSeconds; if (!this._registry) { throw new Error( 'You must call setRegistry(registry) before telling a Reporter to report a metric on an interval.' ); } if (Object.prototype.hasOwnProperty.call(this._intervalToMetric, intervalInSeconds)) { this._intervalToMetric[intervalInSeconds].add(metricKey); } else { this._intervalToMetric[intervalInSeconds] = new Set([metricKey]); this._createIntervalCallback(intervalInSeconds); setImmediate(() => { this._reportMetricsWithInterval(intervalInSeconds); }); } } /** * Creates the timed callback loop for the given interval. * * @param {number} intervalInSeconds the interval in seconds for the timeout callback * @private */ _createIntervalCallback(intervalInSeconds) { this._log.debug(`_createIntervalCallback() called with intervalInSeconds: ${intervalInSeconds}`); const timer = setInterval(() => { this._reportMetricsWithInterval(intervalInSeconds); }, intervalInSeconds * 1000); if (this._unrefTimers) { timer.unref(); } this._intervals.push(timer); } /** * Gathers all the metrics that have been registered to report on the given interval. * * @param {number} interval The interval to look up what metrics to report * @private */ _reportMetricsWithInterval(interval) { this._log.debug(`_reportMetricsWithInterval() called with intervalInSeconds: ${interval}`); try { Optional.of(this._intervalToMetric[interval]).ifPresent(metrics => { const metricsToSend = []; metrics.forEach(metricKey => { metricsToSend.push(this._registry.getMetricWrapperByKey(metricKey)); }); this._reportMetrics(metricsToSend); if (this._resetMetricsOnInterval) { metricsToSend.forEach(({ name, metricImpl }) => { if (metricImpl && metricImpl.reset) { this._log.debug('Resetting metric', name); metricImpl.reset(); } }); } }); } catch (error) { this._log.error('Failed to send metrics to signal fx', error); } } /** * This method gets called with an array of {@link MetricWrapper} on an interval, when metrics should be reported. * * This is the main method that needs to get implemented when created an aggregator specific reporter. * * @param {MetricWrapper[]} metrics The array of metrics to report. * @protected * @abstract */ _reportMetrics(metrics) { throw new TypeError('Abstract method _reportMetrics(metrics) must be implemented in implementation class'); } /** * * @param {MetricWrapper} metric The Wrapped Metric Object. * @return {Dimensions} The left merged default dimensions with the metric specific dimensions * @protected */ _getDimensions(metric) { return Object.assign({}, this._defaultDimensions, metric.dimensions); } /** * Clears the intervals that are running to report metrics at an interval, and resets the state. */ shutdown() { this._intervals.forEach(interval => clearInterval(interval)); this._intervals = []; this._intervalToMetric = {}; } } /** * Options for creating a {@link Reporter} * @interface ReporterOptions * @typedef ReporterOptions * @type {Object} * @property {Dimensions} defaultDimensions A dictionary of dimensions to include with every metric reported * @property {Logger} logger The logger to use, if not supplied a new Buynan logger will be created * @property {string} logLevel The log level to use with the created console logger if you didn't supply your own logger. * @property {number} defaultReportingIntervalInSeconds The default reporting interval to use if non is supplied when registering a metric, defaults to 10 seconds. * @property {boolean} unrefTimers Indicate if reporting timers should be unref'd, defaults to false. * @property {boolean} resetMetricsOnInterval Indicate if metrics should be reset on each reporting interval, defaults to false. */ module.exports = Reporter; ================================================ FILE: packages/measured-reporting/lib/validators/inputValidators.js ================================================ const Optional = require('optional-js'); const { validateMetric } = require('measured-core').metricValidators; /** * This module contains various validators to validate publicly exposed input. * * @module inputValidators */ module.exports = { /** * Validates @{link Gauge} options. * * @param {string} name The metric name * @param {function} callback The callback for the Gauge * @param {Dimensions} dimensions The optional custom dimensions * @param {number} publishingIntervalInSeconds the optional publishing interval */ validateGaugeOptions: (name, callback, dimensions, publishingIntervalInSeconds) => { module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds); module.exports.validateNumberReturningCallback(callback); }, /** * Validates @{link Gauge} options. * * @param {string} name The metric name * @param {function} callback The callback for the CachedGauge * @param {Dimensions} dimensions The optional custom dimensions * @param {number} publishingIntervalInSeconds the optional publishing interval */ validateCachedGaugeOptions: (name, callback, dimensions, publishingIntervalInSeconds) => { module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds); // Should we validate the promise call back, it may be expensive or produce a race condition in some use-cases. }, /** * Validates the create histogram Options. * * @param {string} name The metric name * @param {Dimensions} dimensions The optional custom dimensions * @param {number} publishingIntervalInSeconds the optional publishing interval */ validateHistogramOptions: (name, dimensions, publishingIntervalInSeconds) => { module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds); }, /** * Validates the create counter Options. * * @param {string} name The metric name * @param {Dimensions} dimensions The optional custom dimensions * @param {number} publishingIntervalInSeconds the optional publishing interval */ validateCounterOptions: (name, dimensions, publishingIntervalInSeconds) => { module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds); }, /** * Validates the create timer Options. * * @param {string} name The metric name * @param {Dimensions} dimensions The optional custom dimensions * @param {number} publishingIntervalInSeconds the optional publishing interval */ validateTimerOptions: (name, dimensions, publishingIntervalInSeconds) => { module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds); }, /** * Validates the create timer Options. * * @param {string} name The metric name * @param {Metric} metric The metric instance * @param {Dimensions} dimensions The optional custom dimensions * @param {number} publishingIntervalInSeconds the optional publishing interval */ validateRegisterOptions: (name, metric, dimensions, publishingIntervalInSeconds) => { module.exports.validateMetric(metric); module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds); }, /** * Validates the create settable gauge Options. * * @param {string} name The metric name * @param {Dimensions} dimensions The optional custom dimensions * @param {number} publishingIntervalInSeconds the optional publishing interval */ validateSettableGaugeOptions: (name, dimensions, publishingIntervalInSeconds) => { module.exports.validateCommonMetricParameters(name, dimensions, publishingIntervalInSeconds); }, /** * Validates the options that are common amoung all create metric methods * * @param {string} name The metric name * @param {Dimensions} dimensions The optional custom dimensions * @param {number} publishingIntervalInSeconds the optional publishing interval */ validateCommonMetricParameters: (name, dimensions, publishingIntervalInSeconds) => { module.exports.validateMetricName(name); module.exports.validateOptionalDimensions(dimensions); module.exports.validateOptionalPublishingInterval(publishingIntervalInSeconds); }, /** * Validates the metric name. * * @param name The metric name. */ validateMetricName: name => { const type = typeof name; if (type !== 'string') { throw new TypeError(`options.name is a required option and must be of type string, actual type: ${type}`); } }, /** * Validates that a metric implements the metric interface. * * @function * @name validateMetric * @param {Metric} metric The object that is supposed to be a metric. */ validateMetric, /** * Validates the provided callback. * * @param callback The provided callback for a gauge. */ validateNumberReturningCallback: callback => { const type = typeof callback; if (type !== 'function') { throw new TypeError(`options.callback is a required option and must be function, actual type: ${type}`); } const callbackType = typeof callback(); if (callbackType !== 'number') { throw new TypeError(`options.callback must return a number, actual return type: ${callbackType}`); } }, /** * Validates a set of optional dimensions * @param dimensionsOptional */ validateOptionalDimensions: dimensionsOptional => { Optional.ofNullable(dimensionsOptional).ifPresent(dimensions => { const type = typeof dimensions; if (type !== 'object') { throw new TypeError(`options.dimensions should be an object, actual type: ${type}`); } if (Array.isArray(dimensions)) { throw new TypeError('dimensions where detected to be an array, expected Object'); } Object.keys(dimensions).forEach(key => { const valueType = typeof dimensions[key]; if (valueType !== 'string') { throw new TypeError(`options.dimensions.${key} should be of type string, actual type: ${type}`); } }); }); }, /** * Validates that an optional logger instance at least has the methods we expect. * @param loggerOptional */ validateOptionalLogger: loggerOptional => { Optional.ofNullable(loggerOptional).ifPresent(logger => { if ( typeof logger.debug !== 'function' || typeof logger.info !== 'function' || typeof logger.warn !== 'function' || typeof logger.error !== 'function' ) { throw new TypeError( 'The logger that was passed in does not support all required ' + 'logging methods, expected object to have functions debug, info, warn, and error with ' + 'method signatures (...msgs) => {}' ); } }); }, /** * Validates the optional publishing interval. * * @param publishingIntervalInSecondsOptional The optional publishing interval. */ validateOptionalPublishingInterval: publishingIntervalInSecondsOptional => { Optional.ofNullable(publishingIntervalInSecondsOptional).ifPresent(publishingIntervalInSeconds => { const type = typeof publishingIntervalInSeconds; if (type !== 'number') { throw new TypeError(`options.publishingIntervalInSeconds must be of type number, actual type: ${type}`); } }); }, /** * Validates optional params for a Reporter * @param {ReporterOptions} options The optional params */ validateReporterParameters: options => { if (options) { module.exports.validateOptionalDimensions(options.defaultDimensions); module.exports.validateOptionalLogger(options.logger); const type = typeof options.unrefTimers; if (type !== 'boolean' && type !== 'undefined') { throw new TypeError(`options.unrefTimers should be a boolean or undefined, actual type: ${type}`); } } }, /** * Validates that a valid Reporter object has been supplied * * @param {Reporter} reporter */ validateReporterInstance: reporter => { if (!reporter) { throw new TypeError('The reporter was undefined, when it was required'); } if (typeof reporter.setRegistry !== 'function') { throw new TypeError( 'A reporter must implement setRegistry(registry), see the abstract Reporter class in the docs.' ); } if (typeof reporter.reportMetricOnInterval !== 'function') { throw new TypeError( 'A reporter must implement reportMetricOnInterval(metricKey, intervalInSeconds), see the abstract Reporter class in the docs.' ); } }, /** * Validates the input parameters for a {@link SelfReportingMetricsRegistry} * @param {Reporter[]} reporters * @param {SelfReportingMetricsRegistryOptions} [options] */ validateSelfReportingMetricsRegistryParameters: (reporters, options) => { reporters.forEach(reporter => module.exports.validateReporterInstance(reporter)); if (options) { module.exports.validateOptionalLogger(options.logger); } } }; ================================================ FILE: packages/measured-reporting/package.json ================================================ { "name": "measured-reporting", "description": "The classes needed to create self reporting dimension aware metrics registries", "version": "2.0.0", "homepage": "https://yaorg.github.io/node-measured/", "engines": { "node": ">= 5.12" }, "publishConfig": { "access": "public" }, "main": "./lib/index.js", "scripts": { "clean": "rm -fr build", "format": "prettier --write './lib/**/*.{ts,js}'", "lint": "eslint lib --ext .js", "test:node": "mocha './test/**/test-*.js'", "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'", "test:browser": "exit 0", "test": "yarn test:node:coverage", "coverage": "nyc report --reporter=text-lcov | coveralls" }, "repository": { "url": "git://github.com/yaorg/node-measured.git" }, "dependencies": { "console-log-level": "^1.4.1", "mapcap": "^1.0.0", "measured-core": "^2.0.0", "optional-js": "^2.0.0" }, "files": [ "lib", "README.md" ], "license": "MIT", "devDependencies": { "jsdoc": "^3.5.5", "loglevel": "^1.6.1", "winston": "^2.4.2" } } ================================================ FILE: packages/measured-reporting/test/unit/registries/test-DimensionAwareMetricsRegistry.js ================================================ /*global describe, it, beforeEach, afterEach*/ const assert = require('assert'); const { Counter } = require('measured-core'); const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry'); describe('DimensionAwareMetricsRegistry', () => { let registry; beforeEach(() => { registry = new DimensionAwareMetricsRegistry(); }); it('hasMetric() returns true after putMetric() and getMetric() retrieves it, and it has the expected value', () => { const counter = new Counter({ count: 10 }); const metricName = 'counter'; const dimensions = { foo: 'bar' }; assert(!registry.hasMetric(metricName, dimensions)); registry.putMetric(metricName, counter, dimensions); assert(registry.hasMetric(metricName, dimensions)); assert(counter === registry.getMetric(metricName, dimensions)); assert.equal(10, registry.getMetric(metricName, dimensions).toJSON()); }); it('getMetricByKey() returns the proper metric wrapper', () => { const counter = new Counter({ count: 10 }); const metricName = 'counter'; const dimensions = { foo: 'bar' }; const key = registry.putMetric(metricName, counter, dimensions); assert(key.includes('counter-bar')); const wrapper = registry.getMetricWrapperByKey(key); assert.deepEqual(counter, wrapper.metricImpl); assert.deepEqual(dimensions, wrapper.dimensions); assert.equal(metricName, wrapper.name); }); it('#_generateStorageKey generates the same key for a metric name and dimensions with different ordering', () => { const metricName = 'the-metric-name'; const demensions1 = { foo: 'bar', bam: 'boo' }; const demensions2 = { bam: 'boo', foo: 'bar' }; const key1 = registry._generateStorageKey(metricName, demensions1); const key2 = registry._generateStorageKey(metricName, demensions2); assert.equal(key1, key2); }); it('#_generateStorageKey generates the same key for a metric name and dimensions when called 2x', () => { const metricName = 'the-metric-name'; const demensions1 = { foo: 'bar', bam: 'boo' }; const key1 = registry._generateStorageKey(metricName, demensions1); const key2 = registry._generateStorageKey(metricName, demensions1); assert.equal(key1, key2); }); it('#_generateStorageKey generates the same key for a metric name and no dimensions when called 2x', () => { const metricName = 'the-metric-name'; const demensions1 = {}; const key1 = registry._generateStorageKey(metricName, demensions1); const key2 = registry._generateStorageKey(metricName, demensions1); assert.equal(key1, key2); }); it('metricLimit limits metric count', () => { const limitedRegistry = new DimensionAwareMetricsRegistry({ metricLimit: 10 }); const counter = new Counter({ count: 10 }); const dimensions = { foo: 'bar' }; for (let i = 0; i < 20; i++) { limitedRegistry.putMetric(`metric #${i}`, counter, dimensions); } assert.equal(10, limitedRegistry._metrics.size); assert(!limitedRegistry.hasMetric('metric #0', dimensions)); }); it('lru changes metric dropping strategy', () => { const limitedRegistry = new DimensionAwareMetricsRegistry({ metricLimit: 10, lru: true }); const counter = new Counter({ count: 10 }); const dimensions = { foo: 'bar' }; for (let i = 0; i < 10; i++) { limitedRegistry.putMetric(`metric #${i}`, counter, dimensions); } // Touch the first added metric limitedRegistry.getMetric('metric #0', dimensions); // Put a new metric in to trigger a drop limitedRegistry.putMetric('metric #11', counter, dimensions); // Verify that it dropped metric #1, not metric #0 assert(limitedRegistry.hasMetric('metric #0', dimensions)); assert(!limitedRegistry.hasMetric('metric #1', dimensions)); }); }); ================================================ FILE: packages/measured-reporting/test/unit/registries/test-SelfReportingMetricsRegistry.js ================================================ /*global describe, it, beforeEach, afterEach*/ const assert = require('assert'); const sinon = require('sinon'); const { Counter } = require('measured-core'); const { SelfReportingMetricsRegistry } = require('../../../lib'); const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry'); describe('SelfReportingMetricsRegistry', () => { let selfReportingRegistry; let reporter; let mockReporter; let registry; beforeEach(() => { registry = new DimensionAwareMetricsRegistry(); reporter = { reportMetricOnInterval() {}, setRegistry() {} }; mockReporter = sinon.mock(reporter); selfReportingRegistry = new SelfReportingMetricsRegistry(reporter, { registry }); }); it('throws an error if a metric has already been registered', () => { registry.putMetric('my-metric', new Counter(), {}); assert.throws(() => { selfReportingRegistry.register('my-metric', new Counter(), {}); }); }); it('#register registers the metric and informs the reporter to report', () => { const metricName = 'foo'; const reportInterval = 1; const metricKey = metricName; mockReporter .expects('reportMetricOnInterval') .once() .withArgs(metricKey, reportInterval); selfReportingRegistry.register(metricKey, new Counter(), {}, reportInterval); assert.equal(1, registry._metrics.size); mockReporter.restore(); mockReporter.verify(); }); it('#getOrCreateGauge creates a gauge and when called a second time returns the same gauge', () => { mockReporter.expects('reportMetricOnInterval').once(); const gauge = selfReportingRegistry.getOrCreateGauge('the-metric-name', () => 10, {}, 1); const theSameGauge = selfReportingRegistry.getOrCreateGauge('the-metric-name', () => 10, {}, 1); mockReporter.restore(); mockReporter.verify(); assert.deepEqual(gauge, theSameGauge); }); it('#getOrCreateHistogram creates and registers the metric and when called a second time returns the same metric', () => { mockReporter.expects('reportMetricOnInterval').once(); const metric = selfReportingRegistry.getOrCreateHistogram('the-metric-name', {}, 1); const theSameMetric = selfReportingRegistry.getOrCreateHistogram('the-metric-name', {}, 1); mockReporter.restore(); mockReporter.verify(); assert.deepEqual(metric, theSameMetric); }); it('#getOrCreateMeter creates and registers the metric and when called a second time returns the same metric', () => { mockReporter.expects('reportMetricOnInterval').once(); const metric = selfReportingRegistry.getOrCreateMeter('the-metric-name', {}, 1); const theSameMetric = selfReportingRegistry.getOrCreateMeter('the-metric-name', {}, 1); mockReporter.restore(); mockReporter.verify(); assert.deepEqual(metric, theSameMetric); metric.end(); }); it('#getOrCreateCounter creates and registers the metric and when called a second time returns the same metric', () => { mockReporter.expects('reportMetricOnInterval').once(); const metric = selfReportingRegistry.getOrCreateCounter('the-metric-name', {}, 1); const theSameMetric = selfReportingRegistry.getOrCreateCounter('the-metric-name', {}, 1); mockReporter.restore(); mockReporter.verify(); assert.deepEqual(metric, theSameMetric); }); it('#getOrCreateTimer creates and registers the metric and when called a second time returns the same metric', () => { mockReporter.expects('reportMetricOnInterval').once(); const metric = selfReportingRegistry.getOrCreateTimer('the-metric-name', {}, 1); const theSameMetric = selfReportingRegistry.getOrCreateTimer('the-metric-name', {}, 1); mockReporter.restore(); mockReporter.verify(); assert.deepEqual(metric, theSameMetric); metric.end(); }); it('#getOrCreateSettableGauge creates and registers the metric and when called a second time returns the same metric', () => { mockReporter.expects('reportMetricOnInterval').once(); const metric = selfReportingRegistry.getOrCreateSettableGauge('the-metric-name', {}, 1); const theSameMetric = selfReportingRegistry.getOrCreateSettableGauge('the-metric-name', {}, 1); mockReporter.restore(); mockReporter.verify(); assert.deepEqual(metric, theSameMetric); }); it('#getOrCreateCachedGauge creates and registers the metric and when called a second time returns the same metric', () => { mockReporter.expects('reportMetricOnInterval').once(); const metric = selfReportingRegistry.getOrCreateCachedGauge('the-metric-name', () => { return new Promise((r) => { r(10); }); }, 1, {}, 1); const theSameMetric = selfReportingRegistry.getOrCreateCachedGauge('the-metric-name', () => { return new Promise((r) => { r(10); }); }, 1, {}, 1); // clear the interval metric.end(); mockReporter.restore(); mockReporter.verify(); assert.deepEqual(metric, theSameMetric); }); }); ================================================ FILE: packages/measured-reporting/test/unit/reporters/test-LoggingReporter.js ================================================ /*global describe, it, beforeEach, afterEach*/ const assert = require('assert'); const { LoggingReporter } = require('../../../lib'); describe('LoggingReporter', () => { let loggedMessages = []; let logger; beforeEach(() => { logger = { debug: (...msgs) => { loggedMessages.push('debug: ', ...msgs); }, info: (...msgs) => { loggedMessages.push('info: ', ...msgs); }, warn: (...msgs) => { loggedMessages.push('warn: ', ...msgs); }, error: (...msgs) => { loggedMessages.push('error: ', ...msgs); } }; }); it('uses the supplied log level', () => { let reporter = new LoggingReporter({ logger: logger, logLevelToLogAt: 'debug' }); reporter._reportMetrics([{ name: 'test', dimensions: {}, metricImpl: {toJSON: () => 5} }]); assert.equal(loggedMessages.shift(), "debug: "); assert.equal(loggedMessages.shift(), "{\"metricName\":\"test\",\"dimensions\":{},\"data\":5}") }); it('defaults to info level, if no override supplied', () => { it('uses the supplied log level', () => { let reporter = new LoggingReporter({ logger: logger, }); reporter._reportMetrics([{ name: 'test', dimensions: {}, metricImpl: {toJSON: () => 5} }]); assert.equal(loggedMessages.shift(), "info: "); assert.equal(loggedMessages.shift(), "{\"metricName\":\"test\",\"dimensions\":{},\"data\":5}") }); }); }); ================================================ FILE: packages/measured-reporting/test/unit/reporters/test-Reporter.js ================================================ /*global describe, it, beforeEach, afterEach*/ const assert = require('assert'); const TimeUnits = require('measured-core').units; const { Counter } = require('measured-core'); const DimensionAwareMetricsRegistry = require('../../../lib/registries/DimensionAwareMetricsRegistry'); const { Reporter } = require('../../../lib'); const { validateReporterInstance } = require('../../../lib/validators/inputValidators'); /** * @extends Reporter */ class TestReporter extends Reporter { constructor(options) { super(options); this._reportedMetrics = []; } getReportedMetrics() { return this._reportedMetrics; } _reportMetrics(metrics) { this._reportedMetrics.push(metrics); } } describe('Reporter', () => { let reporter; let counter = new Counter({ count: 5 }); let metricName = 'my-test-metric-key'; let metricKey; let metricInterval = 1; let metricDimensions = { hostname: 'instance-hostname', foo: 'bar' }; let registry; beforeEach(() => { registry = new DimensionAwareMetricsRegistry(); metricKey = registry.putMetric(metricName, counter, metricDimensions); reporter = new TestReporter(); reporter.setRegistry(registry); }); it('throws an error if you try to instantiate the abstract class', () => { assert.throws(() => { new Reporter(); }, /^TypeError: Can\'t instantiate abstract class\!$/); }); it('throws an error if _reportMetrics is not implemented', () => { class BadImpl extends Reporter {} assert.throws(() => { const badImpl = new BadImpl(); badImpl._reportMetrics([]); }, /method _reportMetrics\(metrics\) must be implemented/); }); it('throws an error if reportMetricOnInterval is called before setRegistry', () => { assert.throws(() => { let unsetReporter = new TestReporter(); unsetReporter.reportMetricOnInterval(metricKey, metricInterval); }, /must call setRegistry/); }); it('_reportMetricsWithInterval reports the test metric wrapper', () => { reporter._intervalToMetric[metricInterval] = new Set([metricKey]); reporter._reportMetricsWithInterval(metricInterval); assert.equal(reporter.getReportedMetrics().length, 1); assert.deepEqual(reporter.getReportedMetrics().shift(), [registry.getMetricWrapperByKey(metricKey)]); }); it('should report 5 times in a 5 second window with a metric set to be reporting every 1 second', (done, fail) => { reportAndWait(reporter, metricKey, metricInterval) .then(() => { reporter.shutdown(); const numberOfReports = reporter.getReportedMetrics().length; assert.equal(numberOfReports, 5); done(); }) .catch(() => { reporter.shutdown(); assert.fail('', '', ''); }); }).timeout(10000); it('should only create 1 interval for 2 metrics with the same reporting interval', () => { reporter.reportMetricOnInterval(metricKey, metricInterval); metricKey = registry.putMetric('foo', counter, metricDimensions); reporter.reportMetricOnInterval('foo', metricInterval); const intervalCount = reporter._intervals.length; assert.equal(1, intervalCount); reporter.shutdown(); }); it('should left merge dimensions with the metric dimensions taking precedence when _getDimensions is called', () => { let defaultDimensions = { hostname: 'instance-hostname', foo: 'bar' }; reporter = new TestReporter({ defaultDimensions }); const customDimensions = { foo: 'bam', region: 'us-west-2' }; const merged = reporter._getDimensions({ dimensions: customDimensions }); const expected = { hostname: 'instance-hostname', foo: 'bam', region: 'us-west-2' }; assert.deepEqual(expected, merged); }); it('Can be used to create an anonymous instance of a reporter', () => { const anonymousReporter = new class extends Reporter { _reportMetrics(metrics) { metrics.forEach(metric => console.log(JSON.stringify(metric))); } }(); validateReporterInstance(anonymousReporter); }); it('unrefs timers, when configured to', () => { let calledUnref = false; const timer = setTimeout(() => {}, 100); clearTimeout(timer); const proto = timer.constructor.prototype; const { unref } = proto; proto.unref = function wrappedUnref() { calledUnref = true; return unref.call(this); }; reporter = new TestReporter({ unrefTimers: true }); reporter.setRegistry(registry); reporter.reportMetricOnInterval(metricKey, metricInterval); proto.unref = unref; assert.ok(calledUnref); }); it('resets metrics, when configured to', () => { reporter = new TestReporter({ resetMetricsOnInterval: true }); reporter.setRegistry(registry); reporter._intervalToMetric[metricInterval] = new Set([metricKey]); reporter._reportMetricsWithInterval(metricInterval); const [[{ metricImpl }]] = reporter.getReportedMetrics(); assert.equal(metricImpl.toJSON(), 0); }); }); const reportAndWait = (reporter, metricKey, metricInterval) => { return new Promise(resolve => { reporter.reportMetricOnInterval(metricKey, metricInterval); setTimeout(() => { resolve(); }, 5 * TimeUnits.SECONDS); }); }; ================================================ FILE: packages/measured-reporting/test/unit/validators/test-inputValidators.js ================================================ /*global describe, it, beforeEach, afterEach*/ const consoleLogLevel = require('console-log-level'); const loglevel = require('loglevel'); const winston = require('winston'); const assert = require('assert'); const { Counter } = require('measured-core'); const { validateGaugeOptions, validateOptionalLogger, validateMetric, validateReporterInstance, validateSelfReportingMetricsRegistryParameters, validateOptionalPublishingInterval, validateOptionalDimensions, validateMetricName, validateNumberReturningCallback } = require('../../../lib/validators/inputValidators'); describe('validateGaugeOptions', () => { it('it does nothing for the happy path', () => { validateGaugeOptions('foo', () => 10, { foo: 'bar' }, 1); }); it('throws an error if the call back returns an object', () => { assert.throws(() => { validateGaugeOptions( 'foo', () => { return { value: 10, anotherValue: 10 }; }, { foo: 'bar' }, 1 ); }, /options.callback must return a number, actual return type: object/); }); }); describe('validateNumberReturningCallback', () => { it('throws an error if a non function is supplied', () => { assert.throws(() => { validateNumberReturningCallback({}); }, /must be function/); }); }); describe('validateOptionalLogger', () => { it('validates a Buynan logger', () => { const logger = consoleLogLevel({ name: 'consoleLogLevel-logger' }); validateOptionalLogger(logger); }); it('validates a Winston logger', () => { validateOptionalLogger(winston); }); it('validates a Loglevel logger', () => { validateOptionalLogger(loglevel); }); it('validates an artisanal logger', () => { validateOptionalLogger({ debug: (...msgs) => { console.log('debug: ', ...msgs); }, info: (...msgs) => { console.log('info: ', ...msgs); }, warn: (...msgs) => { console.log('warn: ', ...msgs); }, error: (...msgs) => { console.log('error: ', ...msgs); } }); }); it('throws an error when a logger is missing an expected method', () => { assert.throws(() => { validateOptionalLogger({}); }, /The logger that was passed in does not support/); }); it('does not throw an error if a logger is not passed in as an arg', () => { validateOptionalLogger(null); }); }); describe('validateMetric', () => { it('throws an error if the metric is undefined', () => { assert.throws(() => { validateMetric(undefined); }, /^TypeError: The metric was undefined, when it was required$/); }); it('throws an error if the metric is null', () => { assert.throws(() => { validateMetric(null); }, /^TypeError: The metric was undefined, when it was required$/); }); it('throws an error if toJSON is not a function', () => { assert.throws(() => { validateMetric({}); }, /must implement toJSON()/); }); it('throws an error if getType is not a function', () => { assert.throws(() => { validateMetric({ toJSON: () => {} }); }, /must implement getType()/); }); it('throws an error if #getType() does not return an expected value', () => { assert.throws(() => { validateMetric({ toJSON: () => {}, getType: () => { return 'foo'; } }); }, /Metric#getType\(\), must return a type defined in MetricsTypes/); }); it('does nothing when a valid metric is supplied', () => { validateMetric(new Counter()); }); }); describe('validateReporterInstance', () => { it('throws an error if undefined was passed in', () => { assert.throws(() => { validateReporterInstance(null); }, /The reporter was undefined/); }); it('throws an error if setRegistry is not a function', () => { assert.throws(() => { validateReporterInstance({}); }, /must implement setRegistry/); }); it('throws an error if reportMetricOnInterval is not a function', () => { assert.throws(() => { validateReporterInstance({ setRegistry: () => {} }); }, /must implement reportMetricOnInterval/); }); it('does nothing for a valid reporter instance', () => { validateReporterInstance({ setRegistry: () => {}, reportMetricOnInterval: () => {} }); }); }); describe('validateSelfReportingMetricsRegistryParameters', () => { it('does nothing when a reporter is passed in', () => { validateSelfReportingMetricsRegistryParameters([{ setRegistry: () => {}, reportMetricOnInterval: () => {} }]); }); }); describe('validateOptionalPublishingInterval', () => { it('throws an error if validateOptionalPublishingInterval is not a number', () => { assert.throws(() => { validateOptionalPublishingInterval('1'); }, /must be of type number/); }); }); describe('validateOptionalDimensions', () => { it('throws an error if passed dimensions is not an object', () => { assert.throws(() => { validateOptionalDimensions(1); }, /options.dimensions should be an object/); }); it('throws an error if passed dimensions is not an object.', () => { assert.throws(() => { validateOptionalDimensions(['thing', 'otherthing']); }, /dimensions where detected to be an array/); }); it('throws an error if passed dimensions is not an object has non string values for demension keys', () => { assert.throws(() => { validateOptionalDimensions({ someKeyThatIsANumber: 1 }); }, /should be of type string/); }); }); describe('validateMetricName', () => { it('throw an error if a non-string is passed', () => { assert.throws(() => { validateMetricName({}); }, /options.name is a required option and must be of type string/); }); }); ================================================ FILE: packages/measured-signalfx-reporter/README.md ================================================ # Measured SignalFx Reporter 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/). ## Install ``` npm install measured-signalfx-reporter ``` ## What is in this package ### [SignalFxMetricsReporter](https://yaorg.github.io/node-measured/SignalFxMetricsReporter.html) A SignalFx specific implementation of the [Reporter Abstract Class](https://yaorg.github.io/node-measured/Reporter.html). ### [SignalFxSelfReportingMetricsRegistry](https://yaorg.github.io/node-measured/SignalFxSelfReportingMetricsRegistry.html) Extends [Self Reporting Metrics Registry](https://yaorg.github.io/node-measured/SelfReportingMetricsRegistry.html) but overrides methods that generate Meters to use the NoOpMeter. ### NoOpMeters Please note that this package ignores Meters by default. Meters do not make sense to use with SignalFx because the same values can be calculated using simple counters and the aggregation functions available within SignalFx itself. Additionally, this saves you money because SignalFx charges based on your DPM (Datapoints per Minute) consumption. This can be changed if anyone has a good argument for using Meters. Please file an issue. ### Usage 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) ### Dev There is a user acceptance test server to test this library end-to-end with [SignalFx](https://signalfx.com/). ```bash SIGNALFX_API_KEY=xxxxx yarn uat:server ``` ================================================ FILE: packages/measured-signalfx-reporter/lib/SignalFxEventCategories.js ================================================ /** * Different categories of events supported, within the SignalFx Event API. * * @example * const registry = new SignalFxSelfReportingMetricsRegistry(...); * registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT); * * @module SignalFxEventCategories */ module.exports = { /** * Created by user via UI or API, e.g. a deployment event * @type {SignalFxEventCategoryId} */ USER_DEFINED: 'USER_DEFINED', /** * Output by anomaly detectors * @type {SignalFxEventCategoryId} */ ALERT: 'ALERT', /** * Audit trail events * @type {SignalFxEventCategoryId} */ AUDIT: 'AUDIT', /** * Generated by analytics server * @type {SignalFxEventCategoryId} */ JOB: 'JOB', /** * Event originated within collectd * @type {SignalFxEventCategoryId} */ COLLECTD: 'COLLECTD', /** * Service discovery event * @type {SignalFxEventCategoryId} */ SERVICE_DISCOVERY: 'SERVICE_DISCOVERY', /** * Created by exception appenders to denote exceptional events * @type {SignalFxEventCategoryId} */ EXCEPTION: 'EXCEPTION' }; /** * @interface SignalFxEventCategoryId * @typedef SignalFxEventCategoryId * @type {string} * @example * const registry = new SignalFxSelfReportingMetricsRegistry(...); * registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT); */ ================================================ FILE: packages/measured-signalfx-reporter/lib/index.js ================================================ const SignalFxMetricsReporter = require('./reporters/SignalFxMetricsReporter'); const SignalFxSelfReportingMetricsRegistry = require('./registries/SignalFxSelfReportingMetricsRegistry'); const SignalFxEventCategories = require('./SignalFxEventCategories'); /** * The main measured module that is referenced when require('measured-signalfx-reporter') is used. * @module measured-signalfx-reporter */ module.exports = { /** * {@type SignalFxMetricsReporter} */ SignalFxMetricsReporter, /** * {@type SignalFxSelfReportingMetricsRegistry} */ SignalFxSelfReportingMetricsRegistry, /** * {@type SignalFxEventCategories} */ SignalFxEventCategories }; ================================================ FILE: packages/measured-signalfx-reporter/lib/registries/SignalFxSelfReportingMetricsRegistry.js ================================================ const { NoOpMeter, Timer } = require('measured-core'); const { SelfReportingMetricsRegistry } = require('measured-reporting'); const { validateTimerOptions } = require('measured-reporting').inputValidators; /** * A SignalFx Self Reporting Metrics Registry that disallows the use of meters. * Meters don't make sense to use with SignalFx because the rate aggregations can be done within SignalFx itself. * Meters simply waste DPM (Datapoints per Minute). * * @extends {SelfReportingMetricsRegistry} */ class SignalFxSelfReportingMetricsRegistry extends SelfReportingMetricsRegistry { /** * Creates a {@link Timer} or get the existing Timer for a given name and dimension combo with a NoOpMeter. * * @param {string} name The Metric name * @param {Dimensions} dimensions any custom {@link Dimensions} for the Metric * @param {number} publishingIntervalInSeconds a optional custom publishing interval * @return {Timer} */ getOrCreateTimer(name, dimensions, publishingIntervalInSeconds) { validateTimerOptions(name, dimensions, publishingIntervalInSeconds); let timer; if (this._registry.hasMetric(name, dimensions)) { timer = this._registry.getMetric(name, dimensions); } else { timer = new Timer({ meter: new NoOpMeter() }); const key = this._registry.putMetric(name, timer, dimensions); this._reporters.forEach(reporter => reporter.reportMetricOnInterval(key, publishingIntervalInSeconds)); } return timer; } /** * Meters are not reported to SignalFx. * Meters do not make sense to use with SignalFx because the same values can be calculated * using simple counters and aggregations within SignalFx itself. * * @param {string} name The Metric name * @param {Dimensions} dimensions any custom {@link Dimensions} for the Metric * @param {number} publishingIntervalInSeconds a optional custom publishing interval * @return {NoOpMeter|*} */ getOrCreateMeter(name, dimensions, publishingIntervalInSeconds) { this._log.error( 'Meters will not get reported using the SignalFx reporter as they waste DPM, please use a counter instead' ); return new NoOpMeter(); } /** * Function exposes the event API of Signal Fx. * See {@link https://github.com/signalfx/signalfx-nodejs#sending-events} for more details. * * @param {string} eventType The event type (name of the event time series). * @param {SignalFxEventCategoryId} [category] the category of event. See {@link module:SignalFxEventCategories}. Value by default is USER_DEFINED. * @param {Dimensions} [dimensions] a map of event dimensions, empty dictionary by default * @param {Object.} [properties] a map of extra properties on that event, empty dictionary by default * @param {number} [timestamp] a timestamp, by default is current time. * * @example * const { * SignalFxSelfReportingMetricsRegistry, * SignalFxMetricsReporter, * SignalFxEventCategories * } = require('measured-signalfx-reporter'); * const registry = new SignalFxSelfReportingMetricsRegistry(new SignalFxMetricsReporter(signalFxClient)); * registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT); */ sendEvent(eventType, category, dimensions, properties, timestamp) { return Promise.all( this._reporters.filter(reporter => typeof reporter.sendEvent === 'function').map(reporter => reporter.sendEvent(eventType, category, dimensions, properties, timestamp).catch(error => { return error; }) ) ); } } module.exports = SignalFxSelfReportingMetricsRegistry; ================================================ FILE: packages/measured-signalfx-reporter/lib/reporters/SignalFxMetricsReporter.js ================================================ const { Reporter } = require('measured-reporting'); const { MetricTypes } = require('measured-core'); const { validateSignalFxClient } = require('../validators/inputValidators'); const { USER_DEFINED } = require('../SignalFxEventCategories'); /** * A Reporter that reports metrics to Signal Fx * @extends {Reporter} */ class SignalFxMetricsReporter extends Reporter { /** * @param {SignalFxClient} signalFxClient The configured signal fx client. * @param {ReporterOptions} [options] See {@link ReporterOptions}. */ constructor(signalFxClient, options) { options = options || {}; super(options); validateSignalFxClient(signalFxClient); this._signalFxClient = signalFxClient; this._log.debug(`SignalFx Metrics Reporter Created with the following default reporting interval: ${options.defaultReportingIntervalInSeconds}, default dimensions: ${JSON.stringify(options.defaultDimensions, null, 2)}`); } /** * Sends metrics to signal fx, converting name and dimensions and {@link Metric} to data signal fx can ingest * @param {MetricWrapper[]} metrics The array of metrics to send to signal fx. * @protected */ _reportMetrics(metrics) { this._log.debug('_reportMetrics() called'); let signalFxDataPointRequest = {}; metrics.forEach(metric => { if (!metric) { this._log.warn('Metric was null when it should not have been'); return; } signalFxDataPointRequest = this._processMetric(metric, signalFxDataPointRequest); }); this._log.debug(`Sending data to Signal Fx. Request: ${JSON.stringify(signalFxDataPointRequest)}`); this._signalFxClient.send(signalFxDataPointRequest).catch(error => { this._log.error('Failed to send metrics to signal fx error:', error); }); } /** * Method for getting raw signal fx api request values from the Timer Object. * * @param {MetricWrapper} metric metric The Wrapped Metric Object. * @param {any} currentBuiltRequest The signal fx request that is being built. * @return {any} the currentBuiltRequest The signal fx request that is being built with the given metric in it. * @protected */ _processMetric(metric, currentBuiltRequest) { const newRequest = Object.assign({}, currentBuiltRequest); const { name, metricImpl } = metric; const mergedDimensions = this._getDimensions(metric); const valuesToProcess = this._getValuesToProcessForType(name, metricImpl); valuesToProcess.forEach(metricValueTypeWrapper => { const signalFxDataPointMetric = { metric: metricValueTypeWrapper.metric, value: metricValueTypeWrapper.value, dimensions: mergedDimensions }; if (Object.prototype.hasOwnProperty.call(newRequest, metricValueTypeWrapper.type)) { newRequest[metricValueTypeWrapper.type].push(signalFxDataPointMetric); } else { newRequest[metricValueTypeWrapper.type] = [signalFxDataPointMetric]; } }); return newRequest; } /** * Maps Measured Metrics Object JSON outputs to their respective signal fx metrics using logic from * com.signalfx.codahale.reporter.AggregateMetricSenderSessionWrapper in the java lib to derive naming * * @param {string} name The registered metric base name * @param {Metric} metric The metric. * @return {MetricValueTypeWrapper[]} an array of MetricValueTypeWrapper that can be used to * build the SignalFx data point request * @protected */ _getValuesToProcessForType(name, metric) { const type = metric.getType(); switch (type) { case MetricTypes.TIMER: return this._getValuesToProcessForTimer(name, metric); case MetricTypes.GAUGE: return this._getValuesToProcessForGauge(name, metric); case MetricTypes.COUNTER: return this._getValuesToProcessForCounter(name, metric); case MetricTypes.HISTOGRAM: return this._getValuesToProcessForHistogram(name, metric); case MetricTypes.METER: this._log.warn( 'Meters are not reported to SignalFx. Meters do not make sense to use with SignalFx because the same values ' + 'can be calculated using simple counters and aggregations within SignalFx itself.' ); return []; default: this._log.error(`Metric Type: ${type} has not been implemented to report to signal fx`); return []; } } /** * Maps and Filters values from a Timer to a set of metrics to report to SigFx. * * @param {string} name The registry name * @param {Timer} timer The Timer * @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request * @protected */ _getValuesToProcessForTimer(name, timer) { let valuesToProcess = []; // only grab histogram data as Meters can be accomplished with signal fx using the count from the histogram valuesToProcess = valuesToProcess.concat(this._getValuesToProcessForHistogram(name, timer._histogram)); return valuesToProcess; } /** * Maps values from a Gauge to a set of metrics to report to SigFx. * * @param {string} name The registry name * @param {Gauge} gauge The Gauge * @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request * @protected */ _getValuesToProcessForGauge(name, gauge) { const valuesToProcess = []; valuesToProcess.push({ metric: `${name}`, value: gauge.toJSON(), type: SIGNAL_FX_GAUGE }); return valuesToProcess; } /** * Maps values from a Counter to a set of metrics to report to SigFx. * * @param {string} name The registry name * @param {Counter} counter The data from the measure metric object * @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request * @protected */ _getValuesToProcessForCounter(name, counter) { const valuesToProcess = []; valuesToProcess.push({ metric: `${name}.count`, value: counter.toJSON(), type: SIGNAL_FX_CUMULATIVE_COUNTER }); return valuesToProcess; } /** * Maps and Filters values from a Histogram to a set of metrics to report to SigFx. * * @param {string} name The registry name * @param {Histogram} histogram The Histogram * @return {MetricValueTypeWrapper[]} Returns an array of MetricValueTypeWrapper to use to build the request * @protected */ _getValuesToProcessForHistogram(name, histogram) { // TODO add full list of histogram metrics but enable filter const data = histogram.toJSON(); const valuesToProcess = []; valuesToProcess.push({ metric: `${name}.count`, value: data.count || 0, type: SIGNAL_FX_CUMULATIVE_COUNTER }); valuesToProcess.push({ metric: `${name}.max`, value: data.max || 0, type: SIGNAL_FX_GAUGE }); valuesToProcess.push({ metric: `${name}.min`, value: data.min || 0, type: SIGNAL_FX_GAUGE }); valuesToProcess.push({ metric: `${name}.mean`, value: data.mean || 0, type: SIGNAL_FX_GAUGE }); valuesToProcess.push({ metric: `${name}.p95`, value: data.p95 || 0, type: SIGNAL_FX_GAUGE }); valuesToProcess.push({ metric: `${name}.p99`, value: data.p99 || 0, type: SIGNAL_FX_GAUGE }); return valuesToProcess; } /** * Function exposes the event API of Signal Fx. * See {@link https://github.com/signalfx/signalfx-nodejs#sending-events} for more details. * * @param {string} eventType The event type (name of the event time series). * @param {SignalFxEventCategoryId} [category] the category of event. See {@link module:SignalFxEventCategories}. Value by default is USER_DEFINED. * @param {Dimensions} [dimensions] a map of event dimensions, empty dictionary by default * @param {Object.} [properties] a map of extra properties on that event, empty dictionary by default * @param {number} [timestamp] a timestamp, by default is current time. * * @example * const { * SignalFxSelfReportingMetricsRegistry, * SignalFxMetricsReporter, * SignalFxEventCategories * } = require('measured-signalfx-reporter'); * const registry = new SignalFxSelfReportingMetricsRegistry(new SignalFxMetricsReporter(signalFxClient)); * registry.sendEvent('uncaughtException', SignalFxEventCategories.ALERT); */ sendEvent(eventType, category, dimensions, properties, timestamp) { const body = { eventType, category: category || USER_DEFINED, dimensions: this._getDimensions({ dimensions }), properties, timestamp }; Object.keys(body).forEach(key => body[key] == null && delete body[key]); return this._signalFxClient.sendEvent(body); } } // const SIGNAL_FX_COUNTER = 'counters'; const SIGNAL_FX_GAUGE = 'gauges'; const SIGNAL_FX_CUMULATIVE_COUNTER = 'cumulative_counters'; module.exports = SignalFxMetricsReporter; /** * Wrapper object to wrap metric value and SFX metadata needed to send metric value to SFX data ingestion. * * @interface MetricValueTypeWrapper * @typedef MetricValueTypeWrapper * @type {Object} * @property {string} metric The metric name to report to SignalFx * @property {number} value the value to report to SignalFx * @property {string} type The mapped SignalFx metric type */ ================================================ FILE: packages/measured-signalfx-reporter/lib/validators/inputValidators.js ================================================ /** * Validation functions for validating public input * @module SignalFxReporterInputValidators * @private */ module.exports = { /** * Validates that the object supplied for the sfx client at least has a send function * @param signalFxClient */ validateSignalFxClient: signalFxClient => { if (signalFxClient === undefined) { throw new Error('signalFxClient was undefined when it is required'); } if (typeof signalFxClient.send !== 'function' || signalFxClient.length < 1) { throw new Error('signalFxClient must implement send(data: any)'); } } }; ================================================ FILE: packages/measured-signalfx-reporter/package.json ================================================ { "name": "measured-signalfx-reporter", "description": "A Registry Reporter that knows how to report core metrics to SignalFx", "version": "2.0.0", "homepage": "https://yaorg.github.io/node-measured/", "engines": { "node": ">= 5.12" }, "publishConfig": { "access": "public" }, "main": "./lib/index.js", "scripts": { "clean": "rm -fr build", "format": "prettier --write './lib/**/*.{ts,js}'", "lint": "eslint lib --ext .js", "test:node": "mocha './test/**/test-*.js'", "test:node:coverage": "nyc --report-dir build/coverage/ --reporter=html --reporter=text mocha './test/**/test-*.js'", "test:browser": "exit 0", "test": "yarn test:node:coverage", "coverage": "nyc report --reporter=text-lcov | coveralls", "uat:server": "node --inspect test/user-acceptance-test/index.js" }, "repository": { "url": "git://github.com/yaorg/node-measured.git" }, "dependencies": { "console-log-level": "^1.4.1", "measured-core": "^2.0.0", "measured-reporting": "^2.0.0", "optional-js": "^2.0.0" }, "files": [ "lib", "README.md" ], "license": "MIT", "devDependencies": { "express": "^4.16.3", "jsdoc": "^3.5.5", "signalfx": "^6.0.0" } } ================================================ FILE: packages/measured-signalfx-reporter/test/unit/registries/test-SignalFxSelfReportingMetricsRegistry.js ================================================ /*global describe, it, beforeEach, afterEach*/ const { SignalFxSelfReportingMetricsRegistry } = require('../../../lib'); const { Reporter } = require('measured-reporting'); const sinon = require('sinon'); const assert = require('assert'); /** * @extends Reporter */ class TestReporter extends Reporter { reportMetricOnInterval(metricKey, intervalInSeconds) {} _reportMetrics(metrics) {} } describe('SignalFxSelfReportingMetricsRegistry', () => { let registry; let reporter; let mockReporter; beforeEach(() => { reporter = new TestReporter(); mockReporter = sinon.mock(reporter); registry = new SignalFxSelfReportingMetricsRegistry(reporter); }); it('#getOrCreateTimer uses a no-op meter', () => { mockReporter.expects('reportMetricOnInterval').once(); const timer = registry.getOrCreateTimer('my-timer'); assert.equal(timer._meter.constructor.name, 'NoOpMeter'); const theSameTimer = registry.getOrCreateTimer('my-timer'); assert(timer === theSameTimer); }); it('#getOrCreateMeter uses a no-op meter', () => { mockReporter.expects('reportMetricOnInterval').never(); const meter = registry.getOrCreateMeter('my-meter'); assert.equal(meter.constructor.name, 'NoOpMeter'); }); }); ================================================ FILE: packages/measured-signalfx-reporter/test/unit/reporters/test-SignalFxMetricsReporter.js ================================================ /*global describe, it, beforeEach, afterEach*/ const { SignalFxMetricsReporter } = require('../../../lib'); const { Histogram, MetricTypes, Gauge, Timer, Meter, Counter } = require('measured-core'); const assert = require('assert'); const sinon = require('sinon'); describe('SignalFxMetricsReporter', () => { const name = 'request'; const dimensions = { foo: 'bar' }; let reporter; let signalFxClient; let clientSpy; beforeEach(() => { signalFxClient = { send: data => { return new Promise(resolve => { resolve(); }); } }; clientSpy = sinon.spy(signalFxClient, 'send'); // noinspection JSCheckFunctionSignatures reporter = new SignalFxMetricsReporter(signalFxClient); }); it('#_reportMetrics sends the expected data to signal fx for a histogram', () => { const metric = new Histogram(); const metricMock = sinon.mock(metric); metricMock .expects('getType') .once() .returns(MetricTypes.HISTOGRAM); metricMock .expects('toJSON') .once() .returns({ min: 1, max: 10, sum: 100, variance: 55, mean: 5, stddev: 54, count: 20, median: 50, p75: 4, p95: 6, p99: 7, p999: 9 }); const metricWrapper = { name: name, metricImpl: metric, dimensions: dimensions }; const expected = { gauges: [ { metric: `${name}.max`, value: '10', dimensions: dimensions }, { metric: `${name}.min`, value: '1', dimensions: dimensions }, { metric: `${name}.mean`, value: '5', dimensions: dimensions }, { metric: `${name}.p95`, value: '6', dimensions: dimensions }, { metric: `${name}.p99`, value: '7', dimensions: dimensions } ], cumulative_counters: [ { metric: `${name}.count`, value: '20', dimensions: dimensions } ] }; reporter._reportMetrics([metricWrapper]); assert( clientSpy.withArgs( sinon.match(actual => { assert.deepEqual(expected, actual); return true; }) ).calledOnce ); }); it('#_reportMetrics sends the expected data to signal fx for a gauge', () => { const metric = new Gauge(() => 10); const metricWrapper = { name: name, metricImpl: metric, dimensions: dimensions }; const expected = { gauges: [ { metric: `${name}`, value: '10', dimensions: dimensions } ] }; reporter._reportMetrics([metricWrapper]); assert( clientSpy.withArgs( sinon.match(actual => { assert.deepEqual(expected, actual); return true; }) ).calledOnce ); }); it('#_reportMetrics sends the expected data to signal fx for a counter', () => { const metric = new Counter({ count: 5 }); const metricWrapper = { name: name, metricImpl: metric, dimensions: dimensions }; const expected = { cumulative_counters: [ { metric: `${name}.count`, value: '5', dimensions: dimensions } ] }; reporter._reportMetrics([metricWrapper]); assert( clientSpy.withArgs( sinon.match(actual => { assert.deepEqual(expected, actual); return true; }) ).calledOnce ); }); it('#_reportMetrics sends the expected data to signal fx for a meter', () => { const metric = new Meter(); const metricWrapper = { name: name, metricImpl: metric, dimensions: dimensions }; reporter._reportMetrics([metricWrapper]); assert(clientSpy.withArgs({}).calledOnce); metric.end(); }); it('#_reportMetrics sends the expected data to signal fx for an array multiple metrics', () => { const metric = new Histogram(); const metricMock = sinon.mock(metric); metricMock .expects('getType') .once() .returns(MetricTypes.HISTOGRAM); metricMock .expects('toJSON') .once() .returns({ min: 1, max: 10, sum: 100, variance: 55, mean: 5, stddev: 54, count: 20, median: 50, p75: 4, p95: 6, p99: 7, p999: 9 }); const histogramWRapper = { name: name, metricImpl: metric, dimensions: dimensions }; const counterWrapper = { name: 'my-counter', metricImpl: new Counter({ count: 6 }) }; const gaugeWrapper = { name: 'my-gauge', metricImpl: new Gauge(() => 8) }; const expected = { gauges: [ { metric: `${name}.max`, value: '10', dimensions: dimensions }, { metric: `${name}.min`, value: '1', dimensions: dimensions }, { metric: `${name}.mean`, value: '5', dimensions: dimensions }, { metric: `${name}.p95`, value: '6', dimensions: dimensions }, { metric: `${name}.p99`, value: '7', dimensions: dimensions }, { metric: 'my-gauge', value: '8', dimensions: {} } ], cumulative_counters: [ { metric: `${name}.count`, value: '20', dimensions: dimensions }, { metric: 'my-counter.count', value: '6', dimensions: {} } ] }; reporter._reportMetrics([histogramWRapper, counterWrapper, gaugeWrapper]); assert( clientSpy.withArgs( sinon.match(actual => { assert.deepEqual(expected, actual); return true; }) ).calledOnce ); }); it('#_reportMetrics doesnt add metrics tp]o send if a bad metric was supplied', () => { reporter._reportMetrics([ { name: 'something', metricImpl: { getType: () => 'something random' } } ]); assert(clientSpy.withArgs({}).calledOnce); }); it('#_reportMetrics sends the expected data to signal fx for a timer', () => {}); }); ================================================ FILE: packages/measured-signalfx-reporter/test/unit/validators/test-inputValidators.js ================================================ /*global describe, it, beforeEach, afterEach*/ const { validateSignalFxClient } = require('../../../lib/validators/inputValidators'); const assert = require('assert'); describe('validateRequiredSignalFxMetricsReporterParameters', () => { it('does nothing for the happy path', () => { validateSignalFxClient({ send: () => {} }); }); it('throws an error when a bad signal fx client is supplied', () => { assert.throws(() => { validateSignalFxClient({}); }, /signalFxClient must implement send\(data: any\)/); }); }); ================================================ FILE: packages/measured-signalfx-reporter/test/user-acceptance-test/index.js ================================================ const signalfx = require('signalfx'); const express = require('express'); const consoleLogLevel = require('console-log-level'); const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry, SignalFxEventCategories } = require('../../lib'); const { createOSMetrics, createProcessMetrics, createExpressMiddleware } = require('../../../measured-node-metrics/lib'); const libraryMetadata = require('../../package'); const log = consoleLogLevel({ name: 'SelfReportingMetricsRegistry', level: 'info' }); const library = libraryMetadata.name; const version = libraryMetadata.version; const defaultDimensions = { app: library, app_version: version, env: 'test' }; /** * Get your api key from a secrets provider of some kind. * * Good examples: * *
  • S3 with KMS *
  • Cerberus *
  • AWS Secrets Manager *
  • Vault *
  • Confidant * * Bad examples: * *
  • Checked into SCM in plaintext as a property *
  • Set as a plaintext environment variable * * @return {string} Returns the resolved Signal Fx Api Key */ const apiKeyResolver = () => { // https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/ return process.env.SIGNALFX_API_KEY; }; // Create the signal fx client const signalFxClient = new signalfx.Ingest(apiKeyResolver(), { userAgents: library }); // Create the signal fx reporter with the client const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, { defaultDimensions: defaultDimensions, defaultReportingIntervalInSeconds: 10, logLevel: 'debug' }); // Create the self reporting metrics registry with the signal fx reporter const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' }); metricsRegistry.sendEvent('events.app.starting'); createOSMetrics(metricsRegistry); createProcessMetrics(metricsRegistry); const app = express(); // wire up the metrics middleware app.use(createExpressMiddleware(metricsRegistry)); app.get('/hello', (req, res) => { res.send('hello world'); }); app.get('/path2', (req, res) => { res.send('path2'); }); app.listen(8080, () => log.info('Example app listening on port 8080!')); process.on('SIGINT', async () => { log.info('SIG INT, exiting'); await metricsRegistry.sendEvent('events.app.exiting'); process.exit(0); }); process.on('uncaughtException', async (err) => { log.error('There was an uncaught error', err); await metricsRegistry.sendEvent('events.app.uncaught-exception', SignalFxEventCategories.ALERT, {err: JSON.stringify(err)}); }); ================================================ FILE: scripts/generate-docs.sh ================================================ #!/bin/bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" ROOT_DIR="${SCRIPT_DIR}/.." DOCSTRAP_PATH="${ROOT_DIR}/node_modules/ink-docstrap/template/" # Clear out old docs rm -fr ${ROOT_DIR}/build/docs # create the directory structure mkdir -p ${ROOT_DIR}/build/docs/{img,packages/{measured-core,measured-reporting,measured-signalfx-reporter}} # Copy the image assets cp ${ROOT_DIR}/documentation/assets/measured.* ${ROOT_DIR}/build/docs/img/ # Copy our docpath customizations into the docstrap template dir cp ${ROOT_DIR}/documentation/docstrap_customized/template/* ${DOCSTRAP_PATH} # Generate the complete API docs for all packages export PACKAGE_NAME=root jsdoc --recurse --configure ./.jsdoc.json \ --tutorials ${ROOT_DIR}/tutorials \ --template ${DOCSTRAP_PATH} \ --readme ${ROOT_DIR}/Readme.md \ --destination build/docs/ \ ${ROOT_DIR}/packages/**/lib/ # Create the docs for measured-core export PACKAGE_NAME=measured-core jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \ --tutorials ${ROOT_DIR}/tutorials \ --template ${DOCSTRAP_PATH} \ --readme ${ROOT_DIR}/packages/measured-core/README.md \ --destination build/docs/packages/measured-core/ \ ${ROOT_DIR}/packages/measured-core/lib/ # Create the docs for measured-reporting export PACKAGE_NAME=measured-reporting jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \ --tutorials ${ROOT_DIR}/tutorials \ --template ${DOCSTRAP_PATH} \ --readme ${ROOT_DIR}/packages/measured-reporting/README.md \ --destination build/docs/packages/measured-reporting/ \ ${ROOT_DIR}/packages/measured-reporting/lib/ # Create the docs for measured-signalfx-reporter export PACKAGE_NAME=measured-signalfx-reporter jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \ --tutorials ${ROOT_DIR}/tutorials \ --template ${DOCSTRAP_PATH} \ --readme ${ROOT_DIR}/packages/measured-signalfx-reporter/README.md \ --destination build/docs/packages/measured-signalfx-reporter/ \ ${ROOT_DIR}/packages/measured-signalfx-reporter/lib/ # Create the docs for measured-node-metrics export PACKAGE_NAME=measured-node-metrics jsdoc --recurse --configure ${ROOT_DIR}/.jsdoc.json \ --tutorials ${ROOT_DIR}/tutorials \ --template ${DOCSTRAP_PATH} \ --readme ${ROOT_DIR}/packages/measured-node-metrics/README.md \ --destination build/docs/packages/measured-node-metrics/ \ ${ROOT_DIR}/packages/measured-node-metrics/lib/ ================================================ FILE: scripts/publish.sh ================================================ #!/bin/bash ###################################################################### # # This script is intended to be used in a Travis CI/CD env. # It assumes Travis has been set up with jq, and awk and the following secure env vars. # # GH_TOKEN: A github oath token with perms to create and push tags and call the releases API. # NPM_TOKEN: A npm auth token that can publish the packages. # ###################################################################### set -e LATEST_RELEASE_DATA=$(curl -s --header "Accept: application/json" -L https://github.com/yaorg/node-measured/releases/latest) LATEST_GITHUB_RELEASE=$(echo $LATEST_RELEASE_DATA | jq --raw-output ".tag_name" | sed 's/v\(.*\)/\1/') CURRENT_VERSION=$(cat lerna.json | jq --raw-output ".version") echo "Processing tag information to determine if release is major, minor or patch." echo "The current version tag is: ${CURRENT_VERSION}" echo "The new version tag is: ${LATEST_GITHUB_RELEASE}" if [ -z ${GH_TOKEN} ] then echo "GH_TOKEN is null, you must supply oath token for github. Aborting!" exit 1 fi if [ -z ${NPM_TOKEN} ] then echo "NPM_TOKEN is null, you must supply auth token for npm. Aborting!" exit 1 fi if [ -z ${LATEST_GITHUB_RELEASE} ] then echo "NEW_VERSION is null, aborting!" exit 1 fi if [ -z ${CURRENT_VERSION} ] then echo "CURRENT_VERSION is null, aborting!" exit 1 fi if [ ${CURRENT_VERSION} == ${LATEST_GITHUB_RELEASE} ] then echo "The current version and the new version are the same, aborting!" exit 1 fi CD_VERSION=$(awk -v NEW_VERSION=${LATEST_GITHUB_RELEASE} -v CURRENT_VERSION=${CURRENT_VERSION} 'BEGIN{ split(NEW_VERSION,newVersionParts,/\./) split(CURRENT_VERSION,currentVersionParts,/\./) for (i=1;i in currentVersionParts;i++) { if (newVersionParts[i] != currentVersionParts[i]) { if (i == 1) { printf "major\n" } if (i == 2) { printf "minor\n" } if (i == 3) { printf "patch\n" } break } } }') echo echo "determined to use semver: '${CD_VERSION}' flag for lerna publish --cd-version" echo echo "Re-wireing origin remote to use GH_TOKEN" git remote rm origin git remote add origin https://fieldju:${GH_TOKEN}@github.com/yaorg/node-measured.git git fetch --all git checkout master echo "Deleting tag created by github to allow lerna to create it" RELEASE="v${LATEST_GITHUB_RELEASE}" git tag -d ${RELEASE} git push origin :refs/tags/${RELEASE} echo "Preparing .npmrc" echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc echo 'registry=http://registry.npmjs.org' >> .npmrc lerna publish --cd-version ${CD_VERSION} --yes --force-publish echo "Re-binding orphaned github release to tag, so that it shows up as latest release rather than draft release" 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') 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} ================================================ FILE: tutorials/SignalFx Express Full End to End Example.md ================================================ ### Using Measured to instrument OS, Process and Express Metrics. This tutorial shows how to use the measured libraries to fully instrument OS and Node Process metrics as well as create an express middleware. 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. **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. ```javascript const os = require('os'); const signalfx = require('signalfx'); const express = require('express'); const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry } = require('measured-signalfx-reporter'); const { createProcessMetrics, createOSMetrics, createExpressMiddleware } = require('measured-node-metrics'); const libraryMetadata = require('./package'); // get metadata from package.json const library = libraryMetadata.name; const version = libraryMetadata.version; // Report process and os stats 1x per minute const PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS = 60; // Report the request count and histogram stats every 10 seconds const REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10; const defaultDimensions = { app: library, app_version: version, env: 'test' }; /** * Get your api key from a secrets provider of some kind. * * Good examples: * *
  • S3 with KMS *
  • Cerberus *
  • AWS Secrets Manager *
  • Vault *
  • Confidant * * Bad examples: * *
  • Checked into SCM in plaintext as a property *
  • Set as a plaintext environment variable * * @return {string} Returns the resolved Signal Fx Api Key */ const apiKeyResolver = () => { // https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/ return process.env.SIGNALFX_API_KEY; }; // Create the signal fx client const signalFxClient = new signalfx.Ingest(apiKeyResolver(), { userAgents: library }); // Create the signal fx reporter with the client const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, { defaultDimensions: defaultDimensions, defaultReportingIntervalInSeconds: 10, logLevel: 'debug' }); // Create the self reporting metrics registry with the signal fx reporter const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' }); createOSMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS); createProcessMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS); const app = express(); // wire up the metrics middleware app.use(createExpressMiddleware(metricsRegistry, REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS)); app.get('/hello', (req, res) => { res.send('hello world'); }); app.get('/path2', (req, res) => { res.send('path2'); }); app.listen(8080, () => log.info('Example app listening on port 8080!')); ``` ================================================ FILE: tutorials/SignalFx Koa Full End to End Example.md ================================================ ### Using Measured to instrument OS, Process and Koa Metrics. This tutorial shows how to use the measured libraries to fully instrument OS and Node Process metrics as well as create a Koa middleware. 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. **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. ```javascript const os = require('os'); const signalfx = require('signalfx'); const Koa = require('koa'); const KoaBodyParser = require('koa-bodyparser'); const Router = require('koa-router'); const { SignalFxMetricsReporter, SignalFxSelfReportingMetricsRegistry } = require('measured-signalfx-reporter'); const { createProcessMetrics, createOSMetrics, createKoaMiddleware } = require('measured-node-metrics'); const libraryMetadata = require('./package'); // get metadata from package.json const library = libraryMetadata.name; const version = libraryMetadata.version; // Report process and os stats 1x per minute const PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS = 60; // Report the request count and histogram stats every 10 seconds const REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS = 10; const defaultDimensions = { app: library, app_version: version, env: 'test' }; /** * Get your api key from a secrets provider of some kind. * * Good examples: * *
  • S3 with KMS *
  • Cerberus *
  • AWS Secrets Manager *
  • Vault *
  • Confidant * * Bad examples: * *
  • Checked into SCM in plaintext as a property *
  • Set as a plaintext environment variable * * @return {string} Returns the resolved Signal Fx Api Key */ const apiKeyResolver = () => { // https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/ return process.env.SIGNALFX_API_KEY; }; // Create the signal fx client const signalFxClient = new signalfx.Ingest(apiKeyResolver(), { userAgents: library }); // Create the signal fx reporter with the client const signalFxReporter = new SignalFxMetricsReporter(signalFxClient, { defaultDimensions: defaultDimensions, defaultReportingIntervalInSeconds: 10, logLevel: 'debug' }); // Create the self reporting metrics registry with the signal fx reporter const metricsRegistry = new SignalFxSelfReportingMetricsRegistry(signalFxReporter, { logLevel: 'debug' }); createOSMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS); createProcessMetrics(metricsRegistry, {}, PROCESS_AND_SYSTEM_METRICS_REPORTING_INTERVAL_IN_SECONDS); const app = new Koa(); router = new Router(); router.get('/hello', (req, res) => { res.send('hello world'); }); router.get('/path2', (req, res) => { res.send('path2'); }); // wire up the metrics middleware app.use(createKoaMiddleware(metricsRegistry, REQUEST_METRICS_REPORTING_INTERVAL_IN_SECONDS)); app.use(KoaBodyParser()); app.use(router.routes()); app.listen(8080, () => log.info('Example app listening on port 8080!')); ```