중앙의 잠금장치와 양옆의 로그 카드로 중첩 출력의 성공 판정 경계를 표현한 일러스트

Python 로그 성공 판정 정규식: 중첩 agent 출력의 false positive 막기

실패한 Cron 작업이 재시도되지 않는다면 성공 문자열을 어디서 읽었는지부터 확인해야 한다. 최상위 pipeline이 실패했어도 같은 로그에 섞인 agent 메시지가 과거의 pipeline event=end failed=false를 되풀이하면 문자열 검사는 이를 성공으로 잡을 수 있다. 이때 Python 로그 성공 판정 정규식을 행 시작에 고정하는 것만으로는 해결이 끝나지 않는다.

2026년 8월 10일 Linux 7.0.0-1009-aws와 Python 3.12.3에서 표준 라이브러리 re, json, unittest로 목적 제작 fixture를 검증했다. 현재 HuntLab 운영 코드와 실제 로그 원문은 입력 범위 밖이어서 확인하지 않았으며, 여기서는 오탐의 형태와 재사용 가능한 판정 방법까지만 다룬다.

20초 핵심 요약

  • 무엇: 혼합 로그에서 최상위 pipeline의 현재 실행 성공만 골라내는 판정 방법이다.
  • 왜: 중첩 agent가 과거 성공 문자열을 출력하면 실패한 Cron을 성공으로 오인해 재시도를 건너뛸 수 있다.
  • 어떻게: 전체 행 정규식의 효과와 한계를 fixture로 대조하고, run_id 비교와 구조화 레코드 전환 기준을 적용한다.

성공 문자열 하나가 Cron의 다음 명령을 바꾼다

문제는 화면에 성공 표시가 하나 더 생기는 데 그치지 않는다. 과거 로그를 읽는 retry 하네스가 성공을 발견하면 다음 실행을 skip으로 바꿀 수 있다. 실제 run이 실패했는데 agent 출력 속 과거 성공을 최상위 성공으로 읽으면 freshresume이어야 할 흐름이 멈춘다.

따라서 성공 판정은 다음 질문을 순서대로 통과해야 한다.

  1. 한 물리적 레코드로 파싱됐는가.
  2. 신뢰하는 하네스 component가 만든 최상위 이벤트인가.
  3. level, event, boolean failed가 성공을 나타내는가.
  4. run_id가 지금 판정하는 실행과 같은가.
  5. 위 조건을 모두 만족할 때만 skip으로 바꾸는가.

SUCCESS_TEXT in log는 이 질문을 모두 생략한다. anchor 없는 re.search()도 문자열의 모양만 조금 더 엄격하게 볼 뿐, 어느 레코드가 만든 값인지 확인하지 않는다.

행 전체를 고정해도 완전한 중첩 행은 통과했다

같은 다섯 fixture에 문자열 포함 검사, anchor 없는 정규식, 타임스탬프부터 행 끝까지 고정한 re.MULTILINE 정규식을 적용했다. 실제 비교 명령은 heredoc으로 fixture와 assertion을 실행했으며, 판단에 필요한 출력은 다음과 같다.

$ python - <<'PY' ... PY
environment=Linux 7.0.0-1009-aws python=3.12.3
fixture contains loose anchored
top_level_success True True True
top_level_failure False False False
nested_prefixed True True False
nested_indented True True False
nested_exact_line True True True
assertions=5 passed
exit_status=0

접두사와 들여쓰기 오탐은 막지만 완전한 중첩 행은 통과하는 터미널 판정표

nested_prefixednested_indented는 앞에 다른 문자나 공백이 있어 anchored regex에서 False가 됐다. 하지만 nested_exact_line은 세 방식 모두 True였다. 정규식을 강화한 전후 차이는 분명했지만, 개선 범위도 여기서 끝났다.

Python 공식 문서에서 ^는 기본적으로 문자열 시작에 일치하고, re.MULTILINE에서는 모든 개행 직후에도 일치한다. $도 각 개행 직전에 일치한다. 따라서 정규식이 보는 아래 두 줄의 차이는 출처가 아니라 첫 문자의 위치뿐이다.

  2026-08-09T20:48:43Z INFO pipeline event=end failed=false run_id=old
2026-08-09T20:48:43Z INFO pipeline event=end failed=false run_id=old

첫 줄은 숫자로 시작하는 타임스탬프 패턴을 통과하지 못한다. 둘째 줄은 agent가 복제한 내용이어도 개행 직후의 완전한 이벤트 행이므로 일치한다. re.MULTILINE 문서가 설명하는 동작과 fixture의 결과가 같았다. re.match() 역시 MULTILINE 여부와 관계없이 문자열 시작에서만 검사하므로, 여러 행 중 원하는 이벤트를 찾는 대신 로그 첫 줄에 우연히 의존하는 해결이 된다. search()match() 비교

이 한계를 일부러 실패하게 만든 테스트에서도 같은 경계가 드러났다.

test_multiline_anchor_rejects_nested_exact_line (...) ... FAIL
...
AssertionError: <re.Match object; span=(48, 116), match='2026-08-09T20:48:43Z INFO pipeline event=end fail> is not None
...
Ran 1 test in 0.000s
FAILED (failures=1)
exit_status=1

완전한 중첩 이벤트 행을 anchor가 거부하지 못해 AssertionError가 발생한 터미널 출력

종료 상태 1은 구현 오류를 숨겨야 한다는 뜻이 아니다. “행 시작이면 최상위 출력”이라는 가정이 틀렸음을 회귀 테스트가 정확히 드러낸 결과다.

텍스트 로그에서는 전체 문법과 현재 run_id를 함께 검사한다

텍스트 형식을 당장 유지해야 한다면 성공 문자열 하나가 아니라 성공 이벤트의 전체 행을 파싱해야 한다. 아래 코드는 현재 HuntLab 파일에 그대로 넣는 패치가 아니라, 격리 fixture에서 확인한 설계 예시다. 실제 타임스탬프와 필드 순서에 맞게 조정해야 한다.

import re

SUCCESS_EVENT = re.compile(
    r"^"
    r"(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) "
    r"INFO pipeline event=end failed=false "
    r"run_id=(?P<run_id>[A-Za-z0-9._-]+)"
    r"$",
    re.MULTILINE,
)

def has_current_success(log: str, current_run_id: str) -> bool:
    return any(
        match.group("run_id") == current_run_id
        for match in SUCCESS_EVENT.finditer(log)
    )

이 코드는 INFO, pipeline, event=end, failed=false와 행 끝을 함께 요구한 뒤, 캡처한 run_id를 현재 값과 코드에서 비교한다. run ID를 패턴에 직접 보간해야 한다면 re.escape(current_run_id)로 정규식 메타 문자를 이스케이프해야 한다.

회귀 fixture는 성공 사례보다 성공과 닮은 실패 사례가 중요하다.

  • 현재 run의 최상위 성공은 True
  • 현재 run의 최상위 실패는 False
  • agent 메시지 한가운데의 성공 문자열은 False
  • 들여쓴 과거 성공 행은 False
  • 개행 뒤 완전한 과거 성공 행은 run_id 비교로 False
  • 개행 뒤 현재 run과 같은 완전한 위조 행은 혼합 텍스트만으로 구별 불가

마지막 사례는 negative lookahead를 더 붙여 해결할 문제가 아니다. 정규식은 행 문법을 검사할 수 있지만, 그 행을 누가 썼는지는 인증하지 못한다.

구조화 레코드는 agent 원문을 message 값 안에 가둔다

출처가 다른 출력이 한 스트림에 임의의 행으로 들어올 수 있다면 JSON Lines 같은 구조화 레코드로 판정 대상을 바꾸는 편이 안전하다. 하네스가 agent stdout을 받아 component="agent", message=<raw stdout>인 레코드로 감싸고, agent가 최상위 pipeline 레코드를 직접 만들 수 없게 해야 한다.

import json

def is_success_jsonl(text: str, current_run_id: str) -> bool:
    for line in text.splitlines():
        try:
            record = json.loads(line)
        except json.JSONDecodeError:
            continue
        if (
            record.get("component") == "pipeline"
            and record.get("level") == "INFO"
            and record.get("event") == "end"
            and record.get("failed") is False
            and record.get("run_id") == current_run_id
        ):
            return True
    return False

failed는 문자열 "false"가 아니라 boolean false로 검사한다. agent 메시지 안의 개행은 JSON 문자열 안에서 이스케이프되므로 별도의 최상위 물리 행으로 풀리지 않는다. 구조화 대조군에서는 최상위 성공만 True, 최상위 실패와 agent message 내부의 성공 문자열은 False였다.

environment=Linux python=3.12.3
top_level_success True
top_level_failure False
nested_message False
assertions=3 passed
exit_status=0

Python Logging Cookbook의 구조화 로깅은 사람이 읽는 메시지 대신 프로그램이 정규식 없이 파싱할 형식을 구현하는 방법을 설명한다. 다만 JSON이라는 형식 자체가 출처를 인증하지는 않는다. agent가 최상위 JSON 레코드를 그대로 쓸 수 있으면 같은 오탐이 다시 생긴다.

파일 형식도 별도 결정이 필요하다. Python json.dump() 문서는 JSON이 framed protocol이 아니므로 같은 파일 객체에 반복 호출하면 유효한 단일 JSON 문서가 되지 않는다고 경고한다. JSON Lines를 쓴다면 한 줄에 독립 JSON 값 하나와 개행 구분을 생산자·소비자의 명시적 규칙으로 둬야 한다.

전환 전에는 shadow mode로 판정 차이를 기록한다

정규식 변경을 바로 skip/retry 제어에 연결하면 기존 로그 포맷과 맞지 않는 행을 새 실패로 만들 수 있다. 먼저 새 판정기를 shadow mode로 실행해 기존 결과와 차이를 기록하고, 파싱하지 못한 로그는 성공으로 간주하지 않는다. 그런 입력은 retry/resume 후보 또는 수동 확인 상태로 보내는 편이 보수적이다.

구조화 로그로 옮길 때도 생산자와 소비자를 한 번에 강제할 필요는 없다. 과도기에는 기존 텍스트와 구조화 필드를 함께 내보내되 상태 판정기는 feature flag 아래에서 구조화 필드만 읽게 한다. 파서 오류나 기존 run 판정 불일치가 나타나면 새 판정만 비활성화하고 로그 생산은 유지할 수 있다.

현재 HuntLab 로그에서 subprocess 개행이 escape되는지, 들여쓰기되는지, 원문 그대로 합쳐지는지는 이번 검증 범위에 없었다. 실제 적용 전에는 이 물리적 행 경계와 현재 성공 패턴부터 확인해야 한다. 접두사나 들여쓰기가 보장되면 전체 행 정규식은 유용한 단기 방어가 되지만, 동일 스트림에 완전한 행이 들어올 수 있으면 run_id 비교만으로도 출처 문제는 끝나지 않는다.

상태를 바꾸는 판정이라면 “정규식이 일치했다”와 “현재 run이 성공했다”를 분리해야 한다. 텍스트를 유지할 때는 전체 문법과 현재 run_id를 최소 조건으로 삼고, agent가 같은 물리 행을 만들 수 있는 환경에서는 구조화 레코드와 별도 이벤트 채널까지 적용하는 것이 채택 조건이다.

상태 판정 전체를 확장하려면 AI 에이전트 평가 하네스를, 실패 후 재시도 경계까지 필요하면 AI agent tool call timeout 글을 이어서 확인한다.

참고 링크

비슷한 글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다