typeorm logger

This commit is contained in:
hannathkadher
2025-04-22 10:20:48 +04:00
parent be3c9f730d
commit fcb27155d8
8 changed files with 180 additions and 65 deletions

View File

@ -0,0 +1,8 @@
import { AsyncLocalStorage } from 'async_hooks';
export interface RequestContextStore {
requestId?: string;
userId?: string;
}
export const requestContext = new AsyncLocalStorage<RequestContextStore>();

View File

@ -43,73 +43,82 @@ import { SubspaceProductAllocationEntity } from '../modules/space/entities/subsp
import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity'; import { SubspaceEntity } from '../modules/space/entities/subspace/subspace.entity';
import { TagEntity } from '../modules/space/entities/tag.entity'; import { TagEntity } from '../modules/space/entities/tag.entity';
import { ClientEntity } from '../modules/client/entities'; import { ClientEntity } from '../modules/client/entities';
import { TypeOrmWinstonLogger } from '@app/common/logger/services/typeorm.logger';
import { createLogger } from 'winston';
import { winstonLoggerOptions } from '../logger/services/winston.logger';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forRootAsync({ TypeOrmModule.forRootAsync({
imports: [ConfigModule], imports: [ConfigModule],
inject: [ConfigService], inject: [ConfigService],
useFactory: (configService: ConfigService) => ({ useFactory: (configService: ConfigService) => {
name: 'default', const winstonLogger = createLogger(winstonLoggerOptions);
type: 'postgres', const typeOrmLogger = new TypeOrmWinstonLogger(winstonLogger);
host: configService.get('DB_HOST'), return {
port: configService.get('DB_PORT'), name: 'default',
username: configService.get('DB_USER'), type: 'postgres',
password: configService.get('DB_PASSWORD'), host: configService.get('DB_HOST'),
database: configService.get('DB_NAME'), port: configService.get('DB_PORT'),
entities: [ username: configService.get('DB_USER'),
NewTagEntity, password: configService.get('DB_PASSWORD'),
ProjectEntity, database: configService.get('DB_NAME'),
UserEntity, entities: [
UserSessionEntity, NewTagEntity,
UserOtpEntity, ProjectEntity,
ProductEntity, UserEntity,
DeviceUserPermissionEntity, UserSessionEntity,
DeviceEntity, UserOtpEntity,
PermissionTypeEntity, ProductEntity,
CommunityEntity, DeviceUserPermissionEntity,
SpaceEntity, DeviceEntity,
SpaceLinkEntity, PermissionTypeEntity,
SubspaceEntity, CommunityEntity,
TagEntity, SpaceEntity,
UserSpaceEntity, SpaceLinkEntity,
DeviceUserPermissionEntity, SubspaceEntity,
RoleTypeEntity, TagEntity,
UserNotificationEntity, UserSpaceEntity,
DeviceNotificationEntity, DeviceUserPermissionEntity,
RegionEntity, RoleTypeEntity,
TimeZoneEntity, UserNotificationEntity,
VisitorPasswordEntity, DeviceNotificationEntity,
DeviceStatusLogEntity, RegionEntity,
SceneEntity, TimeZoneEntity,
SceneIconEntity, VisitorPasswordEntity,
SceneDeviceEntity, DeviceStatusLogEntity,
SpaceModelEntity, SceneEntity,
SubspaceModelEntity, SceneIconEntity,
TagModel, SceneDeviceEntity,
InviteUserEntity, SpaceModelEntity,
InviteUserSpaceEntity, SubspaceModelEntity,
InviteSpaceEntity, TagModel,
AutomationEntity, InviteUserEntity,
SpaceModelProductAllocationEntity, InviteUserSpaceEntity,
SubspaceModelProductAllocationEntity, InviteSpaceEntity,
SpaceProductAllocationEntity, AutomationEntity,
SubspaceProductAllocationEntity, SpaceModelProductAllocationEntity,
ClientEntity, SubspaceModelProductAllocationEntity,
], SpaceProductAllocationEntity,
namingStrategy: new SnakeNamingStrategy(), SubspaceProductAllocationEntity,
synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))), ClientEntity,
logging: false, ],
extra: { namingStrategy: new SnakeNamingStrategy(),
charset: 'utf8mb4', synchronize: Boolean(JSON.parse(configService.get('DB_SYNC'))),
max: 20, // set pool max size logging: true,
idleTimeoutMillis: 5000, // close idle clients after 5 second logger: typeOrmLogger,
connectionTimeoutMillis: 11_000, // return an error after 11 second if connection could not be established extra: {
maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion) charset: 'utf8mb4',
}, max: 20, // set pool max size
continuationLocalStorage: true, idleTimeoutMillis: 5000, // close idle clients after 5 second
ssl: Boolean(JSON.parse(configService.get('DB_SSL'))), connectionTimeoutMillis: 11_000, // return an error after 11 second if connection could not be established
}), maxUses: 7500, // close (and replace) a connection after it has been used 7500 times (see below for discussion)
},
continuationLocalStorage: true,
ssl: Boolean(JSON.parse(configService.get('DB_SSL'))),
};
},
}), }),
], ],
providers: [TypeOrmWinstonLogger],
}) })
export class DatabaseModule {} export class DatabaseModule {}

View File

@ -0,0 +1,12 @@
// src/common/logger/logger.module.ts
import { Module } from '@nestjs/common';
import { WinstonModule } from 'nest-winston';
import { winstonLoggerOptions } from './services/winston.logger';
import { TypeOrmWinstonLogger } from './services/typeorm.logger';
@Module({
imports: [WinstonModule.forRoot(winstonLoggerOptions)],
providers: [TypeOrmWinstonLogger],
exports: [TypeOrmWinstonLogger],
})
export class LoggerModule {}

View File

@ -0,0 +1,64 @@
import { Logger as WinstonLogger } from 'winston';
import { Logger as TypeOrmLogger } from 'typeorm';
import { requestContext } from '@app/common/context/request-context';
export class TypeOrmWinstonLogger implements TypeOrmLogger {
constructor(
private readonly logger: WinstonLogger,
private readonly slowQueryThreshold = 500, // ms
) {}
logQuery(query: string, parameters?: any[]) {
const context = requestContext.getStore();
const requestId = context?.requestId ?? 'N/A';
const start = Date.now();
const timeout = setTimeout(() => {
const duration = Date.now() - start;
const isSlow = duration > this.slowQueryThreshold;
this.logger[isSlow ? 'warn' : 'debug'](`[DB][QUERY] ${query}`, {
requestId,
parameters,
duration: `${duration}ms`,
isSlow,
});
}, 0);
// Just ensures the setTimeout fires after this function returns
clearTimeout(timeout);
}
logQueryError(error: string | Error, query: string, parameters?: any[]) {
const requestId = requestContext.getStore()?.requestId ?? 'N/A';
this.logger.error(`[DB][ERROR] ${query}`, {
requestId,
parameters,
error,
});
}
logQuerySlow(time: number, query: string, parameters?: any[]) {
const requestId = requestContext.getStore()?.requestId ?? 'N/A';
this.logger.warn(`🔥 [DB][SLOW > ${time}ms] ${query}`, {
requestId,
parameters,
time,
});
}
logSchemaBuild(message: string) {
this.logger.info(`[DB][SCHEMA] ${message}`);
}
logMigration(message: string) {
this.logger.info(`[DB][MIGRATION] ${message}`);
}
log(level: 'log' | 'info' | 'warn', message: any) {
this.logger.log({
level,
message: `[DB] ${message}`,
});
}
}

View File

@ -2,6 +2,7 @@ import { utilities as nestWinstonModuleUtilities } from 'nest-winston';
import * as winston from 'winston'; import * as winston from 'winston';
export const winstonLoggerOptions: winston.LoggerOptions = { export const winstonLoggerOptions: winston.LoggerOptions = {
level: 'debug',
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
format: winston.format.combine( format: winston.format.combine(

View File

@ -0,0 +1,14 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { requestContext } from '../context/request-context';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class RequestContextMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
const context = {
requestId: req.headers['x-request-id'] || uuidv4(),
};
requestContext.run(context, () => next());
}
}

View File

@ -33,7 +33,7 @@ import { ClientModule } from './client/client.module';
import { DeviceCommissionModule } from './commission-device/commission-device.module'; import { DeviceCommissionModule } from './commission-device/commission-device.module';
import { PowerClampModule } from './power-clamp/power-clamp.module'; import { PowerClampModule } from './power-clamp/power-clamp.module';
import { WinstonModule } from 'nest-winston'; import { WinstonModule } from 'nest-winston';
import { winstonLoggerOptions } from './common/filters/http-exception/logger/winston.logger'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger';
@Module({ @Module({
imports: [ imports: [
ConfigModule.forRoot({ ConfigModule.forRoot({

View File

@ -7,6 +7,9 @@ import { ValidationPipe } from '@nestjs/common';
import { json, urlencoded } from 'body-parser'; import { json, urlencoded } from 'body-parser';
import { SeederService } from '@app/common/seed/services/seeder.service'; import { SeederService } from '@app/common/seed/services/seeder.service';
import { HttpExceptionFilter } from './common/filters/http-exception/http-exception.filter'; import { HttpExceptionFilter } from './common/filters/http-exception/http-exception.filter';
import { Logger } from '@nestjs/common';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { RequestContextMiddleware } from '@app/common/middleware/request-context.middleware';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
@ -18,6 +21,8 @@ async function bootstrap() {
app.use(urlencoded({ limit: '1mb', extended: true })); app.use(urlencoded({ limit: '1mb', extended: true }));
app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalFilters(new HttpExceptionFilter());
app.use(new RequestContextMiddleware().use);
app.use( app.use(
rateLimit({ rateLimit({
windowMs: 5 * 60 * 1000, windowMs: 5 * 60 * 1000,
@ -43,14 +48,16 @@ async function bootstrap() {
); );
const seederService = app.get(SeederService); const seederService = app.get(SeederService);
const logger = app.get<Logger>(WINSTON_MODULE_NEST_PROVIDER);
try { try {
await seederService.seed(); await seederService.seed();
console.log('Seeding complete!'); logger.log('Seeding complete!');
} catch (error) { } catch (error) {
console.error('Seeding failed!', error); logger.error('Seeding failed!', error.stack || error);
} }
console.log('Starting auth at port ...', process.env.PORT || 4000); logger.log('Starting auth at port ...', process.env.PORT || 4000);
await app.listen(process.env.PORT || 4000); await app.listen(process.env.PORT || 4000);
} }
bootstrap(); bootstrap();