Advanced Winston logger with TypeORM support & structured logging.
Blanc Loggerλ NestJS λ° TypeORM μ ν리μΌμ΄μ μ μν κ³ κΈ λ‘κΉ λΌμ΄λΈλ¬λ¦¬μ λλ€.
μ΄ λΌμ΄λΈλ¬λ¦¬λ Winstonμ κΈ°λ°μΌλ‘ μ½μ λ° νμΌ λ‘κΉ μ μ§μνλ©°,
μλ ꡬ쑰νλ JSON ν¬λ§·, SQL 쿼리 νμ΄λΌμ΄ν , λͺ¨λ κΈ°λ° λ‘κ·Έ μΆμ λ± λ€μν κΈ°λ₯μ μ 곡ν©λλ€.
npm install blanc-loggerBlanc Loggerλ₯Ό μ μ λ‘κ±°λ‘ μ μ©νλ©΄, μ ν리μΌμ΄μ μ 체μμ λμΌν λ‘κΉ μ€μ μ μ¬μ©ν μ μμ΅λλ€.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { blancLogger, BlancLoggerMiddleware } from 'blanc-logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: blancLogger, // μ μ Logger μ μ©
});
// λ―Έλ€μ¨μ΄λ₯Ό μ μ©νμ¬ μμ² μ λͺ¨λλͺ
λ± μ»¨ν
μ€νΈ μ 보λ₯Ό μΆκ°
app.use(new BlancLoggerMiddleware().use);
await app.listen(3000);
}
bootstrap();TypeORM μ€μ μμ Blanc Loggerμ μ μ© λ‘κ±°λ₯Ό μ¬μ©νμ¬, SQL 쿼리 λ° λ°μ΄ν°λ² μ΄μ€ κ΄λ ¨ λ‘κ·Έλ₯Ό ν¨κ³Όμ μΌλ‘ κΈ°λ‘ν μ μμ΅λλ€.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmBlancLogger } from 'blanc-logger';
@Module({
imports: [
TypeOrmModule.forRoot({
// ... DB μ€μ
logging: true,
logger: new TypeOrmBlancLogger(), // TypeORMμ© λ‘κ±° μ¬μ©
}),
// λ€λ₯Έ λͺ¨λλ€...
],
})
export class AppModule {}Blanc Loggerλ₯Ό μ¬μ©νμ¬ κΈ°μ‘΄μ NestJS λ΄μ₯ λ‘κ±°λ₯Ό λ체ν μ μμ΅λλ€.
// κΈ°μ‘΄ NestJS λ΄μ₯ λ‘κ±° μ¬μ©:
this.logger.error('Error message', error.stack);
this.logger.log('Log message');// μλ¬ λ°μ μ, μ€ν μ 보μ ν¨κ» κΈ°λ‘
blancLogger.error('Error message', { moduleName: 'ModuleName', stack: error.stack });
blancLogger.log('Log message', { moduleName: 'ModuleName' });
blancLogger.warn('Warn message', { moduleName: 'ModuleName' });
blancLogger.verbose('Verbose message', { moduleName: 'ModuleName' });
blancLogger.debug('Debug message', { moduleName: 'ModuleName' });λ‘κ·Έ νμΌμ΄ μλμΌλ‘ λ μ§λ³λ‘ μμ±λμ΄ logs ν΄λ μλμ λ€μκ³Ό κ°μ΄ μμ λλ€.
logs/
βββ combined-2025-03-03.log # λͺ¨λ λ‘κ·Έ λ©μμ§
βββ error-2025-03-03.log # error λ 벨 λ‘κ·Έ
βββ exceptions-2025-03-03.log # μ²λ¦¬λμ§ μμ μμΈ λ‘κ·Έ
βββ rejections-2025-03-03.log # λ―Έμ²λ¦¬λ Promise κ±°λΆ λ‘κ·Έμμ λ‘κ·Έ (error-2025-03-03.log νμΌ):
{"level":"error","message":"HTTP Exception: DUPLICATION","stack":[{"moduleName":"user","path":"/api/user/profile","stack":"ConflictException: DUPLICATION\n at UserService.getProfile (/path/to/src/user/user.service.ts:60:13)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"}],"timestamp":"2025-03-03 14:29:51"}| Feature | Description |
|---|---|
| λ€μ€ μ μ‘ λ‘κΉ (Multi-Transport Logging) | - μ½μ μΆλ ₯κ³Ό νμΌ μ μ₯μ λμμ μ§μ - μΌμ ν¬κΈ°(μ: 20MB) λλ μΌμ μ£ΌκΈ°(μ: 15μΌ)μ λ°λΌ μλμΌλ‘ λ‘κ·Έ νμΌ νμ - λ‘κ·Έ νμΌ μμ: combined-YYYY-MM-DD.log, error-YYYY-MM-DD.log, exceptions-YYYY-MM-DD.log, rejections-YYYY-MM-DD.log |
| ꡬ쑰νλ λ‘κ·Έ μΆλ ₯ (Structured Logging) | - UUIDv5λ₯Ό μ¬μ©ν΄ κ³ μ LogID μμ± - {timestamp, level, message} νμμ νμ€ JSON ν¬λ§·μΌλ‘ μΆλ ₯λμ΄ μΈλΆ μμ€ν
μ°λ μ©μ΄ |
| λͺ¨λλ³ μ»¨ν μ€νΈ μΆμ (Contextual Tracing) | - BlancLoggerMiddlewareλ₯Ό νμ©νμ¬ HTTP μμ²μμ λͺ¨λλͺ
μλ μΆμΆ- [UserService β AuthModule β Subsystem]κ³Ό κ°μ΄ κ³μΈ΅ κ΅¬μ‘°λ‘ νμ |
| λμ λ‘κ·Έ λ 벨 κ΄λ¦¬ (Dynamic Log Levels) | - κ°λ° λ° μ΄μ νκ²½μ λ°λΌ debug, verbose, info, warn, error λ‘κ·Έ λ 벨μ μ€μκ°μΌλ‘ μ‘°μ κ°λ₯- νμν μ λ³΄λ§ μ λ³ κΈ°λ‘νμ¬ λ‘κ·Έμ κ°λ μ±κ³Ό κ΄λ¦¬ ν¨μ¨μ±μ ν₯μ |
| SQL ꡬ문 νμ΄λΌμ΄ν (SQL Syntax Highlighting) | - SQL 쿼리μ ν€μλ, κ°, ν
μ΄λΈλͺ
μ μμ κ°μ‘° λ° λ€μ¬μ°κΈ°λ‘ κ΅¬λΆ - νλΌλ―Έν° κ° μ»¨ν μ€νΈ μΈμ κ°μ‘° |
| 쿼리 μ±λ₯ λΆμ (Query Analysis) | - SQL μ±λ₯ μ ν μ λ° ν¨ν΄ μλ κ°μ§ (SELECT *, JOIN 쑰건 λλ½ λ±) |
| μλ¬ μ§λ¨ λ° μ€ν μΆμ (Error Diagnostics) | - μλ¬ λ°μ μ λ€μ€ μ€ν λ μ΄μ΄μ μΆκ° λ©νλ°μ΄ν°λ₯Ό ν¬ν¨νμ¬ μμΈν μλ¬ λ‘κ·Έ κΈ°λ‘ |
| μ±λ₯ λͺ¨λν°λ§ (Performance Monitoring) | - HTTP μμ² μ²λ¦¬ μκ° λ° Slow Query(μ: 500ms) κ²½κ³ λ‘κ·Έ μμ±- μ€ν κ³ν(Explain Plan) 리ν¬νΈ μκ°ν |
| 컀μ€ν°λ§μ΄μ§ (Customization) | - logger-config.yamlνμΌμ ν΅ν΄ λ‘κ·Έ μ μ₯ κ²½λ‘, λ‘κ·Έ λ 벨, νμΌ ν¬κΈ°, νμ μ£ΌκΈ° λ±μ μ½κ² μ‘°μ κ°λ₯ |
Blanc Loggerλ κΈ°λ³Έ μ€μ μ μ 곡νμ§λ§, νμμ λ°λΌ μ¬μ©μ νκ²½μ λ§κ² 컀μ€ν°λ§μ΄μ§ ν μ μμ΅λλ€.
μ€μ μ λ³κ²½νλ €λ©΄νλ‘μ νΈ λ£¨νΈμlogger-config.yamlνμΌμ μμ±νκ³ μλμ κ°μ΄Overrideν μ μμ΅λλ€.
LOG_DIR: logs # λ‘κ·Έ νμΌ μ μ₯ κ²½λ‘ (κΈ°λ³Έ: νλ‘μ νΈ λ£¨νΈ/logs)
CONSOLE_LOG_LEVEL: info # μ½μ μΆλ ₯ λ‘κ·Έ λ 벨 (debug, verbose, info, warn, error)
FILE_LOG_LEVEL: error # νμΌ μΆλ ₯ λ‘κ·Έ λ 벨 (debug, verbose, info, warn, error)
ROTATION_DAYS: 30d # λ‘κ·Έ νμΌ λ³΄κ΄ κΈ°κ° (μ: 30μΌ)
MAX_FILE_SIZE: 20m # λ¨μΌ νμΌ μ΅λ ν¬κΈ° (μ: 20MB)
SQL Query Log Example
ββ SQL Query βββββββββββββββββββββββββββββββββ
SELECT
"user"."id" AS "userId",
"user"."email" AS "userEmail"
FROM "user" "user"
WHERE "user"."age" > $1
β β Parameters βββββββββββββββββββββββββββββββ
[18]
β β Analysis βββββββββββββββββββββββββββββββββ
β οΈ Avoid SELECT * - specify columns explicitly
ββββββββββββββββββββββββββββββββββββββββββββββConsole Output Log Example
Console Error Log Example
HTTP μμ² μ²λ¦¬ μκ°μ μΈ‘μ νμ¬ λ‘κ·Έλ‘ κΈ°λ‘νλ μΈν°μ ν° μμ μ λλ€.
BlancLoggerMiddlewareλ₯Ό ν΅ν΄ μ€μ λ λͺ¨λλͺ μ λ³΄κ° μλμΌλ‘ ν¬ν¨λ©λλ€.
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { blancLogger } from 'blanc-logger';
import * as chalk from 'chalk';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
const req = context.switchToHttp().getRequest();
const decodedUrl = decodeURIComponent(req.url);
const moduleName = (req as any).moduleName || 'UnknownModule';
return next.handle().pipe(
tap(() => {
const delay = Date.now() - startTime;
const delayStr =
delay > 500 ? chalk.bold.red(`${delay}ms π¨`) : chalk.magenta(`${delay}ms`);
const message = `Request processed: ${chalk.yellow(req.method)} ${chalk.green(
decodedUrl,
)} ${delayStr}`;
blancLogger.log(message, moduleName);
}),
);
}
}μ μ μΈν°μ ν°λ‘ μ μ©νλ €λ©΄ AppModuleμ μλμ κ°μ΄ λ±λ‘ν©λλ€
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { LoggingInterceptor } from './commons/interceptors/logging.interceptor';
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor, // μ μ μΈν°μ
ν°λ‘ λ±λ‘
},
],
})
export class AppModule {}Console Output Log Example
- μλ΅ μκ° (μ: 500ms) μ΄κ³Ό μ κ°μ‘°
Blanc Loggerλ₯Ό μ¬μ©νμ¬ μμΈ λ°μ μ μμΈν μλ¬ μ 보λ₯Ό λ‘κ·Έλ‘ κΈ°λ‘νλ μ μ μ΅μ μ νν° μμ μ λλ€.
BlancLoggerMiddlewareλ₯Ό ν΅ν΄ μ€μ λ λͺ¨λλͺ μ λ³΄κ° μλμΌλ‘ ν¬ν¨λ©λλ€.
import {
ArgumentsHost,
Catch,
ExceptionFilter,
HttpException,
InternalServerErrorException,
} from '@nestjs/common';
import { blancLogger } from 'blanc-logger';
import { Request, Response } from 'express';
interface ExceptionResponse {
status: number;
message: string;
stack?: string;
}
/** μμΈ κ°μ²΄λ₯Ό μ²λ¦¬νμ¬ μν, λ©μμ§, μ€νμ λ°ννλ ν¨μ */
const handleException = (exception: unknown, _request: Request): ExceptionResponse => {
if (exception instanceof HttpException) {
const status = exception.getStatus();
const res = exception.getResponse();
const message =
typeof res === 'object' && res !== null
? (res as any).message ?? exception.message
: exception.message;
return {
status,
message: `HTTP Exception: ${message}`,
stack: exception instanceof Error ? exception.stack : '',
};
}
if (exception instanceof Error) {
const status = new InternalServerErrorException().getStatus();
return {
status,
message: `Unhandled exception: ${exception.message}`,
stack: exception.stack,
};
}
return { status: 500, message: 'Unknown error' };
};
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost): void {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const moduleName = (request as any)?.moduleName ?? 'Global';
const { status, message, stack } = handleException(exception, request);
blancLogger.error(message, {
moduleName,
path: request.url,
stack,
});
response.status(status).json({
statusCode: status,
status: message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}μ μ νν°λ‘ μ μ©νλ €λ©΄ AppModuleμ μλμ κ°μ΄ λ±λ‘ν©λλ€
import { Module } from '@nestjs/common';
import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './commons/filters/global-exception.filter';
@Module({
providers: [
{
provide: APP_FILTER,
useClass: GlobalExceptionFilter, // μ μ νν°λ‘ λ±λ‘
},
],
})
export class AppModule {}μ μ μ΅μ μ νν°λ₯Ό μ μ©ν ν, λ€μκ³Ό κ°μ΄ μμΈλ₯Ό λμ§ κ²½μ°
throw new ConflictException('DUPLICATION');Console Output Log Example
λν, λμΌνκ² λ‘κ·Έ νμΌλ μμ±λ©λλ€.
{"level":"error","message":"HTTP Exception: DUPLICATION","stack":[{"moduleName":"user","path":"/api/user/profile","stack":"ConflictException: DUPLICATION\n at UserService.getProfile (/path/to/src/user/user.service.ts:60:13)\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"}],"timestamp":"2025-03-03 14:29:51"}μ΄ νλ‘μ νΈλ MIT Licenseμ λ°λΌ λ°°ν¬ λ° μ¬μ©μ΄ κ°λ₯ν©λλ€.



