개인 블로그를 만들면서 로그 모니터링이 필요해졌는데, Loki나 ELK 스택은 최소 비용 목표에서 벗어나는 것 같아서 이미 사용 중인 PostgreSQL을 활용해서 로깅 시스템을 구축한 경험담입니다.
일단 현재 블로그의 로깅상태를 Grafana 대쉬보드에서 보는건 성공한 상태입니다. 항상 로그파일을 날짜별로 만들고 추적이 필요할 때마다 vim 에디터로 슥슥삭삭하기만했는데 감개무량한 느낌입니다..
일단 grafana를 사용하면 + Loki 조합을 대부분 가져가는데, 다른 방식을 선택한 이유를 끄적여보겠습니다.
상당히 잘썼다고 생각했는데 게시하기 전에 읽어보니.. 횡성수설이 심해서 claude에게 깔끔하게 정리부탁하였습니다..^^
원래 계획: 심플한 개인 블로그
현실: 로그 모니터링, 시각화, 알림... 점점 복잡해지는 인프라
물론 단점도 있습니다:
시도: Winston으로 파일 저장 → Grafana에서 파일 읽기
결과: 파일 파싱의 지옥
// 이렇게 하지 마세요...
const logData = fs.readFileSync('app.log', 'utf-8')
.split('\n')
.filter(line => line.includes('ERROR'))
.map(line => JSON.parse(line)); // 파싱 에러의 향연
교훈: 구조화된 데이터는 구조화된 저장소에
삽질을 하진 않았지만 아래처럼 로깅 작성했더니, claude가 기겁을 하면서 배치를 사용하라고 하길래 바로 리팩토링 부탁
// 이것도 하지 마세요...
async logError(message: string) {
await this.db.query('INSERT INTO logs...'); // 매번 DB 호출
}
해결책: 배치 처리 시스템 도입
최종 설계: 실제 운영을 고려한 구조
CREATE TABLE app_logs (
id BIGSERIAL PRIMARY KEY,
timestamp TIMESTAMPTZ DEFAULT NOW() NOT NULL,
level VARCHAR(10) NOT NULL,
message TEXT NOT NULL,
-- HTTP 요청 관련
method VARCHAR(10),
url TEXT,
status_code INTEGER,
response_time INTEGER,
-- 사용자 정보
user_id INTEGER,
session_id VARCHAR(128),
ip_address INET,
-- 요청 추적
request_id UUID,
-- 에러 정보
error_message TEXT,
error_stack TEXT,
-- 추가 메타데이터 (JSON)
metadata JSONB,
tags TEXT[] DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);
-- 성능을 위한 인덱스들 (근데.. 아직 느려본적이 없어서 채감좀 하고싶음 ;ㅅ;)
CREATE INDEX idx_app_logs_timestamp ON app_logs (timestamp DESC);
CREATE INDEX idx_app_logs_level ON app_logs (level);
CREATE INDEX idx_app_logs_user_id ON app_logs (user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_app_logs_level_timestamp ON app_logs (level, timestamp DESC);
-- JSONB와 배열 검색용 인덱스
CREATE INDEX idx_app_logs_metadata ON app_logs USING GIN (metadata);
CREATE INDEX idx_app_logs_tags ON app_logs USING GIN (tags);
NestJS App → Winston Logger → 배치 큐 → PostgreSQL
↓
콘솔 출력 (실시간 디버깅용)
로그를 즉시 DB에 저장하지 않고 큐에 모았다가 배치로 처리:
private logQueue: LogData[] = [];
private readonly BATCH_SIZE = 50; // 50개씩 모아서 처리
private readonly BATCH_TIMEOUT = 5000; // 5초마다 또는 큐가 가득 찰 때
// 로그 발생시
addToQueue(logData: LogData) {
this.logQueue.push(logData);
// 즉시 콘솔 출력 (개발자 경험)
console.log(`${logData.level}: ${logData.message}`);
// 배치 크기 도달시 즉시 저장
if (this.logQueue.length >= this.BATCH_SIZE) {
this.flushLogs();
}
}
@Injectable()
export class PostService {
constructor(private readonly logger: LoggerService) {}
async createPost(createPostDto: CreatePostDto, userId: number) {
const startTime = Date.now();
try {
const post = await this.postRepository.save(createPostDto);
// 성공 로그 - 비즈니스 메트릭까지 포함
this.logger.info('Post created successfully', {
postId: post.id,
userId,
title: post.title,
category: post.category,
responseTime: Date.now() - startTime,
tags: ['post', 'create', 'success']
});
return post;
} catch (error) {
// 에러 로그 - 디버깅에 필요한 모든 정보
this.logger.error('Failed to create post', {
userId,
title: createPostDto.title,
responseTime: Date.now() - startTime,
error: error.message,
stack: error.stack,
tags: ['post', 'create', 'error']
});
throw error;
}
}
}
-- 시간별 에러 발생 추이
SELECT
date_trunc('hour', timestamp) as time,
COUNT(*) FILTER (WHERE level = 'ERROR') as errors,
COUNT(*) FILTER (WHERE level = 'WARN') as warnings,
COUNT(*) as total
FROM app_logs
WHERE timestamp >= $__timeFrom() AND timestamp <= $__timeTo()
GROUP BY time
ORDER BY time;
-- API 엔드포인트별 성능 분석
SELECT
url,
AVG(response_time) as avg_response_time,
MAX(response_time) as max_response_time,
COUNT(*) as request_count,
COUNT(*) FILTER (WHERE status_code >= 400) as error_count
FROM app_logs
WHERE method IS NOT NULL
AND timestamp >= $__timeFrom()
GROUP BY url
ORDER BY avg_response_time DESC;
-- 사용자별 활동 분석 (개인 블로그라서 재밌게 볼 수 있음)
SELECT
user_id,
COUNT(*) as total_requests,
COUNT(DISTINCT DATE(timestamp)) as active_days,
MAX(timestamp) as last_activity
FROM app_logs
WHERE user_id IS NOT NULL
GROUP BY user_id
ORDER BY total_requests DESC;
일일 로그 생성량: ~5,000건
월간 로그 누적: ~150,000건
DB 용량 증가: ~50MB/월
검색 성능: 1-2초 (인덱스 잘 설정했을 때)
결론: 소규모 서비스에서는 충분히 감당 가능
이때는 Loki나 ELK 스택을 진지하게 고려해보세요.
1단계: PostgreSQL로 시작 (지금)
2단계: 로그량 증가시 파티셔닝 적용
3단계: 필요시 Loki 추가 (하이브리드)
4단계: 완전한 로그 시스템 전환
내 선택: 약간의 성능 포기 → 인프라 단순성 확보 결과: 개발 속도 UP, 유지보수 부담 DOWN
무작위 포스트가 추천됩니다.
NestJS MVC Tools는 NestJS에서 전통적인 웹 개발 방식을 좀 더 편하게 시작할 수 있도록 도움을 드리는 작은 도구입니다. 처음에는 NestJS에서 Edge.js 템플릿 엔진을 간편하게 사용하기 위한 단순한 유틸리티로 시작했지만, MVC 패턴 중 View 계층에 필요한 다양한 기능들이 하나씩 추가되면서 지금의 모습이 되었습니다.
자바에서 제공하는 this 키워드는 인스턴스 자기 자신를 가리키는 키워드이다. 이 this 키워드를 통해 클래스 메서드 및 생성자에서 자기 자신의 데이터를 업데이트하거나 조작할 수 있다.
점령전 산군, 귓미, 퓨퓨 관전기록