봇을 위한 구조화 로깅과 관측가능성

🌐 English

봇이 새벽 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는 알림으로 밖에 알린다

관측가능성은 "봇이 무엇을 했는지 나중에 정확히 재구성할 수 있는가"입니다. 화려한 대시보드 이전에, 사고를 설명할 수 있는 로그 한 줄이 무인 운영의 안전망입니다.

댓글

이 블로그의 인기 게시물

한국투자증권 KIS API로 실시간 시세 받기 (WebSocket 실전)

파이썬으로 업비트 API 연동하기 — 시세 조회부터 주문까지 기초

Go로 자동매매 신호봇 프레임워크 설계하기