Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/config/database.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { resolvePoolConfig } from '../database/pool';
import { SlowQueryLogger } from '../database/logging/slow-query.logger';

interface DatabaseConnectionSettings {
host: string;
Expand Down Expand Up @@ -71,9 +72,12 @@ export function getDatabaseConfig(): TypeOrmModuleOptions {
const primary = readPrimarySettings();
const replicas = getReadReplicaConnections(primary);
const pool = resolvePoolConfig();
const isTestEnv = process.env.NODE_ENV === 'test';
const commonOptions = {
autoLoadEntities: true,
synchronize: process.env.NODE_ENV !== 'production',
maxQueryExecutionTime: parseInt(process.env.DB_SLOW_QUERY_THRESHOLD_MS ?? '500', 10),
logger: isTestEnv ? undefined : new SlowQueryLogger(),
extra: {
max: pool.max,
min: pool.min,
Expand Down
86 changes: 86 additions & 0 deletions src/database/logging/slow-query.logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Injectable, Logger } from '@nestjs/common';
import { Logger as TypeOrmLogger, QueryRunner } from 'typeorm';

const EXPLAIN_THRESHOLD_MS = parseInt(process.env.DB_EXPLAIN_THRESHOLD_MS ?? '2000', 10);

/**
* TypeORM logger that emits structured slow-query logs and, for the
* slowest queries, an async EXPLAIN ANALYZE. Wired up only outside the
* test environment via getDatabaseConfig().
*/
@Injectable()
export class SlowQueryLogger implements TypeOrmLogger {
private readonly logger = new Logger(SlowQueryLogger.name);

logQuery(): void {
// Per-query logging is intentionally a no-op; only slow queries are logged.
}

logQueryError(error: string | Error, query: string, parameters?: unknown[]): void {
this.logger.error(
JSON.stringify({
message: 'Query error',
error: error instanceof Error ? error.message : error,
query,
parameters,
}),
);
}

logQuerySlow(time: number, query: string, parameters?: unknown[], queryRunner?: QueryRunner): void {

Check failure on line 30 in src/database/logging/slow-query.logger.ts

View workflow job for this annotation

GitHub Actions / validate

Replace `time:·number,·query:·string,·parameters?:·unknown[],·queryRunner?:·QueryRunner` with `⏎····time:·number,⏎····query:·string,⏎····parameters?:·unknown[],⏎····queryRunner?:·QueryRunner,⏎··`
this.logger.warn(
JSON.stringify({
message: 'Slow query detected',
durationMs: time,
query,
parameters,
}),
);

if (time >= EXPLAIN_THRESHOLD_MS && this.isSelect(query)) {
this.logExplainAnalyze(query, parameters, queryRunner).catch((err: Error) =>
this.logger.error(`Failed to run EXPLAIN ANALYZE: ${err.message}`),
);
}
}

logSchemaBuild(message: string): void {
this.logger.log(message);
}

logMigration(message: string): void {
this.logger.log(message);
}

log(level: 'log' | 'info' | 'warn', message: unknown): void {
if (level === 'warn') {
this.logger.warn(message as string);
} else {
this.logger.log(message as string);
}
}

private isSelect(query: string): boolean {
return /^\s*select/i.test(query);
}

private async logExplainAnalyze(
query: string,
parameters: unknown[] | undefined,
queryRunner?: QueryRunner,
): Promise<void> {
if (!queryRunner) {
return;
}

const explain = await queryRunner.connection.query(`EXPLAIN ANALYZE ${query}`, parameters as any[]);

Check failure on line 76 in src/database/logging/slow-query.logger.ts

View workflow job for this annotation

GitHub Actions / validate

Replace ``EXPLAIN·ANALYZE·${query}`,·parameters·as·any[]` with `⏎······`EXPLAIN·ANALYZE·${query}`,⏎······parameters·as·any[],⏎····`

this.logger.warn(
JSON.stringify({
message: 'EXPLAIN ANALYZE for slow query',
query,
explain,
}),
);
}
}
Loading