diff --git a/package.json b/package.json index e79ab464d4999714c85ef4d7c379aadcb088b918..60894a217ea7ef37461fb92a331d9ba5775a8a8d 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "node-html-parser": "^1.1.11", "p-queue": "^6.0.1", "pg-promise": "^8.5.1", + "prom-client": "^11.3.0", "tslint": "^5.11.0", "typescript": "^3.1.3", "winston": "^3.0.0", diff --git a/src/bot.ts b/src/bot.ts index d88c10bffb5d22c5275948faf90d584151740e5d..a4ad3f165b76522bae8c7a28d2929214da578c02 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -37,6 +37,7 @@ import * as Discord from "discord.js"; import * as mime from "mime"; import { IMatrixEvent, IMatrixMediaInfo } from "./matrixtypes"; import { DiscordCommandHandler } from "./discordcommandhandler"; +import { MetricPeg } from "./metrics"; const log = new Log("DiscordBot"); @@ -288,12 +289,14 @@ export class DiscordBot { }); client.on("message", async (msg: Discord.Message) => { try { + MetricPeg.get.registerRequest(msg.id); await this.waitUnlock(msg.channel); this.discordMessageQueue[msg.channel.id] = (async () => { await (this.discordMessageQueue[msg.channel.id] || Promise.resolve()); try { await this.OnMessage(msg); } catch (err) { + MetricPeg.get.requestOutcome(msg.id, true, "fail"); log.error("Caught while handing 'message'", err); } })(); @@ -738,11 +741,13 @@ export class DiscordBot { if (indexOfMsg !== -1) { log.verbose("Got repeated message, ignoring."); delete this.sentMessages[indexOfMsg]; + MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return; // Skip *our* messages } const chan = msg.channel as Discord.TextChannel; if (msg.author.id === this.bot.user.id) { // We don't support double bridging. + MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return; } // Test for webhooks @@ -751,6 +756,7 @@ export class DiscordBot { .filterArray((h) => h.name === "_matrix").pop(); if (webhook && msg.webhookID === webhook.id) { // Filter out our own webhook messages. + MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return; } } @@ -758,6 +764,7 @@ export class DiscordBot { // check if it is a command to process by the bot itself if (msg.content.startsWith("!matrix")) { await this.discordCommandHandler.Process(msg); + MetricPeg.get.requestOutcome(msg.id, true, "success"); return; } @@ -766,14 +773,13 @@ export class DiscordBot { let rooms; try { rooms = await this.channelSync.GetRoomIdsFromChannel(msg.channel); + if (rooms === null) { throw Error() } } catch (err) { log.verbose("No bridged rooms to send message to. Oh well."); + MetricPeg.get.requestOutcome(msg.id, true, "dropped"); return null; } try { - if (rooms === null) { - return null; - } const intent = this.GetIntentFromDiscordMember(msg.author, msg.webhookID); // Check Attachements await Util.AsyncForEach(msg.attachments.array(), async (attachment) => { @@ -854,7 +860,9 @@ export class DiscordBot { await afterSend(res); } }); + MetricPeg.get.requestOutcome(msg.id, true, "success"); } catch (err) { + MetricPeg.get.requestOutcome(msg.id, true, "fail"); log.verbose("Failed to send message into room.", err); } } diff --git a/src/config.ts b/src/config.ts index 2f0eb76241cfca553350e116ea39b9b45e72d171..2da5b7734e721eca3d77126750cf16957349deb4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -90,6 +90,7 @@ class DiscordBridgeConfigBridge { public disableEveryoneMention: boolean = false; public disableHereMention: boolean = false; public disableJoinLeaveNotifications: boolean = false; + public enableMetrics: boolean = false; } export class DiscordBridgeConfigDatabase { diff --git a/src/db/roomstore.ts b/src/db/roomstore.ts index c04230e0fa56c75046a628935f598454b7c6fc7c..72677cc9b70b66cce86abdfd690a9f45f7093851 100644 --- a/src/db/roomstore.ts +++ b/src/db/roomstore.ts @@ -19,6 +19,7 @@ import { Util } from "../util"; import * as uuid from "uuid/v4"; import { Postgres } from "./postgres"; +import { MetricPeg } from "../metrics"; const log = new Log("DbRoomStore"); @@ -155,8 +156,10 @@ export class DbRoomStore { public async getEntriesByMatrixId(matrixId: string): Promise<IRoomStoreEntry[]> { const cached = this.entriesMatrixIdCache.get(matrixId); if (cached && cached.ts + ENTRY_CACHE_LIMETIME > Date.now()) { + MetricPeg.get.storeCall("getEntriesByMatrixId", true); return cached.e; } + MetricPeg.get.storeCall("getEntriesByMatrixId", false); const entries = await this.db.All( "SELECT * FROM room_entries WHERE matrix_id = $id", {id: matrixId}, ); @@ -190,6 +193,7 @@ export class DbRoomStore { } public async getEntriesByMatrixIds(matrixIds: string[]): Promise<IRoomStoreEntry[]> { + MetricPeg.get.storeCall("getEntriesByMatrixIds", false); const mxIdMap = { }; matrixIds.forEach((mxId, i) => mxIdMap[i] = mxId); const sql = `SELECT * FROM room_entries WHERE matrix_id IN (${matrixIds.map((_, id) => `\$${id}`).join(", ")})`; @@ -222,6 +226,7 @@ export class DbRoomStore { } public async linkRooms(matrixRoom: MatrixStoreRoom, remoteRoom: RemoteStoreRoom) { + MetricPeg.get.storeCall("linkRooms", false); await this.upsertRoom(remoteRoom); const values = { @@ -244,6 +249,7 @@ export class DbRoomStore { } public async getEntriesByRemoteRoomData(data: IRemoteRoomDataLazy): Promise<IRoomStoreEntry[]> { + MetricPeg.get.storeCall("getEntriesByRemoteRoomData", false); Object.keys(data).filter((k) => typeof(data[k]) === "boolean").forEach((k) => { data[k] = Number(data[k]); }); @@ -270,11 +276,13 @@ export class DbRoomStore { } public async removeEntriesByRemoteRoomId(remoteId: string) { + MetricPeg.get.storeCall("removeEntriesByRemoteRoomId", false); await this.db.Run(`DELETE FROM room_entries WHERE remote_id = $remoteId`, {remoteId}); await this.db.Run(`DELETE FROM remote_room_data WHERE room_id = $remoteId`, {remoteId}); } public async removeEntriesByMatrixRoomId(matrixId: string) { + MetricPeg.get.storeCall("removeEntriesByMatrixRoomId", false); const entries = (await this.db.All(`SELECT * FROM room_entries WHERE matrix_id = $matrixId`, {matrixId})) || []; await Util.AsyncForEach(entries, async (entry) => { if (entry.remote_id) { @@ -286,6 +294,7 @@ export class DbRoomStore { } private async upsertRoom(room: RemoteStoreRoom) { + MetricPeg.get.storeCall("upsertRoom", false); if (!room.data) { throw new Error("Tried to upsert a room with undefined data"); } diff --git a/src/db/userstore.ts b/src/db/userstore.ts index cb1251e28c1f08d86e74ba47aa7695da00319474..c85420745df27117040de49a3c2f464ee9b2b5eb 100644 --- a/src/db/userstore.ts +++ b/src/db/userstore.ts @@ -15,8 +15,8 @@ limitations under the License. */ import { IDatabaseConnector } from "./connector"; -import * as uuid from "uuid/v4"; import { Log } from "../log"; +import { MetricPeg } from "../metrics"; /** * A UserStore compatible with @@ -54,8 +54,11 @@ export class DbUserStore { public async getRemoteUser(remoteId: string): Promise<RemoteUser|null> { const cached = this.remoteUserCache.get(remoteId); if (cached && cached.ts + ENTRY_CACHE_LIMETIME > Date.now()) { + MetricPeg.get.storeCall("getRemoteUser", true); return cached.e; } + MetricPeg.get.storeCall("getRemoteUser", false); + const row = await this.db.Get( "SELECT * FROM user_entries WHERE remote_id = $id", {id: remoteId}, ); @@ -86,6 +89,7 @@ export class DbUserStore { } public async setRemoteUser(user: RemoteUser) { + MetricPeg.get.storeCall("setRemoteUser", false); this.remoteUserCache.delete(user.id); const existingData = await this.db.Get( "SELECT * FROM remote_user_data WHERE remote_id = $remoteId", @@ -156,6 +160,7 @@ AND guild_id = $guild_id`, } public async linkUsers(matrixId: string, remoteId: string) { + MetricPeg.get.storeCall("linkUsers", false); // This is used ONCE in the bridge to link two IDs, so do not UPSURT data. try { await this.db.Run(`INSERT INTO user_entries VALUES ($matrixId, $remoteId)`, { diff --git a/src/discordas.ts b/src/discordas.ts index 1f350c3819a8f6c70c924bbc1f289b7af6f92276..574f3968621279fd5933391089082ca943d1ccf6 100644 --- a/src/discordas.ts +++ b/src/discordas.ts @@ -22,6 +22,7 @@ import { DiscordBot } from "./bot"; import { DiscordStore } from "./store"; import { Log } from "./log"; import "source-map-support/register"; +import { MetricPeg } from "./metrics"; const log = new Log("DiscordAS"); @@ -94,13 +95,16 @@ async function run(port: number, fileConfig: DiscordBridgeConfig) { } catch (err) { log.error("Exception thrown while handling \"onAliasQuery\" event", err); } }, onEvent: async (request) => { + const data = request.getData(); try { + MetricPeg.get.registerRequest(data.event_id); // Build our own context. if (!store.roomStore) { log.warn("Discord store not ready yet, dropping message"); + MetricPeg.get.requestOutcome(data.event_id, false, "dropped"); return; } - const roomId = request.getData().room_id; + const roomId = data.room_id; const context: BridgeContext = { rooms: {}, @@ -112,7 +116,9 @@ async function run(port: number, fileConfig: DiscordBridgeConfig) { } await request.outcomeFrom(callbacks.onEvent(request, context)); + MetricPeg.get.requestOutcome(data.event_id, false, "success"); } catch (err) { + MetricPeg.get.requestOutcome(data.event_id, false, "fail"); log.error("Exception thrown while handling \"onEvent\" event", err); await request.outcomeFrom(Promise.reject("Failed to handle")); } diff --git a/src/presencehandler.ts b/src/presencehandler.ts index 16fa4a974510cf405e931a02a278789b3f89e08a..91343e609d39341754bfc8353ab350934bc9bbf0 100644 --- a/src/presencehandler.ts +++ b/src/presencehandler.ts @@ -17,6 +17,7 @@ limitations under the License. import { User, Presence } from "discord.js"; import { DiscordBot } from "./bot"; import { Log } from "./log"; +import { MetricPeg } from "./metrics"; const log = new Log("PresenceHandler"); export class PresenceHandlerStatus { @@ -66,6 +67,7 @@ export class PresenceHandler { if (user.id !== this.bot.GetBotId() && this.presenceQueue.find((u) => u.id === user.id) === undefined) { log.info(`Adding ${user.id} (${user.username}) to the presence queue`); this.presenceQueue.push(user); + MetricPeg.get.setPresenceCount(this.presenceQueue.length); } } @@ -75,6 +77,7 @@ export class PresenceHandler { }); if (index !== -1) { this.presenceQueue.splice(index, 1); + MetricPeg.get.setPresenceCount(this.presenceQueue.length); } else { log.warn( `Tried to remove ${user.id} from the presence queue but it could not be found`, @@ -96,6 +99,7 @@ export class PresenceHandler { this.presenceQueue.push(user); } else { log.info(`Dropping ${user.id} from the presence queue.`); + MetricPeg.get.setPresenceCount(this.presenceQueue.length); } } }