diff --git a/package-lock.json b/package-lock.json index 164b47c..0757e5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,16 +12,17 @@ "@types/express": "^4.16.1", "@types/mustache": "^0.8.32", "@types/node": "^12.0.2", - "@types/request": "^2.48.1", + "@types/request": "^2.48.12", "axios": "1.6.8", "body-parser": "^1.20.2", "express": "^4.19.2", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "json-rules-engine": "^2.3.6", "moment-timezone": "^0.5.31", "mustache": "^3.0.1", "notifme-sdk": "^1.14.1", "pg": "^8.2.1", + "prom-client": "^15.1.2", "reflect-metadata": "^0.1.13", "typeorm": "0.3.17", "winston": "^3.2.1" @@ -148,6 +149,14 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@opentelemetry/api": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.8.0.tgz", + "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@parse/node-apn": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@parse/node-apn/-/node-apn-5.2.3.tgz", @@ -450,12 +459,9 @@ "devOptional": true }, "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -547,6 +553,11 @@ "node": ">=8" } }, + "node_modules/bintrees": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", + "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" + }, "node_modules/bn.js": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", @@ -1137,6 +1148,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -1658,12 +1670,11 @@ "dev": true }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" @@ -2382,6 +2393,18 @@ "node": ">=0.10.0" } }, + "node_modules/prom-client": { + "version": "15.1.2", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.2.tgz", + "integrity": "sha512-on3h1iXb04QFLLThrmVYg1SChBQ9N1c+nKAjebBjokBqipddH3uxmOUcEkTnzmJ8Jh/5TSUnUqS40i2QB2dJHQ==", + "dependencies": { + "@opentelemetry/api": "^1.4.0", + "tdigest": "^0.1.1" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2690,7 +2713,8 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true }, "node_modules/stack-trace": { "version": "0.0.10", @@ -2764,6 +2788,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tdigest": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", + "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", + "dependencies": { + "bintrees": "1.0.2" + } + }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -2912,6 +2944,28 @@ "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev" } }, + "node_modules/tslint/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/tslint/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/tslint/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", diff --git a/package.json b/package.json index 67eb97f..e6cf16f 100644 --- a/package.json +++ b/package.json @@ -26,16 +26,17 @@ "@types/express": "^4.16.1", "@types/mustache": "^0.8.32", "@types/node": "^12.0.2", - "@types/request": "^2.48.1", - "body-parser": "^1.20.2", + "@types/request": "^2.48.12", "axios": "1.6.8", + "body-parser": "^1.20.2", "express": "^4.19.2", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "json-rules-engine": "^2.3.6", "moment-timezone": "^0.5.31", "mustache": "^3.0.1", "notifme-sdk": "^1.14.1", "pg": "^8.2.1", + "prom-client": "^15.1.2", "reflect-metadata": "^0.1.13", "typeorm": "0.3.17", "winston": "^3.2.1" diff --git a/src/server.ts b/src/server.ts index 7ed2516..d5570c1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -29,6 +29,9 @@ import { WebhookService } from './destination/destinationHandlers/webhookHandler import { WebhookConfig } from './entities/webhookconfig'; import * as process from "process"; import bodyParser from 'body-parser'; +import {collectDefaultMetrics, Registry, Counter, Histogram} from 'prom-client'; + + const app = express(); app.use(bodyParser.json({ limit: '10mb' })); @@ -91,22 +94,69 @@ createConnection(dbOptions).then(async connection => { logger.error("shutting down notifier due to un-successful database connection...") process.exit(1) }); +// create a registry to hold metrics +const registry = new Registry() +// Initialize Prometheus metrics +collectDefaultMetrics({register:registry}); + +const requestCounter = new Counter({ + name: 'http_requests_total', + help: 'Total number of HTTP requests', + registers: [registry], + labelNames: ['method', 'path', 'status'], +}) +const httpRequestTimer = new Histogram({ + name: 'http_request_duration_ms', + help: 'Duration of HTTP requests in ms', + labelNames: ['method', 'path', 'status'], + registers: [registry], +}); app.get('/', (req, res) => res.send('Welcome to notifier Notifier!')) app.get('/health', (req, res) =>{ - res.status(200).send("healthy") + requestCounter.labels(req.method, req.path, res.statusCode.toString()).inc() + const start = Date.now(); + try{res.status(200).send("healthy")} +finally { + const responseTimeInMs = Date.now() - start; + httpRequestTimer.labels(req.method, req.route.path, res.statusCode.toString()).observe(responseTimeInMs); + } }) app.get('/test', (req, res) => { - send(); + requestCounter.labels(req.method, req.path, res.statusCode.toString()).inc() + const start = Date.now(); + try{send();} +finally { + const responseTimeInMs = Date.now() - start; + httpRequestTimer.labels(req.method, req.route.path, res.statusCode.toString()).observe(responseTimeInMs); + } res.send('Test!'); }) app.post('/notify', (req, res) => { + requestCounter.labels(req.method, req.path, res.statusCode.toString()).inc() + const start = Date.now(); logger.info("notifications Received") - notificationService.sendNotification(req.body) - res.send('notifications sent') + try{ + notificationService.sendNotification(req.body) + res.send('notifications sent') + }finally { + const responseTimeInMs = Date.now() - start; + httpRequestTimer.labels(req.method, req.route.path, res.statusCode.toString()).observe(responseTimeInMs); + } }); + + + +// Endpoint to expose metrics +app.get('/metrics', async (req, res) => { + const result = await registry.metrics() + res.send(result) +}); + + + app.listen(3000, () => logger.info('Notifier app listening on port 3000!')) \ No newline at end of file