diff --git a/package.json b/package.json index 67ee0a8..f080f0e 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "build/**/*" ], "scripts": { - "build": "tsc && tsc -p tsconfig.cjs.json && node ./scripts/declareTypes.js", + "build": "tsc && tsc -p tsconfig.cjs.json && node ./scripts/declareTypes.js --common --module", + "build:cjs": "tsc -p tsconfig.cjs.json && node ./scripts/declareTypes.js --common", + "build:module": "tsc && node ./scripts/declareTypes.js --module", "test": "yarn build && jest", "release": "yarn build && yarn publish", "lint": "eslint --fix" @@ -41,5 +43,13 @@ "amqplib": "^0.10.3", "mongodb": "^5.7.0", "mysql": "^2.18.1" + }, + "peerDependencies": { + "prom-client": "^15.0.0" + }, + "peerDependenciesMeta": { + "prom-client": { + "optional": true + } } } diff --git a/scripts/declareTypes.js b/scripts/declareTypes.js index 22019d6..568a9de 100644 --- a/scripts/declareTypes.js +++ b/scripts/declareTypes.js @@ -1,3 +1,5 @@ import fs from 'node:fs'; -fs.writeFileSync('./build/cjs/package.json', JSON.stringify({ type: 'commonjs' })); -fs.writeFileSync('./build/esm/package.json', JSON.stringify({ type: 'module' })); \ No newline at end of file +if (process.argv.includes('--common')) + fs.writeFileSync('./build/cjs/package.json', JSON.stringify({ type: 'commonjs' })); +if (process.argv.includes('--module')) + fs.writeFileSync('./build/esm/package.json', JSON.stringify({ type: 'module' })); \ No newline at end of file diff --git a/src/MariaDB.ts b/src/MariaDB.ts index 3b9287a..af29806 100644 --- a/src/MariaDB.ts +++ b/src/MariaDB.ts @@ -25,7 +25,8 @@ export type MariaOptions = { client?: PoolConfig, credentials: Credentials, loggerOptions?: LoggerClientOptions, - donorQuery?: boolean + donorQuery?: boolean, + recordMetrics?: boolean } type MariaError = { @@ -95,6 +96,8 @@ class MariaDB #pool: PoolCluster | null; #nodes: Node[]; #canQueryDonor: boolean; + #queryHistogram?: {startTimer: (labels: object) => () => void}; + #server: IServer; constructor (server: IServer, options: MariaOptions) { @@ -128,7 +131,7 @@ class MariaDB this.#cluster = this.#credentials.length > 1; this.#canQueryDonor = options.donorQuery ?? false; this.#logger = server.createLogger(this, options.loggerOptions); - + this.#server = server; } get ready () @@ -141,6 +144,7 @@ class MariaDB return this.#_activeQueries; } + // eslint-disable-next-line max-lines-per-function async init () { if (!this.#load) @@ -230,10 +234,27 @@ class MariaDB } } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const Prometheus = await import('prom-client').catch(() => null); + if (this.#config.recordMetrics && !Prometheus) + { + this.#logger.warn('Metrics recording was enabled but missing prom-client dependency'); + } + else if (this.#config.recordMetrics && Prometheus) + { + this.#logger.info('Setting up metric recording'); + this.#queryHistogram = new Prometheus.Histogram({ + name: 'sql_queries', + help: 'Tracks query duration and frequency', + buckets: Prometheus?.exponentialBuckets(0.005, 2, 10), + labelNames: [ 'type' ] as const + }); + this.#server.registerMetric(this.#queryHistogram!); + } + this.#_ready = true; - return this; - } async close () @@ -382,10 +403,11 @@ class MariaDB Promise { const connection = await this.getConnection(node ?? null, errorIfNodeUnavailable ?? false); - try + try { const result = await new Promise((resolve, reject) => { + const endTimer = this.#queryHistogram?.startTimer({ type: [ 'SELECT', 'UPDATE', 'INSERT', 'DELETE' ].find(entry => query.toUpperCase().includes(entry)) ?? 'OTHER' }); const q = connection.query({ timeout, sql: query }, values, (err, results, fields) => { if (err) @@ -397,6 +419,8 @@ class MariaDB else resolve(fields); connection.release(); + if (endTimer) + endTimer(); }); this.#logger.debug(`Constructed query: ${q.sql.substring(0, 1024)}`); }); diff --git a/src/MongoDB.ts b/src/MongoDB.ts index 78ea39a..623e9da 100644 --- a/src/MongoDB.ts +++ b/src/MongoDB.ts @@ -21,7 +21,8 @@ export type MongoOptions = { credentials: Credentials, loggerOptions?: LoggerClientOptions, client?: MongoClientOptions, - load?: boolean + load?: boolean, + recordMetrics?: boolean } type StringIndexable = {[key: string]: boolean | string | number | Document | object} @@ -54,6 +55,8 @@ class MongoDB #db: Db | null; #_client: MongoClient; + #queryHistogram?: { startTimer: (labels: object) => () => void }; + #server: IServer; constructor (server: IServer, config: MongoOptions) { @@ -73,6 +76,7 @@ class MongoDB this.#load = config.load ?? true; this.#logger = server.createLogger(this, config.loggerOptions); + this.#server = server; if (URI) { @@ -122,7 +126,6 @@ class MongoDB */ async init () { - if (!this.#load) return this.#logger.info('Not loading MongoDB'); @@ -133,12 +136,29 @@ class MongoDB await this.#_client.connect(); this.#logger.debug('Connected, selecting DB'); - this.#db = await this.#_client.db(this.#_database); + this.#db = this.#_client.db(this.#_database); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const Prometheus = await import('prom-client').catch(() => null); + if (this.#config.recordMetrics && !Prometheus) + { + this.#logger.warn('Metrics recording was enabled but missing prom-client dependency'); + } + else if (this.#config.recordMetrics && Prometheus) + { + this.#logger.info('Setting up metric recording'); + this.#queryHistogram = new Prometheus.Histogram({ + name: 'mongo_queries', + help: 'Tracks query duration and frequency', + buckets: Prometheus?.exponentialBuckets(0.005, 2, 10), + labelNames: [ 'type' ] as const + }); + this.#server.registerMetric(this.#queryHistogram!); + } this.#logger.status('MongoDB ready'); - return this; - } async close () @@ -167,7 +187,6 @@ class MongoDB */ async find (db: string, query: MongoQuery, options?: FindOptions): Promise[]> { - if (!this.#db) throw new Error('MongoDB not connected'); @@ -179,9 +198,12 @@ class MongoDB this.#logger.debug(`Incoming find query for ${db} with parameters ${inspect(query)}`); + const endTimer = this.#queryHistogram?.startTimer({ type: 'find' }); const cursor = this.#db.collection(db).find(query as Filter, options); - return cursor.toArray(); - + const data = await cursor.toArray(); + if (endTimer) + endTimer(); + return data; } /** @@ -194,7 +216,6 @@ class MongoDB */ async findOne (db: string, query: MongoQuery, options: FindOptions = {}): Promise | null> { - if (!this.#db) throw new Error('MongoDB not connected'); if (typeof db !== 'string') @@ -204,9 +225,11 @@ class MongoDB query._id = new ObjectId(query._id); this.#logger.debug(`Incoming findOne query for ${db} with parameters ${inspect(query)}`); + const endTimer = this.#queryHistogram?.startTimer({ type: 'findOne' }); const result = await this.#db.collection(db).findOne(query as Filter, options); + if (endTimer) + endTimer(); return result; - } /** @@ -220,7 +243,6 @@ class MongoDB */ async updateMany (db: string, filter: MongoQuery, data: T, upsert = false) { - if (!this.#db) throw new Error('MongoDB not connected'); if (typeof db !== 'string') @@ -231,9 +253,11 @@ class MongoDB filter._id = new ObjectId(filter._id); this.#logger.debug(`Incoming update query for '${db}' with parameters\n${inspect(filter)}\nand data\n${inspect(data)}`); + const endTimer = this.#queryHistogram?.startTimer({ type: 'updateMany' }); const result = await this.#db.collection(db).updateMany(filter as Filter, { $set: data }, { upsert }); + if (endTimer) + endTimer(); return result; - } /** @@ -247,7 +271,6 @@ class MongoDB */ async updateOne (db: string, filter: MongoQuery, data: Document, upsert = false) { - if (!this.#db) throw new Error('MongoDB not connected'); if (typeof db !== 'string') @@ -256,9 +279,11 @@ class MongoDB filter._id = new ObjectId(filter._id); this.#logger.debug(`Incoming updateOne query for ${db} with parameters ${inspect(filter)}`); + const endTimer = this.#queryHistogram?.startTimer({ type: 'updateOne' }); const result = await this.#db.collection(db).updateOne(filter as Filter, { $set: data }, { upsert }); + if (endTimer) + endTimer(); return result; - } /** @@ -272,7 +297,6 @@ class MongoDB */ async insertOne (db: string, data: Document) { - if (!this.#db) throw new Error('MongoDB not connected'); if (typeof db !== 'string') @@ -281,9 +305,11 @@ class MongoDB data._id = new ObjectId(data._id); this.#logger.debug(`Incoming insertOne query for ${db} with parameters ${inspect(data)}`); + const endTimer = this.#queryHistogram?.startTimer({ type: 'insertOne' }); const result = await this.#db.collection(db).insertOne(data); + if (endTimer) + endTimer(); return result; - } async deleteOne (db: string, filter: Document) @@ -296,7 +322,10 @@ class MongoDB filter._id = new ObjectId(filter._id); this.#logger.debug(`Incoming deleteOne query for ${db} with parameters ${inspect(filter)}`); + const endTimer = this.#queryHistogram?.startTimer({ type: 'deleteOne' }); const result = await this.#db.collection(db).deleteOne(filter); + if (endTimer) + endTimer(); return result; } @@ -311,7 +340,10 @@ class MongoDB if (typeof filter._id === 'string') filter._id = new ObjectId(filter._id); + const endTimer = this.#queryHistogram?.startTimer({ type: 'findOneAndDelete' }); const result = await this.#db.collection(db).findOneAndDelete(filter as Filter, { includeResultMetadata: meta }); + if (endTimer) + endTimer(); return result; } @@ -335,7 +367,10 @@ class MongoDB filter._id = new ObjectId(filter._id); this.#logger.debug(`Incoming push query for ${db}, with upsert ${upsert} and with parameters ${inspect(filter)} and data ${inspect(data)}`); + const endTimer = this.#queryHistogram?.startTimer({ type: 'push' }); const result = await this.#db.collection(db).updateOne(filter, { $push: data }, { upsert }); + if (endTimer) + endTimer(); return result; } @@ -350,7 +385,6 @@ class MongoDB */ random (db: string, filter: Document = {}, amount = 1) { - if (!this.#db) throw new Error('MongoDB not connected'); if (typeof db !== 'string') @@ -365,7 +399,6 @@ class MongoDB const cursor = this.#db.collection(db).aggregate([{ $match: filter }, { $sample: { size: amount } }]); return cursor.toArray(); - } stats (options = {}) diff --git a/src/interfaces/Server.ts b/src/interfaces/Server.ts index 65ff4f6..22ef66a 100644 --- a/src/interfaces/Server.ts +++ b/src/interfaces/Server.ts @@ -1,5 +1,6 @@ -import { ILogger, LoggerClientOptions } from "./Logger.js"; +import { ILogger, LoggerClientOptions } from './Logger.js'; export interface IServer { createLogger(obj: object, options?: Partial): ILogger + registerMetric(metric: object): void } \ No newline at end of file