봇을 위한 구조화 로깅과 관측가능성
봇이 새벽 3시에 이상한 주문을 냈습니다. 다음 날 원인을 찾으려는데 로그가 println("주문함") 한 줄뿐이라면, 그 사고는 영원히 미제로 남습니다. 무인 운영 봇에게 로그는 사치가 아니라 블랙박스입니다.
⚠️ 실거래 자동매매는 소프트웨어·네트워크 장애로 손실이 발생할 수 있습니다. 이 글은 교육·정보 목적입니다.
1. 로그 레벨 — 노이즈와 신호 분리
모든 걸 같은 톤으로 찍으면 정작 중요한 게 묻힙니다. 최소한 네 단계는 나눕니다.
- DEBUG: 개발·재현용 상세(호가 스냅숏, 계산 중간값). 평소엔 끔.
- INFO: 정상 흐름의 이정표(진입·청산·재연결 성공).
- WARN: 이상하지만 계속 가능(미체결 취소, 데이터 지연).
- ERROR: 개입 필요(주문 실패, 인증 만료, 패닉 복구).
운영 중엔 INFO 이상만 파일에 남기고 DEBUG는 스위치로 켜는 구조가 편합니다. 레벨은 나중에 필터링할 수 있게 하는 인덱스입니다.
2. 왜 구조화(JSON) 로그인가
사람이 읽는 문장 로그는 기계가 검색·집계하기 어렵습니다. 필드를 가진 JSON으로 남기면, 나중에 "오늘 ERROR만" "종목 A의 체결만" 같은 질의가 즉시 됩니다.
// 문자열 로그 (검색 지옥)
log.Printf("종목 %s 매수 %d주 @ %.0f", code, qty, price)
// 구조화 로그 (필드로 질의 가능)
logEvent(map[string]any{
"level": "INFO",
"event": "order_filled",
"code": code,
"side": "buy",
"qty": qty,
"price": price,
"trace": traceID,
})
// → {"level":"INFO","event":"order_filled","code":"069500","qty":200,...}
Go 표준 라이브러리라면 log/slog로 바로 이 형태를 얻습니다. 굳이 무거운 프레임워크가 필요 없습니다.
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("order_filled",
"code", code, "qty", qty, "price", price, "trace", traceID)
3. 상관관계 추적 — 한 결정의 전체 궤적
봇의 한 사이클은 신호 → 검증 → 주문 → 체결 → 상태 저장으로 이어집니다. 이 흐름에 같은 trace ID를 달면, 사고가 났을 때 그 결정의 처음부터 끝까지를 한 줄로 이어 볼 수 있습니다.
trace := newTraceID() // 한 사이클 = 하나의 ID
logger.Info("signal_fired", "trace", trace, "gap", gap)
logger.Info("depth_check", "trace", trace, "askQty", askQty)
logger.Info("order_sent", "trace", trace, "qty", qty)
logger.Warn("order_timeout", "trace", trace) // ← 여기서 꼬였다
logger.Info("order_cancel", "trace", trace)
나중에 trace로 필터링하면 "왜 그 주문이 미체결로 취소됐나"의 전체 맥락이 시간순으로 복원됩니다. 로그가 흩어진 점이 아니라 이어진 선이 됩니다.
4. 무엇을 남기고 무엇을 버리나
다 남기면 디스크가 터지고 정작 중요한 게 안 보입니다. 기준은 "이 줄이 사고를 설명하는 데 필요한가"입니다.
- 반드시 남긴다: 모든 상태 전이(진입/청산/정지), 모든 외부 부작용(주문·취소·API 호출 결과), 모든 ERROR/WARN.
- 버리거나 DEBUG로: 초당 수십 개 들어오는 호가 원본, 루프마다 도는 계산 중간값. 필요할 때만 켠다.
- 절대 남기지 않는다: API 키·토큰·계좌번호 같은 비밀. 로그로 새는 것도 유출입니다.
// 민감정보는 마스킹해서만 기록
func maskSecret(s string) string {
if len(s) <= 8 {
return "****"
}
return s[:4] + "****" + s[len(s)-4:]
}
logger.Info("auth_ok", "key", maskSecret(appKey))
5. 운영 위생 — 로테이션과 알림
- 로그 로테이션: 날짜·크기별로 파일을 나눠, 하나가 무한정 커지지 않게. 오래된 건 자동 삭제.
- ERROR는 밖으로: 파일에만 쌓지 말고 메신저 알림으로 즉시 밀어냅니다. 사람이 하루 종일 로그를 볼 수 없으니까요.
정리
- 레벨로 노이즈와 신호를 분리 — 운영은 INFO 이상, DEBUG는 스위치
- JSON 구조화 로그로 나중에 질의·집계 가능하게 (Go는
slog면 충분) - trace ID로 한 결정의 전체 궤적을 이어서 복원
- 상태 전이·외부 부작용·에러는 남기고, 비밀은 마스킹
- 로테이션으로 디스크를 지키고, ERROR는 알림으로 밖에 알린다
관측가능성은 "봇이 무엇을 했는지 나중에 정확히 재구성할 수 있는가"입니다. 화려한 대시보드 이전에, 사고를 설명할 수 있는 로그 한 줄이 무인 운영의 안전망입니다.
댓글
댓글 쓰기