diff --git a/config/default.yaml b/config/default.yaml index b1fab4c..120a945 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -308,4 +308,9 @@ ircBridge: null #channelPrefix: '#conference-' templatesPath: ./srv -runMode: normal \ No newline at end of file +runMode: normal + +metrics: + enabled: false + port: 8081 + address: 127.0.0.1 \ No newline at end of file diff --git a/package.json b/package.json index ba0d0c7..a2671fa 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "moment": "^2.29.4", "node-fetch": "^2.6.1", "pg": "^8.9.0", + "prom-client": "^15.0.0", "qs": "^6.11.2", "rfc4648": "^1.4.0", "xss": "^1.0.14" diff --git a/spec/util/e2e-test.ts b/spec/util/e2e-test.ts index 8f53e0e..54fc4ab 100644 --- a/spec/util/e2e-test.ts +++ b/spec/util/e2e-test.ts @@ -218,6 +218,11 @@ export class E2ETestEnv { managementRoom: mgmntRoom, templatesPath: '/dev/null', mode: RunMode.normal, + metrics: { + enabled: false, + address: '0.0.0.0', + port: 0, + }, ...providedConfig, }; const conferenceBot = await ConferenceBot.start(config); diff --git a/src/ConferenceMatrixClient.ts b/src/ConferenceMatrixClient.ts index db2577b..7cb9376 100644 --- a/src/ConferenceMatrixClient.ts +++ b/src/ConferenceMatrixClient.ts @@ -1,5 +1,12 @@ -import { IStorageProvider, IdentityClient, MatrixClient } from "matrix-bot-sdk"; +import { FunctionCallContext, IStorageProvider, IdentityClient, METRIC_IDENTITY_CLIENT_FAILED_FUNCTION_CALL, METRIC_IDENTITY_CLIENT_SUCCESSFUL_FUNCTION_CALL, METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL, METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL, MatrixClient } from "matrix-bot-sdk"; import { IConfig } from "./config"; +import { Counter } from "prom-client"; + +const matrixApiCalls = new Counter({ name: "matrix_api_calls", help: "The number of Matrix client API calls made.", labelNames: ["method"]}); +const matrixApiCallsFailed = new Counter({ name: "matrix_api_calls_failed", help: "The number of Matrix client API calls which failed.", labelNames: ["method"]}); +const matrixIdentityApiCalls = new Counter({ name: "matrix_identity_api_calls", help: "The number of Matrix identity API calls made.", labelNames: ["method"]}); +const matrixIdentityApiCallsFailed = new Counter({ name: "matrix_identity_api_calls_failed", help: "The number of Matrix identity API calls which failed.", labelNames: ["method"]}); + export class ConferenceMatrixClient extends MatrixClient { static async create(confConfig: IConfig, storage?: IStorageProvider) { @@ -21,5 +28,42 @@ export class ConferenceMatrixClient extends MatrixClient { public readonly managementRoom: string, storage?: IStorageProvider) { super(homeserverUrl, accessToken, storage); + this.metrics.registerListener({ + onStartMetric: () => { + // Not used yet. + }, + onEndMetric: () => { + // Not used yet. + }, + onIncrement: (metricName, context) => { + if (metricName === METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL) { + const ctx = context as FunctionCallContext; + matrixApiCalls.inc({method: ctx.functionName}); + } + if (metricName === METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL) { + const ctx = context as FunctionCallContext; + matrixApiCallsFailed.inc({method: ctx.functionName}); + } + if (metricName === METRIC_IDENTITY_CLIENT_SUCCESSFUL_FUNCTION_CALL) { + const ctx = context as FunctionCallContext; + matrixIdentityApiCalls.inc({method: ctx.functionName}); + } + if (metricName === METRIC_IDENTITY_CLIENT_FAILED_FUNCTION_CALL) { + const ctx = context as FunctionCallContext; + matrixIdentityApiCallsFailed.inc({method: ctx.functionName}); + } + }, + onDecrement: () => { + // Not used yet. + }, + onReset: (metricName) => { + if (metricName === METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL) { + matrixApiCalls.reset(); + } + if (metricName === METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL) { + matrixApiCallsFailed.reset(); + } + }, + }) } } \ No newline at end of file diff --git a/src/Scheduler.ts b/src/Scheduler.ts index bfac5a7..df79184 100644 --- a/src/Scheduler.ts +++ b/src/Scheduler.ts @@ -27,6 +27,9 @@ import { Talk } from "./models/Talk"; import { CheckInMap } from "./CheckInMap"; import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; import { IConfig } from "./config"; +import { Counter } from "prom-client"; + +const matrixIdentityApiCallsFailed = new Counter({ name: "confbot_scheduler_last_run", help: "The last time the Scheduler ran its tasks."}); export enum ScheduledTaskType { TalkStart = "talk_start", @@ -193,7 +196,7 @@ export class Scheduler { private async runTasks() { try { const now = (new Date()).getTime(); - // const pentaDb = await this.conference.getPentaDb(); + matrixIdentityApiCallsFailed.inc(now); await this.lock.acquireAsync(); LogService.info("Scheduler", "Scheduling tasks"); try { diff --git a/src/config.ts b/src/config.ts index 7d4951f..c182d60 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,6 +50,11 @@ export interface IConfig { publicBaseUrl: string; additionalAssetsPath: string; }; + metrics: { + enabled: boolean; + address: string; + port: number; + }; conference: { id: string; name: string; diff --git a/src/index.ts b/src/index.ts index c0ea802..b33d4a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,6 +61,7 @@ import { StatusCommand } from "./commands/StatusCommand"; import { CachingBackend } from "./backends/CachingBackend"; import { ConferenceMatrixClient } from "./ConferenceMatrixClient"; import { Server } from "http"; +import { collectDefaultMetrics, register } from "prom-client"; LogService.setLogger(new CustomLogger()); LogService.setLevel(LogLevel.DEBUG); @@ -101,6 +102,7 @@ export class ConferenceBot { } private webServerInstance?: Server; + private metricsServerInstance?: Server; private constructor( private readonly config: IConfig, @@ -139,6 +141,22 @@ export class ConferenceBot { // Setup the webserver first. await this.setupWebserver(); + // Setup metrics + if (this.config.metrics?.enabled) { + collectDefaultMetrics(); + const metricsApp = express(); + metricsApp.get('/metrics', (_req, res) => { + register.metrics().then( + (m) => res.type('text/plain').send((m)) + ).catch((err) => { + LogService.info("index", "Failed to fetch metrics: ", err); + res.status(500).send('Could not fetch metrics due to an error'); + }); + }); + this.metricsServerInstance = metricsApp.listen(this.config.metrics.port, this.config.metrics.address); + LogService.info("index", `Metrics listening on ${this.config.metrics.address}:${this.config.metrics.port}`) + } + await this.client.joinRoom(this.config.managementRoom); await this.conference.construct(); @@ -177,7 +195,6 @@ export class ConferenceBot { ); } - await this.scheduler.prepare(); // Needs to happen after the sync loop has started @@ -289,6 +306,7 @@ export class ConferenceBot { await this.scheduler.stop(); this.client.stop(); this.webServer?.close(); + this.metricsServerInstance?.close(); } } diff --git a/yarn.lock b/yarn.lock index dfc4edb..6e17db3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -661,6 +661,11 @@ https-proxy-agent "^5.0.1" node-downloader-helper "^2.1.5" +"@opentelemetry/api@^1.4.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.7.0.tgz#b139c81999c23e3c8d3c0a7234480e945920fc40" + integrity sha512-AdY5wvN0P2vXBi3b29hxZgSFvdhdxPB9+f0B6s//P9Q8nibRWeA3cHm8UmLpio9ABigkVHJ5NMPk+Mz8VCCyrw== + "@selderee/plugin-htmlparser2@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.6.0.tgz#27e994afd1c2cb647ceb5406a185a5574188069d" @@ -1471,6 +1476,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== +bintrees@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" + integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== + bluebird@^3.5.0: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" @@ -4715,6 +4725,14 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== +prom-client@^15.0.0: + version "15.0.0" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.0.0.tgz#067da874a2aa5d2e21bd5cdba9f24a8178bdab6a" + integrity sha512-UocpgIrKyA2TKLVZDSfm8rGkL13C19YrQBAiG3xo3aDFWcHedxRxI3z+cIcucoxpSO0h5lff5iv/SXoxyeopeA== + dependencies: + "@opentelemetry/api" "^1.4.0" + tdigest "^0.1.1" + prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -5450,6 +5468,13 @@ tapable@^2.1.1, tapable@^2.2.0: resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b" integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw== +tdigest@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced" + integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA== + dependencies: + bintrees "1.0.2" + terser-webpack-plugin@^5.3.7: version "5.3.9" resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1"