diff --git a/src/config.ts b/src/config.ts index 756595085475055c2cb06386506bf666c3889efa..7197899ebe8356c556c12b4a8fc7f016b056b742 100644 --- a/src/config.ts +++ b/src/config.ts @@ -28,6 +28,7 @@ export class DiscordBridgeConfig { public channel: DiscordBridgeConfigChannel = new DiscordBridgeConfigChannel(); public limits: DiscordBridgeConfigLimits = new DiscordBridgeConfigLimits(); public ghosts: DiscordBridgeConfigGhosts = new DiscordBridgeConfigGhosts(); + public metrics: DiscordBridgeConfigMetrics = new DiscordBridgeConfigMetrics(); /** * Apply a set of keys and values over the default config. @@ -92,7 +93,6 @@ class DiscordBridgeConfigBridge { public disableEveryoneMention: boolean = false; public disableHereMention: boolean = false; public disableJoinLeaveNotifications: boolean = false; - public enableMetrics: boolean = false; } export class DiscordBridgeConfigDatabase { @@ -152,3 +152,9 @@ class DiscordBridgeConfigGhosts { public nickPattern: string = ":nick"; public usernamePattern: string = ":username#:tag"; } + +export class DiscordBridgeConfigMetrics { + public enable: boolean; + public port: number = 9001; + public host: string = "127.0.0.1"; +} diff --git a/src/discordas.ts b/src/discordas.ts index 375f116e895c9ffb4c2d6f233d2e6c9954a7170c..fe1e5f8e5c80f6b46d87c24b228073c7dc049133 100644 --- a/src/discordas.ts +++ b/src/discordas.ts @@ -71,6 +71,7 @@ function generateRegistration(opts, registrationPath) { function setupLogging() { const logMap = new Map<string, Log>(); + // tslint:disable-next-line:no-any const logFunc = (level: string, module: string, args: any[]) => { if (!Array.isArray(args)) { args = [args]; @@ -89,9 +90,13 @@ function setupLogging() { }; LogService.setLogger({ + // tslint:disable-next-line:no-any debug: (mod: string, args: any[]) => logFunc("silly", mod, args), + // tslint:disable-next-line:no-any error: (mod: string, args: any[]) => logFunc("error", mod, args), + // tslint:disable-next-line:no-any info: (mod: string, args: any[]) => logFunc("info", mod, args), + // tslint:disable-next-line:no-any warn: (mod: string, args: any[]) => logFunc("warn", mod, args), }); } @@ -153,12 +158,11 @@ async function run() { + "The config option userStorePath no longer has any use."); } - if (config.bridge.enableMetrics) { + if (config.metrics.enable) { log.info("Enabled metrics"); - MetricPeg.set(new PrometheusBridgeMetrics().init()); + MetricPeg.set(new PrometheusBridgeMetrics().init(appservice, config.metrics)); } - try { await store.init(); } catch (ex) { @@ -170,6 +174,7 @@ async function run() { const roomhandler = discordbot.RoomHandler; const eventProcessor = discordbot.MxEventProcessor; + // tslint:disable-next-line:no-any appservice.on("query.room", async (roomAlias: string, createRoom: (opts: any) => Promise<void>) => { try { const createRoomOpts = await roomhandler.OnAliasQuery(roomAlias); @@ -202,4 +207,4 @@ async function run() { run().catch((err) => { log.error("A fatal error occurred during startup:", err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/src/metrics.ts b/src/metrics.ts index 9e228a11f64b1d0f2d38e755615552399f413a0f..d5a7b10a9dedb7d499237385719b5c1c74ced018 100644 --- a/src/metrics.ts +++ b/src/metrics.ts @@ -14,30 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Gauge, Counter, Histogram } from "prom-client"; +import { Gauge, Counter, Histogram, default as promClient } from "prom-client"; import { Log } from "./log"; -import { Appservice } from "matrix-bot-sdk"; +import { Appservice, + IMetricContext, + METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL, + METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL, + FunctionCallContext, + METRIC_MATRIX_CLIENT_FUNCTION_CALL} from "matrix-bot-sdk"; +import { DiscordBridgeConfigMetrics } from "./config"; +import * as http from "http"; -const AgeCounters = PrometheusMetrics.AgeCounters; const log = new Log("BridgeMetrics"); const REQUEST_EXPIRE_TIME_MS = 30000; -interface IAgeCounter { - setGauge(gauge: Gauge, morelabels: string[]); - bump(age: number); -} - -interface IBridgeGauges { - matrixRoomConfigs: number; - remoteRoomConfigs: number; - matrixGhosts: number; - remoteGhosts: number; - matrixRoomsByAge: IAgeCounter; - remoteRoomsByAge: IAgeCounter; - matrixUsersByAge: IAgeCounter; - remoteUsersByAge: IAgeCounter; -} - export interface IBridgeMetrics { registerRequest(id: string); requestOutcome(id: string, isRemote: boolean, outcome: string); @@ -67,55 +57,73 @@ export class MetricPeg { } export class PrometheusBridgeMetrics implements IBridgeMetrics { - private metrics; + private matrixCallCounter: Counter; private remoteCallCounter: Counter; private storeCallCounter: Counter; private presenceGauge: Gauge; private remoteRequest: Histogram; private matrixRequest: Histogram; private requestsInFlight: Map<string, number>; - private bridgeGauges: IBridgeGauges = { - matrixGhosts: 0, - matrixRoomConfigs: 0, - matrixRoomsByAge: new AgeCounters(), - matrixUsersByAge: new AgeCounters(), - remoteGhosts: 0, - remoteRoomConfigs: 0, - remoteRoomsByAge: new AgeCounters(), - remoteUsersByAge: new AgeCounters(), - }; - - public init(as: Appservice) { - this.metrics = new PrometheusMetrics(); - this.metrics.registerMatrixSdkMetrics(); - this.metrics.registerBridgeGauges(() => this.bridgeGauges); - this.metrics.addAppServicePath(bridge); - this.remoteCallCounter = this.metrics.addCounter({ + private matrixRequestStatus: Map<string, "success"|"failed">; + private httpServer: http.Server; + + public init(as: Appservice, config: DiscordBridgeConfigMetrics) { + promClient.collectDefaultMetrics({ + timeout: 15000, + }); + // TODO: Bind this for every user. + this.httpServer = http.createServer((req, res) => { + if (req.method !== "GET" || req.url !== "/metrics") { + // tslint:disable-next-line:no-magic-numbers + res.writeHead(404, "Not found"); + res.end(); + } + // tslint:disable-next-line:no-magic-numbers + res.writeHead(200, "OK", {"Content-Type": promClient.register.contentType}); + res.write(promClient.register.metrics()); + res.end(); + }); + this.matrixCallCounter = new Counter({ + help: "Count of matrix API calls made", + labelNames: ["method", "result"], + name: "matrix_api_calls", + }); + promClient.register.registerMetric(this.matrixCallCounter); + + this.remoteCallCounter = new Counter({ help: "Count of remote API calls made", - labels: ["method"], + labelNames: ["method"], name: "remote_api_calls", }); - this.storeCallCounter = this.metrics.addCounter({ + promClient.register.registerMetric(this.remoteCallCounter); + + this.storeCallCounter = new Counter({ help: "Count of store function calls made", - labels: ["method", "cached"], + labelNames: ["method", "cached"], name: "store_calls", }); - this.presenceGauge = this.metrics.addGauge({ - help: "Count of users in the presence queue", - labels: [], + promClient.register.registerMetric(this.storeCallCounter); + this.presenceGauge = new Gauge({ + help: "Count of users in the presence queue", name: "active_presence_users", }); - this.matrixRequest = this.metrics.addTimer({ + promClient.register.registerMetric(this.presenceGauge); + + this.matrixRequest = new Histogram({ help: "Histogram of processing durations of received Matrix messages", - labels: ["outcome"], + labelNames: ["outcome"], name: "matrix_request_seconds", }); - this.remoteRequest = this.metrics.addTimer({ + promClient.register.registerMetric(this.matrixRequest); + + this.remoteRequest = new Histogram({ help: "Histogram of processing durations of received remote messages", - labels: ["outcome"], + labelNames: ["outcome"], name: "remote_request_seconds", }); + promClient.register.registerMetric(this.remoteRequest); + this.requestsInFlight = new Map(); setInterval(() => { this.requestsInFlight.forEach((time, id) => { @@ -124,6 +132,17 @@ export class PrometheusBridgeMetrics implements IBridgeMetrics { } }); }, REQUEST_EXPIRE_TIME_MS); + this.httpServer.listen(config.port, config.host); + + // Bind bot-sdk metrics + as.botClient.metrics.registerListener({ + onDecrement: this.sdkDecrementMetric.bind(this), + onEndMetric: this.sdkEndMetric.bind(this), + onIncrement: this.sdkIncrementMetric.bind(this), + onReset: this.sdkResetMetric.bind(this), + onStartMetric: this.sdkStartMetric.bind(this), + }); + return this; } @@ -152,4 +171,36 @@ export class PrometheusBridgeMetrics implements IBridgeMetrics { public storeCall(method: string, cached: boolean) { this.storeCallCounter.inc({method, cached: cached ? "yes" : "no"}); } + + private sdkStartMetric(metricName: string, context: IMetricContext) { + // We don't use this yet. + } + + private sdkEndMetric(metricName: string, context: FunctionCallContext, timeMs: number) { + if (metricName !== METRIC_MATRIX_CLIENT_FUNCTION_CALL) { + return; // We don't handle any other type yet. + } + const successFail = this.matrixRequestStatus.get(context.uniqueId)!; + this.matrixRequestStatus.delete(context.uniqueId); + this.matrixRequest.observe({ + method: context.functionName, + result: successFail, + }, timeMs); + } + + private sdkResetMetric(metricName: string, context: IMetricContext) { + // We don't use this yet. + } + + private sdkIncrementMetric(metricName: string, context: IMetricContext, amount: number) { + if (metricName === METRIC_MATRIX_CLIENT_SUCCESSFUL_FUNCTION_CALL) { + this.matrixRequestStatus.set(context.uniqueId, "success"); + } else if (metricName === METRIC_MATRIX_CLIENT_FAILED_FUNCTION_CALL) { + this.matrixRequestStatus.set(context.uniqueId, "failed"); + } + } + + private sdkDecrementMetric(metricName: string, context: IMetricContext, amount: number) { + // We don't use this yet. + } }