-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add: logging middleware #135
base: dev
Are you sure you want to change the base?
Changes from 5 commits
032c368
38dc4f7
9edc2fc
43753cf
96ea722
c9280c0
33b669d
7643d42
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; | ||
Check warning on line 1 in src/app.module.ts GitHub Actions / Lint
|
||
import { APP_GUARD } from '@nestjs/core'; | ||
import { JwtService } from '@nestjs/jwt'; | ||
import { AppController } from './app.controller'; | ||
|
@@ -29,6 +29,7 @@ | |
import { ClsPluginTransactional } from '@nestjs-cls/transactional'; | ||
import { PrismaService } from '@src/prisma/prisma.service'; | ||
import { TransactionalAdapterPrisma } from '@nestjs-cls/transactional-adapter-prisma'; | ||
// import { LoggingMiddleware } from "@src/common/middleware/http.logging.middleware"; | ||
|
||
@Module({ | ||
imports: [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드 패치에 대한 간단한 검토를 하겠습니다. 개선 제안 및 버그 위험
이러한 점들을 고려하여 코드를 개선하면 더 견고하고 이해하기 쉬운 모듈을 만들 수 있을 것입니다. |
||
|
@@ -83,7 +84,7 @@ | |
{ | ||
provide: APP_GUARD, | ||
useFactory: () => { | ||
const env = process.env.NODE_ENV; | ||
}, | ||
}, | ||
JwtCookieGuard, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ import session from 'express-session'; | |
import { AppModule } from '../app.module'; | ||
import settings from '../settings'; | ||
import morgan = require('morgan'); | ||
import { LoggingInterceptor } from '@src/common/middleware/http.logging.middleware'; | ||
import { HttpExceptionFilter } from '@src/common/filter/http.exception.filter'; | ||
|
||
async function bootstrap() { | ||
const app = await NestFactory.create(AppModule); | ||
|
@@ -53,6 +55,8 @@ async function bootstrap() { | |
}), | ||
); | ||
|
||
app.useGlobalInterceptors(new LoggingInterceptor()); | ||
app.useGlobalFilters(new HttpExceptionFilter()); | ||
app.enableShutdownHooks(); | ||
return app.listen(8000); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드 리뷰를 위해 제공된 패치 내용을 살펴보았습니다. 다음은 몇 가지 버그 위험 요소와 개선 제안입니다:
이러한 점들을 참고하여 코드를 보완하면 보다 안정적이고 효율적인 애플리케이션이 될 것입니다. |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
import { | ||
ExceptionFilter, | ||
Catch, | ||
ArgumentsHost, | ||
HttpException, | ||
HttpStatus, | ||
} from '@nestjs/common'; | ||
import { Request, Response } from 'express'; | ||
import * as winston from 'winston'; | ||
import 'winston-daily-rotate-file'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
|
||
// JSON 포맷을 커스터마이징하여 메시지와 속성 순서를 설정 | ||
const customFormat = winston.format.printf(({ message }) => { | ||
return JSON.stringify(message); | ||
}); | ||
|
||
// 파일 핸들러 설정 | ||
const fileTransport = new winston.transports.DailyRotateFile({ | ||
filename: 'logs/response-%DATE%.log', // 파일 이름 패턴 | ||
datePattern: 'YYYY-MM-DD', // 날짜 패턴 | ||
zippedArchive: true, // 압축 여부 | ||
maxSize: '10m', // 파일 최대 크기 | ||
maxFiles: '10d', // 백업 파일 수 | ||
handleExceptions: true, // 예외 처리 | ||
format: winston.format.combine(customFormat), | ||
}); | ||
|
||
const logger = winston.createLogger({ | ||
transports: [fileTransport], | ||
exitOnError: false, // 예외 발생 시 프로세스 종료하지 않음 | ||
}); | ||
|
||
@Catch(HttpException) | ||
export class HttpExceptionFilter implements ExceptionFilter { | ||
catch(exception: HttpException, host: ArgumentsHost) { | ||
const ctx = host.switchToHttp(); | ||
const response = ctx.getResponse(); | ||
const request = ctx.getRequest(); | ||
const status = exception.getStatus | ||
? exception.getStatus() | ||
: HttpStatus.INTERNAL_SERVER_ERROR; | ||
|
||
const startTime = Date.now(); | ||
const requestId = request.headers['uuid'] || uuidv4(); | ||
|
||
const { method, headers, query } = request; | ||
|
||
// 요청 데이터 처리 | ||
let requestData = {}; | ||
try { | ||
if (method === 'GET') { | ||
requestData = { ...query }; | ||
} else if (method === 'POST') { | ||
requestData = { ...request.body }; | ||
} else { | ||
requestData = { ...request.body }; | ||
} | ||
} catch (error) { | ||
requestData = {}; // 예외 발생 시 빈 객체로 처리 | ||
} | ||
const requestLog = { | ||
method, | ||
path: request.path, // URI만 포함하도록 수정 | ||
UUID: requestId, | ||
data: requestData, | ||
user: request?.user?.sid ?? '', | ||
// meta, | ||
}; | ||
|
||
// 예외 로그 기록 | ||
const errorResponseLog = { | ||
status: status, | ||
data: response?.body ?? '', | ||
}; | ||
|
||
// 전체 로그 객체 | ||
const logData = { | ||
request: requestLog, | ||
response: errorResponseLog, | ||
}; | ||
|
||
// 로그 출력 | ||
logger.error(logData); | ||
|
||
// 응답 생성 | ||
response.status(status).json({ | ||
statusCode: status, | ||
timestamp: new Date().toISOString(), | ||
path: request.url, | ||
}); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드 리뷰 결과는 다음과 같습니다:
이 외에도 전반적으로 구조가 잘 잡혀있고, 기본적인 에러 핸들링 로직은 적절합니다. 그러나 위 사항들을 고려하여 다듬으면 더욱 안정성과 가독성을 높일 수 있을 것입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드 패치는 NestJS의 예외 필터를 사용하여 HTTP 예외를 처리하는 기능을 추가합니다. 전반적으로 잘 구성되어 있지만 몇 가지 주의해야 할 점과 개선 사항이 있습니다. 잠재적인 버그 리스크
개선 제안
위 사항들을 반영하면 코드의 안정성과 가독성을 높이는 데 도움이 될 것입니다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { | ||
CallHandler, | ||
ExecutionContext, | ||
Injectable, | ||
NestInterceptor, | ||
HttpException, | ||
HttpStatus, | ||
} from '@nestjs/common'; | ||
import { Observable, throwError } from 'rxjs'; | ||
import { tap, catchError } from 'rxjs/operators'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import * as winston from 'winston'; | ||
import 'winston-daily-rotate-file'; | ||
|
||
// JSON 포맷을 커스터마이징하여 메시지와 속성 순서를 설정 | ||
const customFormat = winston.format.printf(({ message }) => { | ||
return JSON.stringify(message); | ||
}); | ||
|
||
// 파일 핸들러 설정 | ||
const fileTransport = new winston.transports.DailyRotateFile({ | ||
filename: 'logs/response-%DATE%.log', // 파일 이름 패턴 | ||
datePattern: 'YYYY-MM-DD', // 날짜 패턴 | ||
zippedArchive: true, // 압축 여부 | ||
maxSize: '10m', // 파일 최대 크기 | ||
maxFiles: '10d', // 백업 파일 수 | ||
handleExceptions: true, // 예외 처리 | ||
format: winston.format.combine(customFormat), | ||
}); | ||
|
||
export const logger = winston.createLogger({ | ||
transports: [fileTransport], | ||
exitOnError: false, // 예외 발생 시 프로세스 종료하지 않음 | ||
}); | ||
|
||
@Injectable() | ||
export class LoggingInterceptor implements NestInterceptor { | ||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> { | ||
const req = context.switchToHttp().getRequest(); | ||
const res = context.switchToHttp().getResponse(); | ||
|
||
const startTime = Date.now(); | ||
const requestId = req.headers['uuid'] || uuidv4(); | ||
|
||
const { method, headers, query } = req; | ||
|
||
// 요청 데이터 처리 | ||
let requestData = {}; | ||
try { | ||
if (method === 'GET') { | ||
requestData = { ...query }; | ||
} else if (method === 'POST') { | ||
requestData = { ...req.body }; | ||
} else { | ||
requestData = { ...req.body }; | ||
} | ||
} catch (error) { | ||
requestData = {}; // 예외 발생 시 빈 객체로 처리 | ||
} | ||
|
||
const meta = { | ||
tz: new Date().toLocaleString('en-US', { | ||
timeZone: 'Asia/Seoul', | ||
timeZoneName: 'short', | ||
}), | ||
remote_host: req.hostname, | ||
content_length: headers['content-length'] || '', | ||
path_info: req.path, | ||
remote_addr: req.ip, | ||
content_type: headers['content-type'] || '', | ||
http_host: headers['host'] || '', | ||
http_user_agent: headers['user-agent'] || '', | ||
}; | ||
|
||
const requestLog = { | ||
method, | ||
path: req.path, // URI만 포함하도록 수정 | ||
UUID: requestId, | ||
data: requestData, | ||
user: req?.user?.sid ?? '', | ||
// meta, | ||
}; | ||
|
||
return next.handle().pipe( | ||
tap((responseBody) => { | ||
res.on('finish', () => { | ||
const duration = Date.now() - startTime; | ||
const { statusCode } = res; | ||
|
||
const responseLog = { | ||
status: statusCode, | ||
// headers: { | ||
// 'Content-Type': res.getHeader('Content-Type') || '', | ||
// // ...res.getHeaders(), // 모든 응답 헤더를 포함 | ||
// }, | ||
// charset: res.charset || 'utf-8', | ||
data: responseBody, // Response Body 저장 | ||
}; | ||
|
||
// request와 response를 하나의 객체로 묶어서 로깅 | ||
const logData = { | ||
request: requestLog, | ||
response: responseLog, | ||
duration: duration, | ||
}; | ||
|
||
logger.info(logData); | ||
}); | ||
}), | ||
catchError((err) => { | ||
res.on('finish', () => { | ||
const duration = Date.now() - startTime; | ||
const statusCode = | ||
err instanceof HttpException | ||
? err.getStatus() | ||
: HttpStatus.INTERNAL_SERVER_ERROR; | ||
|
||
const errorResponseLog = { | ||
status: statusCode, | ||
headers: { | ||
'Content-Type': res.getHeader('Content-Type') || '', | ||
...res.getHeaders(), // 모든 응답 헤더를 포함 | ||
}, | ||
charset: res.charset || 'utf-8', | ||
data: { | ||
message: err.message || 'Internal Server Error', | ||
stack: err.stack || '', // Stack trace 포함 (원하는 경우) | ||
}, | ||
duration: `${duration}ms`, | ||
}; | ||
|
||
// 예외 발생 시에도 request는 그대로 남기고, error response를 로깅 | ||
const logData = { | ||
request: requestLog, | ||
response: errorResponseLog, | ||
}; | ||
|
||
logger.error(logData); | ||
}); | ||
|
||
return throwError(() => err); | ||
}), | ||
); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드 리뷰 결과는 다음과 같습니다: 장점:
잠재적 문제:
개선 제안:
this.logger.log(JSON.stringify({
method,
url,
statusCode,
contentLength,
userAgent,
ip,
}));
이와 같은 점들을 개선하면 더욱 안정적이고 사용자 친화적인 로그 시스템이 될 것입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드 리뷰 결과는 다음과 같습니다: 버그 리스크
개선 제안
총평로깅 인터셉터로써 기능은 잘 유지되고 있으나, 약간의 예외 처리와 코드 정리가 필요합니다. 전반적으로 안정성과 가독성을 높이는 방향으로 개선할 수 있을 것입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드 패치에 대한 간단한 코드 리뷰를 제공하겠습니다. 버그 위험 요소:
개선 제안:
이러한 점들을 고려하여 코드의 안정성과 가독성을 높일 수 있습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드 리뷰를 진행하겠습니다. 코드 리뷰 요약
버그 위험
개선 사항
요약하자면, 매우 잘 작성된 코드이며 몇 가지 세부적인 개선을 통해 더욱 견고한 시스템으로 발전할 수 있을 거라 생각됩니다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import { JwtCommand } from './command/jwt.command'; | |
import { SidCommand } from './command/sid.command'; | ||
import { IsPublicCommand } from './command/isPublic.command'; | ||
import { AuthChain } from './auth.chain'; | ||
import { SessionCommand } from '@src/modules/auth/command/session.command'; | ||
|
||
@Injectable() | ||
export class AuthConfig { | ||
|
@@ -14,6 +15,7 @@ export class AuthConfig { | |
private readonly jwtCommand: JwtCommand, | ||
private readonly sidCommand: SidCommand, | ||
private readonly isPublicCommand: IsPublicCommand, | ||
private readonly sessionCommand: SessionCommand, | ||
) {} | ||
|
||
public async config(env: string) { | ||
|
@@ -34,7 +36,7 @@ export class AuthConfig { | |
return this.authChain | ||
.register(this.isPublicCommand) | ||
.register(this.sidCommand) | ||
.register(this.jwtCommand); | ||
.register(this.sessionCommand); | ||
}; | ||
|
||
private getProdGuardConfig = () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 코드 리뷰를 다음과 같이 진행하겠습니다.
전반적으로 잘 구성된 코드이나, 몇 가지 확인해야 할 사항들이 존재합니다. |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { ExecutionContext, Injectable } from '@nestjs/common'; | ||
import { Reflector } from '@nestjs/core'; | ||
import { Request } from 'express'; | ||
import { AuthService } from '../auth.service'; | ||
import { JwtService } from '@nestjs/jwt'; | ||
import { AuthCommand, AuthResult } from '../auth.command'; | ||
|
||
@Injectable() | ||
export class SessionCommand implements AuthCommand { | ||
constructor( | ||
private reflector: Reflector, | ||
private authService: AuthService, | ||
private jwtService: JwtService, | ||
) {} | ||
|
||
public async next( | ||
context: ExecutionContext, | ||
prevResult: AuthResult, | ||
): Promise<AuthResult> { | ||
const request = context.switchToHttp().getRequest<Request>(); | ||
const response = context.switchToHttp().getResponse<Response>(); | ||
const accessToken = this.extractTokenFromCookie(request, 'accessToken'); | ||
const cookie = request.cookies['sessionid']; | ||
if (!cookie) { | ||
return prevResult; | ||
} | ||
const user = await this.authService.findBySessionKey(cookie); | ||
request['user'] = user; | ||
prevResult.authentication = true; | ||
prevResult.authorization = true; | ||
return prevResult; | ||
} | ||
|
||
private extractTokenFromCookie( | ||
request: Request, | ||
type: 'accessToken' | 'refreshToken', | ||
): string | undefined { | ||
const cookie = request.cookies[type]; | ||
if (cookie) { | ||
return cookie; | ||
} | ||
return undefined; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,14 @@ import { Controller, Get, Query } from '@nestjs/common'; | |
import { ISemester } from 'src/common/interfaces/ISemester'; | ||
import { toJsonSemester } from '../../common/interfaces/serializer/semester.serializer'; | ||
import { SemestersService } from './semesters.service'; | ||
import { Public } from '@src/common/decorators/skip-auth.decorator'; | ||
|
||
@Controller('api/semesters') | ||
export class SemestersController { | ||
constructor(private readonly semestersService: SemestersService) {} | ||
|
||
@Get() | ||
@Public() | ||
async getSemesters(@Query() query: ISemester.QueryDto) { | ||
const semesters = await this.semestersService.getSemesters(query); | ||
return semesters.map((semester) => toJsonSemester(semester)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이 코드 패치는 전반적으로 잘 작성되어 있지만, 몇 가지 점검할 만한 사항과 개선 제안을 드립니다. 버그 리스크
개선 제안
이러한 사항들을 기억하고 반영하시면 더욱 안정적이고 가독성 좋은 코드를 만들 수 있을 것입니다. |
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
코드 패치에 대한 간략한 리뷰를 하겠습니다.
버전 호환성: 새로운 패키지(
uuid
,winston
,winston-daily-rotate-file
)의 버전이 기존 프로젝트 의존성과 잘 맞는지 확인해야 합니다. 특히winston
과 관련된 패키지는 서로 호환성을 검토하는 것이 좋습니다.@types
추가:@types/uuid
를 추가한 것은 좋지만, 나머지 새로 추가된 패키지에도 타입 정의가 필요한 경우, 해당 패키지의 타입을 추가하는지 확인하십시오.사용하지 않는 의존성: 추가된 패키지가 실제 코드에서 사용되는지 점검해야 합니다. 필요 없는 의존성이 포함되지 않도록 주의하세요.
패키지 보안:
uuid
,winston
등 새로 추가된 패키지의 보안 취약점 여부를 확인할 필요가 있습니다. 정기적으로 보안 스캐닝 도구를 활용하는 것을 추천합니다.문서화: 새로 추가된 패키지의 사용법에 대한 문서를 업데이트해야 다른 개발자들이 이해하는 데 도움이 됩니다.
이 외에도 전반적인 테스트 케이스가 잘 작성되어 있는지 점검하는 것도 중요합니다.