Skip to content

Commit

Permalink
Merge pull request #271 from DongHoonYu96/feature-be-#229
Browse files Browse the repository at this point in the history
[BE] feat#229 λͺ¨λ‹ˆν„°λ§ κ΅¬ν˜„ v4
  • Loading branch information
DongHoonYu96 authored Nov 27, 2024
2 parents ac5b2f7 + 0004208 commit 0091444
Show file tree
Hide file tree
Showing 8 changed files with 352 additions and 88 deletions.
24 changes: 24 additions & 0 deletions BE/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions BE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@nestjs/swagger": "^8.0.5",
"@nestjs/typeorm": "^10.0.2",
"@nestjs/websockets": "^10.4.7",
"@socket.io/admin-ui": "^0.5.1",
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
Expand Down
140 changes: 70 additions & 70 deletions BE/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,70 +1,70 @@
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GameModule } from './game/game.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RedisModule } from '@nestjs-modules/ioredis';
import { ConfigModule } from '@nestjs/config';
import { QuizSetModel } from './quiz-set/entities/quiz-set.entity';
import { QuizModel } from './quiz-set/entities/quiz.entity';
import { QuizChoiceModel } from './quiz-set/entities/quiz-choice.entity';
import { UserModel } from './user/entities/user.entity';
import { UserQuizArchiveModel } from './user/entities/user-quiz-archive.entity';
import { InitDBModule } from './InitDB/InitDB.module';
import { UserModule } from './user/user.module';
import { QuizSetModule } from './quiz-set/quiz-set.module';
import { WaitingRoomModule } from './waiting-room/waiting-room.module';
import { TimeController } from './time/time.controller';
import { TimeModule } from './time/time.module';
import { AuthModule } from './auth/auth.module';
import { ScheduleModule } from '@nestjs/schedule';
import { GameRedisMemoryService } from './game/redis/game-redis-memory.service';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '../.env',
isGlobal: true
}),
GameModule,
// μŠ€μΌ€μ€„λŸ¬ λͺ¨λ“ˆ μΆ”κ°€ (Redis λ©”λͺ¨λ¦¬ 관리 μ„œλΉ„μŠ€μ˜ @Cron λ°μ½”λ ˆμ΄ν„° μ‚¬μš©μ„ μœ„ν•΄)
ScheduleModule.forRoot(),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: +process.env.DB_PORT || 3306,
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWD || 'test',
database: process.env.DB_NAME || 'test_db',
entities: [QuizSetModel, QuizModel, QuizChoiceModel, UserModel, UserQuizArchiveModel],
synchronize: process.env.DEV ? true : false, // 개발 λͺ¨λ“œμ—μ„œλ§Œ ν™œμ„±ν™”
logging: true, // λͺ¨λ“  쿼리 λ‘œκΉ…
logger: 'advanced-console'
// extra: {
// // κΈ€λ‘œλ²Œ batch size μ„€μ •
// maxBatchSize: 100
// }
}),
RedisModule.forRoot({
type: 'single',
url: process.env.REDIS_URL || 'redis://localhost:6379'
}),
QuizSetModule,
UserModule,
InitDBModule,
WaitingRoomModule,
TimeModule,
AuthModule
],
controllers: [AppController, TimeController],
providers: [
AppService,
GameRedisMemoryService,
{
provide: APP_INTERCEPTOR,
useClass: ClassSerializerInterceptor
}
]
})
export class AppModule {}
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { GameModule } from './game/game.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RedisModule } from '@nestjs-modules/ioredis';
import { ConfigModule } from '@nestjs/config';
import { QuizSetModel } from './quiz-set/entities/quiz-set.entity';
import { QuizModel } from './quiz-set/entities/quiz.entity';
import { QuizChoiceModel } from './quiz-set/entities/quiz-choice.entity';
import { UserModel } from './user/entities/user.entity';
import { UserQuizArchiveModel } from './user/entities/user-quiz-archive.entity';
import { InitDBModule } from './InitDB/InitDB.module';
import { UserModule } from './user/user.module';
import { QuizSetModule } from './quiz-set/quiz-set.module';
import { WaitingRoomModule } from './waiting-room/waiting-room.module';
import { TimeController } from './time/time.controller';
import { TimeModule } from './time/time.module';
import { AuthModule } from './auth/auth.module';
import { ScheduleModule } from '@nestjs/schedule';
import { GameRedisMemoryService } from './game/redis/game-redis-memory.service';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
imports: [
ConfigModule.forRoot({
envFilePath: '../.env',
isGlobal: true
}),
GameModule,
// μŠ€μΌ€μ€„λŸ¬ λͺ¨λ“ˆ μΆ”κ°€ (Redis λ©”λͺ¨λ¦¬ 관리 μ„œλΉ„μŠ€μ˜ @Cron λ°μ½”λ ˆμ΄ν„° μ‚¬μš©μ„ μœ„ν•΄)
ScheduleModule.forRoot(),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: +process.env.DB_PORT || 3306,
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWD || 'test',
database: process.env.DB_NAME || 'test_db',
entities: [QuizSetModel, QuizModel, QuizChoiceModel, UserModel, UserQuizArchiveModel],
synchronize: process.env.DEV ? true : false, // 개발 λͺ¨λ“œμ—μ„œλ§Œ ν™œμ„±ν™”
logging: true, // λͺ¨λ“  쿼리 λ‘œκΉ…
logger: 'advanced-console'
// extra: {
// // κΈ€λ‘œλ²Œ batch size μ„€μ •
// maxBatchSize: 100
// }
}),
RedisModule.forRoot({
type: 'single',
url: process.env.REDIS_URL || 'redis://localhost:6379'
}),
QuizSetModule,
UserModule,
InitDBModule,
WaitingRoomModule,
TimeModule,
AuthModule
],
controllers: [AppController, TimeController],
providers: [
AppService,
GameRedisMemoryService,
{
provide: APP_INTERCEPTOR,
useClass: ClassSerializerInterceptor
}
]
})
export class AppModule {}
9 changes: 9 additions & 0 deletions BE/src/common/CommonModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { SocketEventLoggerInterceptor } from './interceptor/SocketEventLoggerInterceptor';
import { Module } from '@nestjs/common';
import { SystemMetricsService } from './service/SystemMetricsService';

@Module({
providers: [SystemMetricsService, SocketEventLoggerInterceptor],
exports: [SocketEventLoggerInterceptor]
})
export class CommonModule {}
71 changes: 58 additions & 13 deletions BE/src/common/interceptor/SocketEventLoggerInterceptor.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { firstValueFrom, Observable } from 'rxjs';
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';
import { Socket } from 'socket.io';
import { AsyncLocalStorage } from 'async_hooks'; // 이 λΆ€λΆ„ μΆ”κ°€
import { AsyncLocalStorage } from 'async_hooks';
import { SystemMetricsService } from '../service/SystemMetricsService'; // 이 λΆ€λΆ„ μΆ”κ°€

/**
* @class TraceStore
Expand Down Expand Up @@ -54,7 +54,7 @@ export class SocketEventLoggerInterceptor implements NestInterceptor {
private readonly logger = new Logger('SocketEventLogger');
private readonly EXECUTION_TIME_THRESHOLD = 1000;

constructor(private readonly moduleRef: ModuleRef) {}
constructor(private readonly systemMetricsService: SystemMetricsService) {}

intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
if (context.getType() !== 'ws') {
Expand All @@ -72,38 +72,83 @@ export class SocketEventLoggerInterceptor implements NestInterceptor {
const traceContext = new TraceContext();

return new Observable((subscriber) => {
// AsyncLocalStorageλ₯Ό μ‚¬μš©ν•˜μ—¬ 좔적 μ»¨ν…μŠ€νŠΈ μ„€μ •
TraceStore.getStore().run(traceContext, async () => {
try {
// ν•Έλ“€λŸ¬ μ‹€ν–‰ μ „ 둜그
traceContext.addLog(`[${className}.${methodName}] Started`);

// 원본 ν•Έλ“€λŸ¬ μ‹€ν–‰
const result = await firstValueFrom(next.handle());

const executionTime = Date.now() - startTime;
const logs = traceContext.getLogs();

// μ‹œμŠ€ν…œ λ©”νŠΈλ¦­ μˆ˜μ§‘
const metrics = await this.systemMetricsService.getMetrics();

if (executionTime >= this.EXECUTION_TIME_THRESHOLD) {
this.logger.warn(
'🐒 Slow Socket Event Detected!\n' +
'\n=============================\n' +
'🐒 Slow Socket Event Detected!\n' +
logs.join('\n') +
`\nTotal Execution Time: ${executionTime}ms`
`\nTotal Execution Time: ${executionTime}ms\n` +
'\nSystem Metrics:\n' +
`CPU Usage: ${metrics.cpu.toFixed(2)}%\n` +
'\nMemory Usage:\n' +
`System Total: ${metrics.memory.system.total}GB\n` +
`System Used: ${metrics.memory.system.used}GB (${metrics.memory.system.usagePercentage}%)\n` +
`System Free: ${metrics.memory.system.free}GB\n` +
`Process Heap: ${metrics.memory.process.heapUsed}MB / ${metrics.memory.process.heapTotal}MB\n` +
`Process RSS: ${metrics.memory.process.rss}MB\n` +
'\nMySQL Connections:\n' +
`Total: ${metrics.mysql.total}, ` +
`Active: ${metrics.mysql.active}, ` +
`Idle: ${metrics.mysql.idle}, ` +
`Waiting: ${metrics.mysql.waiting}\n` +
'\nRedis Connections:\n' +
`Connected Clients: ${metrics.redis.connectedClients}, ` +
`Used Memory: ${metrics.redis.usedMemoryMB}MB\n` +
// `ν΄λΌμ΄μ–ΈνŠΈ 큐 길이: ${metrics.redis.queueLength}\n` +
// `ν˜„μž¬ μ²˜λ¦¬μ€‘μΈ λͺ…λ Ήμ–΄ 수 : ${metrics.redis.cmdstat}\n` +
'============================='
);
} else {
this.logger.log(
'πŸš€ Socket Event Processed\n' +
'\n=============================\n' +
'πŸš€ Socket Event Processed\n' +
logs.join('\n') +
`\nTotal Execution Time: ${executionTime}ms`
`\nTotal Execution Time: ${executionTime}ms\n` +
'============================='
// 정상 μ²˜λ¦¬μ‹œμ—λŠ” κ°„λ‹¨ν•œ 둜그만
);
}

subscriber.next(result);
subscriber.complete();
} catch (error) {
const logs = traceContext.getLogs();
// μ—λŸ¬ λ°œμƒμ‹œμ—λ„ μ‹œμŠ€ν…œ λ©”νŠΈλ¦­ μˆ˜μ§‘
const metrics = await this.systemMetricsService.getMetrics();

this.logger.error(
'❌ Socket Event Error\n' + logs.join('\n') + `\nError: ${error.message}`
'❌ Socket Event Error\n' +
logs.join('\n') +
`\nError: ${error.message}\n` +
'\nSystem Metrics:\n' +
`CPU Usage: ${metrics.cpu.toFixed(2)}%\n` +
'\nMemory Usage:\n' +
`System Total: ${metrics.memory.system.total}GB\n` +
`System Used: ${metrics.memory.system.used}GB (${metrics.memory.system.usagePercentage}%)\n` +
`System Free: ${metrics.memory.system.free}GB\n` +
`Process Heap: ${metrics.memory.process.heapUsed}MB / ${metrics.memory.process.heapTotal}MB\n` +
`Process RSS: ${metrics.memory.process.rss}MB\n` +
'\nMySQL Connections:\n' +
`Total: ${metrics.mysql.total}, ` +
`Active: ${metrics.mysql.active}, ` +
`Idle: ${metrics.mysql.idle}, ` +
`Waiting: ${metrics.mysql.waiting}\n` +
'\nRedis Connections:\n' +
`Connected Clients: ${metrics.redis.connectedClients}, ` +
`Used Memory: ${metrics.redis.usedMemoryMB}MB\n` +
// `ν΄λΌμ΄μ–ΈνŠΈ 큐 길이: ${metrics.redis.queueLength}\n` +
// `ν˜„μž¬ μ²˜λ¦¬μ€‘μΈ λͺ…λ Ήμ–΄ 수 : ${metrics.redis.cmdstat}\n` +
'============================='
);
subscriber.error(error);
}
Expand Down
Loading

0 comments on commit 0091444

Please sign in to comment.