parent
0bda898e8b
commit
774a623304
@ -1 +1 @@ |
||||
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AACjD,+CAA2C;AAC3C,2CAA6D;AAC7D,6CAAgD;AAChD,+BAA4B;AAC5B,oDAAgD;AAChD,uDAAmD;AACnD,qCAAyC;AACzC,gEAA4D;AAC5D,6DAAyD;AA6BlD,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IA3BrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACxC,uBAAa,CAAC,YAAY,CAAC;gBACzB,OAAO,EAAE,CAAC,qBAAY,CAAC;gBACvB,MAAM,EAAE,CAAC,sBAAa,CAAC;gBACvB,UAAU,EAAE,CAAC,MAAqB,EAAE,EAAE,CAAC,CAAC;oBACtC,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,MAAM,CAAC,GAAG,CAAS,SAAS,CAAC;oBACnC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAS,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;oBACzD,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,CAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;oBACrD,WAAW,EAAE,MAAM,CAAC,GAAG,CAAS,UAAU,CAAC,KAAK,YAAY;oBAC5D,gBAAgB,EAAE,IAAI;oBACtB,OAAO,EAAE,IAAI;iBACd,CAAC;aACH,CAAC;YACF,wBAAU;YACV,0BAAW;YACX,gCAAc;YACd,8BAAa;SACd;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,EAAE,gBAAU,CAAC;KACpC,CAAC;GACW,SAAS,CAAG"} |
||||
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AACjD,+CAA2C;AAC3C,2CAA6D;AAC7D,6CAAgD;AAChD,+BAA4B;AAC5B,oDAAgD;AAChD,uDAAmD;AACnD,qCAAyC;AACzC,gEAA4D;AAC5D,6DAAyD;AACzD,0DAAsD;AACtD,sDAAkD;AA+B3C,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IA7BrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACxC,uBAAa,CAAC,YAAY,CAAC;gBACzB,OAAO,EAAE,CAAC,qBAAY,CAAC;gBACvB,MAAM,EAAE,CAAC,sBAAa,CAAC;gBACvB,UAAU,EAAE,CAAC,MAAqB,EAAE,EAAE,CAAC,CAAC;oBACtC,IAAI,EAAE,OAAO;oBACb,IAAI,EAAE,MAAM,CAAC,GAAG,CAAS,SAAS,CAAC;oBACnC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAS,SAAS,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;oBACzD,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAS,aAAa,CAAC;oBAC3C,QAAQ,EAAE,CAAC,IAAA,WAAI,EAAC,SAAS,EAAE,IAAI,EAAE,kBAAkB,CAAC,CAAC;oBACrD,WAAW,EAAE,MAAM,CAAC,GAAG,CAAS,UAAU,CAAC,KAAK,YAAY;oBAC5D,gBAAgB,EAAE,IAAI;oBACtB,OAAO,EAAE,IAAI;iBACd,CAAC;aACH,CAAC;YACF,wBAAU;YACV,0BAAW;YACX,gCAAc;YACd,8BAAa;YACb,wBAAU;YACV,4BAAY;SACb;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,EAAE,gBAAU,CAAC;KACpC,CAAC;GACW,SAAS,CAAG"} |
File diff suppressed because one or more lines are too long
@ -0,0 +1,13 @@ |
||||
import { Module } from '@nestjs/common'; |
||||
import { TypeOrmModule } from '@nestjs/typeorm'; |
||||
import { ErrorLog } from './entities/error-log.entity'; |
||||
import { ErrorLogsService } from './error-logs/error-logs.service'; |
||||
import { APP_FILTER } from '@nestjs/core'; |
||||
import { GlobalExceptionFilter } from './filters/global-exception.filter'; |
||||
|
||||
@Module({ |
||||
imports: [TypeOrmModule.forFeature([ErrorLog])], |
||||
providers: [ErrorLogsService, { provide: APP_FILTER, useClass: GlobalExceptionFilter }], |
||||
exports: [ErrorLogsService], |
||||
}) |
||||
export class CoreModule {} |
@ -0,0 +1,15 @@ |
||||
import { InjectRepository } from '@nestjs/typeorm'; |
||||
import { ErrorLog } from '../entities/error-log.entity'; |
||||
import { Repository } from 'typeorm'; |
||||
|
||||
export class ErrorLogsService { |
||||
constructor(@InjectRepository(ErrorLog) private readonly repo: Repository<ErrorLog>) {} |
||||
|
||||
async write(log: Partial<ErrorLog>): Promise<void> { |
||||
try { |
||||
await this.repo.insert(log); |
||||
} catch { |
||||
// 로깅 실패는 애플리케이션 흐름에 영향을 주지 않음
|
||||
} |
||||
} |
||||
} |
@ -0,0 +1,86 @@ |
||||
import { |
||||
ArgumentsHost, |
||||
Catch, |
||||
ExceptionFilter, |
||||
HttpException, |
||||
HttpStatus, |
||||
Injectable, |
||||
Logger, |
||||
} from '@nestjs/common'; |
||||
import { Request, Response } from 'express'; |
||||
import { ErrorLogsService } from '../error-logs/error-logs.service'; |
||||
|
||||
type JwtUser = { userId?: number; username?: string }; |
||||
type AuthedRequest = Request & { user?: JwtUser }; |
||||
|
||||
function getStatus(exception: unknown): number { |
||||
return exception instanceof HttpException |
||||
? exception.getStatus() |
||||
: HttpStatus.INTERNAL_SERVER_ERROR; |
||||
} |
||||
|
||||
function extractMessage(exception: unknown): string { |
||||
if (exception instanceof HttpException) { |
||||
const res = exception.getResponse(); |
||||
if (typeof res === 'string') return res; |
||||
if (res && typeof res === 'object') { |
||||
const msg = (res as Record<string, unknown>).message; |
||||
if (typeof msg == 'string') return msg; |
||||
if (Array.isArray(msg)) return msg.join(', '); |
||||
|
||||
const err = (res as Record<string, unknown>).error; |
||||
if (typeof err === 'string') return err; |
||||
} |
||||
return exception.message ?? 'Http exception'; |
||||
} |
||||
if (exception instanceof Error) return exception.message; |
||||
|
||||
return 'Internal server error'; |
||||
} |
||||
|
||||
function extractStack(exception: unknown): string | undefined { |
||||
if (exception instanceof HttpException) return undefined; |
||||
if (exception instanceof Error) return exception.stack; |
||||
|
||||
return undefined; |
||||
} |
||||
|
||||
@Catch() |
||||
@Injectable() |
||||
export class GlobalExceptionFilter implements ExceptionFilter { |
||||
private readonly logger = new Logger(GlobalExceptionFilter.name); |
||||
|
||||
constructor(private readonly errorLogs: ErrorLogsService) {} |
||||
|
||||
catch(exception: unknown, host: ArgumentsHost): void { |
||||
const ctx = host.switchToHttp(); |
||||
const req = ctx.getRequest<AuthedRequest>(); |
||||
const res = ctx.getResponse<Response>(); |
||||
|
||||
const status = getStatus(exception); |
||||
const message = extractMessage(exception); |
||||
const stack = extractStack(exception); |
||||
|
||||
void this.errorLogs |
||||
.write({ |
||||
method: req.method, |
||||
path: req.originalUrl ?? req.url, |
||||
statusCode: status, |
||||
message, |
||||
stack, |
||||
userId: req.user?.userId, |
||||
ip: (req.headers['x-forwarded-for'] as string) || req.socket.remoteAddress || undefined, |
||||
}) |
||||
.catch((err: unknown) => { |
||||
const msg = err instanceof Error ? err.message : String(err); |
||||
this.logger.warn(`Failed to persist error log: ${msg}`); |
||||
}); |
||||
|
||||
res.status(status).json({ |
||||
statusCode: status, |
||||
message, |
||||
path: req.originalUrl || req.url, |
||||
timestamp: new Date().toISOString(), |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,28 @@ |
||||
import { ApiProperty } from '@nestjs/swagger'; |
||||
|
||||
export class HealthResponseDto { |
||||
@ApiProperty({ description: '서버헬스', enum: ['ok', 'down'], example: 'ok' }) |
||||
status!: 'ok' | 'down'; |
||||
@ApiProperty({ description: 'DB헬스', enum: ['up', 'down'], example: 'up' }) |
||||
db!: 'up' | 'down'; |
||||
@ApiProperty({ description: '체크시간', example: '2025-01-01T00:00:00.000Z' }) |
||||
time!: string; |
||||
|
||||
static ok(): HealthResponseDto { |
||||
const d = new HealthResponseDto(); |
||||
d.status = 'ok'; |
||||
d.db = 'up'; |
||||
d.time = new Date().toISOString(); |
||||
|
||||
return d; |
||||
} |
||||
|
||||
static down(): HealthResponseDto { |
||||
const d = new HealthResponseDto(); |
||||
d.status = 'down'; |
||||
d.db = 'down'; |
||||
d.time = new Date().toISOString(); |
||||
|
||||
return d; |
||||
} |
||||
} |
@ -0,0 +1,18 @@ |
||||
import { Test, TestingModule } from '@nestjs/testing'; |
||||
import { HealthController } from './health.controller'; |
||||
|
||||
describe('HealthController', () => { |
||||
let controller: HealthController; |
||||
|
||||
beforeEach(async () => { |
||||
const module: TestingModule = await Test.createTestingModule({ |
||||
controllers: [HealthController], |
||||
}).compile(); |
||||
|
||||
controller = module.get<HealthController>(HealthController); |
||||
}); |
||||
|
||||
it('should be defined', () => { |
||||
expect(controller).toBeDefined(); |
||||
}); |
||||
}); |
@ -0,0 +1,22 @@ |
||||
import { Controller, Get } from '@nestjs/common'; |
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; |
||||
import { DataSource } from 'typeorm'; |
||||
import { HealthResponseDto } from './dto/health-response.dto'; |
||||
|
||||
@ApiTags('헬스체크') |
||||
@Controller('health') |
||||
export class HealthController { |
||||
constructor(private readonly dataSource: DataSource) {} |
||||
|
||||
@Get() |
||||
@ApiOperation({ summary: '어플리케이션 헬스체크' }) |
||||
@ApiOkResponse({ description: '상태', type: HealthResponseDto }) |
||||
async get(): Promise<HealthResponseDto> { |
||||
try { |
||||
await this.dataSource.query('SELECT 1'); // DB ping
|
||||
return HealthResponseDto.ok(); |
||||
} catch { |
||||
return HealthResponseDto.down(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
import { Module } from '@nestjs/common'; |
||||
import { HealthController } from './health.controller'; |
||||
|
||||
@Module({ |
||||
controllers: [HealthController] |
||||
}) |
||||
export class HealthModule {} |
Loading…
Reference in new issue