From b401dbbbb39e76eeb6ac04155086d2f57ddfefa7 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Thu, 11 Jul 2019 11:12:11 -0700 Subject: [PATCH] feat(metrics): add metrics integration with prometheus --- packages/metrics/.npmrc | 1 + packages/metrics/LICENSE | 25 + packages/metrics/README.md | 53 ++ packages/metrics/index.d.ts | 6 + packages/metrics/index.js | 6 + packages/metrics/index.ts | 8 + packages/metrics/package-lock.json | 529 ++++++++++++++++++ packages/metrics/package.json | 51 ++ .../acceptance/metrics.acceptance.ts | 71 +++ packages/metrics/src/controllers/index.ts | 6 + .../src/controllers/metrics.controller.ts | 21 + packages/metrics/src/index.ts | 7 + packages/metrics/src/interceptors/index.ts | 6 + .../src/interceptors/metrics.interceptor.ts | 39 ++ packages/metrics/src/keys.ts | 16 + packages/metrics/src/metrics.component.ts | 25 + packages/metrics/src/observers/index.ts | 6 + .../metrics/src/observers/metrics.observer.ts | 31 + packages/metrics/tsconfig.build.json | 9 + 19 files changed, 916 insertions(+) create mode 100644 packages/metrics/.npmrc create mode 100644 packages/metrics/LICENSE create mode 100644 packages/metrics/README.md create mode 100644 packages/metrics/index.d.ts create mode 100644 packages/metrics/index.js create mode 100644 packages/metrics/index.ts create mode 100644 packages/metrics/package-lock.json create mode 100644 packages/metrics/package.json create mode 100644 packages/metrics/src/__tests__/acceptance/metrics.acceptance.ts create mode 100644 packages/metrics/src/controllers/index.ts create mode 100644 packages/metrics/src/controllers/metrics.controller.ts create mode 100644 packages/metrics/src/index.ts create mode 100644 packages/metrics/src/interceptors/index.ts create mode 100644 packages/metrics/src/interceptors/metrics.interceptor.ts create mode 100644 packages/metrics/src/keys.ts create mode 100644 packages/metrics/src/metrics.component.ts create mode 100644 packages/metrics/src/observers/index.ts create mode 100644 packages/metrics/src/observers/metrics.observer.ts create mode 100644 packages/metrics/tsconfig.build.json diff --git a/packages/metrics/.npmrc b/packages/metrics/.npmrc new file mode 100644 index 000000000000..cafe685a112d --- /dev/null +++ b/packages/metrics/.npmrc @@ -0,0 +1 @@ +package-lock=true diff --git a/packages/metrics/LICENSE b/packages/metrics/LICENSE new file mode 100644 index 000000000000..844948f060c9 --- /dev/null +++ b/packages/metrics/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) IBM Corp. 2019. All Rights Reserved. +Node module: @loopback/metrics +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +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. diff --git a/packages/metrics/README.md b/packages/metrics/README.md new file mode 100644 index 000000000000..a88f6c0f8c6d --- /dev/null +++ b/packages/metrics/README.md @@ -0,0 +1,53 @@ +# @loopback/metrics + +This module contains a component to report metrics to Prometheus. + +## Installation + +```sh +npm install --save @loopback/metrics +``` + +## Basic use + +The component should be loaded in the constructor of your custom Application +class. + +Start by importing the component class: + +```ts +import {MetricsComponent} from '@loopback/metrics'; +``` + +In the constructor, add the component to your application: + +```ts +this.component(MetricsComponent); +``` + +By default, Metrics route is mounted at `/metrics`. This path can be customized +via Metrics configuration as follows: + +```ts +this.bind(MetricsBindings.CONFIG).to({ + path: '/metrics', +}); +``` + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/packages/metrics/index.d.ts b/packages/metrics/index.d.ts new file mode 100644 index 000000000000..e8663372d411 --- /dev/null +++ b/packages/metrics/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2018. All Rights Reserved. +// Node module: @loopback/rest-explorer +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/packages/metrics/index.js b/packages/metrics/index.js new file mode 100644 index 000000000000..77a825f2f7dd --- /dev/null +++ b/packages/metrics/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/packages/metrics/index.ts b/packages/metrics/index.ts new file mode 100644 index 000000000000..7c9bf0d65022 --- /dev/null +++ b/packages/metrics/index.ts @@ -0,0 +1,8 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +// DO NOT EDIT THIS FILE +// Add any additional (re)exports to src/index.ts instead. +export * from './src'; diff --git a/packages/metrics/package-lock.json b/packages/metrics/package-lock.json new file mode 100644 index 000000000000..5ae5b2ea6a70 --- /dev/null +++ b/packages/metrics/package-lock.json @@ -0,0 +1,529 @@ +{ + "name": "@loopback/metrics", + "version": "1.2.5", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ejs": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-2.6.3.tgz", + "integrity": "sha512-/F+qQ0Fr0Dr1YvHjX+FCvbba4sQ27RdCPDqmP/si0e1v1GOkbQ3VRBvZPSQM7NoQ3iz3SyiJVscCP2f0vKuIhQ==", + "dev": true + }, + "@types/express": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz", + "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz", + "integrity": "sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==", + "dev": true + }, + "@types/node": { + "version": "10.14.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.12.tgz", + "integrity": "sha512-QcAKpaO6nhHLlxWBvpc4WeLrTvPqlHOvaj0s5GriKkA1zq+bsFBPpfYCvQhLqLgYlIko8A9YrPdaMHCo5mBcpg==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==", + "dev": true + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "dev": true, + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "bintrees": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.1.tgz", + "integrity": "sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", + "dev": true + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "dev": true, + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "ejs": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.2.tgz", + "integrity": "sha512-PcW2a0tyTuPHz3tWyYqtK6r1fZ3gp+3Sop8Ph+ZYN81Ob5rwmbHEzaqs10N3BEsaGTkh/ooniXK+WwszGlc2+Q==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "dev": true, + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", + "dev": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", + "dev": true + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "dev": true, + "requires": { + "mime-db": "1.40.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", + "dev": true + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "prom-client": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-11.5.3.tgz", + "integrity": "sha512-iz22FmTbtkyL2vt0MdDFY+kWof+S9UB/NACxSn2aJcewtw+EERsen0urSkZ2WrHseNdydsvcxCTAnPcSMZZv4Q==", + "requires": { + "tdigest": "^0.1.1" + } + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "dev": true, + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "dev": true, + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", + "dev": true + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "swagger-ui-dist": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.23.0.tgz", + "integrity": "sha512-DMnt69K3p8BwnKf8f8uOqsts/teZNRS4LlXAqjfa8HS2RKWWOCSiVEHkMzY3zlU4wqu/olPdF0zDEm9Ed5JZ4A==" + }, + "tdigest": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.1.tgz", + "integrity": "sha1-Ljyyw56kSeVdHmzZEReszKRYgCE=", + "requires": { + "bintrees": "1.0.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + } + } +} diff --git a/packages/metrics/package.json b/packages/metrics/package.json new file mode 100644 index 000000000000..78d51ad6d45d --- /dev/null +++ b/packages/metrics/package.json @@ -0,0 +1,51 @@ +{ + "name": "@loopback/metrics", + "version": "1.0.0", + "description": "LoopBack Metrics", + "engines": { + "node": ">=8.9" + }, + "scripts": { + "build": "lb-tsc", + "clean": "lb-clean loopback-metrics*.tgz dist tsconfig.build.tsbuildinfo package", + "pretest": "npm run build", + "test": "lb-mocha \"dist/__tests__/**/*.js\"", + "verify": "npm pack && tar xf loopback-metrics*.tgz && tree package && npm run clean" + }, + "author": "IBM Corp.", + "copyright.owner": "IBM Corp.", + "license": "MIT", + "dependencies": { + "@loopback/context": "^1.20.2", + "@loopback/core": "^1.8.5", + "@loopback/rest": "^1.16.3", + "prom-client": "^11.5.3" + }, + "devDependencies": { + "@loopback/build": "^2.0.3", + "@loopback/eslint-config": "^2.0.0", + "@loopback/testlab": "^1.6.3", + "@types/node": "^10.14.12" + }, + "keywords": [ + "LoopBack", + "Prometheus", + "Metrics" + ], + "files": [ + "README.md", + "index.js", + "index.d.ts", + "dist", + "src", + "!*/__tests__", + "templates" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/strongloop/loopback-next.git" + } +} diff --git a/packages/metrics/src/__tests__/acceptance/metrics.acceptance.ts b/packages/metrics/src/__tests__/acceptance/metrics.acceptance.ts new file mode 100644 index 000000000000..e77a06ee8621 --- /dev/null +++ b/packages/metrics/src/__tests__/acceptance/metrics.acceptance.ts @@ -0,0 +1,71 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/rest-explorer +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {RestApplication, RestServerConfig} from '@loopback/rest'; +import { + Client, + createRestAppClient, + expect, + givenHttpServerConfig, +} from '@loopback/testlab'; +import {DefaultMetricsCollectorConfiguration} from 'prom-client'; +import {MetricsComponent} from '../..'; +import {MetricsBindings} from '../../keys'; + +describe('Metrics (acceptance)', () => { + let app: RestApplication; + let request: Client; + + afterEach(async () => { + if (app) await app.stop(); + (app as unknown) = undefined; + }); + + context('with default config', () => { + beforeEach(async () => { + app = givenRestApplication(); + app.component(MetricsComponent); + await app.start(); + request = createRestAppClient(app); + }); + + it('exposes metrics at "/metrics/"', async () => { + const res = await request + .get('/metrics') + .expect(200) + .expect('content-type', /text/); + expect(res.text).to.match(/# TYPE/); + expect(res.text).to.match(/# HELP/); + }); + }); + + context('with custom MetricsConfig', () => { + it('honors custom explorer path', async () => { + await givenAppWithCustomConfig({ + // `-` is not allowed + prefix: 'myapp', + }); + await request + .get('/metrics') + .expect(200) + .expect('content-type', /text/); + }); + + async function givenAppWithCustomConfig( + config: DefaultMetricsCollectorConfiguration, + ) { + app = givenRestApplication(); + app.bind(MetricsBindings.CONFIG).to(config); + app.component(MetricsComponent); + await app.start(); + request = createRestAppClient(app); + } + }); + + function givenRestApplication(config?: RestServerConfig) { + const rest = Object.assign({}, givenHttpServerConfig(), config); + return new RestApplication({rest}); + } +}); diff --git a/packages/metrics/src/controllers/index.ts b/packages/metrics/src/controllers/index.ts new file mode 100644 index 000000000000..6110d09d0b3b --- /dev/null +++ b/packages/metrics/src/controllers/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './metrics.controller'; diff --git a/packages/metrics/src/controllers/metrics.controller.ts b/packages/metrics/src/controllers/metrics.controller.ts new file mode 100644 index 000000000000..beec7dff9251 --- /dev/null +++ b/packages/metrics/src/controllers/metrics.controller.ts @@ -0,0 +1,21 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Constructor} from '@loopback/core'; +import {get} from '@loopback/rest'; +import {register} from 'prom-client'; + +export function metricsControllerFactory( + path = '/metrics', +): Constructor { + class MetricsController { + @get(path, {responses: {}, 'x-visibility': 'undocumented'}) + report(): string { + return register.metrics(); + } + } + + return MetricsController; +} diff --git a/packages/metrics/src/index.ts b/packages/metrics/src/index.ts new file mode 100644 index 000000000000..6d485ce468f8 --- /dev/null +++ b/packages/metrics/src/index.ts @@ -0,0 +1,7 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './keys'; +export * from './metrics.component'; diff --git a/packages/metrics/src/interceptors/index.ts b/packages/metrics/src/interceptors/index.ts new file mode 100644 index 000000000000..1dcdd42d3075 --- /dev/null +++ b/packages/metrics/src/interceptors/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './metrics.interceptor'; diff --git a/packages/metrics/src/interceptors/metrics.interceptor.ts b/packages/metrics/src/interceptors/metrics.interceptor.ts new file mode 100644 index 000000000000..55b8d125978b --- /dev/null +++ b/packages/metrics/src/interceptors/metrics.interceptor.ts @@ -0,0 +1,39 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + asGlobalInterceptor, + bind, + BindingScope, + Interceptor, + InvocationContext, + Provider, + ValueOrPromise, +} from '@loopback/context'; +import {Gauge} from 'prom-client'; + +@bind(asGlobalInterceptor('metrics'), {scope: BindingScope.SINGLETON}) +export class MetricsInterceptor implements Provider { + private gauge: Gauge = new Gauge('loopback:invocation', 'method invocation', [ + 'targetName', + ]); + constructor() {} + + value() { + return this.intercept.bind(this); + } + + async intercept( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) { + const end = this.gauge.startTimer({targetName: invocationCtx.targetName}); + try { + return await next(); + } finally { + end(); + } + } +} diff --git a/packages/metrics/src/keys.ts b/packages/metrics/src/keys.ts new file mode 100644 index 000000000000..4cd4adf1dc1a --- /dev/null +++ b/packages/metrics/src/keys.ts @@ -0,0 +1,16 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingKey} from '@loopback/context'; +import {DefaultMetricsCollectorConfiguration} from 'prom-client'; + +/** + * Binding keys used by this component. + */ +export namespace MetricsBindings { + export const CONFIG = BindingKey.create( + 'metrics.defaultMetricsCollectorConfiguration', + ); +} diff --git a/packages/metrics/src/metrics.component.ts b/packages/metrics/src/metrics.component.ts new file mode 100644 index 000000000000..33d2d1361d1f --- /dev/null +++ b/packages/metrics/src/metrics.component.ts @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {createBindingFromClass, inject} from '@loopback/context'; +import {Component, CoreBindings} from '@loopback/core'; +import {RestApplication} from '@loopback/rest'; +import {metricsControllerFactory} from './controllers'; +import {MetricsInterceptor} from './interceptors'; +import {MetricsObserver} from './observers'; + +/** + * A component providing a self-hosted API Explorer. + */ +export class MetricsComponent implements Component { + constructor( + @inject(CoreBindings.APPLICATION_INSTANCE) + private application: RestApplication, + ) { + this.application.lifeCycleObserver(MetricsObserver); + this.application.add(createBindingFromClass(MetricsInterceptor)); + this.application.controller(metricsControllerFactory()); + } +} diff --git a/packages/metrics/src/observers/index.ts b/packages/metrics/src/observers/index.ts new file mode 100644 index 000000000000..b9ff987da08e --- /dev/null +++ b/packages/metrics/src/observers/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './metrics.observer'; diff --git a/packages/metrics/src/observers/metrics.observer.ts b/packages/metrics/src/observers/metrics.observer.ts new file mode 100644 index 000000000000..7eb0b9dd11d1 --- /dev/null +++ b/packages/metrics/src/observers/metrics.observer.ts @@ -0,0 +1,31 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/metrics +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {inject, LifeCycleObserver} from '@loopback/core'; +import { + collectDefaultMetrics, + DefaultMetricsCollectorConfiguration, + register, +} from 'prom-client'; + +export class MetricsObserver implements LifeCycleObserver { + private interval: NodeJS.Timeout; + + constructor( + @inject('metrics.defaultMetricsCollectorConfiguration', {optional: true}) + private config: DefaultMetricsCollectorConfiguration = {timeout: 5000}, + ) {} + + start() { + this.interval = collectDefaultMetrics(this.config); + } + + stop() { + if (this.interval) { + clearInterval(this.interval); + register.clear(); + } + } +} diff --git a/packages/metrics/tsconfig.build.json b/packages/metrics/tsconfig.build.json new file mode 100644 index 000000000000..c7b8e49eaca5 --- /dev/null +++ b/packages/metrics/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +}