From 1d3e4d002fb68de04284dd304bfab0a27331b11c Mon Sep 17 00:00:00 2001 From: HC-kang Date: Mon, 23 Oct 2023 16:01:43 +0900 Subject: [PATCH] Feat: simplify logging --- src/app.module.ts | 7 +- src/common/filters/http.exception.filter.ts | 8 ++- .../interceptors/http-logger.interceptor.ts | 59 ++++++++++------- src/config/winston.config.ts | 11 ++-- src/main.ts | 4 +- src/modules/products/products.service.ts | 64 +++++-------------- 6 files changed, 72 insertions(+), 81 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 021115e..b801824 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,7 +14,7 @@ import { HttpExceptionFilter } from './common/filters/http.exception.filter'; import { FavoriteModule } from './modules/favorite/favorite.module'; import { CartModule } from './modules/cart/cart.module'; import { HttpLoggerInterceptor } from './common/interceptors'; -import { APP_INTERCEPTOR } from '@nestjs/core'; +import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; // import { PaymentsModule } from './modules/payments/payments.module'; @Module({ @@ -36,7 +36,10 @@ import { APP_INTERCEPTOR } from '@nestjs/core'; CartModule, ], providers: [ - HttpExceptionFilter, + { + provide: APP_FILTER, + useClass: HttpExceptionFilter, + }, { provide: APP_INTERCEPTOR, useClass: HttpLoggerInterceptor, diff --git a/src/common/filters/http.exception.filter.ts b/src/common/filters/http.exception.filter.ts index a32dfa1..2467e82 100644 --- a/src/common/filters/http.exception.filter.ts +++ b/src/common/filters/http.exception.filter.ts @@ -8,6 +8,7 @@ import { InternalServerErrorException, } from '@nestjs/common'; import { Request, Response } from 'express'; +import { formattedString } from '../utils'; @Catch() export class HttpExceptionFilter implements ExceptionFilter { @@ -27,9 +28,10 @@ export class HttpExceptionFilter implements ExceptionFilter { params: req.params, }; this.cLogger.error( - `${req.method} ${req.url} ${JSON.stringify(errorReq, null, 2)}`, + `[FILTERED] ${req.method} ${req.url} \n ${ + exception.stack + } \n ${JSON.stringify(errorReq)}`, ); - this.cLogger.error(exception.stack); const status = exception instanceof HttpException ? exception.getStatus() : 500; @@ -56,6 +58,6 @@ export class HttpExceptionFilter implements ExceptionFilter { path: req.url, }; - res.status(status).json(response); + return res.status(status).json(response); } } diff --git a/src/common/interceptors/http-logger.interceptor.ts b/src/common/interceptors/http-logger.interceptor.ts index 45e9e42..4cabd34 100644 --- a/src/common/interceptors/http-logger.interceptor.ts +++ b/src/common/interceptors/http-logger.interceptor.ts @@ -6,7 +6,7 @@ import { CallHandler, } from '@nestjs/common'; import { Observable } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { catchError, tap } from 'rxjs/operators'; import { formattedString } from '../utils'; @Injectable() @@ -15,32 +15,47 @@ export class HttpLoggerInterceptor implements NestInterceptor { intercept(context: ExecutionContext, next: CallHandler): Observable { const now = Date.now(); + + return next.handle().pipe( + tap(() => { + this.logRequest(context, now); + }), + catchError((error) => { + this.logRequest(context, now, error); + throw error; + }), + ); + } + + private logRequest( + context: ExecutionContext, + startTime: number, + error?: any, + ) { const httpContext = context.switchToHttp(); const req = httpContext.getRequest(); const res = httpContext.getResponse(); - const { method, originalUrl, ip, headers: reqHeaders, body: reqBody } = req; + const { method, originalUrl, ip } = req; + const { statusCode } = res; + const contentLength = res.get('content-length') || 0; + const userAgent = req.get('user-agent') || ''; - return next.handle().pipe( - tap(() => { - const { statusCode } = res; - const contentLength = res.get('content-length') || 0; - const userAgent = req.get('user-agent') || ''; - const logMessage = `${method} ${originalUrl} ${statusCode} ${contentLength} ${ - Date.now() - now - }ms - ${userAgent} ${ip}`; + const duration = Date.now() - startTime; + const logMessage = `${method} ${originalUrl} ${statusCode} ${contentLength} ${duration}ms - ${userAgent} ${ip}`; - if (statusCode >= 500) { - this.cLogger.error(formattedString(logMessage)); - } else if (statusCode >= 400) { - this.cLogger.warn(formattedString(logMessage)); - } else { - this.cLogger.log(formattedString(logMessage)); - } + // 오류 발생 시에는 에러 메세지도 같이 로깅 + if (error) { + this.cLogger.error(formattedString(`${logMessage} ${error.message}`)); + } else if (statusCode >= 500) { + this.cLogger.error(formattedString(logMessage)); + } else if (statusCode >= 400) { + this.cLogger.warn(formattedString(logMessage)); + } else { + this.cLogger.log(formattedString(logMessage)); + } - // 추가적으로 reqHeaders, reqBody를 로깅하고 싶다면 아래 주석을 해제하세요. - // this.cLogger.verbose(`Headers: ${formattedString(reqHeaders)}`); - // this.cLogger.verbose(`Body: ${formattedString(reqBody)}`); - }), - ); + // 추가적으로 reqHeaders, reqBody를 로깅하고 싶다면 아래 주석을 해제하세요. + // this.cLogger.verbose(`Headers: ${formattedString(reqHeaders)}`); + // this.cLogger.verbose(`Body: ${formattedString(reqBody)}`); } } diff --git a/src/config/winston.config.ts b/src/config/winston.config.ts index 5bb466e..fac1050 100644 --- a/src/config/winston.config.ts +++ b/src/config/winston.config.ts @@ -41,13 +41,13 @@ const customLogFormat = combine( printf((aLog) => { const nestReqId = aLog.alsCtx?.nestReqId; const nestReqIdStr = nestReqId // - ? `${nestReqId}` + ? `[${nestReqId}]` : ''; const stackStr = aLog.stack && aLog.stack[0] !== undefined // ? ` \n ${aLog.stack}` : ''; - return `[${aLog.timestamp}] [${aLog.level}] [${nestReqIdStr}]: ${aLog.message}${stackStr}`; + return `[${aLog.timestamp}] [${aLog.level}] ${nestReqIdStr}: ${aLog.message}${stackStr}`; }), ); @@ -67,13 +67,14 @@ const cloudwatchConfig = { region: process.env.AWS_LOG_REGION, }, }, + retentionInDays: 14, messageFormatter: (aLog) => { const { level, message, additionalInfo, alsCtx } = aLog; const nestReqId = alsCtx?.nestReqId; const nestReqIdStr = nestReqId // - ? `${nestReqId}` - : ''; - return `[${level}] [${nestReqIdStr}]: ${message} \nAdditional Info: ${JSON.stringify( + ? `[${nestReqId}]` + : '[]'; + return `[${level}] ${nestReqIdStr}: ${message} \nAdditional Info: ${JSON.stringify( additionalInfo, )}`; }, diff --git a/src/main.ts b/src/main.ts index 13a04f7..5266856 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,8 +18,8 @@ async function bootstrap() { app.enableVersioning(); app.enableCors({ origin: '*' }); - const httpExceptionFilter = app.get(HttpExceptionFilter); - app.useGlobalFilters(httpExceptionFilter); + // const httpExceptionFilter = app.get(HttpExceptionFilter); + // app.useGlobalFilters(httpExceptionFilter); app.useGlobalInterceptors(new TransformInterceptor()); app.useGlobalPipes( new ValidationPipe({ diff --git a/src/modules/products/products.service.ts b/src/modules/products/products.service.ts index 86f9f35..a462df6 100644 --- a/src/modules/products/products.service.ts +++ b/src/modules/products/products.service.ts @@ -17,71 +17,41 @@ export class ProductsService { sellerId: number, productSpec: ProductSpec, ): Promise { - try { - const productSpecWithStatus = { - ...productSpec, - status: PRODUCT_STATUS.PENDING, - }; - return await this.repo.create(sellerId, productSpecWithStatus); - } catch (err) { - this.cLogger.error(err); - throw err; - } + const productSpecWithStatus = { + ...productSpec, + status: PRODUCT_STATUS.PENDING, + }; + return await this.repo.create(sellerId, productSpecWithStatus); } async findAll(): Promise { - try { - return await this.repo.all(); - } catch (err) { - this.cLogger.error(err); - throw err; - } + return await this.repo.all(); } async findOne(id: number): Promise { - try { - return await this.repo.getByProductId(id); - } catch (err) { - this.cLogger.error(err); - throw err; - } + return await this.repo.getByProductId(id); } async update( id: number, productSpec: Partial, ): Promise { - try { - return await this.repo.update(id, productSpec); - } catch (err) { - this.cLogger.error(err); - throw err; - } + return await this.repo.update(id, productSpec); } async remove(id: number): Promise { - try { - return await this.repo.remove(id); - } catch (err) { - this.cLogger.error(err); - throw err; - } + return await this.repo.remove(id); } async subStock(productId: number, quantity: number): Promise { - try { - const productModel = await this.repo.getByProductId(productId); - if (!productModel) throw new ProductNotFoundException(); - if (productModel.stock < quantity) throw new Error('재고가 부족합니다.'); - const updatedProductModel = { - ...productModel, - stock: productModel.stock - quantity, - }; - return await this.repo.update(productId, updatedProductModel); - } catch (err) { - this.cLogger.error(err); - throw err; - } + const productModel = await this.repo.getByProductId(productId); + if (!productModel) throw new ProductNotFoundException(); + if (productModel.stock < quantity) throw new Error('재고가 부족합니다.'); + const updatedProductModel = { + ...productModel, + stock: productModel.stock - quantity, + }; + return await this.repo.update(productId, updatedProductModel); } async addStock(productId: number, quantity: number): Promise {