봇 구조화 로그: 비밀을 노출하지 않고 주문 판단 재구성하기

English version

TECH INSIGHT / EXPLAINED

쓸모 있는 로그는 주문을 전송하지 못했는지, 접수 후 응답만 못 받았는지, 일부 체결됐는지, 대조까지 끝났는지 답해야 합니다. 이벤트 이름부터 이 상태를 구분해야 합니다.

세 사람이 판단 흐름을 로그로 추적하고 인증 정보는 로그 밖에 보관합니다.
AI로 제작한 설명용 일러스트. 세 사람이 판단 흐름을 로그로 추적하고 인증 정보는 로그 밖에 보관합니다.

레벨보다 이벤트를 먼저 정하기

DEBUG는 상세 진단, INFO는 정상 상태 전이, WARN은 성능·기능 저하, ERROR는 개입이 필요한 실패로 나눕니다. 정상적인 취소가 항상 경고인 것은 아니고, 예상된 토큰 갱신도 항상 오류는 아닙니다. 운영상 의미를 기준으로 정하세요.

판단마다 안정적인 로컬 ID를 두고 관련 주문·고유 체결을 연결합니다. UTC 이벤트 시각, 운영/시험 환경, 앱 버전, 판단 ID, 로컬 주문 참조, 확인된 거래소 주문 ID, 상태, 이유, 시도 번호를 남깁니다. 가격·수량은 소수 문자열로 기록합니다.

package main
import ("log/slog"; "os")
func main() {
    log := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    decision := log.With("decision_id", "demo-001", "order_ref", "local-001")
    decision.Info("order_submit_started", "qty", "0.010", "price", "65000.00")
    decision.Warn("order_outcome_unknown", "reason", "read_timeout", "action", "reconcile")
}

타임아웃은 결과 불명 상태

예제는 제출 시작 뒤 읽기 타임아웃을 기록합니다. 주문 거절이나 취소라고 단정하지 않습니다. 대조 작업이 거래소를 조회해 같은 판단 ID에 확인된 결과를 붙여야 합니다. trace ID는 탐색을 돕지만 회계의 기준은 영속화된 주문 상태입니다.

가상의 사고에서는 demo-001을 찾고 송수신 시각을 비교한 뒤 최초 거래소 응답과 저장된 체결 기록을 대조합니다. 이렇게 조사 경로가 재현되어야 하며, 모호한 “주문 끝” 로그만으로는 부족합니다.

비밀은 일부도 기록하지 않기

API 키·Bearer 토큰·서명 쿼리·전체 계좌 식별자·인증 원문을 남기지 않습니다. 키 앞뒤 몇 글자를 노출하는 마스킹도 비밀 일부를 공개합니다. 대신 trading-reader 같은 비밀이 아닌 설정 별칭을 사용하세요.

안전한 필드 허용 목록을 두고 예외 처리도 살펴봅니다. request 객체를 통째로 출력하면 평소의 삭제 처리를 우회할 수 있습니다. 외부 문자열 길이를 제한하고 문자열 조합 대신 JSON 인코더로 기록합니다.

장애 중에도 관측이 유지되게 하기

크기·기간 기준 보관 한도를 두고 접근 권한과 디스크 여유를 관리합니다. 같은 경고는 묶고 재알림 간격을 둬 한 장애의 반복 메시지가 주문 상태 문제를 묻지 않도록 합니다. 로그와 함께 데이터 경과 시간·미확정 주문의 대기 시간·대조 밀림·오류율을 관찰하세요. 프로세스가 살아 있어도 필요한 일을 못 할 수 있습니다.

발생 시각과 수집 시각을 따로 보관한다

거래소에서 발생한 사건이 네트워크 지연 뒤 들어오면 로그를 쓴 시각과 사건 시각이 다릅니다. OpenTelemetry 로그 데이터 모델도 Timestamp와 ObservedTimestamp를 구분합니다. 원본 시각을 알 수 없을 때 수신 시각을 원본인 것처럼 채우지 말고 미상임을 나타내세요. 시스템 간 시계 오차도 있으므로 두 값의 차이를 순수한 네트워크 지연으로 단정하지 않습니다.

한 프로세스 안의 경과 시간은 단조 시계로 측정하고, 재시작을 구별하는 실행 ID와 지역 순번을 추가할 수 있습니다. 예를 들어 오래된 응답이 늦게 기록돼도 사건 ID와 상태 버전을 따라가면 최신 판단을 덮어쓴 것인지 단지 늦게 관측한 것인지 구분하기 쉬워집니다. 시각만으로 전역적인 인과 순서를 만들지는 않습니다.

로그 스키마도 배포하는 인터페이스다

대시보드가 reason 필드로 오류를 집계하는데 배포 후 같은 뜻을 message에만 쓰면 알림이 조용히 깨집니다. 이벤트 이름, 필드 자료형, 필수 여부, 스키마 버전을 작은 계약으로 관리하세요. 가격을 소수 문자열로 약속했다면 일부 코드 경로가 숫자로 내보내지 않는지 검사하고, 값을 알 수 없는 경우와 숫자 0도 구분합니다.

새 필드는 기존 소비자가 무시할 수 있도록 추가하되 기존 필드의 의미 변경은 별도 버전으로 다룹니다. 표본 로그를 재생해 이전 버전의 검색과 새 버전의 검색이 모두 핵심 사건을 찾는지 확인하세요. 모든 레코드에 모든 필드를 채우기보다 제출·체결·대사 이벤트마다 필요한 필드를 정하면 거대한 빈 객체를 반복할 필요가 없습니다.

사건 ID는 로그에, 집계는 제한된 범주로

판단 ID와 주문 ID는 개별 사건을 추적할 때 유용하지만 메트릭 라벨로 사용하면 새로운 시계열이 계속 생깁니다. Prometheus의 라벨 지침은 이러한 높은 카디널리티를 경계합니다. 오류율은 서비스·환경·제한된 사유 코드로 묶고, 특정 사건의 상세 ID는 로그나 추적 정보에서 찾도록 연결하세요.

정상 상태의 상세 디버그 로그를 표본 추출하더라도 불명 주문으로 전이한 사건과 대사 완료 같은 복구 근거까지 무작위로 버리면 안 됩니다. 필요한 감사 기록을 별도 정책으로 보존하고 삭제·보존 기간을 명시합니다. 로그를 오래 보관하는 것과 주문 상태의 영속성을 확보하는 것은 별개의 요구사항입니다.

가짜 장애 하나를 처음부터 끝까지 조사한다

오프라인 훈련은 하나의 판단에서 제출 시작, 응답 타임아웃, 불명 상태, 조회 시작, 접수 확인, 대사 완료를 생성하도록 구성합니다. 순서를 섞거나 같은 레코드를 반복 입력해도 ID를 따라 실제 전이를 재구성할 수 있는지 확인하세요. 체결이 없었다면 완료 기록이 체결됐다고 말하지 않아야 하며, 중간 기록이 빠졌을 때는 그 공백이 드러나야 합니다.

다음에는 가짜 비밀 문자열, 줄바꿈이 있는 외부 오류, 과도하게 긴 필드, 가득 찬 저장 장치를 흉내 낸 수집기를 주입합니다. 비밀 표본이 최종 출력에 남지 않고 외부 문자열이 새 로그 레코드를 위조하지 못하는지 확인합니다. 로그 저장 실패를 같은 고장 난 로거에 무한히 기록하지 말고, 제한된 별도 상태 카운터와 명시적 기능 저하로 드러내세요.

공식 자료와 함께 읽기

Go slog · OWASP logging guidance

내용·예제 검토: 2026-10-01

댓글