Python 로그 설계: 요청 ID·로그 레벨·개인정보 제외
장애를 추적할 수 있는 필드와 남기면 안 되는 필드를 나누는 로깅 예제입니다.
이 글의 목차
장애 때 필요한 로그는 ‘무언가 실행됐다’는 문장보다 어떤 요청의 어느 단계가 실패했는지 연결해 주는 기록입니다. 동시에 요청 본문을 통째로 남기면 토큰이나 개인정보가 로그 저장소로 복사될 수 있습니다. 기록할 필드를 미리 정하는 방식으로 시작해 보겠습니다.
요청을 연결하는 최소 필드
아래는 실제 고객 정보가 없는 설명용 예제입니다. 요청 ID는 서버에서 생성하거나 신뢰할 수 있는 경계에서 검증한 값을 사용합니다. 임의의 사용자 입력을 길이 제한 없이 로그에 넣지 않습니다.
import logging
logger = logging.getLogger("orders")
def record_failure(request_id, error_code):
logger.warning(
"order_rejected request_id=%s code=%s",
request_id, error_code,
)
record_failure("demo-request-001", "OUT_OF_STOCK")
운영에서는 구조화된 로그 포맷과 필드 허용 목록을 함께 검토합니다. 비밀번호, Authorization 헤더, 세션 쿠키, 카드 정보, 요청 본문 전체는 기본 필드에 넣지 않습니다. 예외 메시지 자체에 입력값이 포함될 수 있으므로 스택 추적도 무조건 안전하다고 가정하지 않습니다.
로그 레벨을 행동과 연결하기
| 상황 | 예시 레벨 | 운영자의 다음 행동 |
|---|---|---|
| 정상적인 중요 상태 변경 | INFO | 흐름 확인 |
| 복구 가능하지만 관찰할 상황 | WARNING | 반복 빈도 확인 |
| 요청 처리 실패 | ERROR | 영향 범위와 원인 조사 |
| 개발 중 세부 진단 | DEBUG | 제한된 환경에서 분석 |
모든 로그를 ERROR로 남기면 실제 장애 신호가 묻힙니다. 반대로 예상하지 못한 예외를 잡고 INFO만 남기면 실패가 성공처럼 보일 수 있습니다. 수준은 팀 기준으로 정하되 어떤 대응을 기대하는지 문서화합니다.
운영에서만 발생하는 문제 찾기
사용자가 ‘안 된다’고 보고하면 요청 시각, 화면, 오류 코드와 요청 ID를 연결합니다. 환경 차이를 확인할 때는 배포 버전, 기능 플래그 이름, 의존 서비스 상태처럼 필요한 정보만 추가합니다. 환경변수 전체를 출력하는 방식은 비밀값 유출 위험이 있습니다.
같은 요청 ID의 시작·완료 기록을 비교하고 DB나 외부 호출 구간을 좁힙니다. 시간 측정은 벽시계 변경의 영향을 받지 않는 단조 시계를 검토합니다. 여러 서버의 시각 비교가 필요하면 시각 동기화와 시간대도 확인해야 합니다.
저장했다면 접근과 삭제도 설계하기
로그 보관 기간은 조사 목적과 실제 운영 요구를 기준으로 정합니다. 누구나 다운로드할 수 있는 저장소에 두지 않고 접근 권한을 제한합니다. 테스트에는 가짜 토큰을 넣고 출력에 포함되지 않는지 검사할 수 있습니다.
로그만으로 서비스 상태를 모두 판단할 수는 없습니다. 실패 횟수와 지연 분포는 모니터링 지표로 보고, 개별 사건의 원인은 로그로 좁히는 식으로 역할을 나누면 찾기 쉽습니다.
허용 목록이 실제로 비밀값을 제외하는지 검사하기
‘민감 정보는 마스킹’이라는 규칙만 남기기보다 입력에서 필요한 키만 선택하고, 출력 문자열에 가짜 비밀값이 남지 않는지 확인할 수 있습니다. 다음 예제는 JSON 직렬화를 사용해 문자열 안의 줄바꿈도 이스케이프합니다.
import json
def safe_event(event):
allowed = {}
for key in ("request_id", "code"):
value = event.get(key)
if isinstance(value, str):
allowed[key] = value[:80]
return json.dumps(allowed, ensure_ascii=False)
line = safe_event({
"request_id": "demo-12",
"code": "INVALID_INPUT\nsecond-line",
"password": "fake-password-DO-NOT-LOG",
"authorization": "fake-token-DO-NOT-LOG",
})
assert "DO-NOT-LOG" not in line
assert "\n" not in line
assert json.loads(line)["request_id"] == "demo-12"
print(line)
예상 출력에는 request_id와 code 두 필드만 남고 입력의 줄바꿈은 JSON의 \n 표현으로 보입니다. 이 함수를 써도 허용된 필드 안에 토큰을 넣으면 노출될 수 있습니다. 요청 ID는 서버 생성값, 오류 코드는 미리 정의한 코드처럼 의미를 제한해야 합니다. 이 실습은 다른 핸들러·프록시·외부 관측 도구가 남기는 로그까지 안전하게 만든다는 보장이 아닙니다.