Optional prom-client peer dependency

This commit is contained in:
Erik 2023-11-11 17:11:47 +02:00
parent ee67d81e94
commit 460ded7641
5 changed files with 97 additions and 27 deletions

View File

@ -22,7 +22,9 @@
"build/**/*" "build/**/*"
], ],
"scripts": { "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", "test": "yarn build && jest",
"release": "yarn build && yarn publish", "release": "yarn build && yarn publish",
"lint": "eslint --fix" "lint": "eslint --fix"
@ -41,5 +43,13 @@
"amqplib": "^0.10.3", "amqplib": "^0.10.3",
"mongodb": "^5.7.0", "mongodb": "^5.7.0",
"mysql": "^2.18.1" "mysql": "^2.18.1"
},
"peerDependencies": {
"prom-client": "^15.0.0"
},
"peerDependenciesMeta": {
"prom-client": {
"optional": true
}
} }
} }

View File

@ -1,3 +1,5 @@
import fs from 'node:fs'; import fs from 'node:fs';
fs.writeFileSync('./build/cjs/package.json', JSON.stringify({ type: 'commonjs' })); if (process.argv.includes('--common'))
fs.writeFileSync('./build/esm/package.json', JSON.stringify({ type: 'module' })); 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' }));

View File

@ -25,7 +25,8 @@ export type MariaOptions = {
client?: PoolConfig, client?: PoolConfig,
credentials: Credentials, credentials: Credentials,
loggerOptions?: LoggerClientOptions, loggerOptions?: LoggerClientOptions,
donorQuery?: boolean donorQuery?: boolean,
recordMetrics?: boolean
} }
type MariaError = { type MariaError = {
@ -95,6 +96,8 @@ class MariaDB
#pool: PoolCluster | null; #pool: PoolCluster | null;
#nodes: Node[]; #nodes: Node[];
#canQueryDonor: boolean; #canQueryDonor: boolean;
#queryHistogram?: {startTimer: (labels: object) => () => void};
#server: IServer;
constructor (server: IServer, options: MariaOptions) constructor (server: IServer, options: MariaOptions)
{ {
@ -128,7 +131,7 @@ class MariaDB
this.#cluster = this.#credentials.length > 1; this.#cluster = this.#credentials.length > 1;
this.#canQueryDonor = options.donorQuery ?? false; this.#canQueryDonor = options.donorQuery ?? false;
this.#logger = server.createLogger(this, options.loggerOptions); this.#logger = server.createLogger(this, options.loggerOptions);
this.#server = server;
} }
get ready () get ready ()
@ -141,6 +144,7 @@ class MariaDB
return this.#_activeQueries; return this.#_activeQueries;
} }
// eslint-disable-next-line max-lines-per-function
async init () async init ()
{ {
if (!this.#load) 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; this.#_ready = true;
return this; return this;
} }
async close () async close ()
@ -386,6 +407,7 @@ class MariaDB
{ {
const result = await new Promise<T[] | FieldInfo[] | [OkPacket]| undefined>((resolve, reject) => const result = await new Promise<T[] | FieldInfo[] | [OkPacket]| undefined>((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) => const q = connection.query({ timeout, sql: query }, values, (err, results, fields) =>
{ {
if (err) if (err)
@ -397,6 +419,8 @@ class MariaDB
else else
resolve(fields); resolve(fields);
connection.release(); connection.release();
if (endTimer)
endTimer();
}); });
this.#logger.debug(`Constructed query: ${q.sql.substring(0, 1024)}`); this.#logger.debug(`Constructed query: ${q.sql.substring(0, 1024)}`);
}); });

View File

@ -21,7 +21,8 @@ export type MongoOptions = {
credentials: Credentials, credentials: Credentials,
loggerOptions?: LoggerClientOptions, loggerOptions?: LoggerClientOptions,
client?: MongoClientOptions, client?: MongoClientOptions,
load?: boolean load?: boolean,
recordMetrics?: boolean
} }
type StringIndexable = {[key: string]: boolean | string | number | Document | object} type StringIndexable = {[key: string]: boolean | string | number | Document | object}
@ -54,6 +55,8 @@ class MongoDB
#db: Db | null; #db: Db | null;
#_client: MongoClient; #_client: MongoClient;
#queryHistogram?: { startTimer: (labels: object) => () => void };
#server: IServer;
constructor (server: IServer, config: MongoOptions) constructor (server: IServer, config: MongoOptions)
{ {
@ -73,6 +76,7 @@ class MongoDB
this.#load = config.load ?? true; this.#load = config.load ?? true;
this.#logger = server.createLogger(this, config.loggerOptions); this.#logger = server.createLogger(this, config.loggerOptions);
this.#server = server;
if (URI) if (URI)
{ {
@ -122,7 +126,6 @@ class MongoDB
*/ */
async init () async init ()
{ {
if (!this.#load) if (!this.#load)
return this.#logger.info('Not loading MongoDB'); return this.#logger.info('Not loading MongoDB');
@ -133,12 +136,29 @@ class MongoDB
await this.#_client.connect(); await this.#_client.connect();
this.#logger.debug('Connected, selecting DB'); 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'); this.#logger.status('MongoDB ready');
return this; return this;
} }
async close () async close ()
@ -167,7 +187,6 @@ class MongoDB
*/ */
async find<T extends Document> (db: string, query: MongoQuery, options?: FindOptions<T>): Promise<WithId<T>[]> async find<T extends Document> (db: string, query: MongoQuery, options?: FindOptions<T>): Promise<WithId<T>[]>
{ {
if (!this.#db) if (!this.#db)
throw new Error('MongoDB not connected'); throw new Error('MongoDB not connected');
@ -179,9 +198,12 @@ class MongoDB
this.#logger.debug(`Incoming find query for ${db} with parameters ${inspect(query)}`); this.#logger.debug(`Incoming find query for ${db} with parameters ${inspect(query)}`);
const endTimer = this.#queryHistogram?.startTimer({ type: 'find' });
const cursor = this.#db.collection<T>(db).find(query as Filter<T>, options); const cursor = this.#db.collection<T>(db).find(query as Filter<T>, options);
return cursor.toArray(); const data = await cursor.toArray();
if (endTimer)
endTimer();
return data;
} }
/** /**
@ -194,7 +216,6 @@ class MongoDB
*/ */
async findOne<T extends Document> (db: string, query: MongoQuery, options: FindOptions<T> = {}): Promise<WithId<T> | null> async findOne<T extends Document> (db: string, query: MongoQuery, options: FindOptions<T> = {}): Promise<WithId<T> | null>
{ {
if (!this.#db) if (!this.#db)
throw new Error('MongoDB not connected'); throw new Error('MongoDB not connected');
if (typeof db !== 'string') if (typeof db !== 'string')
@ -204,9 +225,11 @@ class MongoDB
query._id = new ObjectId(query._id); query._id = new ObjectId(query._id);
this.#logger.debug(`Incoming findOne query for ${db} with parameters ${inspect(query)}`); 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<T>(db).findOne(query as Filter<T>, options); const result = await this.#db.collection<T>(db).findOne(query as Filter<T>, options);
if (endTimer)
endTimer();
return result; return result;
} }
/** /**
@ -220,7 +243,6 @@ class MongoDB
*/ */
async updateMany<T extends Document> (db: string, filter: MongoQuery, data: T, upsert = false) async updateMany<T extends Document> (db: string, filter: MongoQuery, data: T, upsert = false)
{ {
if (!this.#db) if (!this.#db)
throw new Error('MongoDB not connected'); throw new Error('MongoDB not connected');
if (typeof db !== 'string') if (typeof db !== 'string')
@ -231,9 +253,11 @@ class MongoDB
filter._id = new ObjectId(filter._id); filter._id = new ObjectId(filter._id);
this.#logger.debug(`Incoming update query for '${db}' with parameters\n${inspect(filter)}\nand data\n${inspect(data)}`); 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<T>(db).updateMany(filter as Filter<T>, { $set: data }, { upsert }); const result = await this.#db.collection<T>(db).updateMany(filter as Filter<T>, { $set: data }, { upsert });
if (endTimer)
endTimer();
return result; return result;
} }
/** /**
@ -247,7 +271,6 @@ class MongoDB
*/ */
async updateOne (db: string, filter: MongoQuery, data: Document, upsert = false) async updateOne (db: string, filter: MongoQuery, data: Document, upsert = false)
{ {
if (!this.#db) if (!this.#db)
throw new Error('MongoDB not connected'); throw new Error('MongoDB not connected');
if (typeof db !== 'string') if (typeof db !== 'string')
@ -256,9 +279,11 @@ class MongoDB
filter._id = new ObjectId(filter._id); filter._id = new ObjectId(filter._id);
this.#logger.debug(`Incoming updateOne query for ${db} with parameters ${inspect(filter)}`); 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<Document>, { $set: data }, { upsert }); const result = await this.#db.collection(db).updateOne(filter as Filter<Document>, { $set: data }, { upsert });
if (endTimer)
endTimer();
return result; return result;
} }
/** /**
@ -272,7 +297,6 @@ class MongoDB
*/ */
async insertOne (db: string, data: Document) async insertOne (db: string, data: Document)
{ {
if (!this.#db) if (!this.#db)
throw new Error('MongoDB not connected'); throw new Error('MongoDB not connected');
if (typeof db !== 'string') if (typeof db !== 'string')
@ -281,9 +305,11 @@ class MongoDB
data._id = new ObjectId(data._id); data._id = new ObjectId(data._id);
this.#logger.debug(`Incoming insertOne query for ${db} with parameters ${inspect(data)}`); 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); const result = await this.#db.collection(db).insertOne(data);
if (endTimer)
endTimer();
return result; return result;
} }
async deleteOne (db: string, filter: Document) async deleteOne (db: string, filter: Document)
@ -296,7 +322,10 @@ class MongoDB
filter._id = new ObjectId(filter._id); filter._id = new ObjectId(filter._id);
this.#logger.debug(`Incoming deleteOne query for ${db} with parameters ${inspect(filter)}`); 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); const result = await this.#db.collection(db).deleteOne(filter);
if (endTimer)
endTimer();
return result; return result;
} }
@ -311,7 +340,10 @@ class MongoDB
if (typeof filter._id === 'string') if (typeof filter._id === 'string')
filter._id = new ObjectId(filter._id); filter._id = new ObjectId(filter._id);
const endTimer = this.#queryHistogram?.startTimer({ type: 'findOneAndDelete' });
const result = await this.#db.collection<T>(db).findOneAndDelete(filter as Filter<T>, { includeResultMetadata: meta }); const result = await this.#db.collection<T>(db).findOneAndDelete(filter as Filter<T>, { includeResultMetadata: meta });
if (endTimer)
endTimer();
return result; return result;
} }
@ -335,7 +367,10 @@ class MongoDB
filter._id = new ObjectId(filter._id); 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)}`); 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 }); const result = await this.#db.collection(db).updateOne(filter, { $push: data }, { upsert });
if (endTimer)
endTimer();
return result; return result;
} }
@ -350,7 +385,6 @@ class MongoDB
*/ */
random<T extends Document> (db: string, filter: Document = {}, amount = 1) random<T extends Document> (db: string, filter: Document = {}, amount = 1)
{ {
if (!this.#db) if (!this.#db)
throw new Error('MongoDB not connected'); throw new Error('MongoDB not connected');
if (typeof db !== 'string') if (typeof db !== 'string')
@ -365,7 +399,6 @@ class MongoDB
const cursor = this.#db.collection(db).aggregate<T>([{ $match: filter }, { $sample: { size: amount } }]); const cursor = this.#db.collection(db).aggregate<T>([{ $match: filter }, { $sample: { size: amount } }]);
return cursor.toArray(); return cursor.toArray();
} }
stats (options = {}) stats (options = {})

View File

@ -1,5 +1,6 @@
import { ILogger, LoggerClientOptions } from "./Logger.js"; import { ILogger, LoggerClientOptions } from './Logger.js';
export interface IServer { export interface IServer {
createLogger(obj: object, options?: Partial<LoggerClientOptions>): ILogger createLogger(obj: object, options?: Partial<LoggerClientOptions>): ILogger
registerMetric(metric: object): void
} }