From 3ac4ddfa368917ff0a8abe1c6ffb2637e6cedc98 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Fri, 2 Oct 2020 17:41:31 -0700 Subject: [PATCH 01/21] initial commit, kind of works, needs more metrics --- .eslintrc.js | 19 + .gitignore | 2 + README.md | 108 ++- index.ts | 159 ++++ package.json | 24 + tsconfig.json | 70 ++ yarn.lock | 2095 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 2476 insertions(+), 1 deletion(-) create mode 100644 .eslintrc.js create mode 100644 .gitignore create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json create mode 100644 yarn.lock diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..dd8adab --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,19 @@ +module.exports = { + "env": { + "browser": true, + "es2020": true + }, + "extends": [ + "airbnb-base" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + } +}; diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de4d1f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/README.md b/README.md index cf8529d..8663c1f 100644 --- a/README.md +++ b/README.md @@ -1 +1,107 @@ -# teraslice-exporter +# Teraslice Job Exporter README + +## Usage + +So far it works like this: + +```bash +TERASLICE_URL="https://localhost" \ + DEBUG=True \ + NODE_EXTRA_CA_CERTS=~/Downloads/ca.crt \ + node dist/index.js | bunyan +``` + +## Design + +Scrape the `/v1/cluster/controllers` endpoint periodically to get an array of +active controllers, like this + +```json +[ + { + "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510e59", + "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad18a", + "name": "my_job_name", + "workers_available": 0, + "workers_active": 6, + "workers_joined": 6, + "workers_reconnected": 0, + "workers_disconnected": 0, + "job_duration": 0, + "failed": 1, + "subslices": 0, + "queued": 7, + "slice_range_expansion": 0, + "processed": 204156, + "slicers": 1, + "subslice_by_key": 0, + "started": "2020-09-17T21:08:58.905Z", + "queuing_complete": "" + } +] +``` + +Labels? + +```txt +cluster = ts-prod +ex_id = 5ba1da6a-0ba2-49f4-92c3-d436ba510e59, +job_id = 7e6dfa3c-6665-455d-9d52-f11bd32ad18a, +name = my_job_name, +``` + +The following metrics related to each job: + +```txt +"workers_available": 0, +"workers_active": 6, +"workers_joined": 6, +"workers_reconnected": 0, +"workers_disconnected": 0, +"job_duration": 0, +"failed": 1, +"subslices": 0, +"queued": 7, +"slice_range_expansion": 0, +"processed": 204156, +"slicers": 1, +"subslice_by_key": 0, +"started": "2020-09-17T21:08:58.905Z", +"queuing_complete": "" +``` + +The following metrics related to the query response itself (timing info) derived +from this timing info: + +```json +{ + start: 1601417492206, + socket: 1601417492208, + lookup: 1601417492213, + connect: 1601417492247, + secureConnect: 1601417492315, + upload: 1601417492316, + response: 1601417493491, + end: 1601417493518, + error: undefined, + abort: undefined, + phases: { + wait: 2, + dns: 5, + tcp: 34, + tls: 68, + request: 1, + firstByte: 1175, + download: 27, + total: 1312 + } +} +``` + +Use the following: + +```txt +got_phase_firstByte: 1175, +got_phase_download: 27, +got_phase_total: 1312 +``` diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..4f220b3 --- /dev/null +++ b/index.ts @@ -0,0 +1,159 @@ +import { URL } from 'url'; + +import { Gauge, Registry } from 'prom-client'; +import got from 'got'; +import express from 'express'; +import bunyan from 'bunyan'; + +const server = express(); + +const metricsRegistry = new Registry(); +const logger = bunyan.createLogger({ name: 'teraslice_exporter' }); + +const terasliceQueryDelay = 30000; +const metricPrefix = 'teraslice'; +const standardLabelNames = ['ex_id', 'job_id', 'job_name']; + +const gaugeWorkersActive = new Gauge({ + name: `${metricPrefix}_workers_active`, + help: 'Number of Teraslice workers actively processing slices.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + +const gaugeWorkersAvailable = new Gauge({ + name: `${metricPrefix}_workers_available`, + help: 'Number of Teraslice workers running and waiting for work.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + +// The following Guages should be Counters by my reconing, but as far as +// prom-client is concerned, this usage is fine: +// https://github.com/siimon/prom-client/issues/192 +const guageSlicesProcessed = new Gauge({ + name: `${metricPrefix}_slices_processed`, + help: 'Number of slices processed.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + +/** + * parseController adds the teraslice execution controller metrics to the + * metricsRegistry for a single execution. + * + * { + * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510e59", + * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad18a", + * "name": "my_job_name", + * "workers_available": 0, + * "workers_active": 6, + * "workers_joined": 6, + * "workers_reconnected": 0, + * "workers_disconnected": 0, + * "job_duration": 0, + * "failed": 1, + * "subslices": 0, + * "queued": 7, + * "slice_range_expansion": 0, + * "processed": 204156, + * "slicers": 1, + * "subslice_by_key": 0, + * "started": "2020-09-17T21:08:58.905Z", + * "queuing_complete": "" + * } + * + * @param controller + */ +function parseController(controller:any) { + // p(controller); + + const standardLabels = { + ex_id: controller.ex_id, + job_id: controller.job_id, + job_name: controller.name, + }; + + gaugeWorkersActive.set(standardLabels, controller.workers_active); + gaugeWorkersAvailable.set(standardLabels, controller.workers_available); + guageSlicesProcessed.set(standardLabels, controller.processed); +} + +async function getTerasliceClusterState(baseUrl: URL) { + const url = new URL('/v1/cluster/controllers', baseUrl); + let response; + try { + response = await got(url); + } catch (error) { + logger.error(error); + } + + if (response && response.statusCode === 200) { + logger.debug(response.statusCode); + + const controllers = JSON.parse(response.body); + + // eslint-disable-next-line no-restricted-syntax + for (const controller of controllers) { + parseController(controller); + } + } else if (response) { + logger.error(`Error getting ${url}: ${response.statusCode}`); + } + + // p(response.timings) +} + +async function getTerasliceClusterInfo(baseUrl:URL) { + let response; + let info; + try { + response = await got(baseUrl, { responseType: 'json' }); + } catch (error) { + logger.error(error); + } + + if (response && response.statusCode === 200 && response.body) { + // p(response.body); + info = response.body; + } else if (response) { + logger.error(`Error getting ${baseUrl}: ${response.statusCode}`); + } + return info; +} + +async function updateTerasliceInfo(url:URL) { + const clusterInfo = await getTerasliceClusterInfo(url); + // console.log(clusterInfo); + await getTerasliceClusterState(url); +} + +function main() { + const { DEBUG, TERASLICE_URL } = process.env; + + server.get('/metrics', (req, res) => { + res.set('Content-Type', metricsRegistry.contentType); + res.end(metricsRegistry.metrics()); + }); + + if (DEBUG) { + logger.level('debug'); + } + logger.debug('DEBUG'); + + const baseUrl = new URL(TERASLICE_URL); + + logger.info(`Getting intial Teraslice Cluster Information from ${baseUrl}`); + updateTerasliceInfo(baseUrl); + + setInterval(() => { + logger.debug(`Updating Teraslice Cluster Information from ${baseUrl}`); + updateTerasliceInfo(baseUrl); + }, terasliceQueryDelay); + + const port = process.env.PORT || 3000; + logger.info(`Server listening to ${port}, metrics exposed on /metrics endpoint`); + server.listen(port); +} + +main(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..9216707 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "dependencies": { + "@types/bunyan": "^1.8.6", + "bunyan": "^1.8.14", + "express": "^4.17.1", + "got": "^11.6.2", + "prom-client": "^12.0.0" + }, + "devDependencies": { + "@types/express": "^4.17.8", + "@types/node": "^14.10.1", + "@typescript-eslint/eslint-plugin": "^4.3.0", + "@typescript-eslint/parser": "^4.3.0", + "eslint": "^7.10.0", + "eslint-config-airbnb-base": "^14.2.0", + "eslint-plugin-import": "^2.22.1", + "typescript": "^4.0.2" + }, + "scripts": { + "build": "rm -rf dist/ && npx tsc", + "build:watch": "rm -rf dist/ && npx tsc -w", + "run": "node dist/index.js" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..19e0a49 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,70 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "resolveJsonModule": true + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..0aabc3c --- /dev/null +++ b/yarn.lock @@ -0,0 +1,2095 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@eslint/eslintrc@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" + integrity sha512-4YVwPkANLeNtRjMekzux1ci8hIaH5eGKktGqR0d3LWsKNn5B2X/1Z6Trxy7jQXl9EBGE6Yj02O+t09FMeRllaA== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + lodash "^4.17.19" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@sindresorhus/is@^3.1.1": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-3.1.2.tgz#548650de521b344e3781fbdb0ece4aa6f729afb8" + integrity sha512-JiX9vxoKMmu8Y3Zr2RVathBL1Cdu4Nt4MuNWemt1Nc06A0RAin9c5FArkhGsyMBWfCu4zj+9b+GxtjAnE4qqLQ== + +"@szmarczak/http-timer@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.5.tgz#bfbd50211e9dfa51ba07da58a14cdfd333205152" + integrity sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ== + dependencies: + defer-to-connect "^2.0.0" + +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bunyan@^1.8.6": + version "1.8.6" + resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.6.tgz#6527641cca30bedec5feb9ab527b7803b8000582" + integrity sha512-YiozPOOsS6bIuz31ilYqR5SlLif4TBWsousN2aCWLi5233nZSX19tFbcQUPdR7xJ8ypPyxkCGNxg0CIV5n9qxQ== + dependencies: + "@types/node" "*" + +"@types/cacheable-request@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.1.tgz#5d22f3dded1fd3a84c0bbeb5039a7419c2c91976" + integrity sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ== + dependencies: + "@types/http-cache-semantics" "*" + "@types/keyv" "*" + "@types/node" "*" + "@types/responselike" "*" + +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.12" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.12.tgz#9a487da757425e4f267e7d1c5720226af7f89591" + integrity sha512-EaEdY+Dty1jEU7U6J4CUWwxL+hyEGMkO5jan5gplfegUgCUsIUWqXxqw47uGjimeT4Qgkz/XUfwoau08+fgvKA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.8": + version "4.17.8" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.8.tgz#3df4293293317e61c60137d273a2e96cd8d5f27a" + integrity sha512-wLhcKh3PMlyA2cNAB9sjM1BntnhPMiM0JOBwPBqttjHev2428MLEB4AYVN+d8s2iyCVZac+o41Pflm/ZH5vLXQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-cache-semantics@*": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz#9140779736aa2655635ee756e2467d787cfe8a2a" + integrity sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A== + +"@types/json-schema@^7.0.3": + version "7.0.6" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" + integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/keyv@*": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" + integrity sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw== + dependencies: + "@types/node" "*" + +"@types/mime@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.3.tgz#c893b73721db73699943bfc3653b1deb7faa4a3a" + integrity sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q== + +"@types/node@*", "@types/node@^14.10.1": + version "14.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.10.1.tgz#cc323bad8e8a533d4822f45ce4e5326f36e42177" + integrity sha512-aYNbO+FZ/3KGeQCEkNhHFRIzBOUgc7QvcVNKXbfnhDkSfwUv91JsQQa10rDgKSTSLkXZ1UIyPe4FJJNVgw1xWQ== + +"@types/qs@*": + version "6.9.4" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.4.tgz#a59e851c1ba16c0513ea123830dd639a0a15cb6a" + integrity sha512-+wYo+L6ZF6BMoEjtf8zB2esQsqdV6WsjRK/GP9WOgLPrq87PbNWgIxS76dS5uvl/QXtHGakZmwTznIfcPXcKlQ== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/responselike@*", "@types/responselike@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" + integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + dependencies: + "@types/node" "*" + +"@types/serve-static@*": + version "1.13.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.5.tgz#3d25d941a18415d3ab092def846e135a08bbcf53" + integrity sha512-6M64P58N+OXjU432WoLLBQxbA0LRGBCRm7aAGQJ+SMC1IMl0dgRVi9EFfoDcS2a7Xogygk/eGN94CfwU9UF7UQ== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@typescript-eslint/eslint-plugin@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.3.0.tgz#1a23d904bf8ea248d09dc3761af530d90f39c8fa" + integrity sha512-RqEcaHuEKnn3oPFislZ6TNzsBLqpZjN93G69SS+laav/I8w/iGMuMq97P0D2/2/kW4SCebHggqhbcCfbDaaX+g== + dependencies: + "@typescript-eslint/experimental-utils" "4.3.0" + "@typescript-eslint/scope-manager" "4.3.0" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.3.0.tgz#3f3c6c508e01b8050d51b016e7f7da0e3aefcb87" + integrity sha512-cmmIK8shn3mxmhpKfzMMywqiEheyfXLV/+yPDnOTvQX/ztngx7Lg/OD26J8gTZfkLKUmaEBxO2jYP3keV7h2OQ== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/scope-manager" "4.3.0" + "@typescript-eslint/types" "4.3.0" + "@typescript-eslint/typescript-estree" "4.3.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/parser@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.3.0.tgz#684fc0be6551a2bfcb253991eec3c786a8c063a3" + integrity sha512-JyfRnd72qRuUwItDZ00JNowsSlpQGeKfl9jxwO0FHK1qQ7FbYdoy5S7P+5wh1ISkT2QyAvr2pc9dAemDxzt75g== + dependencies: + "@typescript-eslint/scope-manager" "4.3.0" + "@typescript-eslint/types" "4.3.0" + "@typescript-eslint/typescript-estree" "4.3.0" + debug "^4.1.1" + +"@typescript-eslint/scope-manager@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.3.0.tgz#c743227e087545968080d2362cfb1273842cb6a7" + integrity sha512-cTeyP5SCNE8QBRfc+Lgh4Xpzje46kNUhXYfc3pQWmJif92sjrFuHT9hH4rtOkDTo/si9Klw53yIr+djqGZS1ig== + dependencies: + "@typescript-eslint/types" "4.3.0" + "@typescript-eslint/visitor-keys" "4.3.0" + +"@typescript-eslint/types@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf" + integrity sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw== + +"@typescript-eslint/typescript-estree@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.3.0.tgz#0edc1068e6b2e4c7fdc54d61e329fce76241cee8" + integrity sha512-ZAI7xjkl+oFdLV/COEz2tAbQbR3XfgqHEGy0rlUXzfGQic6EBCR4s2+WS3cmTPG69aaZckEucBoTxW9PhzHxxw== + dependencies: + "@typescript-eslint/types" "4.3.0" + "@typescript-eslint/visitor-keys" "4.3.0" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + +"@typescript-eslint/visitor-keys@4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz#0e5ab0a09552903edeae205982e8521e17635ae0" + integrity sha512-xZxkuR7XLM6RhvLkgv9yYlTcBHnTULzfnw4i6+z2TGBLy9yljAypQaZl9c3zFvy7PNI7fYWyvKYtohyF8au3cw== + dependencies: + "@typescript-eslint/types" "4.3.0" + eslint-visitor-keys "^2.0.0" + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-jsx@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" + integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== + +acorn@^7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.0.tgz#e1ad486e6c54501634c6c397c5c121daa383607c" + integrity sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w== + +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.4: + version "6.12.5" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" + integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-includes@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" + integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + is-string "^1.0.5" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" + integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bintrees@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" + integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= + +body-parser@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + 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" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +bunyan@^1.8.14: + version "1.8.14" + resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.14.tgz#3d8c1afea7de158a5238c7cb8a66ab6b38dd45b4" + integrity sha512-LlahJUxXzZLuw/hetUQJmRgZ1LF6+cr5TPpRj6jf327AsiIq2jhYEH4oqUUkVKTor+9w2BT3oxVwhzE5lw9tcg== + optionalDependencies: + dtrace-provider "~0.8" + moment "^2.19.3" + mv "~2" + safe-json-stringify "~1" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cacheable-lookup@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.3.tgz#049fdc59dffdd4fc285e8f4f82936591bd59fec3" + integrity sha512-W+JBqF9SWe18A72XFzN/V/CULFzPm7sBXzzR6ekkE+3tLG72wFZrBiBZhrZuDoYexop4PHJVdFAKb/Nj9+tm9w== + +cacheable-request@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.1.tgz#062031c2856232782ed694a257fa35da93942a58" + integrity sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^4.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^2.0.0" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" + integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +confusing-browser-globals@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.9.tgz#72bc13b483c0276801681871d4898516f8f54fdd" + integrity sha512-KbS1Y0jMtyPgIxjO7ZzMAuUpAKMt1SzCL9fsrKsX6b0zJPTaT0SiSPmewwVZg9UAO83HVIlEhZF84LIjZ0lmAw== + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@2.6.9, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.0.1, debug@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" + integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== + dependencies: + ms "2.1.2" + +decompress-response@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" + integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== + dependencies: + mimic-response "^3.1.0" + +deep-is@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +defer-to-connect@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.0.tgz#83d6b199db041593ac84d781b5222308ccf4c2c1" + integrity sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg== + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dtrace-provider@~0.8: + version "0.8.8" + resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.8.tgz#2996d5490c37e1347be263b423ed7b297fb0d97e" + integrity sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg== + dependencies: + nan "^2.14.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enquirer@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + +error-ex@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: + version "1.17.7" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" + integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-abstract@^1.18.0-next.0: + version "1.18.0-next.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" + integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.2.2" + is-negative-zero "^2.0.0" + is-regex "^1.1.1" + object-inspect "^1.8.0" + object-keys "^1.1.1" + object.assign "^4.1.1" + string.prototype.trimend "^1.0.1" + string.prototype.trimstart "^1.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-config-airbnb-base@^14.2.0: + version "14.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-14.2.0.tgz#fe89c24b3f9dc8008c9c0d0d88c28f95ed65e9c4" + integrity sha512-Snswd5oC6nJaevs3nZoLSTvGJBvzTfnBqOIArkf3cbyTyq9UD79wOk8s+RiL6bhca0p/eRO6veczhf6A/7Jy8Q== + dependencies: + confusing-browser-globals "^1.0.9" + object.assign "^4.1.0" + object.entries "^1.1.2" + +eslint-import-resolver-node@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" + integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== + dependencies: + debug "^2.6.9" + resolve "^1.13.1" + +eslint-module-utils@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" + integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== + dependencies: + debug "^2.6.9" + pkg-dir "^2.0.0" + +eslint-plugin-import@^2.22.1: + version "2.22.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" + integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== + dependencies: + array-includes "^3.1.1" + array.prototype.flat "^1.2.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.4" + eslint-module-utils "^2.6.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.1" + read-pkg-up "^2.0.0" + resolve "^1.17.0" + tsconfig-paths "^3.9.0" + +eslint-scope@^5.0.0, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^2.0.0, eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8" + integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ== + +eslint@^7.10.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.10.0.tgz#494edb3e4750fb791133ca379e786a8f648c72b9" + integrity sha512-BDVffmqWl7JJXqCjAK6lWtcQThZB/aP1HXSH1JKwGwv0LQEdvpR7qzNrUT487RM39B5goWuboFad5ovMBmD8yA== + dependencies: + "@babel/code-frame" "^7.0.0" + "@eslint/eslintrc" "^0.1.3" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^1.3.0" + espree "^7.3.0" + esquery "^1.2.0" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^12.1.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash "^4.17.19" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" + integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.3.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" + integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880" + integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + 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" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.1.1: + version "3.2.4" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.4.tgz#d20aefbf99579383e7f3cc66529158c9b98554d3" + integrity sha512-kr/Oo6PX51265qeuCYsyGypiO5uJFgBS0jksyG7FUeCyQzNwYnzrNIMR1NXfkZXsMYXYLRAHgISHBz8gQcxKHQ== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fastq@^1.6.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.8.0.tgz#550e1f9f59bbc65fe185cb6a9b4d95357107f481" + integrity sha512-SMIZoZdLh/fgofivvIkmknUXyPnvxRE3DhtZ5Me3Mrsk5gyPL42F0xr51TdRXskBxHfMp+07bcYzfsYEsSQA9Q== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + 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" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +glob-parent@^5.0.0, glob-parent@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + +glob@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" + integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "2 || 3" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^12.1.0: + version "12.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" + integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== + dependencies: + type-fest "^0.8.1" + +globby@^11.0.1: + version "11.0.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" + integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + +got@^11.6.2: + version "11.6.2" + resolved "https://registry.yarnpkg.com/got/-/got-11.6.2.tgz#79d7bb8c11df212b97f25565407a1f4ae73210ec" + integrity sha512-/21qgUePCeus29Jk7MEti8cgQUNXFSWfIevNIk4H7u1wmXNDrGPKPY6YsPY+o9CIT/a2DjCjRz0x1nM9FtS2/A== + dependencies: + "@sindresorhus/is" "^3.1.1" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.1" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + +graceful-fs@^4.1.2: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +http-cache-semantics@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.0-beta.5.2" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3" + integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ== + dependencies: + quick-lru "^5.1.1" + resolve-alpn "^1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.4: + version "5.1.8" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" + integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" + integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-callable@^1.1.4, is-callable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" + integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-negative-zero@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" + integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-regex@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" + integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== + dependencies: + has-symbols "^1.0.1" + +is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +isarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +keyv@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.1.tgz#9fe703cb4a94d6d11729d320af033307efd02ee6" + integrity sha512-xz6Jv6oNkbhrFCvCP7HQa8AaII8y8LRpoSm661NOKLr4uHuBwhX4epXrPQgF3+xdJnN4Esm5X0xwY4bOlALOtw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mimic-response@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" + integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== + +"minimatch@2 || 3", minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1, mkdirp@~0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +moment@^2.19.3: + version "2.29.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.0.tgz#fcbef955844d91deb55438613ddcec56e86a3425" + integrity sha512-z6IJ5HXYiuxvFTI6eiQ9dm77uE0gyy1yXNApVHqTcnIKfY9tIwEjlzsZ6u1LQXvVgKeTnv9Xm7NDvJ7lso3MtA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +mv@~2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/mv/-/mv-2.1.1.tgz#ae6ce0d6f6d5e0a4f7d893798d03c1ea9559b6a2" + integrity sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI= + dependencies: + mkdirp "~0.5.1" + ncp "~2.0.0" + rimraf "~2.4.0" + +nan@^2.14.0: + version "2.14.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" + integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +ncp@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3" + integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M= + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-url@^4.1.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== + +object-inspect@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" + integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.0, object.assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" + integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.18.0-next.0" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.entries@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" + integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + has "^1.0.3" + +object.values@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" + integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +p-cancelable@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" + integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= + dependencies: + pify "^2.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +prom-client@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-12.0.0.tgz#9689379b19bd3f6ab88a9866124db9da3d76c6ed" + integrity sha512-JbzzHnw0VDwCvoqf8y1WDtq4wSBAbthMB1pcVI/0lzdqHGJI3KBJDXle70XK+c7Iv93Gihqo0a5LlOn+g8+DrQ== + dependencies: + tdigest "^0.1.1" + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +regexpp@^3.0.0, regexpp@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" + integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== + +resolve-alpn@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.0.0.tgz#745ad60b3d6aff4b4a48e01b8c0bdc70959e0e8c" + integrity sha512-rTuiIEqFmGxne4IovivKSDzld2lWW9QCjqv80SYjPgf+gS35eaCAjaP54CCwGAwBtnCsvNLYtqxe1Nw+i6JEmA== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== + dependencies: + path-parse "^1.0.6" + +responselike@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" + integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== + dependencies: + lowercase-keys "^2.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +rimraf@~2.4.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.4.5.tgz#ee710ce5d93a8fdb856fb5ea8ff0e2d75934b2da" + integrity sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto= + dependencies: + glob "^6.0.1" + +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-json-stringify@~1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz#356e44bc98f1f93ce45df14bcd7c01cda86e0afd" + integrity sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"semver@2 || 3 || 4 || 5": + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^7.2.1, semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + 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" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.6" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.6.tgz#c80757383c28abf7296744998cbc106ae8b854ce" + integrity sha512-+orQK83kyMva3WyPf59k1+Y525csj5JejicWut55zeTWANuN17qSiSLUXWtzHeNWORSvT7GLDJ/E/XiIWoXBTw== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +string-width@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimend@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" + integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trimstart@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" + integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +strip-ansi@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +tdigest@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" + integrity sha1-Ljyyw56kSeVdHmzZEReszKRYgCE= + dependencies: + bintrees "1.0.1" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tsconfig-paths@^3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" + integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + +tslib@^1.8.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== + +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" + integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +uri-js@^4.2.2: + version "4.4.0" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.0.tgz#aa714261de793e8a82347a7bcc9ce74e86f28602" + integrity sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +v8-compile-cache@^2.0.3: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" + integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" From 877545a5d68f8ab89500f18db5342f35c45888d0 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Mon, 5 Oct 2020 16:37:21 -0700 Subject: [PATCH 02/21] added metrics --- index.ts | 154 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 124 insertions(+), 30 deletions(-) diff --git a/index.ts b/index.ts index 4f220b3..a56d64b 100644 --- a/index.ts +++ b/index.ts @@ -10,9 +10,8 @@ const server = express(); const metricsRegistry = new Registry(); const logger = bunyan.createLogger({ name: 'teraslice_exporter' }); -const terasliceQueryDelay = 30000; const metricPrefix = 'teraslice'; -const standardLabelNames = ['ex_id', 'job_id', 'job_name']; +const standardLabelNames = ['ex_id', 'job_id', 'job_name', 'teraslice_cluster_url']; const gaugeWorkersActive = new Gauge({ name: `${metricPrefix}_workers_active`, @@ -28,6 +27,48 @@ const gaugeWorkersAvailable = new Gauge({ registers: [metricsRegistry], }); +const gaugeWorkersJoined = new Gauge({ + name: `${metricPrefix}_workers_joined`, + help: 'Total number of Teraslice workers that have joined the execution controller for this job.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + +const gaugeWorkersReconnected = new Gauge({ + name: `${metricPrefix}_workers_reconnected`, + help: 'Total number of Teraslice workers that have reconnected to the execution controller for this job.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + +const gaugeWorkersDisconnected = new Gauge({ + name: `${metricPrefix}_workers_disconnected`, + help: 'Total number of Teraslice workers that have disconnected from execution controller for this job.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + +const guageTerasliceMasterInfo = new Gauge({ + name: `${metricPrefix}_master_info`, + help: 'Information about the teraslice master node.', + labelNames: ['arch', 'clustering_type', 'name', 'node_version', 'platform', 'teraslice_version'], + registers: [metricsRegistry], +}); + +const guageNumSlicers = new Gauge({ + name: `${metricPrefix}_number_of_slicers`, + help: 'Number of execution controllers running for this execution.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + +const guageControllerQueryDuration = new Gauge({ + name: `${metricPrefix}_controller_query_duration`, + help: 'Total time in ms to query the Teraslice controller endpoint.', + labelNames: ['teraslice_cluster_url'], + registers: [metricsRegistry], +}); + // The following Guages should be Counters by my reconing, but as far as // prom-client is concerned, this usage is fine: // https://github.com/siimon/prom-client/issues/192 @@ -38,6 +79,20 @@ const guageSlicesProcessed = new Gauge({ registers: [metricsRegistry], }); +const guageSlicesFailed = new Gauge({ + name: `${metricPrefix}_slices_failed`, + help: 'Number of slices failed.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + +const guageSlicesQueued = new Gauge({ + name: `${metricPrefix}_slices_queued`, + help: 'Number of slices queued for processing.', + labelNames: standardLabelNames, + registers: [metricsRegistry], +}); + /** * parseController adds the teraslice execution controller metrics to the * metricsRegistry for a single execution. @@ -65,83 +120,123 @@ const guageSlicesProcessed = new Gauge({ * * @param controller */ -function parseController(controller:any) { - // p(controller); - +function parseController(controller:any, url: string) { const standardLabels = { ex_id: controller.ex_id, job_id: controller.job_id, job_name: controller.name, + teraslice_cluster_url: url, }; gaugeWorkersActive.set(standardLabels, controller.workers_active); gaugeWorkersAvailable.set(standardLabels, controller.workers_available); + gaugeWorkersJoined.set(standardLabels, controller.workers_joined); + gaugeWorkersReconnected.set(standardLabels, controller.workers_reconnected); + gaugeWorkersDisconnected.set(standardLabels, controller.workers_disconnected); + guageSlicesProcessed.set(standardLabels, controller.processed); + guageSlicesFailed.set(standardLabels, controller.failed); + guageSlicesQueued.set(standardLabels, controller.queued); + + guageNumSlicers.set(standardLabels, controller.slicers); } async function getTerasliceClusterState(baseUrl: URL) { const url = new URL('/v1/cluster/controllers', baseUrl); let response; try { + logger.debug(`Getting ${url}`); response = await got(url); } catch (error) { - logger.error(error); + logger.error(`Error getting ${url}: ${error}`); } if (response && response.statusCode === 200) { - logger.debug(response.statusCode); - const controllers = JSON.parse(response.body); // eslint-disable-next-line no-restricted-syntax for (const controller of controllers) { - parseController(controller); + parseController(controller, baseUrl.toString()); } } else if (response) { logger.error(`Error getting ${url}: ${response.statusCode}`); } - // p(response.timings) + logger.debug(JSON.stringify(response?.timings)); + guageControllerQueryDuration.set( + { teraslice_cluster_url: baseUrl.toString() }, + response?.timings.phases.total, + ); } async function getTerasliceClusterInfo(baseUrl:URL) { - let response; let info; + let response : { + body: { + arch: string, + // eslint-disable-next-line camelcase + clustering_type: string, + name: string, + // eslint-disable-next-line camelcase + node_version: string, + platform: string, + // eslint-disable-next-line camelcase + teraslice_version: string + }, + statusCode: number + }; + try { response = await got(baseUrl, { responseType: 'json' }); + if (response && response.statusCode === 200 && response.body) { + info = response.body; + guageTerasliceMasterInfo.set(info, 1); + } else if (response) { + logger.error(`Error getting ${baseUrl}: ${response.statusCode}`); + } } catch (error) { - logger.error(error); + logger.error(`Error getting ${baseUrl}: ${error}`); } - - if (response && response.statusCode === 200 && response.body) { - // p(response.body); - info = response.body; - } else if (response) { - logger.error(`Error getting ${baseUrl}: ${response.statusCode}`); - } - return info; } async function updateTerasliceInfo(url:URL) { - const clusterInfo = await getTerasliceClusterInfo(url); - // console.log(clusterInfo); + await getTerasliceClusterInfo(url); await getTerasliceClusterState(url); } +declare let process : { + env: { + DEBUG: string, + PORT: number, + TERASLICE_URL: string + TERASLICE_QUERY_DELAY: number + } +}; + function main() { - const { DEBUG, TERASLICE_URL } = process.env; + let baseUrl: URL; + const metricsEndpoint = '/metrics'; + + if (process.env.TERASLICE_URL) { + baseUrl = new URL(process.env.TERASLICE_URL); + } else { + throw new Error('The TERASLICE_URL environment variable must be a valid URL to the root of your teraslice instance.'); + } + const port = process.env.PORT || 3000; + const terasliceQueryDelay = process.env.TERASLICE_QUERY_DELAY || 30000; // ms - server.get('/metrics', (req, res) => { + server.get(metricsEndpoint, (req, res) => { res.set('Content-Type', metricsRegistry.contentType); res.end(metricsRegistry.metrics()); }); - if (DEBUG) { + server.get('/', (req, res) => { + res.send(`See the '${metricsEndpoint}' endpoint for the teraslice exporter.`); + }); + + if (process.env.DEBUG) { logger.level('debug'); } - logger.debug('DEBUG'); - - const baseUrl = new URL(TERASLICE_URL); logger.info(`Getting intial Teraslice Cluster Information from ${baseUrl}`); updateTerasliceInfo(baseUrl); @@ -151,8 +246,7 @@ function main() { updateTerasliceInfo(baseUrl); }, terasliceQueryDelay); - const port = process.env.PORT || 3000; - logger.info(`Server listening to ${port}, metrics exposed on /metrics endpoint`); + logger.info(`HTTP server listening to ${port}, metrics exposed on ${metricsEndpoint} endpoint`); server.listen(port); } From ee84fa7a091b9b9c365c197031d26f790d26c38c Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Tue, 6 Oct 2020 17:36:36 -0700 Subject: [PATCH 03/21] reworked api queries and added jobs query --- index.ts | 124 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/index.ts b/index.ts index a56d64b..464a377 100644 --- a/index.ts +++ b/index.ts @@ -13,6 +13,27 @@ const logger = bunyan.createLogger({ name: 'teraslice_exporter' }); const metricPrefix = 'teraslice'; const standardLabelNames = ['ex_id', 'job_id', 'job_name', 'teraslice_cluster_url']; +interface TerasliceInfo { + arch: string, + // eslint-disable-next-line camelcase + clustering_type: string, + name: string, + // eslint-disable-next-line camelcase + node_version: string, + platform: string, + // eslint-disable-next-line camelcase + teraslice_version: string +} + +declare let process : { + env: { + DEBUG: string, + PORT: number, + TERASLICE_URL: string + TERASLICE_QUERY_DELAY: number + } +}; + const gaugeWorkersActive = new Gauge({ name: `${metricPrefix}_workers_active`, help: 'Number of Teraslice workers actively processing slices.', @@ -141,77 +162,62 @@ function parseController(controller:any, url: string) { guageNumSlicers.set(standardLabels, controller.slicers); } -async function getTerasliceClusterState(baseUrl: URL) { - const url = new URL('/v1/cluster/controllers', baseUrl); - let response; - try { - logger.debug(`Getting ${url}`); - response = await got(url); - } catch (error) { - logger.error(`Error getting ${url}: ${error}`); - } - - if (response && response.statusCode === 200) { - const controllers = JSON.parse(response.body); - - // eslint-disable-next-line no-restricted-syntax - for (const controller of controllers) { - parseController(controller, baseUrl.toString()); - } - } else if (response) { - logger.error(`Error getting ${url}: ${response.statusCode}`); - } - - logger.debug(JSON.stringify(response?.timings)); - guageControllerQueryDuration.set( - { teraslice_cluster_url: baseUrl.toString() }, - response?.timings.phases.total, - ); -} - -async function getTerasliceClusterInfo(baseUrl:URL) { - let info; +async function getTerasliceApi(baseUrl:URL, path:string) { + const url = new URL(path, baseUrl); + let r: { + data: any, + queryDuration: number + }; let response : { - body: { - arch: string, - // eslint-disable-next-line camelcase - clustering_type: string, - name: string, - // eslint-disable-next-line camelcase - node_version: string, - platform: string, - // eslint-disable-next-line camelcase - teraslice_version: string - }, - statusCode: number + body: any, + statusCode: number, + timings: any }; try { - response = await got(baseUrl, { responseType: 'json' }); + response = await got(url, { responseType: 'json' }); if (response && response.statusCode === 200 && response.body) { - info = response.body; - guageTerasliceMasterInfo.set(info, 1); - } else if (response) { - logger.error(`Error getting ${baseUrl}: ${response.statusCode}`); + r = { + data: response.body, + queryDuration: response.timings.phases.total, + }; + } else { + throw new Error(`Error getting ${url}: ${response.statusCode}`); } } catch (error) { - logger.error(`Error getting ${baseUrl}: ${error}`); + throw new Error(`Error getting ${url}: ${error}`); } + return r; } +// I think I've been doing this sort of thing wrong in the past. +// https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await async function updateTerasliceInfo(url:URL) { - await getTerasliceClusterInfo(url); - await getTerasliceClusterState(url); -} + const querySize = 200; + // eslint-disable-next-line func-names + const run = async function () { + const [info, jobs, controllers] = await Promise.all([ + getTerasliceApi(url, '/'), + getTerasliceApi(url, `/v1/jobs?size=${querySize}`), + getTerasliceApi(url, '/v1/cluster/controllers'), + ]); + // logger.info(JSON.stringify(info)); + guageTerasliceMasterInfo.set(info.data, 1); + logger.info(`Number Jobs: ${jobs.data.length}`); + logger.info(`Number of Controllers: ${controllers.data.length}`); + + // FIXME: I should rethink this warning + // eslint-disable-next-line no-restricted-syntax + for (const controller of controllers.data) { + parseController(controller, url.toString()); + } -declare let process : { - env: { - DEBUG: string, - PORT: number, - TERASLICE_URL: string - TERASLICE_QUERY_DELAY: number - } -}; + guageControllerQueryDuration.set({ teraslice_cluster_url: '/' }, info.queryDuration); + guageControllerQueryDuration.set({ teraslice_cluster_url: `/v1/jobs?size=${querySize}` }, jobs.queryDuration); + guageControllerQueryDuration.set({ teraslice_cluster_url: '/v1/cluster/controllers' }, controllers.queryDuration); + }; + run().catch((err) => { logger.error('Error updating Teraslice Info', err); }); +} function main() { let baseUrl: URL; From 8d61507cf0081dc8c00c4cd2b7bd3ed3697a8e69 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 8 Oct 2020 18:01:33 -0700 Subject: [PATCH 04/21] factored out TerasliceStats class --- index.ts => src/index.ts | 112 ++++++++++++------------------------- src/teraslice-stats.ts | 118 +++++++++++++++++++++++++++++++++++++++ tsconfig.json | 6 +- 3 files changed, 158 insertions(+), 78 deletions(-) rename index.ts => src/index.ts (68%) create mode 100644 src/teraslice-stats.ts diff --git a/index.ts b/src/index.ts similarity index 68% rename from index.ts rename to src/index.ts index 464a377..2f44e6a 100644 --- a/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ import { URL } from 'url'; import { Gauge, Registry } from 'prom-client'; -import got from 'got'; import express from 'express'; import bunyan from 'bunyan'; +import TerasliceStats from './teraslice-stats'; + const server = express(); const metricsRegistry = new Registry(); @@ -13,18 +14,6 @@ const logger = bunyan.createLogger({ name: 'teraslice_exporter' }); const metricPrefix = 'teraslice'; const standardLabelNames = ['ex_id', 'job_id', 'job_name', 'teraslice_cluster_url']; -interface TerasliceInfo { - arch: string, - // eslint-disable-next-line camelcase - clustering_type: string, - name: string, - // eslint-disable-next-line camelcase - node_version: string, - platform: string, - // eslint-disable-next-line camelcase - teraslice_version: string -} - declare let process : { env: { DEBUG: string, @@ -83,6 +72,12 @@ const guageNumSlicers = new Gauge({ registers: [metricsRegistry], }); +// FIXME: Missing labels on this one: +// # HELP teraslice_controller_query_duration Total time in ms to query the Teraslice controller endpoint. +// # TYPE teraslice_controller_query_duration gauge +// teraslice_controller_query_duration{teraslice_cluster_url="/"} 293 +// teraslice_controller_query_duration{teraslice_cluster_url="/v1/jobs"} 674 +// teraslice_controller_query_duration{teraslice_cluster_url="/v1/cluster/controllers"} 774 const guageControllerQueryDuration = new Gauge({ name: `${metricPrefix}_controller_query_duration`, help: 'Total time in ms to query the Teraslice controller endpoint.', @@ -162,69 +157,36 @@ function parseController(controller:any, url: string) { guageNumSlicers.set(standardLabels, controller.slicers); } -async function getTerasliceApi(baseUrl:URL, path:string) { - const url = new URL(path, baseUrl); - let r: { - data: any, - queryDuration: number - }; - let response : { - body: any, - statusCode: number, - timings: any - }; +function updateTerasliceStats(terasliceStats: TerasliceStats) { + guageTerasliceMasterInfo.set(terasliceStats.info, 1); + logger.debug(`Number Jobs: ${terasliceStats.jobs.length}`); + logger.debug(`Number of Controllers: ${terasliceStats.controllers.length}`); - try { - response = await got(url, { responseType: 'json' }); - if (response && response.statusCode === 200 && response.body) { - r = { - data: response.body, - queryDuration: response.timings.phases.total, - }; - } else { - throw new Error(`Error getting ${url}: ${response.statusCode}`); - } - } catch (error) { - throw new Error(`Error getting ${url}: ${error}`); + // FIXME: I should rethink this warning + // eslint-disable-next-line no-restricted-syntax + for (const controller of terasliceStats.controllers) { + parseController(controller, terasliceStats.baseUrl.toString()); } - return r; -} -// I think I've been doing this sort of thing wrong in the past. -// https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await -async function updateTerasliceInfo(url:URL) { - const querySize = 200; - // eslint-disable-next-line func-names - const run = async function () { - const [info, jobs, controllers] = await Promise.all([ - getTerasliceApi(url, '/'), - getTerasliceApi(url, `/v1/jobs?size=${querySize}`), - getTerasliceApi(url, '/v1/cluster/controllers'), - ]); - // logger.info(JSON.stringify(info)); - guageTerasliceMasterInfo.set(info.data, 1); - logger.info(`Number Jobs: ${jobs.data.length}`); - logger.info(`Number of Controllers: ${controllers.data.length}`); - - // FIXME: I should rethink this warning - // eslint-disable-next-line no-restricted-syntax - for (const controller of controllers.data) { - parseController(controller, url.toString()); - } - - guageControllerQueryDuration.set({ teraslice_cluster_url: '/' }, info.queryDuration); - guageControllerQueryDuration.set({ teraslice_cluster_url: `/v1/jobs?size=${querySize}` }, jobs.queryDuration); - guageControllerQueryDuration.set({ teraslice_cluster_url: '/v1/cluster/controllers' }, controllers.queryDuration); - }; - run().catch((err) => { logger.error('Error updating Teraslice Info', err); }); + guageControllerQueryDuration.set( + { teraslice_cluster_url: '/' }, terasliceStats.queryDuration.info, + ); + guageControllerQueryDuration.set( + { teraslice_cluster_url: '/v1/jobs' }, terasliceStats.queryDuration.jobs, + ); + guageControllerQueryDuration.set( + { teraslice_cluster_url: '/v1/cluster/controllers' }, terasliceStats.queryDuration.controllers, + ); } -function main() { - let baseUrl: URL; +async function main() { + let baseUrl: string; const metricsEndpoint = '/metrics'; if (process.env.TERASLICE_URL) { - baseUrl = new URL(process.env.TERASLICE_URL); + // I instantiate a URL, then immediately call toString() just to get the + // URL validation but keep a string type + baseUrl = new URL(process.env.TERASLICE_URL).toString(); } else { throw new Error('The TERASLICE_URL environment variable must be a valid URL to the root of your teraslice instance.'); } @@ -240,16 +202,16 @@ function main() { res.send(`See the '${metricsEndpoint}' endpoint for the teraslice exporter.`); }); - if (process.env.DEBUG) { - logger.level('debug'); - } + if (process.env.DEBUG) logger.level('debug'); - logger.info(`Getting intial Teraslice Cluster Information from ${baseUrl}`); - updateTerasliceInfo(baseUrl); + const terasliceStats = new TerasliceStats(baseUrl); + await terasliceStats.update(); + updateTerasliceStats(terasliceStats); - setInterval(() => { + setInterval(async () => { logger.debug(`Updating Teraslice Cluster Information from ${baseUrl}`); - updateTerasliceInfo(baseUrl); + await terasliceStats.update(); + updateTerasliceStats(terasliceStats); }, terasliceQueryDelay); logger.info(`HTTP server listening to ${port}, metrics exposed on ${metricsEndpoint} endpoint`); diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts new file mode 100644 index 0000000..9223aab --- /dev/null +++ b/src/teraslice-stats.ts @@ -0,0 +1,118 @@ +import got from 'got'; + +// TODO: Get logging in here somehow +// TODO: move these interfaces out into their own file + +interface TerasliceInfo { + arch: string, + // eslint-disable-next-line camelcase + clustering_type: string, + name: string, + // eslint-disable-next-line camelcase + node_version: string, + platform: string, + // eslint-disable-next-line camelcase + teraslice_version: string +} + +interface TerasliceQueryDuration { + info: number, + jobs: number, + controllers: number +} + +interface TerasliceStatsInterface { + baseUrl: URL, + info: TerasliceInfo, + jobs: any[], + controllers: any[] + queryDuration: TerasliceQueryDuration +} + +export default class TerasliceStats implements TerasliceStatsInterface { + baseUrl: URL; + + info: any; + + jobs: any[]; + + controllers: any[]; + + queryDuration: TerasliceQueryDuration; + + constructor(baseUrl:string) { + this.baseUrl = new URL(baseUrl); + this.jobs = []; + this.controllers = []; + // FIXME: should I use something other than 0 here? + this.queryDuration = { info: 0, jobs: 0, controllers: 0 }; + } + + async getTerasliceApi(path:string) { + const url = new URL(path, this.baseUrl); + let r: { + data: any, + queryDuration: number + }; + let response : { + body: any, + statusCode: number, + timings: any + }; + + try { + // FIXME: the .toString is to eliminate a 'No overload matches this call' + response = await got(url.toString(), { responseType: 'json' }); + if (response && response.statusCode === 200 && response.body) { + r = { + data: response.body, + queryDuration: response.timings.phases.total, + }; + } else { + throw new Error(`Error getting ${url}: ${response.statusCode}`); + } + } catch (error) { + throw new Error(`Error getting ${url}: ${error}`); + } + return r; + } + + // I think I've been doing this sort of thing wrong in the past. + // https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await + async update() { + // eslint-disable-next-line no-console + const querySize = 200; + // eslint-disable-next-line func-names + const run = async () => { + const [info, jobs, controllers] = await Promise.all([ + this.getTerasliceApi('/'), + this.getTerasliceApi(`/v1/jobs?size=${querySize}`), + this.getTerasliceApi('/v1/cluster/controllers'), + ]); + this.info = info.data; + this.jobs = jobs.data; + this.controllers = controllers.data; + this.queryDuration = { + info: info.queryDuration, + jobs: jobs.queryDuration, + controllers: controllers.queryDuration, + }; + }; + await run().catch((err) => { + // eslint-disable-next-line no-console + console.log(err); + // logger.error('Error updating Teraslice Info', err); + }); + } +} + +// async function main() { +// const terasliceStats = new TerasliceStats('http://ts-prod3.tera1.lan'); +// await terasliceStats.update(); + +// console.log(`info: ${JSON.stringify(terasliceStats.info, null, 2)}`); +// console.log(`jobs: ${JSON.stringify(terasliceStats.jobs[0], null, 2)}`); +// console.log(`controllers: ${JSON.stringify(terasliceStats.controllers[0], null, 2)}`); +// } + +// main(); diff --git a/tsconfig.json b/tsconfig.json index 19e0a49..3ef48d0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,10 +12,10 @@ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ // "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - "sourceMap": true, /* Generates corresponding '.map' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ // "outFile": "./", /* Concatenate and emit output to single file. */ - "outDir": "dist", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "outDir": "dist", /* Redirect output structure to the directory. */ + "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ // "composite": true, /* Enable project compilation */ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ // "removeComments": true, /* Do not emit comments to output. */ From 593429d2fce772ba6e1f5b451a222b84cb3aba4f Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Tue, 13 Oct 2020 17:48:20 -0700 Subject: [PATCH 05/21] added updateExecutions --- package.json | 2 +- src/teraslice-stats.ts | 87 ++++++++++++++++++++++++++++-------------- 2 files changed, 60 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 9216707..d46f1ca 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "dependencies": { - "@types/bunyan": "^1.8.6", "bunyan": "^1.8.14", "express": "^4.17.1", "got": "^11.6.2", "prom-client": "^12.0.0" }, "devDependencies": { + "@types/bunyan": "^1.8.6", "@types/express": "^4.17.8", "@types/node": "^14.10.1", "@typescript-eslint/eslint-plugin": "^4.3.0", diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index 9223aab..f587588 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -1,6 +1,5 @@ import got from 'got'; -// TODO: Get logging in here somehow // TODO: move these interfaces out into their own file interface TerasliceInfo { @@ -15,37 +14,57 @@ interface TerasliceInfo { teraslice_version: string } +/** + * These are all in ms + */ interface TerasliceQueryDuration { + controllers: number, + executions: number, info: number, jobs: number, - controllers: number } interface TerasliceStatsInterface { baseUrl: URL, + controllers: any[], + executions: any[], info: TerasliceInfo, jobs: any[], - controllers: any[] queryDuration: TerasliceQueryDuration } +/** promisified setTimeout */ +export function pDelay(delay = 1, arg?: T): Promise { + return new Promise((resolve) => { + setTimeout(resolve, delay, arg); + }); +} + export default class TerasliceStats implements TerasliceStatsInterface { baseUrl: URL; + controllers: any[]; + + executions: any[]; + info: any; jobs: any[]; - controllers: any[]; - queryDuration: TerasliceQueryDuration; constructor(baseUrl:string) { this.baseUrl = new URL(baseUrl); - this.jobs = []; this.controllers = []; + this.executions = []; + this.jobs = []; // FIXME: should I use something other than 0 here? - this.queryDuration = { info: 0, jobs: 0, controllers: 0 }; + this.queryDuration = { + controllers: 0, + executions: 0, + info: 0, + jobs: 0, + }; } async getTerasliceApi(path:string) { @@ -77,12 +96,37 @@ export default class TerasliceStats implements TerasliceStatsInterface { return r; } + async updateExecutions() { + const time = process.hrtime(); + this.executions = []; + const maxConcurrency = 10; + + for (let i = 0; i < this.controllers.length; i += maxConcurrency) { + const controllersSlice = this.controllers.slice(i, i + maxConcurrency); + + // eslint-disable-next-line no-await-in-loop + const r = await Promise.all( + controllersSlice.map((x) => this.getTerasliceApi(`/v1/ex/${x.ex_id}`)), + ); + this.executions = this.executions.concat(r.map((x) => x.data)); + // eslint-disable-next-line no-await-in-loop + await pDelay(25); + } + + const NS_PER_SEC = 1e9; + const diff = process.hrtime(time); + this.queryDuration.executions = (diff[0] * NS_PER_SEC + diff[1]) / 1e6; + } + + // FIXME: I don't think /jobs is even relevant here ... what I should really + // do is get the ex_ids from the controllers, and then go get the ex for each + // one of those. Jobs are mostly irrelevant in this context. + // I think I've been doing this sort of thing wrong in the past. // https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await async update() { - // eslint-disable-next-line no-console + // TODO: hard coding querySize is dumb const querySize = 200; - // eslint-disable-next-line func-names const run = async () => { const [info, jobs, controllers] = await Promise.all([ this.getTerasliceApi('/'), @@ -92,27 +136,14 @@ export default class TerasliceStats implements TerasliceStatsInterface { this.info = info.data; this.jobs = jobs.data; this.controllers = controllers.data; - this.queryDuration = { - info: info.queryDuration, - jobs: jobs.queryDuration, - controllers: controllers.queryDuration, - }; + await this.updateExecutions(); + + this.queryDuration.info = info.queryDuration; + this.queryDuration.jobs = jobs.queryDuration; + this.queryDuration.controllers = controllers.queryDuration; }; await run().catch((err) => { - // eslint-disable-next-line no-console - console.log(err); - // logger.error('Error updating Teraslice Info', err); + throw new Error(`Error caught on run() ${this.baseUrl}: ${err}`); }); } } - -// async function main() { -// const terasliceStats = new TerasliceStats('http://ts-prod3.tera1.lan'); -// await terasliceStats.update(); - -// console.log(`info: ${JSON.stringify(terasliceStats.info, null, 2)}`); -// console.log(`jobs: ${JSON.stringify(terasliceStats.jobs[0], null, 2)}`); -// console.log(`controllers: ${JSON.stringify(terasliceStats.controllers[0], null, 2)}`); -// } - -// main(); From c878a8ced8ad942624f871c60ed8cebd2bede34e Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Wed, 14 Oct 2020 11:21:13 -0700 Subject: [PATCH 06/21] append to this.executions properly --- src/teraslice-stats.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index f587588..47a0515 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -108,14 +108,17 @@ export default class TerasliceStats implements TerasliceStatsInterface { const r = await Promise.all( controllersSlice.map((x) => this.getTerasliceApi(`/v1/ex/${x.ex_id}`)), ); - this.executions = this.executions.concat(r.map((x) => x.data)); + + // eslint-disable-next-line prefer-spread + this.executions.push.apply(this.executions, r.map((x) => x.data)); + // eslint-disable-next-line no-await-in-loop await pDelay(25); } const NS_PER_SEC = 1e9; const diff = process.hrtime(time); - this.queryDuration.executions = (diff[0] * NS_PER_SEC + diff[1]) / 1e6; + this.queryDuration.executions = Math.round((diff[0] * NS_PER_SEC + diff[1]) / 1e6); } // FIXME: I don't think /jobs is even relevant here ... what I should really From cc77a1a95de8665ee66e16e6275b57a7b8cd78cb Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Wed, 14 Oct 2020 16:21:15 -0700 Subject: [PATCH 07/21] add queryDelay var --- src/teraslice-stats.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index 47a0515..5d75b01 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -58,7 +58,7 @@ export default class TerasliceStats implements TerasliceStatsInterface { this.controllers = []; this.executions = []; this.jobs = []; - // FIXME: should I use something other than 0 here? + this.queryDuration = { controllers: 0, executions: 0, @@ -100,6 +100,7 @@ export default class TerasliceStats implements TerasliceStatsInterface { const time = process.hrtime(); this.executions = []; const maxConcurrency = 10; + const queryDelay = 25; for (let i = 0; i < this.controllers.length; i += maxConcurrency) { const controllersSlice = this.controllers.slice(i, i + maxConcurrency); @@ -113,7 +114,7 @@ export default class TerasliceStats implements TerasliceStatsInterface { this.executions.push.apply(this.executions, r.map((x) => x.data)); // eslint-disable-next-line no-await-in-loop - await pDelay(25); + await pDelay(queryDelay); } const NS_PER_SEC = 1e9; @@ -121,10 +122,6 @@ export default class TerasliceStats implements TerasliceStatsInterface { this.queryDuration.executions = Math.round((diff[0] * NS_PER_SEC + diff[1]) / 1e6); } - // FIXME: I don't think /jobs is even relevant here ... what I should really - // do is get the ex_ids from the controllers, and then go get the ex for each - // one of those. Jobs are mostly irrelevant in this context. - // I think I've been doing this sort of thing wrong in the past. // https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await async update() { From e4cd17d66ba65fc07fa2c143dfc6cbe4ca71de21 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Wed, 14 Oct 2020 16:49:32 -0700 Subject: [PATCH 08/21] restructure and rename a bunch of things --- src/index.ts | 134 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 87 insertions(+), 47 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2f44e6a..72bbdb7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,16 @@ const metricsRegistry = new Registry(); const logger = bunyan.createLogger({ name: 'teraslice_exporter' }); const metricPrefix = 'teraslice'; -const standardLabelNames = ['ex_id', 'job_id', 'job_name', 'teraslice_cluster_url']; +const globalLabelNames = [ + 'url', + 'name', +]; +const exLabelNames = [ + 'ex_id', + 'job_id', + 'job_name', + ...globalLabelNames, +]; declare let process : { env: { @@ -26,62 +35,64 @@ declare let process : { const gaugeWorkersActive = new Gauge({ name: `${metricPrefix}_workers_active`, help: 'Number of Teraslice workers actively processing slices.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeWorkersAvailable = new Gauge({ name: `${metricPrefix}_workers_available`, help: 'Number of Teraslice workers running and waiting for work.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeWorkersJoined = new Gauge({ name: `${metricPrefix}_workers_joined`, help: 'Total number of Teraslice workers that have joined the execution controller for this job.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeWorkersReconnected = new Gauge({ name: `${metricPrefix}_workers_reconnected`, help: 'Total number of Teraslice workers that have reconnected to the execution controller for this job.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeWorkersDisconnected = new Gauge({ name: `${metricPrefix}_workers_disconnected`, help: 'Total number of Teraslice workers that have disconnected from execution controller for this job.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); const guageTerasliceMasterInfo = new Gauge({ name: `${metricPrefix}_master_info`, help: 'Information about the teraslice master node.', - labelNames: ['arch', 'clustering_type', 'name', 'node_version', 'platform', 'teraslice_version'], + labelNames: [ + 'arch', + 'clustering_type', + 'name', + 'node_version', + 'platform', + 'teraslice_version', + ...globalLabelNames, + ], registers: [metricsRegistry], }); const guageNumSlicers = new Gauge({ name: `${metricPrefix}_number_of_slicers`, help: 'Number of execution controllers running for this execution.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); -// FIXME: Missing labels on this one: -// # HELP teraslice_controller_query_duration Total time in ms to query the Teraslice controller endpoint. -// # TYPE teraslice_controller_query_duration gauge -// teraslice_controller_query_duration{teraslice_cluster_url="/"} 293 -// teraslice_controller_query_duration{teraslice_cluster_url="/v1/jobs"} 674 -// teraslice_controller_query_duration{teraslice_cluster_url="/v1/cluster/controllers"} 774 -const guageControllerQueryDuration = new Gauge({ - name: `${metricPrefix}_controller_query_duration`, - help: 'Total time in ms to query the Teraslice controller endpoint.', - labelNames: ['teraslice_cluster_url'], +const guageQueryDuration = new Gauge({ + name: `${metricPrefix}_query_duration`, + help: 'Total time to complete the named query, in ms.', + labelNames: ['query_name', ...globalLabelNames], registers: [metricsRegistry], }); @@ -91,21 +102,21 @@ const guageControllerQueryDuration = new Gauge({ const guageSlicesProcessed = new Gauge({ name: `${metricPrefix}_slices_processed`, help: 'Number of slices processed.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); const guageSlicesFailed = new Gauge({ name: `${metricPrefix}_slices_failed`, help: 'Number of slices failed.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); const guageSlicesQueued = new Gauge({ name: `${metricPrefix}_slices_queued`, help: 'Number of slices queued for processing.', - labelNames: standardLabelNames, + labelNames: exLabelNames, registers: [metricsRegistry], }); @@ -136,46 +147,64 @@ const guageSlicesQueued = new Gauge({ * * @param controller */ -function parseController(controller:any, url: string) { - const standardLabels = { +function parseController(controller:any, labels:any) { + const controllerLabels = { ex_id: controller.ex_id, job_id: controller.job_id, job_name: controller.name, - teraslice_cluster_url: url, + ...labels, }; - gaugeWorkersActive.set(standardLabels, controller.workers_active); - gaugeWorkersAvailable.set(standardLabels, controller.workers_available); - gaugeWorkersJoined.set(standardLabels, controller.workers_joined); - gaugeWorkersReconnected.set(standardLabels, controller.workers_reconnected); - gaugeWorkersDisconnected.set(standardLabels, controller.workers_disconnected); + gaugeWorkersActive.set(controllerLabels, controller.workers_active); + gaugeWorkersAvailable.set(controllerLabels, controller.workers_available); + gaugeWorkersJoined.set(controllerLabels, controller.workers_joined); + gaugeWorkersReconnected.set(controllerLabels, controller.workers_reconnected); + gaugeWorkersDisconnected.set(controllerLabels, controller.workers_disconnected); - guageSlicesProcessed.set(standardLabels, controller.processed); - guageSlicesFailed.set(standardLabels, controller.failed); - guageSlicesQueued.set(standardLabels, controller.queued); + guageSlicesProcessed.set(controllerLabels, controller.processed); + guageSlicesFailed.set(controllerLabels, controller.failed); + guageSlicesQueued.set(controllerLabels, controller.queued); - guageNumSlicers.set(standardLabels, controller.slicers); + guageNumSlicers.set(controllerLabels, controller.slicers); } -function updateTerasliceStats(terasliceStats: TerasliceStats) { - guageTerasliceMasterInfo.set(terasliceStats.info, 1); - logger.debug(`Number Jobs: ${terasliceStats.jobs.length}`); - logger.debug(`Number of Controllers: ${terasliceStats.controllers.length}`); - +function generateControllerStats(terasliceStats:TerasliceStats, labels:any) { // FIXME: I should rethink this warning // eslint-disable-next-line no-restricted-syntax for (const controller of terasliceStats.controllers) { - parseController(controller, terasliceStats.baseUrl.toString()); + parseController(controller, labels); } +} + +function updateTerasliceMetrics(terasliceStats: TerasliceStats) { + const globalLabels = { + url: terasliceStats.baseUrl.toString(), + name: terasliceStats.info.name, + }; + // NOTE: This set of labels expands out to including 'name' twice, right now + // they reduce to a single 'name' label ... I could end up regretting this. + guageTerasliceMasterInfo.set( + { ...terasliceStats.info, ...globalLabels }, + 1, + ); + + generateControllerStats(terasliceStats, globalLabels); - guageControllerQueryDuration.set( - { teraslice_cluster_url: '/' }, terasliceStats.queryDuration.info, + guageQueryDuration.set( + { query_name: 'info', ...globalLabels }, + terasliceStats.queryDuration.info, ); - guageControllerQueryDuration.set( - { teraslice_cluster_url: '/v1/jobs' }, terasliceStats.queryDuration.jobs, + guageQueryDuration.set( + { query_name: 'jobs', ...globalLabels }, + terasliceStats.queryDuration.jobs, ); - guageControllerQueryDuration.set( - { teraslice_cluster_url: '/v1/cluster/controllers' }, terasliceStats.queryDuration.controllers, + guageQueryDuration.set( + { query_name: 'controllers', ...globalLabels }, + terasliceStats.queryDuration.controllers, + ); + guageQueryDuration.set( + { query_name: 'executions', ...globalLabels }, + terasliceStats.queryDuration.executions, ); } @@ -206,12 +235,23 @@ async function main() { const terasliceStats = new TerasliceStats(baseUrl); await terasliceStats.update(); - updateTerasliceStats(terasliceStats); + updateTerasliceMetrics(terasliceStats); + + logger.debug(`executions: ${JSON.stringify(terasliceStats.executions.slice(0, 2), null, 2)}`); + logger.debug(`controllers: ${JSON.stringify(terasliceStats.controllers.slice(0, 2), null, 2)}`); setInterval(async () => { logger.debug(`Updating Teraslice Cluster Information from ${baseUrl}`); await terasliceStats.update(); - updateTerasliceStats(terasliceStats); + updateTerasliceMetrics(terasliceStats); + + logger.debug(`queryDurations: ${JSON.stringify(terasliceStats.queryDuration)}`); + logger.debug(`datasetSizes: ${JSON.stringify({ + info: terasliceStats.info.length, + controllers: terasliceStats.controllers.length, + executions: terasliceStats.executions.length, + jobs: terasliceStats.jobs.length, + })}`); }, terasliceQueryDelay); logger.info(`HTTP server listening to ${port}, metrics exposed on ${metricsEndpoint} endpoint`); From ddfcefd82a5be56849d3a75ed029b50347ee4691 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Wed, 14 Oct 2020 18:11:06 -0700 Subject: [PATCH 09/21] fix spelling of gauge, start adding ex metrics --- src/index.ts | 147 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 128 insertions(+), 19 deletions(-) diff --git a/src/index.ts b/src/index.ts index 72bbdb7..db34bf6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,7 +67,7 @@ const gaugeWorkersDisconnected = new Gauge({ registers: [metricsRegistry], }); -const guageTerasliceMasterInfo = new Gauge({ +const gaugeTerasliceMasterInfo = new Gauge({ name: `${metricPrefix}_master_info`, help: 'Information about the teraslice master node.', labelNames: [ @@ -82,38 +82,68 @@ const guageTerasliceMasterInfo = new Gauge({ registers: [metricsRegistry], }); -const guageNumSlicers = new Gauge({ +const gaugeNumSlicers = new Gauge({ name: `${metricPrefix}_number_of_slicers`, help: 'Number of execution controllers running for this execution.', labelNames: exLabelNames, registers: [metricsRegistry], }); -const guageQueryDuration = new Gauge({ +const gaugeQueryDuration = new Gauge({ name: `${metricPrefix}_query_duration`, help: 'Total time to complete the named query, in ms.', labelNames: ['query_name', ...globalLabelNames], registers: [metricsRegistry], }); -// The following Guages should be Counters by my reconing, but as far as +// Execution Related Metrics + +const gaugeCpuLimit = new Gauge({ + name: `${metricPrefix}_ex_cpu_limit`, + help: 'CPU core limit for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeCpuRequest = new Gauge({ + name: `${metricPrefix}_ex_cpu_request`, + help: 'Requested number of CPU cores for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeMemoryLimit = new Gauge({ + name: `${metricPrefix}_ex_memory_limit`, + help: 'Memory limit for Teraslice a worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeMemoryRequest = new Gauge({ + name: `${metricPrefix}_ex_memory_request`, + help: 'Requested amount of memory for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +// The following gauges should be Counters by my reconing, but as far as // prom-client is concerned, this usage is fine: // https://github.com/siimon/prom-client/issues/192 -const guageSlicesProcessed = new Gauge({ +const gaugeSlicesProcessed = new Gauge({ name: `${metricPrefix}_slices_processed`, help: 'Number of slices processed.', labelNames: exLabelNames, registers: [metricsRegistry], }); -const guageSlicesFailed = new Gauge({ +const gaugeSlicesFailed = new Gauge({ name: `${metricPrefix}_slices_failed`, help: 'Number of slices failed.', labelNames: exLabelNames, registers: [metricsRegistry], }); -const guageSlicesQueued = new Gauge({ +const gaugeSlicesQueued = new Gauge({ name: `${metricPrefix}_slices_queued`, help: 'Number of slices queued for processing.', labelNames: exLabelNames, @@ -125,9 +155,9 @@ const guageSlicesQueued = new Gauge({ * metricsRegistry for a single execution. * * { - * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510e59", - * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad18a", - * "name": "my_job_name", + * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", + * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", + * "name": "my-job-name", * "workers_available": 0, * "workers_active": 6, * "workers_joined": 6, @@ -161,11 +191,81 @@ function parseController(controller:any, labels:any) { gaugeWorkersReconnected.set(controllerLabels, controller.workers_reconnected); gaugeWorkersDisconnected.set(controllerLabels, controller.workers_disconnected); - guageSlicesProcessed.set(controllerLabels, controller.processed); - guageSlicesFailed.set(controllerLabels, controller.failed); - guageSlicesQueued.set(controllerLabels, controller.queued); + gaugeSlicesProcessed.set(controllerLabels, controller.processed); + gaugeSlicesFailed.set(controllerLabels, controller.failed); + gaugeSlicesQueued.set(controllerLabels, controller.queued); - guageNumSlicers.set(controllerLabels, controller.slicers); + gaugeNumSlicers.set(controllerLabels, controller.slicers); +} + +/** + * { + "analytics": true, + "performance_metrics": false, + "assets": [ + "19b4f13148f64bc5a3fcfc53f96a5d646141a111", + "b652a2d09f71e68dd0ca15f6b5a14136b181e111" + ], + "autorecover": false, + "lifecycle": "persistent", + "max_retries": 3, + "name": "my-job-name", + "operations": [ + { + ... + }, + ... + ], + "apis": [], + "probation_window": 300000, + "slicers": 1, + "workers": 6, + "labels": null, + "env_vars": {}, + "targets": [ + { + "key": "failure-domain.beta.kubernetes.io/zone", + "value": "west" + } + ], + "cpu": 1.5, + "memory": 3221225472, + "volumes": [], + "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", + "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", + "metadata": {}, + "slicer_port": 45680, + "slicer_hostname": "10.32.97.23", + "_context": "ex", + "_created": "2020-07-09T21:30:34.537Z", + "_updated": "2020-07-09T21:30:42.745Z", + "_status": "running", + "_has_errors": false, + "_slicer_stats": {}, + "_failureReason": "" + } + * @param execution + * @param labels + */ +function parseExecution(execution:any, labels:any) { + const executionLabels = { + ex_id: execution.ex_id, + job_id: execution.job_id, + job_name: execution.name, + ...labels, + }; + + // NOTE: Optional settings that are undefined are just excluded with a + // conditional below. + + // TODO: At some point workers will have different CPU Limits and Requests, + // https://github.com/terascope/teraslice/issues/2202 + // for now, these are set to the same thing, but I split them since I know a + // change is coming. + if (execution.cpu) gaugeCpuRequest.set(executionLabels, execution.cpu); + if (execution.cpu) gaugeCpuLimit.set(executionLabels, execution.cpu); + if (execution.memory) gaugeMemoryRequest.set(executionLabels, execution.memory); + if (execution.memory) gaugeMemoryLimit.set(executionLabels, execution.memory); } function generateControllerStats(terasliceStats:TerasliceStats, labels:any) { @@ -176,6 +276,14 @@ function generateControllerStats(terasliceStats:TerasliceStats, labels:any) { } } +function generateExecutionStats(terasliceStats:TerasliceStats, labels:any) { + // FIXME: I should rethink this warning + // eslint-disable-next-line no-restricted-syntax + for (const execution of terasliceStats.executions) { + parseExecution(execution, labels); + } +} + function updateTerasliceMetrics(terasliceStats: TerasliceStats) { const globalLabels = { url: terasliceStats.baseUrl.toString(), @@ -183,26 +291,27 @@ function updateTerasliceMetrics(terasliceStats: TerasliceStats) { }; // NOTE: This set of labels expands out to including 'name' twice, right now // they reduce to a single 'name' label ... I could end up regretting this. - guageTerasliceMasterInfo.set( + gaugeTerasliceMasterInfo.set( { ...terasliceStats.info, ...globalLabels }, 1, ); generateControllerStats(terasliceStats, globalLabels); + generateExecutionStats(terasliceStats, globalLabels); - guageQueryDuration.set( + gaugeQueryDuration.set( { query_name: 'info', ...globalLabels }, terasliceStats.queryDuration.info, ); - guageQueryDuration.set( + gaugeQueryDuration.set( { query_name: 'jobs', ...globalLabels }, terasliceStats.queryDuration.jobs, ); - guageQueryDuration.set( + gaugeQueryDuration.set( { query_name: 'controllers', ...globalLabels }, terasliceStats.queryDuration.controllers, ); - guageQueryDuration.set( + gaugeQueryDuration.set( { query_name: 'executions', ...globalLabels }, terasliceStats.queryDuration.executions, ); From 9119f7813899c583983fe76f9b6f76e876e6606d Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 15 Oct 2020 18:24:27 -0700 Subject: [PATCH 10/21] added more ex metrics, renamed some metrics --- src/index.ts | 109 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 14 deletions(-) diff --git a/src/index.ts b/src/index.ts index db34bf6..3ed7f86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,35 +33,35 @@ declare let process : { }; const gaugeWorkersActive = new Gauge({ - name: `${metricPrefix}_workers_active`, + name: `${metricPrefix}_controller_workers_active`, help: 'Number of Teraslice workers actively processing slices.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeWorkersAvailable = new Gauge({ - name: `${metricPrefix}_workers_available`, + name: `${metricPrefix}_controller_workers_available`, help: 'Number of Teraslice workers running and waiting for work.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeWorkersJoined = new Gauge({ - name: `${metricPrefix}_workers_joined`, + name: `${metricPrefix}_controller_workers_joined`, help: 'Total number of Teraslice workers that have joined the execution controller for this job.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeWorkersReconnected = new Gauge({ - name: `${metricPrefix}_workers_reconnected`, + name: `${metricPrefix}_controller_workers_reconnected`, help: 'Total number of Teraslice workers that have reconnected to the execution controller for this job.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeWorkersDisconnected = new Gauge({ - name: `${metricPrefix}_workers_disconnected`, + name: `${metricPrefix}_controller_workers_disconnected`, help: 'Total number of Teraslice workers that have disconnected from execution controller for this job.', labelNames: exLabelNames, registers: [metricsRegistry], @@ -83,8 +83,8 @@ const gaugeTerasliceMasterInfo = new Gauge({ }); const gaugeNumSlicers = new Gauge({ - name: `${metricPrefix}_number_of_slicers`, - help: 'Number of execution controllers running for this execution.', + name: `${metricPrefix}_controller_slicers_count`, + help: 'Number of execution controllers (slicers) running for this execution.', labelNames: exLabelNames, registers: [metricsRegistry], }); @@ -99,57 +99,94 @@ const gaugeQueryDuration = new Gauge({ // Execution Related Metrics const gaugeCpuLimit = new Gauge({ - name: `${metricPrefix}_ex_cpu_limit`, + name: `${metricPrefix}_execution_cpu_limit`, help: 'CPU core limit for a Teraslice worker container.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeCpuRequest = new Gauge({ - name: `${metricPrefix}_ex_cpu_request`, + name: `${metricPrefix}_execution_cpu_request`, help: 'Requested number of CPU cores for a Teraslice worker container.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeMemoryLimit = new Gauge({ - name: `${metricPrefix}_ex_memory_limit`, + name: `${metricPrefix}_execution_memory_limit`, help: 'Memory limit for Teraslice a worker container.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeMemoryRequest = new Gauge({ - name: `${metricPrefix}_ex_memory_request`, + name: `${metricPrefix}_execution_memory_request`, help: 'Requested amount of memory for a Teraslice worker container.', labelNames: exLabelNames, registers: [metricsRegistry], }); +const gaugeExStatus = new Gauge({ + name: `${metricPrefix}_execution_status`, + help: 'Current status of the Teraslice execution.', + labelNames: [...exLabelNames, 'status'], + registers: [metricsRegistry], +}); + // The following gauges should be Counters by my reconing, but as far as // prom-client is concerned, this usage is fine: // https://github.com/siimon/prom-client/issues/192 const gaugeSlicesProcessed = new Gauge({ - name: `${metricPrefix}_slices_processed`, + name: `${metricPrefix}_controller_slices_processed`, help: 'Number of slices processed.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeSlicesFailed = new Gauge({ - name: `${metricPrefix}_slices_failed`, + name: `${metricPrefix}_controller_slices_failed`, help: 'Number of slices failed.', labelNames: exLabelNames, registers: [metricsRegistry], }); const gaugeSlicesQueued = new Gauge({ - name: `${metricPrefix}_slices_queued`, + name: `${metricPrefix}_controller_slices_queued`, help: 'Number of slices queued for processing.', labelNames: exLabelNames, registers: [metricsRegistry], }); +// Execution Related Metrics + +const gaugeCreatedTime = new Gauge({ + name: `${metricPrefix}_execution_created_timestamp_seconds`, + help: 'Execution creation time.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeUpdatedTime = new Gauge({ + name: `${metricPrefix}_execution_updated_timestamp_seconds`, + help: 'Execution update time.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeExSlicers = new Gauge({ + name: `${metricPrefix}_execution_slicers`, + help: 'Number of slicers defined on the execution.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeExWorkers = new Gauge({ + name: `${metricPrefix}_execution_workers`, + help: 'Number of workers defined on the execution. Note that the number of actual workers can differ from this value.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + /** * parseController adds the teraslice execution controller metrics to the * metricsRegistry for a single execution. @@ -198,6 +235,41 @@ function parseController(controller:any, labels:any) { gaugeNumSlicers.set(controllerLabels, controller.slicers); } +function generateExecutionStatusMetrics(execution:any, executionLabels:any) { + const statusList = [ + 'completed', + 'failed', + 'failing', + 'initializing', + 'paused', + 'pending', + 'recovering', + 'rejected', + 'running', + 'scheduling', + 'stopped', + 'stopping', + 'terminated', + ]; + + // eslint-disable-next-line no-restricted-syntax + for (const status of statusList) { + const statusLabels = { + ...executionLabels, + status, + }; + // if (status === execution._status) + let state:number; + // eslint-disable-next-line no-underscore-dangle + if (status === execution._status) { + state = 1; + } else { + state = 0; + } + gaugeExStatus.set(statusLabels, state); + } +} + /** * { "analytics": true, @@ -266,6 +338,15 @@ function parseExecution(execution:any, labels:any) { if (execution.cpu) gaugeCpuLimit.set(executionLabels, execution.cpu); if (execution.memory) gaugeMemoryRequest.set(executionLabels, execution.memory); if (execution.memory) gaugeMemoryLimit.set(executionLabels, execution.memory); + + // eslint-disable-next-line no-underscore-dangle + gaugeCreatedTime.set(executionLabels, new Date(execution._created).getTime() / 1000); + // eslint-disable-next-line no-underscore-dangle + gaugeUpdatedTime.set(executionLabels, new Date(execution._updated).getTime() / 1000); + + gaugeExSlicers.set(executionLabels, execution.slicers); + gaugeExWorkers.set(executionLabels, execution.workers); + generateExecutionStatusMetrics(execution, executionLabels); } function generateControllerStats(terasliceStats:TerasliceStats, labels:any) { From 4661302f8428fbfec4e294566f006a04e79e41a9 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Wed, 21 Oct 2020 18:18:19 -0700 Subject: [PATCH 11/21] Add execution version info. --- src/index.ts | 107 ++++++++++++++++++++++++++++++++++++++++- src/teraslice-stats.ts | 84 +++++++++++++++++++++++++++++++- tsconfig.json | 2 +- 3 files changed, 190 insertions(+), 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3ed7f86..7bffe65 100644 --- a/src/index.ts +++ b/src/index.ts @@ -69,7 +69,7 @@ const gaugeWorkersDisconnected = new Gauge({ const gaugeTerasliceMasterInfo = new Gauge({ name: `${metricPrefix}_master_info`, - help: 'Information about the teraslice master node.', + help: 'Information about the Teraslice master node.', labelNames: [ 'arch', 'clustering_type', @@ -82,6 +82,19 @@ const gaugeTerasliceMasterInfo = new Gauge({ registers: [metricsRegistry], }); +const gaugeExecutionInfo = new Gauge({ + name: `${metricPrefix}_execution_info`, + help: 'Information about Teraslice execution.', + labelNames: [ + 'ex_id', + 'job_id', + 'image', + 'version', + ...globalLabelNames, + ], + registers: [metricsRegistry], +}); + const gaugeNumSlicers = new Gauge({ name: `${metricPrefix}_controller_slicers_count`, help: 'Number of execution controllers (slicers) running for this execution.', @@ -365,6 +378,93 @@ function generateExecutionStats(terasliceStats:TerasliceStats, labels:any) { } } +interface StateExecution { + exId: string, + jobId: string, + image: string +} +interface StateExecutionList { + [key: string]: StateExecution +} + +/** + * NOTE: This assumes Teraslice is running in Kubernetes mode. + * + * generateExecutionVersions - takes the /cluster/state output, which looks like + * this: + * + * "10.123.4.111": { + * "node_id": "10.123.4.111", + * "hostname": "10.123.4.111", + * "pid": "N/A", + * "node_version": "N/A", + * "teraslice_version": "N/A", + * "total": "N/A", + * "state": "connected", + * "available": "N/A", + * "active": [ + * { + * "assets": [], + * "assignment": "worker", + * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", + * "image": "teraslice:v0.70.0", + * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", + * "pod_name": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa", + * "pod_ip": "10.132.86.111", + * "worker_id": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa" + * } + * ] + * } + * + * and makes something like this: + * + * { + * exId: {exId, jobId, image}, + * exId: {exId, jobId, image} + * } + * + * That then gets used to generate the metrics. + * + * @param terasliceStats + * @param labels + */ +function generateExecutionVersions(terasliceStats:TerasliceStats, labels:any) { + const executions:StateExecutionList = {}; + + // eslint-disable-next-line no-restricted-syntax + for (const [, workerNode] of Object.entries(terasliceStats.state)) { + // eslint-disable-next-line no-restricted-syntax + for (const worker of workerNode.active) { + if (worker.ex_id && !Object.prototype.hasOwnProperty.call(executions, worker.ex_id)) { + executions[worker.ex_id] = { + exId: worker.ex_id, + jobId: worker.job_id, + image: worker.image, + }; + } + } + } + + // eslint-disable-next-line no-restricted-syntax + for (const [, execution] of Object.entries(executions)) { + const regex = /.*:(.*)_.*/g; + const m = [...execution.image.matchAll(regex)]; + let version = ''; + if (m[0] !== []) { + // eslint-disable-next-line prefer-destructuring + version = m[0][1]; + } + const executionLabels = { + ex_id: execution.exId, + job_id: execution.jobId, + image: execution.image, + version, + ...labels, + }; + gaugeExecutionInfo.set(executionLabels, 1); + } +} + function updateTerasliceMetrics(terasliceStats: TerasliceStats) { const globalLabels = { url: terasliceStats.baseUrl.toString(), @@ -379,6 +479,7 @@ function updateTerasliceMetrics(terasliceStats: TerasliceStats) { generateControllerStats(terasliceStats, globalLabels); generateExecutionStats(terasliceStats, globalLabels); + generateExecutionVersions(terasliceStats, globalLabels); gaugeQueryDuration.set( { query_name: 'info', ...globalLabels }, @@ -396,6 +497,10 @@ function updateTerasliceMetrics(terasliceStats: TerasliceStats) { { query_name: 'executions', ...globalLabels }, terasliceStats.queryDuration.executions, ); + gaugeQueryDuration.set( + { query_name: 'state', ...globalLabels }, + terasliceStats.queryDuration.state, + ); } async function main() { diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index 5d75b01..b2844c0 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -1,6 +1,79 @@ +/* eslint-disable camelcase */ import got from 'got'; // TODO: move these interfaces out into their own file +/** + * TerasliceWorker - The individual teraslice worker object in the + * TerasliceWorkerNode.active array. This corresponds to a k8s pod in k8s mode. + * + * Example: + * { + * "assets": [], + * "assignment": "worker", + * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", + * "image": "teraslice:v0.70.0", + * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", + * "pod_name": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa", + * "pod_ip": "10.132.86.111", + * "worker_id": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa" + * } + */ +interface TerasliceWorker { + assets: string[], + assignment: string, + ex_id: string, + image: string, + job_id: string, + pod_name: string, + pod_ip: string, + worker_id: string +} + +/** + * { + * "node_id": "10.123.4.111", + * "hostname": "10.123.4.111", + * "pid": "N/A", + * "node_version": "N/A", + * "teraslice_version": "N/A", + * "total": "N/A", + * "state": "connected", + * "available": "N/A", + * "active": [...] + * } + */ +interface TerasliceWorkerNodeInfo { + node_id: string, + hostname: string, + pid: string, + node_version: string, + teraslice_version: string, + total: string, + state: string, + available: string, + active: TerasliceWorker[] +} + +/** + * "10.123.4.111": { + * "node_id": "10.123.4.111", + * "hostname": "10.123.4.111", + * "pid": "N/A", + * "node_version": "N/A", + * "teraslice_version": "N/A", + * "total": "N/A", + * "state": "connected", + * "available": "N/A", + * "active": [...] + * } + */ +// interface TerasliceWorkerNode { +// [key: string]: TerasliceWorkerNodeInfo +// } + +interface TerasliceClusterState { + [key: string]: TerasliceWorkerNodeInfo +} interface TerasliceInfo { arch: string, @@ -22,6 +95,7 @@ interface TerasliceQueryDuration { executions: number, info: number, jobs: number, + state: number, } interface TerasliceStatsInterface { @@ -30,6 +104,7 @@ interface TerasliceStatsInterface { executions: any[], info: TerasliceInfo, jobs: any[], + state: TerasliceClusterState, queryDuration: TerasliceQueryDuration } @@ -51,6 +126,8 @@ export default class TerasliceStats implements TerasliceStatsInterface { jobs: any[]; + state: TerasliceClusterState; + queryDuration: TerasliceQueryDuration; constructor(baseUrl:string) { @@ -58,12 +135,14 @@ export default class TerasliceStats implements TerasliceStatsInterface { this.controllers = []; this.executions = []; this.jobs = []; + this.state = {}; this.queryDuration = { controllers: 0, executions: 0, info: 0, jobs: 0, + state: 0, }; } @@ -128,19 +207,22 @@ export default class TerasliceStats implements TerasliceStatsInterface { // TODO: hard coding querySize is dumb const querySize = 200; const run = async () => { - const [info, jobs, controllers] = await Promise.all([ + const [info, jobs, controllers, state] = await Promise.all([ this.getTerasliceApi('/'), this.getTerasliceApi(`/v1/jobs?size=${querySize}`), this.getTerasliceApi('/v1/cluster/controllers'), + this.getTerasliceApi('/v1/cluster/state'), ]); this.info = info.data; this.jobs = jobs.data; this.controllers = controllers.data; + this.state = state.data; await this.updateExecutions(); this.queryDuration.info = info.queryDuration; this.queryDuration.jobs = jobs.queryDuration; this.queryDuration.controllers = controllers.queryDuration; + this.queryDuration.state = state.queryDuration; }; await run().catch((err) => { throw new Error(`Error caught on run() ${this.baseUrl}: ${err}`); diff --git a/tsconfig.json b/tsconfig.json index 3ef48d0..58f0a45 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ From 6b4d6c0d0ab6bc7cc1dcab90d1de9d7cb7e881f4 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 22 Oct 2020 14:54:40 -0700 Subject: [PATCH 12/21] Add teraslice eslint and editerconfig, reformat --- .editorconfig | 19 ++ .eslintrc | 3 + .eslintrc.js | 19 -- package.json | 47 ++-- src/index.ts | 604 ++++++++++++++++++++--------------------- src/teraslice-stats.ts | 254 ++++++++--------- tsconfig.json | 2 +- yarn.lock | 334 ++++++++++++++++++++++- 8 files changed, 809 insertions(+), 473 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintrc delete mode 100644 .eslintrc.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..324d7ff --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ + Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{js,ts,jsx,tsx,json,md}] +indent_style = space +indent_size = 4 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.md] +max_line_length = off diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..f33256b --- /dev/null +++ b/.eslintrc @@ -0,0 +1,3 @@ +{ + "extends": "@terascope" +} diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index dd8adab..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - "env": { - "browser": true, - "es2020": true - }, - "extends": [ - "airbnb-base" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 11, - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint" - ], - "rules": { - } -}; diff --git a/package.json b/package.json index d46f1ca..92e7c30 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,27 @@ { - "dependencies": { - "bunyan": "^1.8.14", - "express": "^4.17.1", - "got": "^11.6.2", - "prom-client": "^12.0.0" - }, - "devDependencies": { - "@types/bunyan": "^1.8.6", - "@types/express": "^4.17.8", - "@types/node": "^14.10.1", - "@typescript-eslint/eslint-plugin": "^4.3.0", - "@typescript-eslint/parser": "^4.3.0", - "eslint": "^7.10.0", - "eslint-config-airbnb-base": "^14.2.0", - "eslint-plugin-import": "^2.22.1", - "typescript": "^4.0.2" - }, - "scripts": { - "build": "rm -rf dist/ && npx tsc", - "build:watch": "rm -rf dist/ && npx tsc -w", - "run": "node dist/index.js" - } + "author": "Terascope, LLC ", + "license": "MIT", + "dependencies": { + "bunyan": "^1.8.14", + "express": "^4.17.1", + "got": "^11.6.2", + "prom-client": "^12.0.0" + }, + "devDependencies": { + "@terascope/eslint-config": "^0.5.0", + "@types/bunyan": "^1.8.6", + "@types/express": "^4.17.8", + "@types/node": "^14.10.1", + "@typescript-eslint/eslint-plugin": "^4.3.0", + "@typescript-eslint/parser": "^4.3.0", + "eslint": "^7.10.0", + "eslint-config-airbnb-base": "^14.2.0", + "eslint-plugin-import": "^2.22.1", + "typescript": "^4.0.2" + }, + "scripts": { + "build": "rm -rf dist/ && npx tsc", + "build:watch": "rm -rf dist/ && npx tsc -w", + "run": "node dist/index.js" + } } diff --git a/src/index.ts b/src/index.ts index 7bffe65..c1eed9d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,191 +13,191 @@ const logger = bunyan.createLogger({ name: 'teraslice_exporter' }); const metricPrefix = 'teraslice'; const globalLabelNames = [ - 'url', - 'name', + 'url', + 'name', ]; const exLabelNames = [ - 'ex_id', - 'job_id', - 'job_name', - ...globalLabelNames, + 'ex_id', + 'job_id', + 'job_name', + ...globalLabelNames, ]; declare let process : { - env: { - DEBUG: string, - PORT: number, - TERASLICE_URL: string - TERASLICE_QUERY_DELAY: number - } + env: { + DEBUG: string, + PORT: number, + TERASLICE_URL: string + TERASLICE_QUERY_DELAY: number + } }; const gaugeWorkersActive = new Gauge({ - name: `${metricPrefix}_controller_workers_active`, - help: 'Number of Teraslice workers actively processing slices.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_workers_active`, + help: 'Number of Teraslice workers actively processing slices.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeWorkersAvailable = new Gauge({ - name: `${metricPrefix}_controller_workers_available`, - help: 'Number of Teraslice workers running and waiting for work.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_workers_available`, + help: 'Number of Teraslice workers running and waiting for work.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeWorkersJoined = new Gauge({ - name: `${metricPrefix}_controller_workers_joined`, - help: 'Total number of Teraslice workers that have joined the execution controller for this job.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_workers_joined`, + help: 'Total number of Teraslice workers that have joined the execution controller for this job.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeWorkersReconnected = new Gauge({ - name: `${metricPrefix}_controller_workers_reconnected`, - help: 'Total number of Teraslice workers that have reconnected to the execution controller for this job.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_workers_reconnected`, + help: 'Total number of Teraslice workers that have reconnected to the execution controller for this job.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeWorkersDisconnected = new Gauge({ - name: `${metricPrefix}_controller_workers_disconnected`, - help: 'Total number of Teraslice workers that have disconnected from execution controller for this job.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_workers_disconnected`, + help: 'Total number of Teraslice workers that have disconnected from execution controller for this job.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeTerasliceMasterInfo = new Gauge({ - name: `${metricPrefix}_master_info`, - help: 'Information about the Teraslice master node.', - labelNames: [ - 'arch', - 'clustering_type', - 'name', - 'node_version', - 'platform', - 'teraslice_version', - ...globalLabelNames, - ], - registers: [metricsRegistry], + name: `${metricPrefix}_master_info`, + help: 'Information about the Teraslice master node.', + labelNames: [ + 'arch', + 'clustering_type', + 'name', + 'node_version', + 'platform', + 'teraslice_version', + ...globalLabelNames, + ], + registers: [metricsRegistry], }); const gaugeExecutionInfo = new Gauge({ - name: `${metricPrefix}_execution_info`, - help: 'Information about Teraslice execution.', - labelNames: [ - 'ex_id', - 'job_id', - 'image', - 'version', - ...globalLabelNames, - ], - registers: [metricsRegistry], + name: `${metricPrefix}_execution_info`, + help: 'Information about Teraslice execution.', + labelNames: [ + 'ex_id', + 'job_id', + 'image', + 'version', + ...globalLabelNames, + ], + registers: [metricsRegistry], }); const gaugeNumSlicers = new Gauge({ - name: `${metricPrefix}_controller_slicers_count`, - help: 'Number of execution controllers (slicers) running for this execution.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_slicers_count`, + help: 'Number of execution controllers (slicers) running for this execution.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeQueryDuration = new Gauge({ - name: `${metricPrefix}_query_duration`, - help: 'Total time to complete the named query, in ms.', - labelNames: ['query_name', ...globalLabelNames], - registers: [metricsRegistry], + name: `${metricPrefix}_query_duration`, + help: 'Total time to complete the named query, in ms.', + labelNames: ['query_name', ...globalLabelNames], + registers: [metricsRegistry], }); // Execution Related Metrics const gaugeCpuLimit = new Gauge({ - name: `${metricPrefix}_execution_cpu_limit`, - help: 'CPU core limit for a Teraslice worker container.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_execution_cpu_limit`, + help: 'CPU core limit for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeCpuRequest = new Gauge({ - name: `${metricPrefix}_execution_cpu_request`, - help: 'Requested number of CPU cores for a Teraslice worker container.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_execution_cpu_request`, + help: 'Requested number of CPU cores for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeMemoryLimit = new Gauge({ - name: `${metricPrefix}_execution_memory_limit`, - help: 'Memory limit for Teraslice a worker container.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_execution_memory_limit`, + help: 'Memory limit for Teraslice a worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeMemoryRequest = new Gauge({ - name: `${metricPrefix}_execution_memory_request`, - help: 'Requested amount of memory for a Teraslice worker container.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_execution_memory_request`, + help: 'Requested amount of memory for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeExStatus = new Gauge({ - name: `${metricPrefix}_execution_status`, - help: 'Current status of the Teraslice execution.', - labelNames: [...exLabelNames, 'status'], - registers: [metricsRegistry], + name: `${metricPrefix}_execution_status`, + help: 'Current status of the Teraslice execution.', + labelNames: [...exLabelNames, 'status'], + registers: [metricsRegistry], }); // The following gauges should be Counters by my reconing, but as far as // prom-client is concerned, this usage is fine: // https://github.com/siimon/prom-client/issues/192 const gaugeSlicesProcessed = new Gauge({ - name: `${metricPrefix}_controller_slices_processed`, - help: 'Number of slices processed.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_slices_processed`, + help: 'Number of slices processed.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeSlicesFailed = new Gauge({ - name: `${metricPrefix}_controller_slices_failed`, - help: 'Number of slices failed.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_slices_failed`, + help: 'Number of slices failed.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeSlicesQueued = new Gauge({ - name: `${metricPrefix}_controller_slices_queued`, - help: 'Number of slices queued for processing.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_controller_slices_queued`, + help: 'Number of slices queued for processing.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); // Execution Related Metrics const gaugeCreatedTime = new Gauge({ - name: `${metricPrefix}_execution_created_timestamp_seconds`, - help: 'Execution creation time.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_execution_created_timestamp_seconds`, + help: 'Execution creation time.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeUpdatedTime = new Gauge({ - name: `${metricPrefix}_execution_updated_timestamp_seconds`, - help: 'Execution update time.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_execution_updated_timestamp_seconds`, + help: 'Execution update time.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeExSlicers = new Gauge({ - name: `${metricPrefix}_execution_slicers`, - help: 'Number of slicers defined on the execution.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_execution_slicers`, + help: 'Number of slicers defined on the execution.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); const gaugeExWorkers = new Gauge({ - name: `${metricPrefix}_execution_workers`, - help: 'Number of workers defined on the execution. Note that the number of actual workers can differ from this value.', - labelNames: exLabelNames, - registers: [metricsRegistry], + name: `${metricPrefix}_execution_workers`, + help: 'Number of workers defined on the execution. Note that the number of actual workers can differ from this value.', + labelNames: exLabelNames, + registers: [metricsRegistry], }); /** @@ -228,59 +228,59 @@ const gaugeExWorkers = new Gauge({ * @param controller */ function parseController(controller:any, labels:any) { - const controllerLabels = { - ex_id: controller.ex_id, - job_id: controller.job_id, - job_name: controller.name, - ...labels, - }; - - gaugeWorkersActive.set(controllerLabels, controller.workers_active); - gaugeWorkersAvailable.set(controllerLabels, controller.workers_available); - gaugeWorkersJoined.set(controllerLabels, controller.workers_joined); - gaugeWorkersReconnected.set(controllerLabels, controller.workers_reconnected); - gaugeWorkersDisconnected.set(controllerLabels, controller.workers_disconnected); - - gaugeSlicesProcessed.set(controllerLabels, controller.processed); - gaugeSlicesFailed.set(controllerLabels, controller.failed); - gaugeSlicesQueued.set(controllerLabels, controller.queued); - - gaugeNumSlicers.set(controllerLabels, controller.slicers); + const controllerLabels = { + ex_id: controller.ex_id, + job_id: controller.job_id, + job_name: controller.name, + ...labels, + }; + + gaugeWorkersActive.set(controllerLabels, controller.workers_active); + gaugeWorkersAvailable.set(controllerLabels, controller.workers_available); + gaugeWorkersJoined.set(controllerLabels, controller.workers_joined); + gaugeWorkersReconnected.set(controllerLabels, controller.workers_reconnected); + gaugeWorkersDisconnected.set(controllerLabels, controller.workers_disconnected); + + gaugeSlicesProcessed.set(controllerLabels, controller.processed); + gaugeSlicesFailed.set(controllerLabels, controller.failed); + gaugeSlicesQueued.set(controllerLabels, controller.queued); + + gaugeNumSlicers.set(controllerLabels, controller.slicers); } function generateExecutionStatusMetrics(execution:any, executionLabels:any) { - const statusList = [ - 'completed', - 'failed', - 'failing', - 'initializing', - 'paused', - 'pending', - 'recovering', - 'rejected', - 'running', - 'scheduling', - 'stopped', - 'stopping', - 'terminated', - ]; - - // eslint-disable-next-line no-restricted-syntax - for (const status of statusList) { - const statusLabels = { - ...executionLabels, - status, - }; - // if (status === execution._status) - let state:number; - // eslint-disable-next-line no-underscore-dangle - if (status === execution._status) { - state = 1; - } else { - state = 0; + const statusList = [ + 'completed', + 'failed', + 'failing', + 'initializing', + 'paused', + 'pending', + 'recovering', + 'rejected', + 'running', + 'scheduling', + 'stopped', + 'stopping', + 'terminated', + ]; + + // eslint-disable-next-line no-restricted-syntax + for (const status of statusList) { + const statusLabels = { + ...executionLabels, + status, + }; + // if (status === execution._status) + let state:number; + // eslint-disable-next-line no-underscore-dangle + if (status === execution._status) { + state = 1; + } else { + state = 0; + } + gaugeExStatus.set(statusLabels, state); } - gaugeExStatus.set(statusLabels, state); - } } /** @@ -333,58 +333,58 @@ function generateExecutionStatusMetrics(execution:any, executionLabels:any) { * @param labels */ function parseExecution(execution:any, labels:any) { - const executionLabels = { - ex_id: execution.ex_id, - job_id: execution.job_id, - job_name: execution.name, - ...labels, - }; - - // NOTE: Optional settings that are undefined are just excluded with a - // conditional below. - - // TODO: At some point workers will have different CPU Limits and Requests, - // https://github.com/terascope/teraslice/issues/2202 - // for now, these are set to the same thing, but I split them since I know a - // change is coming. - if (execution.cpu) gaugeCpuRequest.set(executionLabels, execution.cpu); - if (execution.cpu) gaugeCpuLimit.set(executionLabels, execution.cpu); - if (execution.memory) gaugeMemoryRequest.set(executionLabels, execution.memory); - if (execution.memory) gaugeMemoryLimit.set(executionLabels, execution.memory); - - // eslint-disable-next-line no-underscore-dangle - gaugeCreatedTime.set(executionLabels, new Date(execution._created).getTime() / 1000); - // eslint-disable-next-line no-underscore-dangle - gaugeUpdatedTime.set(executionLabels, new Date(execution._updated).getTime() / 1000); - - gaugeExSlicers.set(executionLabels, execution.slicers); - gaugeExWorkers.set(executionLabels, execution.workers); - generateExecutionStatusMetrics(execution, executionLabels); + const executionLabels = { + ex_id: execution.ex_id, + job_id: execution.job_id, + job_name: execution.name, + ...labels, + }; + + // NOTE: Optional settings that are undefined are just excluded with a + // conditional below. + + // TODO: At some point workers will have different CPU Limits and Requests, + // https://github.com/terascope/teraslice/issues/2202 + // for now, these are set to the same thing, but I split them since I know a + // change is coming. + if (execution.cpu) gaugeCpuRequest.set(executionLabels, execution.cpu); + if (execution.cpu) gaugeCpuLimit.set(executionLabels, execution.cpu); + if (execution.memory) gaugeMemoryRequest.set(executionLabels, execution.memory); + if (execution.memory) gaugeMemoryLimit.set(executionLabels, execution.memory); + + // eslint-disable-next-line no-underscore-dangle + gaugeCreatedTime.set(executionLabels, new Date(execution._created).getTime() / 1000); + // eslint-disable-next-line no-underscore-dangle + gaugeUpdatedTime.set(executionLabels, new Date(execution._updated).getTime() / 1000); + + gaugeExSlicers.set(executionLabels, execution.slicers); + gaugeExWorkers.set(executionLabels, execution.workers); + generateExecutionStatusMetrics(execution, executionLabels); } function generateControllerStats(terasliceStats:TerasliceStats, labels:any) { - // FIXME: I should rethink this warning - // eslint-disable-next-line no-restricted-syntax - for (const controller of terasliceStats.controllers) { - parseController(controller, labels); - } + // FIXME: I should rethink this warning + // eslint-disable-next-line no-restricted-syntax + for (const controller of terasliceStats.controllers) { + parseController(controller, labels); + } } function generateExecutionStats(terasliceStats:TerasliceStats, labels:any) { - // FIXME: I should rethink this warning - // eslint-disable-next-line no-restricted-syntax - for (const execution of terasliceStats.executions) { - parseExecution(execution, labels); - } + // FIXME: I should rethink this warning + // eslint-disable-next-line no-restricted-syntax + for (const execution of terasliceStats.executions) { + parseExecution(execution, labels); + } } interface StateExecution { - exId: string, - jobId: string, - image: string + exId: string, + jobId: string, + image: string } interface StateExecutionList { - [key: string]: StateExecution + [key: string]: StateExecution } /** @@ -429,128 +429,128 @@ interface StateExecutionList { * @param labels */ function generateExecutionVersions(terasliceStats:TerasliceStats, labels:any) { - const executions:StateExecutionList = {}; + const executions:StateExecutionList = {}; - // eslint-disable-next-line no-restricted-syntax - for (const [, workerNode] of Object.entries(terasliceStats.state)) { // eslint-disable-next-line no-restricted-syntax - for (const worker of workerNode.active) { - if (worker.ex_id && !Object.prototype.hasOwnProperty.call(executions, worker.ex_id)) { - executions[worker.ex_id] = { - exId: worker.ex_id, - jobId: worker.job_id, - image: worker.image, - }; - } + for (const [, workerNode] of Object.entries(terasliceStats.state)) { + // eslint-disable-next-line no-restricted-syntax + for (const worker of workerNode.active) { + if (worker.ex_id && !Object.prototype.hasOwnProperty.call(executions, worker.ex_id)) { + executions[worker.ex_id] = { + exId: worker.ex_id, + jobId: worker.job_id, + image: worker.image, + }; + } + } } - } - - // eslint-disable-next-line no-restricted-syntax - for (const [, execution] of Object.entries(executions)) { - const regex = /.*:(.*)_.*/g; - const m = [...execution.image.matchAll(regex)]; - let version = ''; - if (m[0] !== []) { - // eslint-disable-next-line prefer-destructuring - version = m[0][1]; + + // eslint-disable-next-line no-restricted-syntax + for (const [, execution] of Object.entries(executions)) { + const regex = /.*:(.*)_.*/g; + const m = [...execution.image.matchAll(regex)]; + let version = ''; + if (m[0] !== []) { + // eslint-disable-next-line prefer-destructuring + version = m[0][1]; + } + const executionLabels = { + ex_id: execution.exId, + job_id: execution.jobId, + image: execution.image, + version, + ...labels, + }; + gaugeExecutionInfo.set(executionLabels, 1); } - const executionLabels = { - ex_id: execution.exId, - job_id: execution.jobId, - image: execution.image, - version, - ...labels, - }; - gaugeExecutionInfo.set(executionLabels, 1); - } } function updateTerasliceMetrics(terasliceStats: TerasliceStats) { - const globalLabels = { - url: terasliceStats.baseUrl.toString(), - name: terasliceStats.info.name, - }; - // NOTE: This set of labels expands out to including 'name' twice, right now - // they reduce to a single 'name' label ... I could end up regretting this. - gaugeTerasliceMasterInfo.set( - { ...terasliceStats.info, ...globalLabels }, - 1, - ); - - generateControllerStats(terasliceStats, globalLabels); - generateExecutionStats(terasliceStats, globalLabels); - generateExecutionVersions(terasliceStats, globalLabels); - - gaugeQueryDuration.set( - { query_name: 'info', ...globalLabels }, - terasliceStats.queryDuration.info, - ); - gaugeQueryDuration.set( - { query_name: 'jobs', ...globalLabels }, - terasliceStats.queryDuration.jobs, - ); - gaugeQueryDuration.set( - { query_name: 'controllers', ...globalLabels }, - terasliceStats.queryDuration.controllers, - ); - gaugeQueryDuration.set( - { query_name: 'executions', ...globalLabels }, - terasliceStats.queryDuration.executions, - ); - gaugeQueryDuration.set( - { query_name: 'state', ...globalLabels }, - terasliceStats.queryDuration.state, - ); + const globalLabels = { + url: terasliceStats.baseUrl.toString(), + name: terasliceStats.info.name, + }; + // NOTE: This set of labels expands out to including 'name' twice, right now + // they reduce to a single 'name' label ... I could end up regretting this. + gaugeTerasliceMasterInfo.set( + { ...terasliceStats.info, ...globalLabels }, + 1, + ); + + generateControllerStats(terasliceStats, globalLabels); + generateExecutionStats(terasliceStats, globalLabels); + generateExecutionVersions(terasliceStats, globalLabels); + + gaugeQueryDuration.set( + { query_name: 'info', ...globalLabels }, + terasliceStats.queryDuration.info, + ); + gaugeQueryDuration.set( + { query_name: 'jobs', ...globalLabels }, + terasliceStats.queryDuration.jobs, + ); + gaugeQueryDuration.set( + { query_name: 'controllers', ...globalLabels }, + terasliceStats.queryDuration.controllers, + ); + gaugeQueryDuration.set( + { query_name: 'executions', ...globalLabels }, + terasliceStats.queryDuration.executions, + ); + gaugeQueryDuration.set( + { query_name: 'state', ...globalLabels }, + terasliceStats.queryDuration.state, + ); } async function main() { - let baseUrl: string; - const metricsEndpoint = '/metrics'; + let baseUrl: string; + const metricsEndpoint = '/metrics'; - if (process.env.TERASLICE_URL) { + if (process.env.TERASLICE_URL) { // I instantiate a URL, then immediately call toString() just to get the // URL validation but keep a string type - baseUrl = new URL(process.env.TERASLICE_URL).toString(); - } else { - throw new Error('The TERASLICE_URL environment variable must be a valid URL to the root of your teraslice instance.'); - } - const port = process.env.PORT || 3000; - const terasliceQueryDelay = process.env.TERASLICE_QUERY_DELAY || 30000; // ms - - server.get(metricsEndpoint, (req, res) => { - res.set('Content-Type', metricsRegistry.contentType); - res.end(metricsRegistry.metrics()); - }); - - server.get('/', (req, res) => { - res.send(`See the '${metricsEndpoint}' endpoint for the teraslice exporter.`); - }); + baseUrl = new URL(process.env.TERASLICE_URL).toString(); + } else { + throw new Error('The TERASLICE_URL environment variable must be a valid URL to the root of your teraslice instance.'); + } + const port = process.env.PORT || 3000; + const terasliceQueryDelay = process.env.TERASLICE_QUERY_DELAY || 30000; // ms - if (process.env.DEBUG) logger.level('debug'); + server.get(metricsEndpoint, (req, res) => { + res.set('Content-Type', metricsRegistry.contentType); + res.end(metricsRegistry.metrics()); + }); - const terasliceStats = new TerasliceStats(baseUrl); - await terasliceStats.update(); - updateTerasliceMetrics(terasliceStats); + server.get('/', (req, res) => { + res.send(`See the '${metricsEndpoint}' endpoint for the teraslice exporter.`); + }); - logger.debug(`executions: ${JSON.stringify(terasliceStats.executions.slice(0, 2), null, 2)}`); - logger.debug(`controllers: ${JSON.stringify(terasliceStats.controllers.slice(0, 2), null, 2)}`); + if (process.env.DEBUG) logger.level('debug'); - setInterval(async () => { - logger.debug(`Updating Teraslice Cluster Information from ${baseUrl}`); + const terasliceStats = new TerasliceStats(baseUrl); await terasliceStats.update(); updateTerasliceMetrics(terasliceStats); - logger.debug(`queryDurations: ${JSON.stringify(terasliceStats.queryDuration)}`); - logger.debug(`datasetSizes: ${JSON.stringify({ - info: terasliceStats.info.length, - controllers: terasliceStats.controllers.length, - executions: terasliceStats.executions.length, - jobs: terasliceStats.jobs.length, - })}`); - }, terasliceQueryDelay); - - logger.info(`HTTP server listening to ${port}, metrics exposed on ${metricsEndpoint} endpoint`); - server.listen(port); + logger.debug(`executions: ${JSON.stringify(terasliceStats.executions.slice(0, 2), null, 2)}`); + logger.debug(`controllers: ${JSON.stringify(terasliceStats.controllers.slice(0, 2), null, 2)}`); + + setInterval(async () => { + logger.debug(`Updating Teraslice Cluster Information from ${baseUrl}`); + await terasliceStats.update(); + updateTerasliceMetrics(terasliceStats); + + logger.debug(`queryDurations: ${JSON.stringify(terasliceStats.queryDuration)}`); + logger.debug(`datasetSizes: ${JSON.stringify({ + info: terasliceStats.info.length, + controllers: terasliceStats.controllers.length, + executions: terasliceStats.executions.length, + jobs: terasliceStats.jobs.length, + })}`); + }, terasliceQueryDelay); + + logger.info(`HTTP server listening to ${port}, metrics exposed on ${metricsEndpoint} endpoint`); + server.listen(port); } main(); diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index b2844c0..3dc3e61 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -19,14 +19,14 @@ import got from 'got'; * } */ interface TerasliceWorker { - assets: string[], - assignment: string, - ex_id: string, - image: string, - job_id: string, - pod_name: string, - pod_ip: string, - worker_id: string + assets: string[], + assignment: string, + ex_id: string, + image: string, + job_id: string, + pod_name: string, + pod_ip: string, + worker_id: string } /** @@ -43,15 +43,15 @@ interface TerasliceWorker { * } */ interface TerasliceWorkerNodeInfo { - node_id: string, - hostname: string, - pid: string, - node_version: string, - teraslice_version: string, - total: string, - state: string, - available: string, - active: TerasliceWorker[] + node_id: string, + hostname: string, + pid: string, + node_version: string, + teraslice_version: string, + total: string, + state: string, + available: string, + active: TerasliceWorker[] } /** @@ -72,30 +72,30 @@ interface TerasliceWorkerNodeInfo { // } interface TerasliceClusterState { - [key: string]: TerasliceWorkerNodeInfo + [key: string]: TerasliceWorkerNodeInfo } interface TerasliceInfo { - arch: string, - // eslint-disable-next-line camelcase - clustering_type: string, - name: string, - // eslint-disable-next-line camelcase - node_version: string, - platform: string, - // eslint-disable-next-line camelcase - teraslice_version: string + arch: string, + // eslint-disable-next-line camelcase + clustering_type: string, + name: string, + // eslint-disable-next-line camelcase + node_version: string, + platform: string, + // eslint-disable-next-line camelcase + teraslice_version: string } /** * These are all in ms */ interface TerasliceQueryDuration { - controllers: number, - executions: number, - info: number, - jobs: number, - state: number, + controllers: number, + executions: number, + info: number, + jobs: number, + state: number, } interface TerasliceStatsInterface { @@ -110,122 +110,122 @@ interface TerasliceStatsInterface { /** promisified setTimeout */ export function pDelay(delay = 1, arg?: T): Promise { - return new Promise((resolve) => { - setTimeout(resolve, delay, arg); - }); + return new Promise((resolve) => { + setTimeout(resolve, delay, arg); + }); } export default class TerasliceStats implements TerasliceStatsInterface { - baseUrl: URL; + baseUrl: URL; - controllers: any[]; + controllers: any[]; - executions: any[]; + executions: any[]; - info: any; + info: any; - jobs: any[]; + jobs: any[]; - state: TerasliceClusterState; + state: TerasliceClusterState; - queryDuration: TerasliceQueryDuration; + queryDuration: TerasliceQueryDuration; - constructor(baseUrl:string) { - this.baseUrl = new URL(baseUrl); - this.controllers = []; - this.executions = []; - this.jobs = []; - this.state = {}; + constructor(baseUrl:string) { + this.baseUrl = new URL(baseUrl); + this.controllers = []; + this.executions = []; + this.jobs = []; + this.state = {}; - this.queryDuration = { - controllers: 0, - executions: 0, - info: 0, - jobs: 0, - state: 0, - }; - } + this.queryDuration = { + controllers: 0, + executions: 0, + info: 0, + jobs: 0, + state: 0, + }; + } - async getTerasliceApi(path:string) { - const url = new URL(path, this.baseUrl); - let r: { - data: any, - queryDuration: number + async getTerasliceApi(path:string) { + const url = new URL(path, this.baseUrl); + let r: { + data: any, + queryDuration: number }; - let response : { - body: any, - statusCode: number, - timings: any - }; - - try { - // FIXME: the .toString is to eliminate a 'No overload matches this call' - response = await got(url.toString(), { responseType: 'json' }); - if (response && response.statusCode === 200 && response.body) { - r = { - data: response.body, - queryDuration: response.timings.phases.total, + let response : { + body: any, + statusCode: number, + timings: any }; - } else { - throw new Error(`Error getting ${url}: ${response.statusCode}`); - } - } catch (error) { - throw new Error(`Error getting ${url}: ${error}`); + + try { + // FIXME: the .toString is to eliminate a 'No overload matches this call' + response = await got(url.toString(), { responseType: 'json' }); + if (response && response.statusCode === 200 && response.body) { + r = { + data: response.body, + queryDuration: response.timings.phases.total, + }; + } else { + throw new Error(`Error getting ${url}: ${response.statusCode}`); + } + } catch (error) { + throw new Error(`Error getting ${url}: ${error}`); + } + return r; } - return r; - } - async updateExecutions() { - const time = process.hrtime(); - this.executions = []; - const maxConcurrency = 10; - const queryDelay = 25; + async updateExecutions() { + const time = process.hrtime(); + this.executions = []; + const maxConcurrency = 10; + const queryDelay = 25; - for (let i = 0; i < this.controllers.length; i += maxConcurrency) { - const controllersSlice = this.controllers.slice(i, i + maxConcurrency); + for (let i = 0; i < this.controllers.length; i += maxConcurrency) { + const controllersSlice = this.controllers.slice(i, i + maxConcurrency); - // eslint-disable-next-line no-await-in-loop - const r = await Promise.all( - controllersSlice.map((x) => this.getTerasliceApi(`/v1/ex/${x.ex_id}`)), - ); + // eslint-disable-next-line no-await-in-loop + const r = await Promise.all( + controllersSlice.map((x) => this.getTerasliceApi(`/v1/ex/${x.ex_id}`)), + ); - // eslint-disable-next-line prefer-spread - this.executions.push.apply(this.executions, r.map((x) => x.data)); + // eslint-disable-next-line prefer-spread + this.executions.push.apply(this.executions, r.map((x) => x.data)); - // eslint-disable-next-line no-await-in-loop - await pDelay(queryDelay); - } + // eslint-disable-next-line no-await-in-loop + await pDelay(queryDelay); + } - const NS_PER_SEC = 1e9; - const diff = process.hrtime(time); - this.queryDuration.executions = Math.round((diff[0] * NS_PER_SEC + diff[1]) / 1e6); - } + const NS_PER_SEC = 1e9; + const diff = process.hrtime(time); + this.queryDuration.executions = Math.round((diff[0] * NS_PER_SEC + diff[1]) / 1e6); + } - // I think I've been doing this sort of thing wrong in the past. - // https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await - async update() { + // I think I've been doing this sort of thing wrong in the past. + // https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await + async update() { // TODO: hard coding querySize is dumb - const querySize = 200; - const run = async () => { - const [info, jobs, controllers, state] = await Promise.all([ - this.getTerasliceApi('/'), - this.getTerasliceApi(`/v1/jobs?size=${querySize}`), - this.getTerasliceApi('/v1/cluster/controllers'), - this.getTerasliceApi('/v1/cluster/state'), - ]); - this.info = info.data; - this.jobs = jobs.data; - this.controllers = controllers.data; - this.state = state.data; - await this.updateExecutions(); - - this.queryDuration.info = info.queryDuration; - this.queryDuration.jobs = jobs.queryDuration; - this.queryDuration.controllers = controllers.queryDuration; - this.queryDuration.state = state.queryDuration; - }; - await run().catch((err) => { - throw new Error(`Error caught on run() ${this.baseUrl}: ${err}`); - }); - } + const querySize = 200; + const run = async () => { + const [info, jobs, controllers, state] = await Promise.all([ + this.getTerasliceApi('/'), + this.getTerasliceApi(`/v1/jobs?size=${querySize}`), + this.getTerasliceApi('/v1/cluster/controllers'), + this.getTerasliceApi('/v1/cluster/state'), + ]); + this.info = info.data; + this.jobs = jobs.data; + this.controllers = controllers.data; + this.state = state.data; + await this.updateExecutions(); + + this.queryDuration.info = info.queryDuration; + this.queryDuration.jobs = jobs.queryDuration; + this.queryDuration.controllers = controllers.queryDuration; + this.queryDuration.state = state.queryDuration; + }; + await run().catch((err) => { + throw new Error(`Error caught on run() ${this.baseUrl}: ${err}`); + }); + } } diff --git a/tsconfig.json b/tsconfig.json index 58f0a45..0b8531e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ /* Basic Options */ // "incremental": true, /* Enable incremental compilation */ - "target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ // "lib": [], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ diff --git a/yarn.lock b/yarn.lock index 0aabc3c..c61fc7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23,6 +23,21 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/runtime-corejs3@^7.10.2": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.12.1.tgz#51b9092befbeeed938335a109dbe0df51451e9dc" + integrity sha512-umhPIcMrlBZ2aTWlWjUseW9LjQKxi1dpFlQS8DzsxB//5K+u6GLTC/JliPKHsd5kJVPIU6X/Hy0YvWOYPcMxBw== + dependencies: + core-js-pure "^3.0.0" + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.10.2": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.1.tgz#b4116a6b6711d010b2dad3b7b6e43bf1b9954740" + integrity sha512-J5AIf3vPj3UwXaAzb5j1xM4WAQDX3EMgemF8rjCP3SoW09LfRKAXQKt6CoVYl230P6iWdRcBbnLDDdnqWxZSCA== + dependencies: + regenerator-runtime "^0.13.4" + "@eslint/eslintrc@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.1.3.tgz#7d1a2b2358552cc04834c0979bd4275362e37085" @@ -72,6 +87,21 @@ dependencies: defer-to-connect "^2.0.0" +"@terascope/eslint-config@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@terascope/eslint-config/-/eslint-config-0.5.0.tgz#34dd510890312be613c98c814af13258a0967520" + integrity sha512-p/f6UZkWlCUfqsX1St8/YKXfvURdG8DVe4GLJw2QPMO+jhhhoFoBL/lnXsWx7bQkC2G4tnIjfa8VMsUL2mNORw== + dependencies: + "@typescript-eslint/eslint-plugin" "^4.0.1" + "@typescript-eslint/parser" "^4.0.1" + eslint-config-airbnb "^18.2.0" + eslint-config-airbnb-base "^14.2.0" + eslint-plugin-import "~2.22.0" + eslint-plugin-jest "^24.0.0" + eslint-plugin-jsx-a11y "^6.3.1" + eslint-plugin-react "^7.20.6" + eslint-plugin-react-hooks "^4.1.0" + "@types/body-parser@*": version "1.19.0" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" @@ -185,6 +215,19 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@typescript-eslint/eslint-plugin@^4.0.1": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.5.0.tgz#4ff9c1d8535ae832e239f0ef6d7210592d9b0b07" + integrity sha512-mjb/gwNcmDKNt+6mb7Aj/TjKzIJjOPcoCJpjBQC9ZnTRnBt1p4q5dJSSmIqAtsZ/Pff5N+hJlbiPc5bl6QN4OQ== + dependencies: + "@typescript-eslint/experimental-utils" "4.5.0" + "@typescript-eslint/scope-manager" "4.5.0" + debug "^4.1.1" + functional-red-black-tree "^1.0.1" + regexpp "^3.0.0" + semver "^7.3.2" + tsutils "^3.17.1" + "@typescript-eslint/eslint-plugin@^4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.3.0.tgz#1a23d904bf8ea248d09dc3761af530d90f39c8fa" @@ -210,6 +253,28 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" +"@typescript-eslint/experimental-utils@4.5.0", "@typescript-eslint/experimental-utils@^4.0.1": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.5.0.tgz#547fe1158609143ce60645383aa1d6f83ada28df" + integrity sha512-bW9IpSAKYvkqDGRZzayBXIgPsj2xmmVHLJ+flGSoN0fF98pGoKFhbunIol0VF2Crka7z984EEhFi623Rl7e6gg== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/scope-manager" "4.5.0" + "@typescript-eslint/types" "4.5.0" + "@typescript-eslint/typescript-estree" "4.5.0" + eslint-scope "^5.0.0" + eslint-utils "^2.0.0" + +"@typescript-eslint/parser@^4.0.1": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.5.0.tgz#b2d659f25eec0041c7bc5660b91db1eefe8d7122" + integrity sha512-xb+gmyhQcnDWe+5+xxaQk5iCw6KqXd8VQxGiTeELTMoYeRjpocZYYRP1gFVM2C8Yl0SpUvLa1lhprwqZ00w3Iw== + dependencies: + "@typescript-eslint/scope-manager" "4.5.0" + "@typescript-eslint/types" "4.5.0" + "@typescript-eslint/typescript-estree" "4.5.0" + debug "^4.1.1" + "@typescript-eslint/parser@^4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.3.0.tgz#684fc0be6551a2bfcb253991eec3c786a8c063a3" @@ -228,11 +293,24 @@ "@typescript-eslint/types" "4.3.0" "@typescript-eslint/visitor-keys" "4.3.0" +"@typescript-eslint/scope-manager@4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.5.0.tgz#8dfd53c3256d4357e7d66c2fc8956835f4d239be" + integrity sha512-C0cEO0cTMPJ/w4RA/KVe4LFFkkSh9VHoFzKmyaaDWAnPYIEzVCtJ+Un8GZoJhcvq+mPFXEsXa01lcZDHDG6Www== + dependencies: + "@typescript-eslint/types" "4.5.0" + "@typescript-eslint/visitor-keys" "4.5.0" + "@typescript-eslint/types@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf" integrity sha512-Cx9TpRvlRjOppGsU6Y6KcJnUDOelja2NNCX6AZwtVHRzaJkdytJWMuYiqi8mS35MRNA3cJSwDzXePfmhU6TANw== +"@typescript-eslint/types@4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.5.0.tgz#98256e07bad1c8d15d0c9627ebec82fd971bb3c3" + integrity sha512-n2uQoXnyWNk0Les9MtF0gCK3JiWd987JQi97dMSxBOzVoLZXCNtxFckVqt1h8xuI1ix01t+iMY4h4rFMj/303g== + "@typescript-eslint/typescript-estree@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.3.0.tgz#0edc1068e6b2e4c7fdc54d61e329fce76241cee8" @@ -247,6 +325,20 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.5.0.tgz#d50cf91ae3a89878401111031eb6fb6d03554f64" + integrity sha512-gN1mffq3zwRAjlYWzb5DanarOPdajQwx5MEWkWCk0XvqC8JpafDTeioDoow2L4CA/RkYZu7xEsGZRhqrTsAG8w== + dependencies: + "@typescript-eslint/types" "4.5.0" + "@typescript-eslint/visitor-keys" "4.5.0" + debug "^4.1.1" + globby "^11.0.1" + is-glob "^4.0.1" + lodash "^4.17.15" + semver "^7.3.2" + tsutils "^3.17.1" + "@typescript-eslint/visitor-keys@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz#0e5ab0a09552903edeae205982e8521e17635ae0" @@ -255,6 +347,14 @@ "@typescript-eslint/types" "4.3.0" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.5.0": + version "4.5.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.5.0.tgz#b59f26213ac597efe87f6b13cf2aabee70542af0" + integrity sha512-UHq4FSa55NDZqscRU//O5ROFhHa9Hqn9KWTEvJGTArtTQp5GKv9Zqf6d/Q3YXXcFv4woyBml7fJQlQ+OuqRcHA== + dependencies: + "@typescript-eslint/types" "4.5.0" + eslint-visitor-keys "^2.0.0" + accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -320,6 +420,14 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -347,11 +455,35 @@ array.prototype.flat@^1.2.3: define-properties "^1.1.3" es-abstract "^1.17.0-next.1" +array.prototype.flatmap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443" + integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" + integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +axe-core@^3.5.4: + version "3.5.5" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" + integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== + +axobject-query@^2.1.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" + integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -516,6 +648,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +core-js-pure@^3.0.0: + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" + integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== + cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -525,6 +662,11 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" +damerau-levenshtein@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" + integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== + debug@2.6.9, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -588,6 +730,13 @@ doctrine@1.5.0: esutils "^2.0.2" isarray "^1.0.0" +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" @@ -612,6 +761,11 @@ emoji-regex@^7.0.1: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +emoji-regex@^9.0.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.0.tgz#a26da8e832b16a9753309f25e35e3c0efb9a066a" + integrity sha512-DNc3KFPK18bPdElMJnf/Pkv5TXhxFU3YFDEuGLDRtPmV4rkmCjBkCSEp22u6rBHdSN9Vlp/GK7k98prmE1Jgug== + encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -701,6 +855,15 @@ eslint-config-airbnb-base@^14.2.0: object.assign "^4.1.0" object.entries "^1.1.2" +eslint-config-airbnb@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/eslint-config-airbnb/-/eslint-config-airbnb-18.2.0.tgz#8a82168713effce8fc08e10896a63f1235499dcd" + integrity sha512-Fz4JIUKkrhO0du2cg5opdyPKQXOI2MvF8KUvN2710nJMT6jaRUpRE2swrJftAjVGL7T1otLM5ieo5RqS1v9Udg== + dependencies: + eslint-config-airbnb-base "^14.2.0" + object.assign "^4.1.0" + object.entries "^1.1.2" + eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" @@ -717,7 +880,7 @@ eslint-module-utils@^2.6.0: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-import@^2.22.1: +eslint-plugin-import@^2.22.1, eslint-plugin-import@~2.22.0: version "2.22.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== @@ -736,6 +899,52 @@ eslint-plugin-import@^2.22.1: resolve "^1.17.0" tsconfig-paths "^3.9.0" +eslint-plugin-jest@^24.0.0: + version "24.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.1.0.tgz#6708037d7602e5288ce877fd0103f329dc978361" + integrity sha512-827YJ+E8B9PvXu/0eiVSNFfxxndbKv+qE/3GSMhdorCaeaOehtqHGX2YDW9B85TEOre9n/zscledkFW/KbnyGg== + dependencies: + "@typescript-eslint/experimental-utils" "^4.0.1" + +eslint-plugin-jsx-a11y@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz#99ef7e97f567cc6a5b8dd5ab95a94a67058a2660" + integrity sha512-i1S+P+c3HOlBJzMFORRbC58tHa65Kbo8b52/TwCwSKLohwvpfT5rm2GjGWzOHTEuq4xxf2aRlHHTtmExDQOP+g== + dependencies: + "@babel/runtime" "^7.10.2" + aria-query "^4.2.2" + array-includes "^3.1.1" + ast-types-flow "^0.0.7" + axe-core "^3.5.4" + axobject-query "^2.1.2" + damerau-levenshtein "^1.0.6" + emoji-regex "^9.0.0" + has "^1.0.3" + jsx-ast-utils "^2.4.1" + language-tags "^1.0.5" + +eslint-plugin-react-hooks@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" + integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== + +eslint-plugin-react@^7.20.6: + version "7.21.5" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.21.5.tgz#50b21a412b9574bfe05b21db176e8b7b3b15bff3" + integrity sha512-8MaEggC2et0wSF6bUeywF7qQ46ER81irOdWS4QWxnnlAEsnzeBevk1sWh7fhpCghPpXb+8Ks7hvaft6L/xsR6g== + dependencies: + array-includes "^3.1.1" + array.prototype.flatmap "^1.2.3" + doctrine "^2.1.0" + has "^1.0.3" + jsx-ast-utils "^2.4.1 || ^3.0.0" + object.entries "^1.1.2" + object.fromentries "^2.0.2" + object.values "^1.1.1" + prop-types "^15.7.2" + resolve "^1.18.1" + string.prototype.matchall "^4.0.2" + eslint-scope@^5.0.0, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -1183,6 +1392,15 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +internal-slot@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" + integrity sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g== + dependencies: + es-abstract "^1.17.0-next.1" + has "^1.0.3" + side-channel "^1.0.2" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -1198,6 +1416,13 @@ is-callable@^1.1.4, is-callable@^1.2.2: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== +is-core-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.0.0.tgz#58531b70aed1db7c0e8d4eb1a0a2d1ddd64bd12d" + integrity sha512-jq1AH6C8MuteOoBPwkxHafmByhL9j5q4OaPGdbuD+ZtQJVzH+i6E3BJDQcBA09k57i2Hh2yQbEG8yObZ0jdlWw== + dependencies: + has "^1.0.3" + is-date-object@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" @@ -1259,7 +1484,7 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -1294,6 +1519,22 @@ json5@^1.0.1: dependencies: minimist "^1.2.0" +jsx-ast-utils@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e" + integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w== + dependencies: + array-includes "^3.1.1" + object.assign "^4.1.0" + +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.1.0.tgz#642f1d7b88aa6d7eb9d8f2210e166478444fa891" + integrity sha512-d4/UOjg+mxAWxCiF0c5UTSwyqbchkbqCvK87aBovhnh8GtysTjWmgC63tY0cJx/HzGgm9qnA147jVBdpOiQ2RA== + dependencies: + array-includes "^3.1.1" + object.assign "^4.1.1" + keyv@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.0.1.tgz#9fe703cb4a94d6d11729d320af033307efd02ee6" @@ -1301,6 +1542,18 @@ keyv@^4.0.0: dependencies: json-buffer "3.0.1" +language-subtag-registry@~0.3.2: + version "0.3.20" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755" + integrity sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg== + +language-tags@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" + integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= + dependencies: + language-subtag-registry "~0.3.2" + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -1332,6 +1585,13 @@ lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" @@ -1475,6 +1735,11 @@ normalize-url@^4.1.0: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + object-inspect@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" @@ -1504,6 +1769,16 @@ object.entries@^1.1.2: es-abstract "^1.17.5" has "^1.0.3" +object.fromentries@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" + integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + function-bind "^1.1.1" + has "^1.0.3" + object.values@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" @@ -1654,6 +1929,15 @@ prom-client@^12.0.0: dependencies: tdigest "^0.1.1" +prop-types@^15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -1700,6 +1984,11 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + read-pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" @@ -1717,6 +2006,19 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +regexp.prototype.flags@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + regexpp@^3.0.0, regexpp@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" @@ -1739,6 +2041,14 @@ resolve@^1.10.0, resolve@^1.13.1, resolve@^1.17.0: dependencies: path-parse "^1.0.6" +resolve@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" + integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== + dependencies: + is-core-module "^2.0.0" + path-parse "^1.0.6" + responselike@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" @@ -1841,6 +2151,14 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +side-channel@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" + integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== + dependencies: + es-abstract "^1.18.0-next.0" + object-inspect "^1.8.0" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" @@ -1900,6 +2218,18 @@ string-width@^3.0.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string.prototype.matchall@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e" + integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0" + has-symbols "^1.0.1" + internal-slot "^1.0.2" + regexp.prototype.flags "^1.3.0" + side-channel "^1.0.2" + string.prototype.trimend@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" From aa028b44292254b2933cbc89137ece6053542394 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 22 Oct 2020 15:13:40 -0700 Subject: [PATCH 13/21] clean up eslint comments since switching --- src/index.ts | 14 ++------------ src/teraslice-stats.ts | 6 ------ 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/index.ts b/src/index.ts index c1eed9d..fa6f444 100644 --- a/src/index.ts +++ b/src/index.ts @@ -265,15 +265,14 @@ function generateExecutionStatusMetrics(execution:any, executionLabels:any) { 'terminated', ]; - // eslint-disable-next-line no-restricted-syntax for (const status of statusList) { const statusLabels = { ...executionLabels, status, }; - // if (status === execution._status) + let state:number; - // eslint-disable-next-line no-underscore-dangle + if (status === execution._status) { state = 1; } else { @@ -352,9 +351,7 @@ function parseExecution(execution:any, labels:any) { if (execution.memory) gaugeMemoryRequest.set(executionLabels, execution.memory); if (execution.memory) gaugeMemoryLimit.set(executionLabels, execution.memory); - // eslint-disable-next-line no-underscore-dangle gaugeCreatedTime.set(executionLabels, new Date(execution._created).getTime() / 1000); - // eslint-disable-next-line no-underscore-dangle gaugeUpdatedTime.set(executionLabels, new Date(execution._updated).getTime() / 1000); gaugeExSlicers.set(executionLabels, execution.slicers); @@ -363,16 +360,12 @@ function parseExecution(execution:any, labels:any) { } function generateControllerStats(terasliceStats:TerasliceStats, labels:any) { - // FIXME: I should rethink this warning - // eslint-disable-next-line no-restricted-syntax for (const controller of terasliceStats.controllers) { parseController(controller, labels); } } function generateExecutionStats(terasliceStats:TerasliceStats, labels:any) { - // FIXME: I should rethink this warning - // eslint-disable-next-line no-restricted-syntax for (const execution of terasliceStats.executions) { parseExecution(execution, labels); } @@ -431,9 +424,7 @@ interface StateExecutionList { function generateExecutionVersions(terasliceStats:TerasliceStats, labels:any) { const executions:StateExecutionList = {}; - // eslint-disable-next-line no-restricted-syntax for (const [, workerNode] of Object.entries(terasliceStats.state)) { - // eslint-disable-next-line no-restricted-syntax for (const worker of workerNode.active) { if (worker.ex_id && !Object.prototype.hasOwnProperty.call(executions, worker.ex_id)) { executions[worker.ex_id] = { @@ -445,7 +436,6 @@ function generateExecutionVersions(terasliceStats:TerasliceStats, labels:any) { } } - // eslint-disable-next-line no-restricted-syntax for (const [, execution] of Object.entries(executions)) { const regex = /.*:(.*)_.*/g; const m = [...execution.image.matchAll(regex)]; diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index 3dc3e61..c01472d 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -1,4 +1,3 @@ -/* eslint-disable camelcase */ import got from 'got'; // TODO: move these interfaces out into their own file @@ -77,13 +76,10 @@ interface TerasliceClusterState { interface TerasliceInfo { arch: string, - // eslint-disable-next-line camelcase clustering_type: string, name: string, - // eslint-disable-next-line camelcase node_version: string, platform: string, - // eslint-disable-next-line camelcase teraslice_version: string } @@ -184,7 +180,6 @@ export default class TerasliceStats implements TerasliceStatsInterface { for (let i = 0; i < this.controllers.length; i += maxConcurrency) { const controllersSlice = this.controllers.slice(i, i + maxConcurrency); - // eslint-disable-next-line no-await-in-loop const r = await Promise.all( controllersSlice.map((x) => this.getTerasliceApi(`/v1/ex/${x.ex_id}`)), ); @@ -192,7 +187,6 @@ export default class TerasliceStats implements TerasliceStatsInterface { // eslint-disable-next-line prefer-spread this.executions.push.apply(this.executions, r.map((x) => x.data)); - // eslint-disable-next-line no-await-in-loop await pDelay(queryDelay); } From 23a37482789d0b4f4196ed6ef9872b14407d38c7 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 22 Oct 2020 15:30:42 -0700 Subject: [PATCH 14/21] add return types --- src/teraslice-stats.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index c01472d..fd61632 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -1,5 +1,10 @@ import got from 'got'; +interface GetTerasliceApiResponse { + data: any, + queryDuration: number +} + // TODO: move these interfaces out into their own file /** * TerasliceWorker - The individual teraslice worker object in the @@ -142,13 +147,10 @@ export default class TerasliceStats implements TerasliceStatsInterface { }; } - async getTerasliceApi(path:string) { + async getTerasliceApi(path:string):Promise { const url = new URL(path, this.baseUrl); - let r: { - data: any, - queryDuration: number - }; - let response : { + let r:GetTerasliceApiResponse; + let response: { body: any, statusCode: number, timings: any @@ -171,7 +173,7 @@ export default class TerasliceStats implements TerasliceStatsInterface { return r; } - async updateExecutions() { + async updateExecutions():Promise { const time = process.hrtime(); this.executions = []; const maxConcurrency = 10; @@ -197,7 +199,7 @@ export default class TerasliceStats implements TerasliceStatsInterface { // I think I've been doing this sort of thing wrong in the past. // https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await - async update() { + async update():Promise { // TODO: hard coding querySize is dumb const querySize = 200; const run = async () => { From 327c3081c7a4630817fc47eb5e20654ab01a1230 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 22 Oct 2020 15:54:38 -0700 Subject: [PATCH 15/21] extract out interfaces and util --- src/index.ts | 12 +--- src/interfaces.ts | 113 +++++++++++++++++++++++++++++++++++++ src/teraslice-stats.ts | 123 +++-------------------------------------- src/util.ts | 6 ++ 4 files changed, 129 insertions(+), 125 deletions(-) create mode 100644 src/interfaces.ts create mode 100644 src/util.ts diff --git a/src/index.ts b/src/index.ts index fa6f444..b20a2ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,9 @@ import { Gauge, Registry } from 'prom-client'; import express from 'express'; import bunyan from 'bunyan'; +import { + StateExecutionList +} from './interfaces'; import TerasliceStats from './teraslice-stats'; const server = express(); @@ -371,15 +374,6 @@ function generateExecutionStats(terasliceStats:TerasliceStats, labels:any) { } } -interface StateExecution { - exId: string, - jobId: string, - image: string -} -interface StateExecutionList { - [key: string]: StateExecution -} - /** * NOTE: This assumes Teraslice is running in Kubernetes mode. * diff --git a/src/interfaces.ts b/src/interfaces.ts new file mode 100644 index 0000000..eb7b118 --- /dev/null +++ b/src/interfaces.ts @@ -0,0 +1,113 @@ +export interface StateExecution { + exId: string, + jobId: string, + image: string +} + +export interface StateExecutionList { + [key: string]: StateExecution +} + +export interface GetTerasliceApiResponse { + data: any, + queryDuration: number +} + +/** + * TerasliceWorker - The individual teraslice worker object in the + * TerasliceWorkerNode.active array. This corresponds to a k8s pod in k8s mode. + * + * Example: + * { + * "assets": [], + * "assignment": "worker", + * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", + * "image": "teraslice:v0.70.0", + * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", + * "pod_name": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa", + * "pod_ip": "10.132.86.111", + * "worker_id": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa" + * } + */ +export interface TerasliceWorker { + assets: string[], + assignment: string, + ex_id: string, + image: string, + job_id: string, + pod_name: string, + pod_ip: string, + worker_id: string +} + +/** + * { + * "node_id": "10.123.4.111", + * "hostname": "10.123.4.111", + * "pid": "N/A", + * "node_version": "N/A", + * "teraslice_version": "N/A", + * "total": "N/A", + * "state": "connected", + * "available": "N/A", + * "active": [...] + * } + */ +export interface TerasliceWorkerNodeInfo { + node_id: string, + hostname: string, + pid: string, + node_version: string, + teraslice_version: string, + total: string, + state: string, + available: string, + active: TerasliceWorker[] +} + +/** + * "10.123.4.111": { + * "node_id": "10.123.4.111", + * "hostname": "10.123.4.111", + * "pid": "N/A", + * "node_version": "N/A", + * "teraslice_version": "N/A", + * "total": "N/A", + * "state": "connected", + * "available": "N/A", + * "active": [...] + * } + */ +export interface TerasliceClusterState { + [key: string]: TerasliceWorkerNodeInfo +} + +export interface TerasliceInfo { + arch: string, + clustering_type: string, + name: string, + node_version: string, + platform: string, + teraslice_version: string +} + +/** + * These are all in ms + */ +export interface TerasliceQueryDuration { + controllers: number, + executions: number, + info: number, + jobs: number, + state: number, +} + +export interface TerasliceStatsInterface { + baseUrl: URL, + controllers: any[], + executions: any[], + info: TerasliceInfo, + jobs: any[], + state: TerasliceClusterState, + queryDuration: TerasliceQueryDuration +} diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index fd61632..5bc1b68 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -1,120 +1,11 @@ import got from 'got'; - -interface GetTerasliceApiResponse { - data: any, - queryDuration: number -} - -// TODO: move these interfaces out into their own file -/** - * TerasliceWorker - The individual teraslice worker object in the - * TerasliceWorkerNode.active array. This corresponds to a k8s pod in k8s mode. - * - * Example: - * { - * "assets": [], - * "assignment": "worker", - * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", - * "image": "teraslice:v0.70.0", - * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", - * "pod_name": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa", - * "pod_ip": "10.132.86.111", - * "worker_id": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa" - * } - */ -interface TerasliceWorker { - assets: string[], - assignment: string, - ex_id: string, - image: string, - job_id: string, - pod_name: string, - pod_ip: string, - worker_id: string -} - -/** - * { - * "node_id": "10.123.4.111", - * "hostname": "10.123.4.111", - * "pid": "N/A", - * "node_version": "N/A", - * "teraslice_version": "N/A", - * "total": "N/A", - * "state": "connected", - * "available": "N/A", - * "active": [...] - * } - */ -interface TerasliceWorkerNodeInfo { - node_id: string, - hostname: string, - pid: string, - node_version: string, - teraslice_version: string, - total: string, - state: string, - available: string, - active: TerasliceWorker[] -} - -/** - * "10.123.4.111": { - * "node_id": "10.123.4.111", - * "hostname": "10.123.4.111", - * "pid": "N/A", - * "node_version": "N/A", - * "teraslice_version": "N/A", - * "total": "N/A", - * "state": "connected", - * "available": "N/A", - * "active": [...] - * } - */ -// interface TerasliceWorkerNode { -// [key: string]: TerasliceWorkerNodeInfo -// } - -interface TerasliceClusterState { - [key: string]: TerasliceWorkerNodeInfo -} - -interface TerasliceInfo { - arch: string, - clustering_type: string, - name: string, - node_version: string, - platform: string, - teraslice_version: string -} - -/** - * These are all in ms - */ -interface TerasliceQueryDuration { - controllers: number, - executions: number, - info: number, - jobs: number, - state: number, -} - -interface TerasliceStatsInterface { - baseUrl: URL, - controllers: any[], - executions: any[], - info: TerasliceInfo, - jobs: any[], - state: TerasliceClusterState, - queryDuration: TerasliceQueryDuration -} - -/** promisified setTimeout */ -export function pDelay(delay = 1, arg?: T): Promise { - return new Promise((resolve) => { - setTimeout(resolve, delay, arg); - }); -} +import { + TerasliceStatsInterface, + GetTerasliceApiResponse, + TerasliceClusterState, + TerasliceQueryDuration +} from './interfaces'; +import { pDelay } from './util'; export default class TerasliceStats implements TerasliceStatsInterface { baseUrl: URL; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..c4c0304 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,6 @@ +/** promisified setTimeout */ +export function pDelay(delay = 1, arg?: T): Promise { + return new Promise((resolve) => { + setTimeout(resolve, delay, arg); + }); +} From 4bb971c356cd3e4b2b7f169c7fb967eb55f72b37 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 22 Oct 2020 17:17:38 -0700 Subject: [PATCH 16/21] Extract metrics out into module --- src/index.ts | 471 +---------------------------------------- src/metrics.ts | 471 +++++++++++++++++++++++++++++++++++++++++ src/teraslice-stats.ts | 3 +- 3 files changed, 473 insertions(+), 472 deletions(-) create mode 100644 src/metrics.ts diff --git a/src/index.ts b/src/index.ts index b20a2ac..fc7f258 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,31 +1,14 @@ import { URL } from 'url'; -import { Gauge, Registry } from 'prom-client'; import express from 'express'; import bunyan from 'bunyan'; -import { - StateExecutionList -} from './interfaces'; import TerasliceStats from './teraslice-stats'; +import { metricsRegistry, updateTerasliceMetrics } from './metrics'; const server = express(); - -const metricsRegistry = new Registry(); const logger = bunyan.createLogger({ name: 'teraslice_exporter' }); -const metricPrefix = 'teraslice'; -const globalLabelNames = [ - 'url', - 'name', -]; -const exLabelNames = [ - 'ex_id', - 'job_id', - 'job_name', - ...globalLabelNames, -]; - declare let process : { env: { DEBUG: string, @@ -35,458 +18,6 @@ declare let process : { } }; -const gaugeWorkersActive = new Gauge({ - name: `${metricPrefix}_controller_workers_active`, - help: 'Number of Teraslice workers actively processing slices.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeWorkersAvailable = new Gauge({ - name: `${metricPrefix}_controller_workers_available`, - help: 'Number of Teraslice workers running and waiting for work.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeWorkersJoined = new Gauge({ - name: `${metricPrefix}_controller_workers_joined`, - help: 'Total number of Teraslice workers that have joined the execution controller for this job.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeWorkersReconnected = new Gauge({ - name: `${metricPrefix}_controller_workers_reconnected`, - help: 'Total number of Teraslice workers that have reconnected to the execution controller for this job.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeWorkersDisconnected = new Gauge({ - name: `${metricPrefix}_controller_workers_disconnected`, - help: 'Total number of Teraslice workers that have disconnected from execution controller for this job.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeTerasliceMasterInfo = new Gauge({ - name: `${metricPrefix}_master_info`, - help: 'Information about the Teraslice master node.', - labelNames: [ - 'arch', - 'clustering_type', - 'name', - 'node_version', - 'platform', - 'teraslice_version', - ...globalLabelNames, - ], - registers: [metricsRegistry], -}); - -const gaugeExecutionInfo = new Gauge({ - name: `${metricPrefix}_execution_info`, - help: 'Information about Teraslice execution.', - labelNames: [ - 'ex_id', - 'job_id', - 'image', - 'version', - ...globalLabelNames, - ], - registers: [metricsRegistry], -}); - -const gaugeNumSlicers = new Gauge({ - name: `${metricPrefix}_controller_slicers_count`, - help: 'Number of execution controllers (slicers) running for this execution.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeQueryDuration = new Gauge({ - name: `${metricPrefix}_query_duration`, - help: 'Total time to complete the named query, in ms.', - labelNames: ['query_name', ...globalLabelNames], - registers: [metricsRegistry], -}); - -// Execution Related Metrics - -const gaugeCpuLimit = new Gauge({ - name: `${metricPrefix}_execution_cpu_limit`, - help: 'CPU core limit for a Teraslice worker container.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeCpuRequest = new Gauge({ - name: `${metricPrefix}_execution_cpu_request`, - help: 'Requested number of CPU cores for a Teraslice worker container.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeMemoryLimit = new Gauge({ - name: `${metricPrefix}_execution_memory_limit`, - help: 'Memory limit for Teraslice a worker container.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeMemoryRequest = new Gauge({ - name: `${metricPrefix}_execution_memory_request`, - help: 'Requested amount of memory for a Teraslice worker container.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeExStatus = new Gauge({ - name: `${metricPrefix}_execution_status`, - help: 'Current status of the Teraslice execution.', - labelNames: [...exLabelNames, 'status'], - registers: [metricsRegistry], -}); - -// The following gauges should be Counters by my reconing, but as far as -// prom-client is concerned, this usage is fine: -// https://github.com/siimon/prom-client/issues/192 -const gaugeSlicesProcessed = new Gauge({ - name: `${metricPrefix}_controller_slices_processed`, - help: 'Number of slices processed.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeSlicesFailed = new Gauge({ - name: `${metricPrefix}_controller_slices_failed`, - help: 'Number of slices failed.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeSlicesQueued = new Gauge({ - name: `${metricPrefix}_controller_slices_queued`, - help: 'Number of slices queued for processing.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -// Execution Related Metrics - -const gaugeCreatedTime = new Gauge({ - name: `${metricPrefix}_execution_created_timestamp_seconds`, - help: 'Execution creation time.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeUpdatedTime = new Gauge({ - name: `${metricPrefix}_execution_updated_timestamp_seconds`, - help: 'Execution update time.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeExSlicers = new Gauge({ - name: `${metricPrefix}_execution_slicers`, - help: 'Number of slicers defined on the execution.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -const gaugeExWorkers = new Gauge({ - name: `${metricPrefix}_execution_workers`, - help: 'Number of workers defined on the execution. Note that the number of actual workers can differ from this value.', - labelNames: exLabelNames, - registers: [metricsRegistry], -}); - -/** - * parseController adds the teraslice execution controller metrics to the - * metricsRegistry for a single execution. - * - * { - * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", - * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", - * "name": "my-job-name", - * "workers_available": 0, - * "workers_active": 6, - * "workers_joined": 6, - * "workers_reconnected": 0, - * "workers_disconnected": 0, - * "job_duration": 0, - * "failed": 1, - * "subslices": 0, - * "queued": 7, - * "slice_range_expansion": 0, - * "processed": 204156, - * "slicers": 1, - * "subslice_by_key": 0, - * "started": "2020-09-17T21:08:58.905Z", - * "queuing_complete": "" - * } - * - * @param controller - */ -function parseController(controller:any, labels:any) { - const controllerLabels = { - ex_id: controller.ex_id, - job_id: controller.job_id, - job_name: controller.name, - ...labels, - }; - - gaugeWorkersActive.set(controllerLabels, controller.workers_active); - gaugeWorkersAvailable.set(controllerLabels, controller.workers_available); - gaugeWorkersJoined.set(controllerLabels, controller.workers_joined); - gaugeWorkersReconnected.set(controllerLabels, controller.workers_reconnected); - gaugeWorkersDisconnected.set(controllerLabels, controller.workers_disconnected); - - gaugeSlicesProcessed.set(controllerLabels, controller.processed); - gaugeSlicesFailed.set(controllerLabels, controller.failed); - gaugeSlicesQueued.set(controllerLabels, controller.queued); - - gaugeNumSlicers.set(controllerLabels, controller.slicers); -} - -function generateExecutionStatusMetrics(execution:any, executionLabels:any) { - const statusList = [ - 'completed', - 'failed', - 'failing', - 'initializing', - 'paused', - 'pending', - 'recovering', - 'rejected', - 'running', - 'scheduling', - 'stopped', - 'stopping', - 'terminated', - ]; - - for (const status of statusList) { - const statusLabels = { - ...executionLabels, - status, - }; - - let state:number; - - if (status === execution._status) { - state = 1; - } else { - state = 0; - } - gaugeExStatus.set(statusLabels, state); - } -} - -/** - * { - "analytics": true, - "performance_metrics": false, - "assets": [ - "19b4f13148f64bc5a3fcfc53f96a5d646141a111", - "b652a2d09f71e68dd0ca15f6b5a14136b181e111" - ], - "autorecover": false, - "lifecycle": "persistent", - "max_retries": 3, - "name": "my-job-name", - "operations": [ - { - ... - }, - ... - ], - "apis": [], - "probation_window": 300000, - "slicers": 1, - "workers": 6, - "labels": null, - "env_vars": {}, - "targets": [ - { - "key": "failure-domain.beta.kubernetes.io/zone", - "value": "west" - } - ], - "cpu": 1.5, - "memory": 3221225472, - "volumes": [], - "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", - "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", - "metadata": {}, - "slicer_port": 45680, - "slicer_hostname": "10.32.97.23", - "_context": "ex", - "_created": "2020-07-09T21:30:34.537Z", - "_updated": "2020-07-09T21:30:42.745Z", - "_status": "running", - "_has_errors": false, - "_slicer_stats": {}, - "_failureReason": "" - } - * @param execution - * @param labels - */ -function parseExecution(execution:any, labels:any) { - const executionLabels = { - ex_id: execution.ex_id, - job_id: execution.job_id, - job_name: execution.name, - ...labels, - }; - - // NOTE: Optional settings that are undefined are just excluded with a - // conditional below. - - // TODO: At some point workers will have different CPU Limits and Requests, - // https://github.com/terascope/teraslice/issues/2202 - // for now, these are set to the same thing, but I split them since I know a - // change is coming. - if (execution.cpu) gaugeCpuRequest.set(executionLabels, execution.cpu); - if (execution.cpu) gaugeCpuLimit.set(executionLabels, execution.cpu); - if (execution.memory) gaugeMemoryRequest.set(executionLabels, execution.memory); - if (execution.memory) gaugeMemoryLimit.set(executionLabels, execution.memory); - - gaugeCreatedTime.set(executionLabels, new Date(execution._created).getTime() / 1000); - gaugeUpdatedTime.set(executionLabels, new Date(execution._updated).getTime() / 1000); - - gaugeExSlicers.set(executionLabels, execution.slicers); - gaugeExWorkers.set(executionLabels, execution.workers); - generateExecutionStatusMetrics(execution, executionLabels); -} - -function generateControllerStats(terasliceStats:TerasliceStats, labels:any) { - for (const controller of terasliceStats.controllers) { - parseController(controller, labels); - } -} - -function generateExecutionStats(terasliceStats:TerasliceStats, labels:any) { - for (const execution of terasliceStats.executions) { - parseExecution(execution, labels); - } -} - -/** - * NOTE: This assumes Teraslice is running in Kubernetes mode. - * - * generateExecutionVersions - takes the /cluster/state output, which looks like - * this: - * - * "10.123.4.111": { - * "node_id": "10.123.4.111", - * "hostname": "10.123.4.111", - * "pid": "N/A", - * "node_version": "N/A", - * "teraslice_version": "N/A", - * "total": "N/A", - * "state": "connected", - * "available": "N/A", - * "active": [ - * { - * "assets": [], - * "assignment": "worker", - * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", - * "image": "teraslice:v0.70.0", - * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", - * "pod_name": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa", - * "pod_ip": "10.132.86.111", - * "worker_id": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa" - * } - * ] - * } - * - * and makes something like this: - * - * { - * exId: {exId, jobId, image}, - * exId: {exId, jobId, image} - * } - * - * That then gets used to generate the metrics. - * - * @param terasliceStats - * @param labels - */ -function generateExecutionVersions(terasliceStats:TerasliceStats, labels:any) { - const executions:StateExecutionList = {}; - - for (const [, workerNode] of Object.entries(terasliceStats.state)) { - for (const worker of workerNode.active) { - if (worker.ex_id && !Object.prototype.hasOwnProperty.call(executions, worker.ex_id)) { - executions[worker.ex_id] = { - exId: worker.ex_id, - jobId: worker.job_id, - image: worker.image, - }; - } - } - } - - for (const [, execution] of Object.entries(executions)) { - const regex = /.*:(.*)_.*/g; - const m = [...execution.image.matchAll(regex)]; - let version = ''; - if (m[0] !== []) { - // eslint-disable-next-line prefer-destructuring - version = m[0][1]; - } - const executionLabels = { - ex_id: execution.exId, - job_id: execution.jobId, - image: execution.image, - version, - ...labels, - }; - gaugeExecutionInfo.set(executionLabels, 1); - } -} - -function updateTerasliceMetrics(terasliceStats: TerasliceStats) { - const globalLabels = { - url: terasliceStats.baseUrl.toString(), - name: terasliceStats.info.name, - }; - // NOTE: This set of labels expands out to including 'name' twice, right now - // they reduce to a single 'name' label ... I could end up regretting this. - gaugeTerasliceMasterInfo.set( - { ...terasliceStats.info, ...globalLabels }, - 1, - ); - - generateControllerStats(terasliceStats, globalLabels); - generateExecutionStats(terasliceStats, globalLabels); - generateExecutionVersions(terasliceStats, globalLabels); - - gaugeQueryDuration.set( - { query_name: 'info', ...globalLabels }, - terasliceStats.queryDuration.info, - ); - gaugeQueryDuration.set( - { query_name: 'jobs', ...globalLabels }, - terasliceStats.queryDuration.jobs, - ); - gaugeQueryDuration.set( - { query_name: 'controllers', ...globalLabels }, - terasliceStats.queryDuration.controllers, - ); - gaugeQueryDuration.set( - { query_name: 'executions', ...globalLabels }, - terasliceStats.queryDuration.executions, - ); - gaugeQueryDuration.set( - { query_name: 'state', ...globalLabels }, - terasliceStats.queryDuration.state, - ); -} - async function main() { let baseUrl: string; const metricsEndpoint = '/metrics'; diff --git a/src/metrics.ts b/src/metrics.ts new file mode 100644 index 0000000..42021ce --- /dev/null +++ b/src/metrics.ts @@ -0,0 +1,471 @@ +import { Gauge, Registry } from 'prom-client'; +import { + StateExecutionList +} from './interfaces'; +import TerasliceStats from './teraslice-stats'; + +export const metricsRegistry = new Registry(); + +const metricPrefix = 'teraslice'; +const globalLabelNames = [ + 'url', + 'name', +]; +const exLabelNames = [ + 'ex_id', + 'job_id', + 'job_name', + ...globalLabelNames, +]; + +const gaugeWorkersActive = new Gauge({ + name: `${metricPrefix}_controller_workers_active`, + help: 'Number of Teraslice workers actively processing slices.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeWorkersAvailable = new Gauge({ + name: `${metricPrefix}_controller_workers_available`, + help: 'Number of Teraslice workers running and waiting for work.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeWorkersJoined = new Gauge({ + name: `${metricPrefix}_controller_workers_joined`, + help: 'Total number of Teraslice workers that have joined the execution controller for this job.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeWorkersReconnected = new Gauge({ + name: `${metricPrefix}_controller_workers_reconnected`, + help: 'Total number of Teraslice workers that have reconnected to the execution controller for this job.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeWorkersDisconnected = new Gauge({ + name: `${metricPrefix}_controller_workers_disconnected`, + help: 'Total number of Teraslice workers that have disconnected from execution controller for this job.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeTerasliceMasterInfo = new Gauge({ + name: `${metricPrefix}_master_info`, + help: 'Information about the Teraslice master node.', + labelNames: [ + 'arch', + 'clustering_type', + 'name', + 'node_version', + 'platform', + 'teraslice_version', + ...globalLabelNames, + ], + registers: [metricsRegistry], +}); + +const gaugeExecutionInfo = new Gauge({ + name: `${metricPrefix}_execution_info`, + help: 'Information about Teraslice execution.', + labelNames: [ + 'ex_id', + 'job_id', + 'image', + 'version', + ...globalLabelNames, + ], + registers: [metricsRegistry], +}); + +const gaugeNumSlicers = new Gauge({ + name: `${metricPrefix}_controller_slicers_count`, + help: 'Number of execution controllers (slicers) running for this execution.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeQueryDuration = new Gauge({ + name: `${metricPrefix}_query_duration`, + help: 'Total time to complete the named query, in ms.', + labelNames: ['query_name', ...globalLabelNames], + registers: [metricsRegistry], +}); + +// Execution Related Metrics + +const gaugeCpuLimit = new Gauge({ + name: `${metricPrefix}_execution_cpu_limit`, + help: 'CPU core limit for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeCpuRequest = new Gauge({ + name: `${metricPrefix}_execution_cpu_request`, + help: 'Requested number of CPU cores for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeMemoryLimit = new Gauge({ + name: `${metricPrefix}_execution_memory_limit`, + help: 'Memory limit for Teraslice a worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeMemoryRequest = new Gauge({ + name: `${metricPrefix}_execution_memory_request`, + help: 'Requested amount of memory for a Teraslice worker container.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeExStatus = new Gauge({ + name: `${metricPrefix}_execution_status`, + help: 'Current status of the Teraslice execution.', + labelNames: [...exLabelNames, 'status'], + registers: [metricsRegistry], +}); + +// The following gauges should be Counters by my reconing, but as far as +// prom-client is concerned, this usage is fine: +// https://github.com/siimon/prom-client/issues/192 +const gaugeSlicesProcessed = new Gauge({ + name: `${metricPrefix}_controller_slices_processed`, + help: 'Number of slices processed.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeSlicesFailed = new Gauge({ + name: `${metricPrefix}_controller_slices_failed`, + help: 'Number of slices failed.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeSlicesQueued = new Gauge({ + name: `${metricPrefix}_controller_slices_queued`, + help: 'Number of slices queued for processing.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +// Execution Related Metrics + +const gaugeCreatedTime = new Gauge({ + name: `${metricPrefix}_execution_created_timestamp_seconds`, + help: 'Execution creation time.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeUpdatedTime = new Gauge({ + name: `${metricPrefix}_execution_updated_timestamp_seconds`, + help: 'Execution update time.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeExSlicers = new Gauge({ + name: `${metricPrefix}_execution_slicers`, + help: 'Number of slicers defined on the execution.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +const gaugeExWorkers = new Gauge({ + name: `${metricPrefix}_execution_workers`, + help: 'Number of workers defined on the execution. Note that the number of actual workers can differ from this value.', + labelNames: exLabelNames, + registers: [metricsRegistry], +}); + +/** + * parseController adds the teraslice execution controller metrics to the + * metricsRegistry for a single execution. + * + * { + * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", + * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", + * "name": "my-job-name", + * "workers_available": 0, + * "workers_active": 6, + * "workers_joined": 6, + * "workers_reconnected": 0, + * "workers_disconnected": 0, + * "job_duration": 0, + * "failed": 1, + * "subslices": 0, + * "queued": 7, + * "slice_range_expansion": 0, + * "processed": 204156, + * "slicers": 1, + * "subslice_by_key": 0, + * "started": "2020-09-17T21:08:58.905Z", + * "queuing_complete": "" + * } + * + * @param controller + */ +function parseController(controller:any, labels:any) { + const controllerLabels = { + ex_id: controller.ex_id, + job_id: controller.job_id, + job_name: controller.name, + ...labels, + }; + + gaugeWorkersActive.set(controllerLabels, controller.workers_active); + gaugeWorkersAvailable.set(controllerLabels, controller.workers_available); + gaugeWorkersJoined.set(controllerLabels, controller.workers_joined); + gaugeWorkersReconnected.set(controllerLabels, controller.workers_reconnected); + gaugeWorkersDisconnected.set(controllerLabels, controller.workers_disconnected); + + gaugeSlicesProcessed.set(controllerLabels, controller.processed); + gaugeSlicesFailed.set(controllerLabels, controller.failed); + gaugeSlicesQueued.set(controllerLabels, controller.queued); + + gaugeNumSlicers.set(controllerLabels, controller.slicers); +} + +function generateExecutionStatusMetrics(execution:any, executionLabels:any) { + const statusList = [ + 'completed', + 'failed', + 'failing', + 'initializing', + 'paused', + 'pending', + 'recovering', + 'rejected', + 'running', + 'scheduling', + 'stopped', + 'stopping', + 'terminated', + ]; + + for (const status of statusList) { + const statusLabels = { + ...executionLabels, + status, + }; + + let state:number; + + if (status === execution._status) { + state = 1; + } else { + state = 0; + } + gaugeExStatus.set(statusLabels, state); + } +} + +/** + * { + "analytics": true, + "performance_metrics": false, + "assets": [ + "19b4f13148f64bc5a3fcfc53f96a5d646141a111", + "b652a2d09f71e68dd0ca15f6b5a14136b181e111" + ], + "autorecover": false, + "lifecycle": "persistent", + "max_retries": 3, + "name": "my-job-name", + "operations": [ + { + ... + }, + ... + ], + "apis": [], + "probation_window": 300000, + "slicers": 1, + "workers": 6, + "labels": null, + "env_vars": {}, + "targets": [ + { + "key": "failure-domain.beta.kubernetes.io/zone", + "value": "west" + } + ], + "cpu": 1.5, + "memory": 3221225472, + "volumes": [], + "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", + "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", + "metadata": {}, + "slicer_port": 45680, + "slicer_hostname": "10.32.97.23", + "_context": "ex", + "_created": "2020-07-09T21:30:34.537Z", + "_updated": "2020-07-09T21:30:42.745Z", + "_status": "running", + "_has_errors": false, + "_slicer_stats": {}, + "_failureReason": "" + } + * @param execution + * @param labels + */ +function parseExecution(execution:any, labels:any) { + const executionLabels = { + ex_id: execution.ex_id, + job_id: execution.job_id, + job_name: execution.name, + ...labels, + }; + + // NOTE: Optional settings that are undefined are just excluded with a + // conditional below. + + // TODO: At some point workers will have different CPU Limits and Requests, + // https://github.com/terascope/teraslice/issues/2202 + // for now, these are set to the same thing, but I split them since I know a + // change is coming. + if (execution.cpu) gaugeCpuRequest.set(executionLabels, execution.cpu); + if (execution.cpu) gaugeCpuLimit.set(executionLabels, execution.cpu); + if (execution.memory) gaugeMemoryRequest.set(executionLabels, execution.memory); + if (execution.memory) gaugeMemoryLimit.set(executionLabels, execution.memory); + + gaugeCreatedTime.set(executionLabels, new Date(execution._created).getTime() / 1000); + gaugeUpdatedTime.set(executionLabels, new Date(execution._updated).getTime() / 1000); + + gaugeExSlicers.set(executionLabels, execution.slicers); + gaugeExWorkers.set(executionLabels, execution.workers); + generateExecutionStatusMetrics(execution, executionLabels); +} + +function generateControllerStats(terasliceStats:TerasliceStats, labels:any) { + for (const controller of terasliceStats.controllers) { + parseController(controller, labels); + } +} + +function generateExecutionStats(terasliceStats:TerasliceStats, labels:any) { + for (const execution of terasliceStats.executions) { + parseExecution(execution, labels); + } +} + +/** + * NOTE: This assumes Teraslice is running in Kubernetes mode. + * + * generateExecutionVersions - takes the /cluster/state output, which looks like + * this: + * + * "10.123.4.111": { + * "node_id": "10.123.4.111", + * "hostname": "10.123.4.111", + * "pid": "N/A", + * "node_version": "N/A", + * "teraslice_version": "N/A", + * "total": "N/A", + * "state": "connected", + * "available": "N/A", + * "active": [ + * { + * "assets": [], + * "assignment": "worker", + * "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510111", + * "image": "teraslice:v0.70.0", + * "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad111", + * "pod_name": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa", + * "pod_ip": "10.132.86.111", + * "worker_id": "ts-wkr-my-job-name-1d940e75-58d9-74c54e7dc1-nxaaa" + * } + * ] + * } + * + * and makes something like this: + * + * { + * exId: {exId, jobId, image}, + * exId: {exId, jobId, image} + * } + * + * That then gets used to generate the metrics. + * + * @param terasliceStats + * @param labels + */ +function generateExecutionVersions(terasliceStats:TerasliceStats, labels:any) { + const executions:StateExecutionList = {}; + + for (const [, workerNode] of Object.entries(terasliceStats.state)) { + for (const worker of workerNode.active) { + if (worker.ex_id && !Object.prototype.hasOwnProperty.call(executions, worker.ex_id)) { + executions[worker.ex_id] = { + exId: worker.ex_id, + jobId: worker.job_id, + image: worker.image, + }; + } + } + } + + for (const [, execution] of Object.entries(executions)) { + const regex = /.*:(.*)_.*/g; + const m = [...execution.image.matchAll(regex)]; + let version = ''; + if (m[0] !== []) { + // eslint-disable-next-line prefer-destructuring + version = m[0][1]; + } + const executionLabels = { + ex_id: execution.exId, + job_id: execution.jobId, + image: execution.image, + version, + ...labels, + }; + gaugeExecutionInfo.set(executionLabels, 1); + } +} + +export function updateTerasliceMetrics(terasliceStats: TerasliceStats): void { + const globalLabels = { + url: terasliceStats.baseUrl.toString(), + name: terasliceStats.info.name, + }; + // NOTE: This set of labels expands out to including 'name' twice, right now + // they reduce to a single 'name' label ... I could end up regretting this. + gaugeTerasliceMasterInfo.set( + { ...terasliceStats.info, ...globalLabels }, + 1, + ); + + generateControllerStats(terasliceStats, globalLabels); + generateExecutionStats(terasliceStats, globalLabels); + generateExecutionVersions(terasliceStats, globalLabels); + + gaugeQueryDuration.set( + { query_name: 'info', ...globalLabels }, + terasliceStats.queryDuration.info, + ); + gaugeQueryDuration.set( + { query_name: 'jobs', ...globalLabels }, + terasliceStats.queryDuration.jobs, + ); + gaugeQueryDuration.set( + { query_name: 'controllers', ...globalLabels }, + terasliceStats.queryDuration.controllers, + ); + gaugeQueryDuration.set( + { query_name: 'executions', ...globalLabels }, + terasliceStats.queryDuration.executions, + ); + gaugeQueryDuration.set( + { query_name: 'state', ...globalLabels }, + terasliceStats.queryDuration.state, + ); +} diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index 5bc1b68..51af93c 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -48,8 +48,7 @@ export default class TerasliceStats implements TerasliceStatsInterface { }; try { - // FIXME: the .toString is to eliminate a 'No overload matches this call' - response = await got(url.toString(), { responseType: 'json' }); + response = await got(url, { responseType: 'json' }); if (response && response.statusCode === 200 && response.body) { r = { data: response.body, From aab835322e858311eb5c676988b1e8d47548db98 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 22 Oct 2020 17:19:14 -0700 Subject: [PATCH 17/21] remove unneeded logging --- src/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/index.ts b/src/index.ts index fc7f258..88c4cc4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -47,9 +47,6 @@ async function main() { await terasliceStats.update(); updateTerasliceMetrics(terasliceStats); - logger.debug(`executions: ${JSON.stringify(terasliceStats.executions.slice(0, 2), null, 2)}`); - logger.debug(`controllers: ${JSON.stringify(terasliceStats.controllers.slice(0, 2), null, 2)}`); - setInterval(async () => { logger.debug(`Updating Teraslice Cluster Information from ${baseUrl}`); await terasliceStats.update(); From 7fcbd14a632fdb466bf75dbf01012a79fc4a9478 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Thu, 22 Oct 2020 18:21:51 -0700 Subject: [PATCH 18/21] tweak readme and index --- README.md | 134 ++++++++++++++++++--------------------------------- src/index.ts | 8 +-- 2 files changed, 53 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 8663c1f..87e9b64 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # Teraslice Job Exporter README +Note: This exporter is only meant for use with Teraslice using Kubernetes +clustering. It hasn't been tested with Teraslice running in Native clustering +mode. + ## Usage So far it works like this: @@ -7,101 +11,59 @@ So far it works like this: ```bash TERASLICE_URL="https://localhost" \ DEBUG=True \ - NODE_EXTRA_CA_CERTS=~/Downloads/ca.crt \ + NODE_EXTRA_CA_CERTS=/path/to/ca.crt \ node dist/index.js | bunyan ``` -## Design - -Scrape the `/v1/cluster/controllers` endpoint periodically to get an array of -active controllers, like this +All options are passed as environment variables -```json -[ - { - "ex_id": "5ba1da6a-0ba2-49f4-92c3-d436ba510e59", - "job_id": "7e6dfa3c-6665-455d-9d52-f11bd32ad18a", - "name": "my_job_name", - "workers_available": 0, - "workers_active": 6, - "workers_joined": 6, - "workers_reconnected": 0, - "workers_disconnected": 0, - "job_duration": 0, - "failed": 1, - "subslices": 0, - "queued": 7, - "slice_range_expansion": 0, - "processed": 204156, - "slicers": 1, - "subslice_by_key": 0, - "started": "2020-09-17T21:08:58.905Z", - "queuing_complete": "" - } -] -``` - -Labels? - -```txt -cluster = ts-prod -ex_id = 5ba1da6a-0ba2-49f4-92c3-d436ba510e59, -job_id = 7e6dfa3c-6665-455d-9d52-f11bd32ad18a, -name = my_job_name, +```bash +TERASLICE_URL="https://localhost" \ + DEBUG=True \ + NODE_EXTRA_CA_CERTS=/path/to/ca.crt \ + PORT=4242 \ + TERASLICE_QUERY_DELAY=90000 + node dist/index.js | bunyan ``` -The following metrics related to each job: +The `TERASLICE_URL` is the only environment variable that is required. -```txt -"workers_available": 0, -"workers_active": 6, -"workers_joined": 6, -"workers_reconnected": 0, -"workers_disconnected": 0, -"job_duration": 0, -"failed": 1, -"subslices": 0, -"queued": 7, -"slice_range_expansion": 0, -"processed": 204156, -"slicers": 1, -"subslice_by_key": 0, -"started": "2020-09-17T21:08:58.905Z", -"queuing_complete": "" -``` +### Environment variables -The following metrics related to the query response itself (timing info) derived -from this timing info: +* `TERASLICE_URL` - URL to the Teraslice Instance to Monitor +* `DEBUG` - Enable debug logging +* `NODE_EXTRA_CA_CERTS` - Standard Node variable to specify CA cert for SSL +connections +* `PORT` - The port that the http express server will listen on +* `TERASLICE_QUERY_DELAY` - The delay between updating the Teraslice stats, this +value is in ms. -```json -{ - start: 1601417492206, - socket: 1601417492208, - lookup: 1601417492213, - connect: 1601417492247, - secureConnect: 1601417492315, - upload: 1601417492316, - response: 1601417493491, - end: 1601417493518, - error: undefined, - abort: undefined, - phases: { - wait: 2, - dns: 5, - tcp: 34, - tls: 68, - request: 1, - firstByte: 1175, - download: 27, - total: 1312 - } -} -``` +## Design -Use the following: +The exporter will scrape several of the Teraslice API endpoints every +`TERASLICE_QUERY_DELAY` milliseconds and update it's exported metrics after that +update is completed. -```txt -got_phase_firstByte: 1175, -got_phase_download: 27, -got_phase_total: 1312 +```text +# HELP teraslice_controller_slicers_count Number of execution controllers (slicers) running for this execution. +# HELP teraslice_controller_slices_failed Number of slices failed. +# HELP teraslice_controller_slices_processed Number of slices processed. +# HELP teraslice_controller_slices_queued Number of slices queued for processing. +# HELP teraslice_controller_workers_active Number of Teraslice workers actively processing slices. +# HELP teraslice_controller_workers_available Number of Teraslice workers running and waiting for work. +# HELP teraslice_controller_workers_disconnected Total number of Teraslice workers that have disconnected from execution controller for this job. +# HELP teraslice_controller_workers_joined Total number of Teraslice workers that have joined the execution controller for this job. +# HELP teraslice_controller_workers_reconnected Total number of Teraslice workers that have reconnected to the execution controller for this job. +# HELP teraslice_execution_cpu_limit CPU core limit for a Teraslice worker container. +# HELP teraslice_execution_cpu_request Requested number of CPU cores for a Teraslice worker container. +# HELP teraslice_execution_created_timestamp_seconds Execution creation time. +# HELP teraslice_execution_info Information about Teraslice execution. +# HELP teraslice_execution_memory_limit Memory limit for Teraslice a worker container. +# HELP teraslice_execution_memory_request Requested amount of memory for a Teraslice worker container. +# HELP teraslice_execution_slicers Number of slicers defined on the execution. +# HELP teraslice_execution_status Current status of the Teraslice execution. +# HELP teraslice_execution_updated_timestamp_seconds Execution update time. +# HELP teraslice_execution_workers Number of workers defined on the execution. Note that the number of actual workers can differ from this value. +# HELP teraslice_master_info Information about the Teraslice master node. +# HELP teraslice_query_duration Total time to complete the named query, in ms. ``` diff --git a/src/index.ts b/src/index.ts index 88c4cc4..d4955ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,19 +41,21 @@ async function main() { res.send(`See the '${metricsEndpoint}' endpoint for the teraslice exporter.`); }); - if (process.env.DEBUG) logger.level('debug'); + // Node 14 introduces the ?. operator, which can be chained. This forces + // node v14+ + if (process?.env?.DEBUG?.toLowerCase() === 'true') logger.level('debug'); const terasliceStats = new TerasliceStats(baseUrl); await terasliceStats.update(); updateTerasliceMetrics(terasliceStats); setInterval(async () => { - logger.debug(`Updating Teraslice Cluster Information from ${baseUrl}`); + logger.info(`Updating Teraslice Cluster Information from ${baseUrl}`); await terasliceStats.update(); updateTerasliceMetrics(terasliceStats); logger.debug(`queryDurations: ${JSON.stringify(terasliceStats.queryDuration)}`); - logger.debug(`datasetSizes: ${JSON.stringify({ + logger.info(`datasetSizes: ${JSON.stringify({ info: terasliceStats.info.length, controllers: terasliceStats.controllers.length, executions: terasliceStats.executions.length, From fe5c61c78333b3db5f9e1afa75b85ff983412183 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Fri, 23 Oct 2020 14:20:04 -0700 Subject: [PATCH 19/21] Structured logging improvements, add Dockerfile --- Dockerfile | 14 ++++++++++++++ README.md | 16 ++++++++++++++++ package.json | 2 +- src/index.ts | 22 ++++++++++++++-------- 4 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..498e8d8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM node:14-buster-slim + +WORKDIR /app/source + +COPY package.json . + +RUN npm install && npm install typescript + +ADD . /app/source + +RUN npx tsc + +CMD [ "npm", "start" ] +EXPOSE 3000 diff --git a/README.md b/README.md index 87e9b64..f0d3836 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,22 @@ connections * `TERASLICE_QUERY_DELAY` - The delay between updating the Teraslice stats, this value is in ms. +### Docker + +Build the docker image: + +```bash +docker build -t teraslice-exporter:v0.1.0 . +``` + +Run the docker image: + +```bash +docker run --rm -p 3000:3000 \ + -e TERASLICE_URL="http://url.to.teraslice/" \ + teraslice-exporter:v0.1.0 | bunyan +``` + ## Design The exporter will scrape several of the Teraslice API endpoints every diff --git a/package.json b/package.json index 92e7c30..67708b3 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,6 @@ "scripts": { "build": "rm -rf dist/ && npx tsc", "build:watch": "rm -rf dist/ && npx tsc -w", - "run": "node dist/index.js" + "start": "node dist/index.js" } } diff --git a/src/index.ts b/src/index.ts index d4955ec..0ace1a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,17 +50,23 @@ async function main() { updateTerasliceMetrics(terasliceStats); setInterval(async () => { - logger.info(`Updating Teraslice Cluster Information from ${baseUrl}`); + logger.info({ terasliceUrl: baseUrl }, 'Begining update of Teraslice state'); await terasliceStats.update(); updateTerasliceMetrics(terasliceStats); - logger.debug(`queryDurations: ${JSON.stringify(terasliceStats.queryDuration)}`); - logger.info(`datasetSizes: ${JSON.stringify({ - info: terasliceStats.info.length, - controllers: terasliceStats.controllers.length, - executions: terasliceStats.executions.length, - jobs: terasliceStats.jobs.length, - })}`); + const datasetSizes = { + terasliceUrl: baseUrl, + datasetSizes: { + info: terasliceStats.info.length, + controllers: terasliceStats.controllers.length, + executions: terasliceStats.executions.length, + jobs: terasliceStats.jobs.length, + } + }; + logger.debug( + { terasliceUrl: baseUrl, queryDurations: terasliceStats.queryDuration }, 'Query Durations' + ); + logger.info(datasetSizes, 'Update complete.'); }, terasliceQueryDelay); logger.info(`HTTP server listening to ${port}, metrics exposed on ${metricsEndpoint} endpoint`); From a837a4ccd9a6a7b7baeefae1c02d2019f93441c4 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Fri, 23 Oct 2020 15:41:08 -0700 Subject: [PATCH 20/21] add url to logging and use docke build image --- Dockerfile | 10 ++++------ src/index.ts | 18 +++++++++--------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/Dockerfile b/Dockerfile index 498e8d8..5817c4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,12 @@ -FROM node:14-buster-slim - +FROM node:14-buster-slim as build WORKDIR /app/source - COPY package.json . - RUN npm install && npm install typescript - ADD . /app/source - RUN npx tsc +FROM node:14-buster-slim +WORKDIR /app/source +COPY --from=build /app/source /app/source CMD [ "npm", "start" ] EXPOSE 3000 diff --git a/src/index.ts b/src/index.ts index 0ace1a5..32c4f30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,9 +6,6 @@ import bunyan from 'bunyan'; import TerasliceStats from './teraslice-stats'; import { metricsRegistry, updateTerasliceMetrics } from './metrics'; -const server = express(); -const logger = bunyan.createLogger({ name: 'teraslice_exporter' }); - declare let process : { env: { DEBUG: string, @@ -20,6 +17,9 @@ declare let process : { async function main() { let baseUrl: string; + + const server = express(); + const port = process.env.PORT || 3000; const metricsEndpoint = '/metrics'; if (process.env.TERASLICE_URL) { @@ -29,8 +29,11 @@ async function main() { } else { throw new Error('The TERASLICE_URL environment variable must be a valid URL to the root of your teraslice instance.'); } - const port = process.env.PORT || 3000; const terasliceQueryDelay = process.env.TERASLICE_QUERY_DELAY || 30000; // ms + const logger = bunyan.createLogger({ + name: 'teraslice_exporter', + terasliceUrl: baseUrl + }); server.get(metricsEndpoint, (req, res) => { res.set('Content-Type', metricsRegistry.contentType); @@ -50,12 +53,11 @@ async function main() { updateTerasliceMetrics(terasliceStats); setInterval(async () => { - logger.info({ terasliceUrl: baseUrl }, 'Begining update of Teraslice state'); + logger.info('Begining update of Teraslice state'); await terasliceStats.update(); updateTerasliceMetrics(terasliceStats); const datasetSizes = { - terasliceUrl: baseUrl, datasetSizes: { info: terasliceStats.info.length, controllers: terasliceStats.controllers.length, @@ -63,9 +65,7 @@ async function main() { jobs: terasliceStats.jobs.length, } }; - logger.debug( - { terasliceUrl: baseUrl, queryDurations: terasliceStats.queryDuration }, 'Query Durations' - ); + logger.debug({ queryDurations: terasliceStats.queryDuration }, 'Query Durations'); logger.info(datasetSizes, 'Update complete.'); }, terasliceQueryDelay); From dfb1c250ebc4888449c764759c0b6dc9928abd13 Mon Sep 17 00:00:00 2001 From: Austin Godber Date: Fri, 23 Oct 2020 15:44:25 -0700 Subject: [PATCH 21/21] remove catch to let the error raise on its own --- src/teraslice-stats.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/teraslice-stats.ts b/src/teraslice-stats.ts index 51af93c..6daa58a 100644 --- a/src/teraslice-stats.ts +++ b/src/teraslice-stats.ts @@ -110,8 +110,6 @@ export default class TerasliceStats implements TerasliceStatsInterface { this.queryDuration.controllers = controllers.queryDuration; this.queryDuration.state = state.queryDuration; }; - await run().catch((err) => { - throw new Error(`Error caught on run() ${this.baseUrl}: ${err}`); - }); + await run(); } }