diff --git a/apps/api/src/libs/pgp.ts b/apps/api/src/libs/pgp.ts index b6469d039..0d3fc9e6a 100644 --- a/apps/api/src/libs/pgp.ts +++ b/apps/api/src/libs/pgp.ts @@ -3,6 +3,7 @@ import { ISSLConfig } from 'pg-promise/typescript/pg-subset.js'; import config from '#config'; import logger from '#libs/logger'; +import { recordQuery } from '#libs/queryStats'; export const ssl: ISSLConfig = { rejectUnauthorized: true, @@ -16,6 +17,8 @@ if (config.dbCa) { export const pgp = pgpromise({ error: (_err, e) => { + recordQuery(e.queryFilePath, 0, true); + if (e.cn) { logger.error(e.cn); } @@ -32,6 +35,9 @@ export const pgp = pgpromise({ logger.error(e.ctx); } }, + receive: (e) => { + recordQuery(e.ctx.queryFilePath, e.result?.duration ?? 0, false); + }, }); /** diff --git a/apps/api/src/libs/queryStats.ts b/apps/api/src/libs/queryStats.ts new file mode 100644 index 000000000..ecc1a7e3e --- /dev/null +++ b/apps/api/src/libs/queryStats.ts @@ -0,0 +1,58 @@ +import logger from '#libs/logger'; + +type Tally = { ms: number; n: number }; + +type Entry = { error: Tally; ok: Tally }; + +const REPORT_INTERVAL_MS = 60_000; + +const tallies = new Map(); + +const empty = (): Entry => ({ + error: { ms: 0, n: 0 }, + ok: { ms: 0, n: 0 }, +}); + +const mean = (tally: Tally): number => + tally.n ? Math.round(tally.ms / tally.n) : 0; + +const shortPath = (path: string): string => + path.replace(/^.*[/\\]sql[/\\]/, ''); + +export const recordQuery = ( + queryFilePath: string | undefined, + ms: number, + failed: boolean, +): void => { + const label = queryFilePath ? shortPath(queryFilePath) : 'adhoc'; + const entry = tallies.get(label) ?? empty(); + const tally = failed ? entry.error : entry.ok; + + tally.ms += ms; + tally.n += 1; + tallies.set(label, entry); +}; + +const report = (): void => { + for (const [label, entry] of tallies) { + const calls = entry.ok.n + entry.error.n; + + if (!calls) continue; + + logger.info( + { + calls, + db_ms: Math.round(entry.ok.ms + entry.error.ms), + error_n: entry.error.n, + label, + ok_mean_ms: mean(entry.ok), + ok_n: entry.ok.n, + }, + 'query stats', + ); + } + + tallies.clear(); +}; + +setInterval(report, REPORT_INTERVAL_MS).unref();