diff --git a/Backend/apps/api/src/app.module.ts b/Backend/apps/api/src/app.module.ts index 1a44ed1a..2e92a848 100644 --- a/Backend/apps/api/src/app.module.ts +++ b/Backend/apps/api/src/app.module.ts @@ -16,6 +16,9 @@ import { RedisModule } from '@nestjs-modules/ioredis'; import { redisConfig } from './config/redis.config'; import { WinstonModule } from 'nest-winston'; import { winstonConfig } from './config/logger.config'; +import { APP_INTERCEPTOR, APP_FILTER } from '@nestjs/core'; +import { LoggingInterceptor } from './common/interceptors/logging.interceptor'; +import { HttpExceptionFilter } from './common/filters/http-exception.filter'; @Module({ imports: [ @@ -42,6 +45,15 @@ import { winstonConfig } from './config/logger.config'; FollowModule, ], controllers: [AppController], - providers: [AppService], + providers: [ + AppService, + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + }, + { + provide: APP_FILTER, + useClass: HttpExceptionFilter, + },], }) export class AppModule {} diff --git a/Backend/apps/api/src/common/filters/http-exception.filter.ts b/Backend/apps/api/src/common/filters/http-exception.filter.ts new file mode 100644 index 00000000..195c2907 --- /dev/null +++ b/Backend/apps/api/src/common/filters/http-exception.filter.ts @@ -0,0 +1,45 @@ +import { + ExceptionFilter, + Catch, + ArgumentsHost, + HttpException, + HttpStatus, + Inject, +} from '@nestjs/common'; +import { Request, Response } from 'express'; +import { Logger } from 'winston'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + constructor( + @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, + ) {} + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const status = + exception instanceof HttpException + ? exception.getStatus() + : HttpStatus.INTERNAL_SERVER_ERROR; + + const message = + exception instanceof HttpException + ? exception.getResponse() + : exception; + + this.logger.error( + `HTTP Status: ${status} Error Message: ${JSON.stringify(message)}`, + ); + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + }); + } +} diff --git a/Backend/apps/api/src/common/interceptors/logging.interceptor.ts b/Backend/apps/api/src/common/interceptors/logging.interceptor.ts new file mode 100644 index 00000000..f9eb35ac --- /dev/null +++ b/Backend/apps/api/src/common/interceptors/logging.interceptor.ts @@ -0,0 +1,45 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, + Inject, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { Request, Response } from 'express'; +import { Logger } from 'winston'; +import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + constructor( + @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, + ) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + const now = Date.now(); + + const ctx = context.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); + + const { method, originalUrl } = request; + const headers = { ...request.headers }; + delete headers['authorization']; // Authorization 헤더 제외 - 보안상 + + return next.handle().pipe( + tap(() => { + const statusCode = response.statusCode; + const contentLength = response.get('content-length') || '0'; + + this.logger.info( + `${method} ${originalUrl} ${statusCode} ${contentLength} - ${ + Date.now() - now + }ms`, + { headers }, // 제외한 헤더를 로그에 포함 + ); + }), + ); + } +} diff --git a/Backend/apps/api/src/config/logger.config.ts b/Backend/apps/api/src/config/logger.config.ts index 9c872a6d..0d7a8bb9 100644 --- a/Backend/apps/api/src/config/logger.config.ts +++ b/Backend/apps/api/src/config/logger.config.ts @@ -1,16 +1,26 @@ import * as winston from 'winston'; +import 'winston-daily-rotate-file'; + +const logLevel = 'warn'; +const logDir = './logs'; export const winstonConfig: winston.LoggerOptions = { - level: 'error', // 'error' 레벨 이상의 로그만 출력 + level: logLevel, format: winston.format.combine( winston.format.timestamp(), - winston.format.printf(({ timestamp, level, message, stack }) => { - return `${timestamp} [${level.toUpperCase()}]: ${message} ${ - stack ? '\n' + stack : '' - }`; + winston.format.printf(({ timestamp, level, message }) => { + return `${timestamp} [${level.toUpperCase()}]: ${message}`; }), ), transports: [ new winston.transports.Console(), + new winston.transports.DailyRotateFile({ + dirname: logDir, + filename: 'application-%DATE%.log', + datePattern: 'YYYY-MM-DD', + zippedArchive: true, + maxSize: '20m', + maxFiles: '14d', + }), ], }; \ No newline at end of file diff --git a/Backend/apps/api/src/follow/follow.service.ts b/Backend/apps/api/src/follow/follow.service.ts index 6f169086..59ef1f33 100644 --- a/Backend/apps/api/src/follow/follow.service.ts +++ b/Backend/apps/api/src/follow/follow.service.ts @@ -83,6 +83,7 @@ export class FollowService { usersNickname: streamer.nickname, usersProfileImage: streamer.profileImage, onAir: streamer.live?.onAir || false, + viewers: streamer.live?.viewers || 0 })); } diff --git a/Backend/package-lock.json b/Backend/package-lock.json index 040330a8..2cc3d57a 100644 --- a/Backend/package-lock.json +++ b/Backend/package-lock.json @@ -40,7 +40,8 @@ "rxjs": "^7.8.1", "socket.io": "^4.8.1", "typeorm": "^0.3.20", - "winston": "^3.15.0" + "winston": "^3.15.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", @@ -8406,6 +8407,15 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/file-stream-rotator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", + "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.1" + } + }, "node_modules/file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", @@ -11783,6 +11793,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -15843,6 +15862,24 @@ "node": ">= 12.0.0" } }, + "node_modules/winston-daily-rotate-file": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", + "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", + "license": "MIT", + "dependencies": { + "file-stream-rotator": "^0.6.1", + "object-hash": "^3.0.0", + "triple-beam": "^1.4.1", + "winston-transport": "^4.7.0" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "winston": "^3" + } + }, "node_modules/winston-transport": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.8.0.tgz", diff --git a/Backend/package.json b/Backend/package.json index 98f4c7dd..a077ada2 100644 --- a/Backend/package.json +++ b/Backend/package.json @@ -51,7 +51,8 @@ "rxjs": "^7.8.1", "socket.io": "^4.8.1", "typeorm": "^0.3.20", - "winston": "^3.15.0" + "winston": "^3.15.0", + "winston-daily-rotate-file": "^5.0.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0",