logging
This commit is contained in:
@@ -7,7 +7,10 @@ DB_PORT=3306
|
|||||||
DB_USERNAME=listify
|
DB_USERNAME=listify
|
||||||
DB_PASSWORD=change-me
|
DB_PASSWORD=change-me
|
||||||
DB_DATABASE=listify
|
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_ACCESS_SECRET=change-me-access-secret
|
||||||
JWT_REFRESH_SECRET=change-me-refresh-secret
|
JWT_REFRESH_SECRET=change-me-refresh-secret
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ DB_PORT=3306
|
|||||||
DB_USERNAME=listify
|
DB_USERNAME=listify
|
||||||
DB_PASSWORD=change-me
|
DB_PASSWORD=change-me
|
||||||
DB_DATABASE=listify
|
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_ACCESS_SECRET=change-me-access-secret
|
||||||
JWT_REFRESH_SECRET=change-me-refresh-secret
|
JWT_REFRESH_SECRET=change-me-refresh-secret
|
||||||
|
|||||||
@@ -8,13 +8,33 @@ import { AuthModule } from './auth/auth.module';
|
|||||||
import { ListTemplatesModule } from './list-templates/list-templates.module';
|
import { ListTemplatesModule } from './list-templates/list-templates.module';
|
||||||
import { ListsModule } from './lists/lists.module';
|
import { ListsModule } from './lists/lists.module';
|
||||||
import { MailModule } from './mail/mail.module';
|
import { MailModule } from './mail/mail.module';
|
||||||
|
import {
|
||||||
|
databaseLoggerOptionsFromEnv,
|
||||||
|
parseDatabaseLogging,
|
||||||
|
slowQueryThresholdFromEnv,
|
||||||
|
} from './database/database-logging.config';
|
||||||
|
import { DatabaseLogger } from './database/database.logger';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({ isGlobal: true }),
|
||||||
TypeOrmModule.forRootAsync({
|
TypeOrmModule.forRootAsync({
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (configService: ConfigService) => ({
|
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',
|
type: 'mysql',
|
||||||
host: configService.get<string>('DB_HOST', 'localhost'),
|
host: configService.get<string>('DB_HOST', 'localhost'),
|
||||||
port: Number(configService.get<string>('DB_PORT', '3306')),
|
port: Number(configService.get<string>('DB_PORT', '3306')),
|
||||||
@@ -23,8 +43,11 @@ import { MailModule } from './mail/mail.module';
|
|||||||
database: configService.get<string>('DB_DATABASE', 'listify'),
|
database: configService.get<string>('DB_DATABASE', 'listify'),
|
||||||
autoLoadEntities: true,
|
autoLoadEntities: true,
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: configService.get<string>('DB_LOGGING', 'false') === 'true',
|
logging: parseDatabaseLogging(env.DB_LOGGING),
|
||||||
}),
|
logger: new DatabaseLogger(databaseLoggerOptionsFromEnv(env)),
|
||||||
|
maxQueryExecutionTime: slowQueryThresholdFromEnv(env),
|
||||||
|
};
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
AuthModule,
|
AuthModule,
|
||||||
@@ -35,8 +58,4 @@ import { MailModule } from './mail/mail.module';
|
|||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
export class AppModule {
|
export class AppModule {}
|
||||||
constructor(configService: ConfigService) {
|
|
||||||
console.log(configService.get<string>('DB_HOST', 'localhost'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import { ListTemplateItemEntity } from '../list-templates/list-template-item.ent
|
|||||||
import { TemplateSeedEntity } from '../list-templates/template-seed.entity';
|
import { TemplateSeedEntity } from '../list-templates/template-seed.entity';
|
||||||
import { UserListEntity } from '../lists/user-list.entity';
|
import { UserListEntity } from '../lists/user-list.entity';
|
||||||
import { UserListItemEntity } from '../lists/user-list-item.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({
|
export default new DataSource({
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
@@ -17,7 +23,9 @@ export default new DataSource({
|
|||||||
password: process.env.DB_PASSWORD ?? '',
|
password: process.env.DB_PASSWORD ?? '',
|
||||||
database: process.env.DB_DATABASE ?? 'listify',
|
database: process.env.DB_DATABASE ?? 'listify',
|
||||||
synchronize: false,
|
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: [
|
entities: [
|
||||||
UserEntity,
|
UserEntity,
|
||||||
RefreshTokenEntity,
|
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