logging
This commit is contained in:
@@ -7,7 +7,10 @@ DB_PORT=3306
|
||||
DB_USERNAME=listify
|
||||
DB_PASSWORD=change-me
|
||||
DB_DATABASE=listify
|
||||
DB_LOGGING=false
|
||||
DB_LOGGING=all
|
||||
DB_LOG_PARAMETERS=false
|
||||
DB_LOG_MAX_PARAM_LENGTH=500
|
||||
DB_SLOW_QUERY_THRESHOLD_MS=500
|
||||
|
||||
JWT_ACCESS_SECRET=change-me-access-secret
|
||||
JWT_REFRESH_SECRET=change-me-refresh-secret
|
||||
|
||||
@@ -5,7 +5,10 @@ DB_PORT=3306
|
||||
DB_USERNAME=listify
|
||||
DB_PASSWORD=change-me
|
||||
DB_DATABASE=listify
|
||||
DB_LOGGING=false
|
||||
DB_LOGGING=all
|
||||
DB_LOG_PARAMETERS=false
|
||||
DB_LOG_MAX_PARAM_LENGTH=500
|
||||
DB_SLOW_QUERY_THRESHOLD_MS=500
|
||||
|
||||
JWT_ACCESS_SECRET=change-me-access-secret
|
||||
JWT_REFRESH_SECRET=change-me-refresh-secret
|
||||
|
||||
@@ -8,23 +8,46 @@ import { AuthModule } from './auth/auth.module';
|
||||
import { ListTemplatesModule } from './list-templates/list-templates.module';
|
||||
import { ListsModule } from './lists/lists.module';
|
||||
import { MailModule } from './mail/mail.module';
|
||||
import {
|
||||
databaseLoggerOptionsFromEnv,
|
||||
parseDatabaseLogging,
|
||||
slowQueryThresholdFromEnv,
|
||||
} from './database/database-logging.config';
|
||||
import { DatabaseLogger } from './database/database.logger';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
TypeOrmModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
type: 'mysql',
|
||||
host: configService.get<string>('DB_HOST', 'localhost'),
|
||||
port: Number(configService.get<string>('DB_PORT', '3306')),
|
||||
username: configService.get<string>('DB_USERNAME', 'root'),
|
||||
password: configService.get<string>('DB_PASSWORD', ''),
|
||||
database: configService.get<string>('DB_DATABASE', 'listify'),
|
||||
autoLoadEntities: true,
|
||||
synchronize: false,
|
||||
logging: configService.get<string>('DB_LOGGING', 'false') === 'true',
|
||||
}),
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const env = {
|
||||
DB_LOGGING: configService.get<string>('DB_LOGGING', 'false'),
|
||||
DB_LOG_PARAMETERS: configService.get<string>('DB_LOG_PARAMETERS', 'false'),
|
||||
DB_LOG_MAX_PARAM_LENGTH: configService.get<string>(
|
||||
'DB_LOG_MAX_PARAM_LENGTH',
|
||||
'500',
|
||||
),
|
||||
DB_SLOW_QUERY_THRESHOLD_MS: configService.get<string>(
|
||||
'DB_SLOW_QUERY_THRESHOLD_MS',
|
||||
'500',
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'mysql',
|
||||
host: configService.get<string>('DB_HOST', 'localhost'),
|
||||
port: Number(configService.get<string>('DB_PORT', '3306')),
|
||||
username: configService.get<string>('DB_USERNAME', 'root'),
|
||||
password: configService.get<string>('DB_PASSWORD', ''),
|
||||
database: configService.get<string>('DB_DATABASE', 'listify'),
|
||||
autoLoadEntities: true,
|
||||
synchronize: false,
|
||||
logging: parseDatabaseLogging(env.DB_LOGGING),
|
||||
logger: new DatabaseLogger(databaseLoggerOptionsFromEnv(env)),
|
||||
maxQueryExecutionTime: slowQueryThresholdFromEnv(env),
|
||||
};
|
||||
},
|
||||
}),
|
||||
EventEmitterModule.forRoot(),
|
||||
AuthModule,
|
||||
@@ -35,8 +58,4 @@ import { MailModule } from './mail/mail.module';
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {
|
||||
constructor(configService: ConfigService) {
|
||||
console.log(configService.get<string>('DB_HOST', 'localhost'))
|
||||
}
|
||||
}
|
||||
export class AppModule {}
|
||||
|
||||
@@ -8,6 +8,12 @@ import { ListTemplateItemEntity } from '../list-templates/list-template-item.ent
|
||||
import { TemplateSeedEntity } from '../list-templates/template-seed.entity';
|
||||
import { UserListEntity } from '../lists/user-list.entity';
|
||||
import { UserListItemEntity } from '../lists/user-list-item.entity';
|
||||
import {
|
||||
databaseLoggerOptionsFromEnv,
|
||||
parseDatabaseLogging,
|
||||
slowQueryThresholdFromEnv,
|
||||
} from './database-logging.config';
|
||||
import { DatabaseLogger } from './database.logger';
|
||||
|
||||
export default new DataSource({
|
||||
type: 'mysql',
|
||||
@@ -17,7 +23,9 @@ export default new DataSource({
|
||||
password: process.env.DB_PASSWORD ?? '',
|
||||
database: process.env.DB_DATABASE ?? 'listify',
|
||||
synchronize: false,
|
||||
logging: process.env.DB_LOGGING === 'true',
|
||||
logging: parseDatabaseLogging(process.env.DB_LOGGING),
|
||||
logger: new DatabaseLogger(databaseLoggerOptionsFromEnv(process.env)),
|
||||
maxQueryExecutionTime: slowQueryThresholdFromEnv(process.env),
|
||||
entities: [
|
||||
UserEntity,
|
||||
RefreshTokenEntity,
|
||||
|
||||
53
listify-api/src/database/database-logging.config.ts
Normal file
53
listify-api/src/database/database-logging.config.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { LoggerOptions } from 'typeorm';
|
||||
|
||||
export interface DatabaseLoggerOptions {
|
||||
includeParameters: boolean;
|
||||
maxParameterLength: number;
|
||||
}
|
||||
|
||||
const ALL_LOGGING_OPTIONS: LoggerOptions = [
|
||||
'query',
|
||||
'error',
|
||||
'schema',
|
||||
'warn',
|
||||
'info',
|
||||
'log',
|
||||
'migration',
|
||||
];
|
||||
|
||||
export function parseDatabaseLogging(value?: string | boolean): LoggerOptions {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? ALL_LOGGING_OPTIONS : false;
|
||||
}
|
||||
|
||||
const normalizedValue = value?.trim().toLowerCase();
|
||||
|
||||
if (!normalizedValue || normalizedValue === 'false' || normalizedValue === 'off') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedValue === 'true' || normalizedValue === 'all') {
|
||||
return ALL_LOGGING_OPTIONS;
|
||||
}
|
||||
|
||||
return normalizedValue
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean) as LoggerOptions;
|
||||
}
|
||||
|
||||
export function databaseLoggerOptionsFromEnv(
|
||||
values: Record<string, string | undefined>,
|
||||
): DatabaseLoggerOptions {
|
||||
return {
|
||||
includeParameters: values.DB_LOG_PARAMETERS === 'true',
|
||||
maxParameterLength: Number(values.DB_LOG_MAX_PARAM_LENGTH ?? '500'),
|
||||
};
|
||||
}
|
||||
|
||||
export function slowQueryThresholdFromEnv(
|
||||
values: Record<string, string | undefined>,
|
||||
): number | undefined {
|
||||
const threshold = Number(values.DB_SLOW_QUERY_THRESHOLD_MS ?? '500');
|
||||
return Number.isFinite(threshold) && threshold > 0 ? threshold : undefined;
|
||||
}
|
||||
126
listify-api/src/database/database.logger.ts
Normal file
126
listify-api/src/database/database.logger.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Logger as NestLogger } from '@nestjs/common';
|
||||
import type { Logger as TypeOrmLogger, QueryRunner } from 'typeorm';
|
||||
import type { DatabaseLoggerOptions } from './database-logging.config';
|
||||
|
||||
const REDACTED = '[redacted]';
|
||||
const SENSITIVE_KEY_PATTERN = /password|token|secret|authorization|cookie|hash/i;
|
||||
|
||||
export class DatabaseLogger implements TypeOrmLogger {
|
||||
private readonly logger = new NestLogger('Database');
|
||||
|
||||
constructor(private readonly options: DatabaseLoggerOptions) {}
|
||||
|
||||
logQuery(
|
||||
query: string,
|
||||
parameters?: unknown[],
|
||||
queryRunner?: QueryRunner,
|
||||
): void {
|
||||
this.logger.debug(this.formatMessage('query', query, parameters, queryRunner));
|
||||
}
|
||||
|
||||
logQueryError(
|
||||
error: string | Error,
|
||||
query: string,
|
||||
parameters?: unknown[],
|
||||
queryRunner?: QueryRunner,
|
||||
): void {
|
||||
const errorMessage = error instanceof Error ? error.message : error;
|
||||
const errorStack = error instanceof Error ? error.stack : undefined;
|
||||
this.logger.error(
|
||||
this.formatMessage('query-error', query, parameters, queryRunner, {
|
||||
error: errorMessage,
|
||||
}),
|
||||
errorStack,
|
||||
);
|
||||
}
|
||||
|
||||
logQuerySlow(
|
||||
time: number,
|
||||
query: string,
|
||||
parameters?: unknown[],
|
||||
queryRunner?: QueryRunner,
|
||||
): void {
|
||||
this.logger.warn(
|
||||
this.formatMessage('slow-query', query, parameters, queryRunner, {
|
||||
durationMs: time,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
logSchemaBuild(message: string, queryRunner?: QueryRunner): void {
|
||||
this.logger.log(this.formatMessage('schema', message, undefined, queryRunner));
|
||||
}
|
||||
|
||||
logMigration(message: string, queryRunner?: QueryRunner): void {
|
||||
this.logger.log(this.formatMessage('migration', message, undefined, queryRunner));
|
||||
}
|
||||
|
||||
log(
|
||||
level: 'log' | 'info' | 'warn',
|
||||
message: string,
|
||||
queryRunner?: QueryRunner,
|
||||
): void {
|
||||
const formattedMessage = this.formatMessage(level, message, undefined, queryRunner);
|
||||
|
||||
if (level === 'warn') {
|
||||
this.logger.warn(formattedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (level === 'info') {
|
||||
this.logger.debug(formattedMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(formattedMessage);
|
||||
}
|
||||
|
||||
private formatMessage(
|
||||
category: string,
|
||||
message: string,
|
||||
parameters?: unknown[],
|
||||
queryRunner?: QueryRunner,
|
||||
extra: Record<string, unknown> = {},
|
||||
): string {
|
||||
const payload = {
|
||||
category,
|
||||
database: queryRunner?.connection.options.database,
|
||||
message: this.compactWhitespace(message),
|
||||
...extra,
|
||||
...(this.options.includeParameters && parameters?.length
|
||||
? { parameters: this.sanitizeValue(parameters) }
|
||||
: {}),
|
||||
};
|
||||
|
||||
return JSON.stringify(payload);
|
||||
}
|
||||
|
||||
private sanitizeValue(value: unknown): unknown {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => this.sanitizeValue(entry));
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
|
||||
key,
|
||||
SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : this.sanitizeValue(entry),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.length > this.options.maxParameterLength) {
|
||||
return `${value.slice(0, this.options.maxParameterLength)}...`;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private compactWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user