Skip to content

Commit 1be20c5

Browse files
committed
♻️ refactor: Enhanced type-safety, modular helper functions
- Refactored blancLogger code by separating inline logic into dedicated helper functions for module name formatting, message formatting, and log level transformation. - Improved type-safety by explicitly defining types and refining interfaces. - Added concise, official one-line comments to all logging-related functions and classes for clarity. - Implemented YAML configuration override for logging settings by merging default configurations with user-provided settings.
1 parent 039ebeb commit 1be20c5

11 files changed

Lines changed: 128 additions & 105 deletions

.npmignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# 소스 코드
22
src/
33

4-
# Rollup 설정 파일 등 빌드 스크립트
5-
rollup.config.js
6-
74
# tsconfig 파일들 (필요에 따라)
85
tsconfig.json
96
tsconfig.*.json

README.md

Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ logs/
118118
| **SQL 구문 하이라이팅 (SQL Syntax Highlighting)** | - SQL 쿼리의 키워드, 값, 테이블명을 색상 강조 및 들여쓰기로 구분<br>- 파라미터 값 컨텍스트 인식 강조 |
119119
| **쿼리 성능 분석 (Query Analysis)** | - SQL 성능 저하 유발 패턴 자동 감지 (`SELECT *`, `JOIN 조건 누락` 등) |
120120
| **에러 진단 및 스택 추적 (Error Diagnostics)** | - 에러 발생 시 다중 스택 레이어와 추가 메타데이터를 포함하여 상세한 에러 로그 기록 |
121-
| **성능 모니터링 (Performance Monitoring)** | - HTTP 요청 처리 시간 및 `Slow Query`(예: 100ms) 경고 로그 생성<br>- 실행 계획(Explain Plan) 리포트 시각화 |
121+
| **성능 모니터링 (Performance Monitoring)** | - HTTP 요청 처리 시간 및 `Slow Query`(예: 500ms) 경고 로그 생성<br>- 실행 계획(Explain Plan) 리포트 시각화 |
122122
| **커스터마이징 (Customization)** | - `logger-config.yaml`파일을 통해 로그 저장 경로, 로그 레벨, 파일 크기, 회전 주기 등을 쉽게 조정 가능 |
123123

124124
---
@@ -130,8 +130,8 @@ logs/
130130
131131
```yaml
132132
LOG_DIR: logs # 로그 파일 저장 경로 (기본: 프로젝트 루트/logs)
133-
CONSOLE_LOG_LEVEL: info # 콘솔 출력 로그 레벨 (debug, info, warn, error)
134-
FILE_LOG_LEVEL: error # 파일 출력 로그 레벨
133+
CONSOLE_LOG_LEVEL: info # 콘솔 출력 로그 레벨 (debug, verbose, info, warn, error)
134+
FILE_LOG_LEVEL: error # 파일 출력 로그 레벨 (debug, verbose, info, warn, error)
135135
ROTATION_DAYS: 30d # 로그 파일 보관 기간 (예: 30일)
136136
MAX_FILE_SIZE: 20m # 단일 파일 최대 크기 (예: 20MB)
137137

@@ -203,7 +203,7 @@ export class LoggingInterceptor implements NestInterceptor {
203203
tap(() => {
204204
const delay = Date.now() - startTime;
205205
const delayStr =
206-
delay > 100 ? chalk.bold.red(`${delay}ms 🚨`) : chalk.magenta(`${delay}ms`);
206+
delay > 500 ? chalk.bold.red(`${delay}ms 🚨`) : chalk.magenta(`${delay}ms`);
207207
const message = `Request processed: ${chalk.yellow(req.method)} ${chalk.green(
208208
decodedUrl,
209209
)} ${delayStr}`;
@@ -233,7 +233,7 @@ export class AppModule {}
233233
```
234234

235235
> **Console Output Log Example**
236-
> _(응답 시간 초과 시 (예: 100ms)강조)_
236+
> _- 응답 시간 (예: 500ms) 초과 시 강조_
237237
238238
<p align="left">
239239
<img src="https://github.com/user-attachments/assets/315ac55c-7f06-44b6-a4bb-66a465186dd3" alt="blanc-logger-output-log" width="600">
@@ -258,45 +258,53 @@ import {
258258
import { blancLogger } from 'blanc-logger';
259259
import { Request, Response } from 'express';
260260

261+
interface ExceptionResponse {
262+
status: number;
263+
message: string;
264+
stack?: string;
265+
}
266+
267+
/** 예외 객체를 처리하여 상태, 메시지, 스택을 반환하는 함수 */
268+
const handleException = (exception: unknown, _request: Request): ExceptionResponse => {
269+
if (exception instanceof HttpException) {
270+
const status = exception.getStatus();
271+
const res = exception.getResponse();
272+
const message =
273+
typeof res === 'object' && res !== null
274+
? (res as any).message ?? exception.message
275+
: exception.message;
276+
return {
277+
status,
278+
message: `HTTP Exception: ${message}`,
279+
stack: exception instanceof Error ? exception.stack : '',
280+
};
281+
}
282+
if (exception instanceof Error) {
283+
const status = new InternalServerErrorException().getStatus();
284+
return {
285+
status,
286+
message: `Unhandled exception: ${exception.message}`,
287+
stack: exception.stack,
288+
};
289+
}
290+
return { status: 500, message: 'Unknown error' };
291+
};
292+
261293
@Catch()
262294
export class GlobalExceptionFilter implements ExceptionFilter {
263295
catch(exception: unknown, host: ArgumentsHost): void {
264296
const ctx = host.switchToHttp();
265297
const response = ctx.getResponse<Response>();
266298
const request = ctx.getRequest<Request>();
267-
const moduleName = (request as any).moduleName || 'Global';
268-
269-
let status: number;
270-
let message: string;
271-
272-
if (exception instanceof HttpException) {
273-
status = exception.getStatus();
274-
const resObj = exception.getResponse();
275-
message =
276-
typeof resObj === 'object' && resObj !== null
277-
? (resObj as any).message || exception.message
278-
: exception.message;
279-
blancLogger.error(`HTTP Exception: ${message}`, {
280-
moduleName,
281-
path: request.url,
282-
stack: exception instanceof Error ? exception.stack : '',
283-
});
284-
} else if (exception instanceof Error) {
285-
status = new InternalServerErrorException().getStatus();
286-
message = 'Internal Server Error';
287-
blancLogger.error(`Unhandled exception: ${exception.message}`, {
288-
moduleName,
289-
path: request.url,
290-
stack: exception.stack,
291-
});
292-
} else {
293-
status = 500;
294-
message = 'Unknown error';
295-
blancLogger.error(`Unknown exception: ${JSON.stringify(exception)}`, {
296-
moduleName,
297-
path: request.url,
298-
});
299-
}
299+
300+
const moduleName = (request as any)?.moduleName ?? 'Global';
301+
const { status, message, stack } = handleException(exception, request);
302+
303+
blancLogger.error(message, {
304+
moduleName,
305+
path: request.url,
306+
stack,
307+
});
300308

301309
response.status(status).json({
302310
statusCode: status,
@@ -306,6 +314,7 @@ export class GlobalExceptionFilter implements ExceptionFilter {
306314
});
307315
}
308316
}
317+
309318
```
310319

311320
> **전역 필터로 적용하려면 AppModule에 아래와 같이 등록합니다**

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "blanc-logger",
3-
"version": "1.0.9",
4-
"description": "Advanced Winston logger for NestJS & TypeORM with structured logging (npm package).",
3+
"version": "1.1.0",
4+
"description": "Advanced Winston logger for NestJS & TypeORM with structured logging.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
77
"files": [

src/helper/sql-formatter.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import * as chalk from 'chalk';
22

3+
/** 주어진 텍스트의 각 줄에 지정된 공백 수만큼 들여쓰기를 적용 */
34
export const indent = (text: string, spaces: number = 4): string =>
45
text
56
.split('\n')
67
.map((line) => ' '.repeat(spaces) + line)
78
.join('\n');
89

10+
/** SQL 쿼리 문자열의 키워드, 숫자, 문자열 및 식별자에 색상 포맷을 적용하여 하이라이트 */
911
export const sqlHighlighter = (sql: string): string => {
1012
const keywords = [
1113
'SELECT',
@@ -50,9 +52,11 @@ export const sqlHighlighter = (sql: string): string => {
5052
.join('');
5153
};
5254

55+
/** SQL 쿼리의 파라미터 배열을 JSON 문자열로 변환하여 색상 포맷을 적용 */
5356
export const formatParameters = (params: unknown[]): string =>
5457
params.length > 0 ? chalk.hex('#2E8B57')(JSON.stringify(params)) : '';
5558

59+
/** SQL 쿼리를 분석하여 성능 이슈가 발생할 수 있는 부분에 대한 경고 메시지를 반환 */
5660
export const addQueryAnalysis = (query: string): string => {
5761
const warnings: string[] = [];
5862
const simplified = query.toLowerCase().replace(/\s+/g, ' ');
@@ -66,4 +70,4 @@ export const addQueryAnalysis = (query: string): string => {
6670
warnings.push(chalk.magentaBright('🚨 Leading % in LIKE can cause full table scan'));
6771
}
6872
return warnings.join('\n');
69-
};
73+
};

src/helper/uuid.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
22

3+
/** 입력 문자열을 기반으로 UUID를 생성하며, 입력이 없을 경우 랜덤 UUID를 사용하고 하이픈을 제거 */
34
export const generateUUID = (from?: string): string => {
45
const uid = from ?? uuidv4();
56
return uuidv5(uid, uuidv5.DNS).replace(/-/g, '');

src/logger/blanc-logger.ts

Lines changed: 49 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// blanc-logger.ts
21
import * as chalk from 'chalk';
32
import { WinstonModule } from 'nest-winston';
43
import * as util from 'util';
@@ -8,7 +7,6 @@ import { generateUUID } from '../helper/uuid';
87
import { LOGGING_CONFIG } from './logger.config';
98

109
const { LOG_DIR, CONSOLE_LOG_LEVEL, FILE_LOG_LEVEL, ROTATION_DAYS, MAX_FILE_SIZE } = LOGGING_CONFIG;
11-
1210
const MAX_OBJECT_DEPTH = 5;
1311
const MAX_OBJECT_WIDTH = 120;
1412

@@ -20,6 +18,7 @@ export interface CustomTransformableInfo extends winston.Logform.TransformableIn
2018
context?: { moduleName?: string } | string;
2119
}
2220

21+
/** util.inspect의 사전 정의된 옵션을 사용하여 객체를 문자열로 포맷팅 */
2322
const deepInspector = (obj: unknown): string =>
2423
util.inspect(obj, {
2524
colors: true,
@@ -29,6 +28,7 @@ const deepInspector = (obj: unknown): string =>
2928
sorted: true,
3029
});
3130

31+
/** 로그 레벨에 해당하는 이모지를 매핑 */
3232
const emojiMap: Record<string, string> = {
3333
error: '🚨',
3434
warn: '⚠️',
@@ -37,6 +37,7 @@ const emojiMap: Record<string, string> = {
3737
debug: '🐛',
3838
};
3939

40+
/** 로그 레벨에 해당하는 chalk 색상 함수를 매핑 */
4041
const levelColorMap: Record<string, (text: string) => string> = {
4142
error: chalk.hex('#FF6B6B'),
4243
warn: chalk.hex('#FFD93D'),
@@ -45,23 +46,21 @@ const levelColorMap: Record<string, (text: string) => string> = {
4546
verbose: chalk.hex('#6BCB77'),
4647
};
4748

48-
const moduleHierarchyFormat = winston.format(
49-
(info: CustomTransformableInfo): CustomTransformableInfo => {
50-
if (typeof info.moduleName === 'string') {
51-
info.moduleName = info.moduleName
52-
.split('/')
53-
.map((part: string) => chalk.cyan(part))
54-
.join(chalk.dim(' → '));
55-
}
56-
return info;
57-
},
58-
);
49+
/** 모듈 경로를 색상이 적용된 계층적 문자열로 변환 */
50+
const moduleHierarchyTransform = (info: CustomTransformableInfo): CustomTransformableInfo => {
51+
info.moduleName =
52+
info.moduleName?.split('/')?.map((part: string) => chalk.cyan(part))?.join(chalk.dim(' → ')) ??
53+
info.moduleName;
54+
return info;
55+
};
5956

57+
/** 파일 로그에 대한 타임스탬프와 JSON 포맷을 결합 */
6058
const fileLogFormat = winston.format.combine(
6159
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
62-
winston.format.json(),
60+
winston.format.json()
6361
);
6462

63+
/** 로그 파일 순환을 위한 DailyRotateFile 전송 객체를 생성 */
6564
const createDailyRotateFileTransport = (filename: string, level: string): DailyRotateFile =>
6665
new DailyRotateFile({
6766
dirname: LOG_DIR,
@@ -74,52 +73,52 @@ const createDailyRotateFileTransport = (filename: string, level: string): DailyR
7473
format: fileLogFormat,
7574
});
7675

77-
const addMetaData = winston.format((info: CustomTransformableInfo): CustomTransformableInfo => {
78-
if (!info.logId) {
79-
info.logId = generateUUID();
80-
}
76+
/** 로그 메타데이터에 고유한 logId가 없으면 추가 */
77+
const addMetaDataTransform = (info: CustomTransformableInfo): CustomTransformableInfo => {
78+
info.logId ??= generateUUID();
79+
return info;
80+
};
81+
82+
/** 제공된 moduleName 또는 context 속성을 사용하여 모듈 이름을 포맷팅 */
83+
const formatModuleName = (info: CustomTransformableInfo): string => {
84+
const moduleName =
85+
info.moduleName ?? (typeof info.context === 'object' ? info.context?.moduleName : info.context);
86+
return moduleName ? `[${moduleName}]` : '';
87+
};
88+
89+
/** 로그 메시지를 포맷팅하며, 객체인 경우 deepInspector를 사용하여 포맷팅 */
90+
const formatMessage = (info: CustomTransformableInfo): string => {
91+
const message = info.stack || info.message;
92+
return typeof message === 'object' ? deepInspector(message) : String(message);
93+
};
94+
95+
/** 이모지를 추가하고 대문자로 변환하여 로그 레벨을 변환 */
96+
const transformLogLevel = (info: CustomTransformableInfo): CustomTransformableInfo => {
97+
info.rawLevel = info.level;
98+
info.level = `${emojiMap[info.level] || ''} ${info.level.toUpperCase()}`.padEnd(5);
8199
return info;
82-
});
100+
};
83101

102+
/** 다양한 포맷팅 함수를 결합하여 최종 콘솔 로그 포맷을 생성 */
84103
const customConsoleFormat = winston.format.combine(
85-
addMetaData(),
104+
winston.format(addMetaDataTransform)(),
86105
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
87-
moduleHierarchyFormat(),
88-
winston.format((info: CustomTransformableInfo): CustomTransformableInfo => {
89-
info.rawLevel = info.level;
90-
info.level = `${emojiMap[info.level] || ''} ${info.level.toUpperCase()}`.padEnd(5);
91-
return info;
92-
})(),
106+
winston.format(moduleHierarchyTransform)(),
107+
winston.format(transformLogLevel)(),
93108
winston.format.printf((info: CustomTransformableInfo): string => {
94109
const colorFunc =
95110
levelColorMap[info.rawLevel ?? ''] || ((text: string): string => chalk.bold.white(text));
96111
const coloredLevel = colorFunc(info.level);
97112
const simpleLogId = info.logId ? chalk.hex('#00008B')(`[${info.logId.substring(0, 8)}]`) : '';
98-
let moduleName: string | undefined;
99-
if (!info.moduleName && info.context) {
100-
if (
101-
typeof info.context === 'object' &&
102-
info.context !== null &&
103-
'moduleName' in info.context
104-
) {
105-
moduleName = (info.context as { moduleName?: string }).moduleName;
106-
} else if (typeof info.context === 'string') {
107-
moduleName = info.context;
108-
}
109-
} else {
110-
moduleName = info.moduleName;
111-
}
112-
const moduleStr = moduleName ? `[${moduleName}]` : '';
113-
let message: unknown = info.stack || info.message;
114-
if (typeof message === 'object') {
115-
message = deepInspector(message);
116-
}
117-
return `${simpleLogId} ${chalk.dim(
118-
info.timestamp,
119-
)} ${coloredLevel} ${moduleStr} - ${chalk.blueBright(message as string)}`;
120-
}),
113+
const moduleStr = formatModuleName(info);
114+
const messageStr = formatMessage(info);
115+
return `${simpleLogId} ${chalk.dim(info.timestamp)} ${coloredLevel} ${moduleStr} - ${chalk.blueBright(
116+
messageStr
117+
)}`;
118+
})
121119
);
122120

121+
/** 구성된 전송 및 포맷을 사용하여 WinstonModule로 blancLogger를 생성 */
123122
export const blancLogger = WinstonModule.createLogger({
124123
transports: [
125124
new winston.transports.Console({
@@ -133,4 +132,4 @@ export const blancLogger = WinstonModule.createLogger({
133132
],
134133
exceptionHandlers: [createDailyRotateFileTransport('exceptions', 'error')],
135134
rejectionHandlers: [createDailyRotateFileTransport('rejections', 'error')],
136-
});
135+
});

src/logger/custom-blanc.logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { LoggerService } from '@nestjs/common';
22
import { blancLogger } from './blanc-logger';
33

4+
/** Nest의 LoggerService를 대체하여 blancLogger를 통해 로그를 기록하는 커스텀 로거 */
45
export const customBlancLogger: LoggerService = {
56
log: (message: unknown, context?: string): void =>
67
blancLogger.log('info', message, { moduleName: context }),

0 commit comments

Comments
 (0)