여러 줄 오류 로그를 한 사건으로 묶어 CSV에 기록하기
P22-reader 실습 패키지

목적
명시한 사건 시작 형식과 두 종류의 허용 연속행만 묶습니다.
불일치·잘못된 시각·빈 줄에서는 연결을 끊습니다.
시각 없는 줄에 발생 시각이나 코드를 추정해 넣지 않습니다.
입력 물리행마다 사건 또는 보류에 정확히 한 번 대응하는지 검산합니다.
입력은 모두 가상이며 로그 원인을 진단하지 않습니다.

실행 환경
CPython 3.12.14 / Linux x86_64 / 표준 라이브러리만 사용
Windows에서는 실행하지 않았습니다.
공식 /3.12 문서 페이지는 2026-10-05 확인 시 3.12.15로 표기됐습니다.
VERSION.json과 TEST_RESULTS.txt, RUN_RESULTS.json에 실제 시험 범위가 있습니다.

시작
ZIP을 새 개인 연습 폴더에 풀고 P22-reader 폴더로 이동하세요.
예: cd P22-reader
아래 명령은 Linux의 python3 호출로 검증했습니다.
출력 부모 폴더는 이미 있어야 합니다.
새 출력은 입력 파일이 든 폴더와 하위 트리 밖이어야 합니다.
이미 존재하는 출력 폴더에는 쓰지 않습니다.

python3 --version
python3 group_logs.py --input fixtures/normal.log --out-dir ../p22_run_01
python3 inspect_result.py --input fixtures/normal.log --out-dir ../p22_run_01
python3 -m unittest -v
python3 verify_practice.py
python3 verify_package.py

기본 입력 fixtures/normal.log
orphan before first event
2026-10-05T09:15:00Z ERR-14 import failed
    at demo.Parser.read(Parser.java:12)
    at demo.Worker.run(Worker.java:44)
Caused by: ValueError: invalid count
2026-10-05T09:16:00Z OK-00 ready

기본 검산
입력 217바이트 / 물리행6
E0001: L2-L5, 원시행4
E0002: L6, 원시행1
보류: L1, UNRECOGNIZED_LINE
4 + 1 + 1 = 6 / 대장6행 / 사건2레코드
status=HOLD_PRESENT는 보류가 남은 파일 생성 성공입니다.
장애가 없거나 서비스가 정상이라는 의미로 쓰지 마세요.

출력
- events.csv: 사건 레코드, timestamp_original은 시작 줄의 원문 값만
- held.csv: 보류한 물리행, 시각·코드 열 없음
- line_ledger.csv: 모든 물리행의 EVENT_START/CONTINUATION/HOLD와 연결 ID
- summary.json: 행 수·원본 SHA-256·CSV 실제 물리행 수
raw_line_json과 header_message_json은 JSON 문자열입니다.
json.loads()로 원문을 복원합니다. 따옴표도 저장 데이터의 일부입니다.
raw_text는 사건 시작 줄과 스택을 원래 줄바꿈까지 보존합니다.
line_ending은 LF/CRLF/NONE입니다.
events.csv는 셀 내부 줄바꿈 때문에 헤더 포함 물리행8이지만 사건2레코드입니다.
CSV를 줄별 split으로 파싱하지 말고 csv.DictReader와 newline=''를 사용하세요.
스프레드시트에 실제 로그를 가져올 때 텍스트 열을 지정하고 수식·민감값을 먼저 확인하세요.

사건 문법
시작: YYYY-MM-DDTHH:MM:SSZ (ERR|OK)-두자리숫자 메시지
항목 사이 공백 한 개, 메시지 한 글자 이상, 날짜·시각 유효성 검사
연속1: 공백 또는 탭 하나 이상 + at 메서드(파일:숫자)
메서드: [A-Za-z_][A-Za-z0-9_.]*
파일: [A-Za-z0-9_.-]+ / 행번호: [0-9]+
연속2: Caused by: [A-Za-z_][A-Za-z0-9_.]*: 설명 한 글자 이상
문서의 가상 애플리케이션 계약이며 모든 제품의 스택 공통 규칙이 아닙니다.
허용 연속행이라도 시작 사건이 없으면 ORPHAN_CONTINUATION입니다.
어떤 보류행이라도 기존 연결을 끊어 뒤 스택을 앞 사건에 건너뛰어 붙이지 않습니다.

반례 실행
python3 group_logs.py --input fixtures/invalid_time.log --out-dir ../p22_bad_time_01
python3 group_logs.py --input fixtures/unallowed.log --out-dir ../p22_unallowed_01
python3 group_logs.py --input fixtures/no_timezone.log --out-dir ../p22_no_timezone_01
python3 group_logs.py --input fixtures/blank_boundary.log --out-dir ../p22_blank_01
python3 group_logs.py --input fixtures/empty.log --out-dir ../p22_empty_01
python3 group_logs.py --input fixtures/invalid_utf8.log --out-dir ../p22_utf8_01

기대 결과
normal: 입력6 / 사건2 / 사건원시행5 / 보류1 / 대장6
invalid_time: 입력5 / 사건2 / 사건원시행3 / 보류2 / 대장5
unallowed: 입력5 / 사건2 / 사건원시행3 / 보류2 / 대장5
no_timezone: 입력3 / 사건1 / 사건원시행1 / 보류2 / 대장3
blank_boundary: 입력4 / 사건2 / 사건원시행2 / 보류2 / 대장4
위 다섯 입력은 exit 0과 HOLD_PRESENT; expected/<이름>/의 CSV·요약과 일치
empty: 0바이트, exit 1, 출력 폴더 미생성
invalid_utf8: 바이트 0xff 포함, exit 1, 출력 폴더 미생성
verify_practice.py는 다섯 기대 결과의 바이트 일치와 두 중단 사례를 임시 폴더에서 확인합니다.

21개 unittest 시험 범위
정상 사건·스택, 시각 오류, 미허용 들여쓰기, 시간대 없음, 빈 줄,
시작 전 고아 연속행, CRLF·마지막 줄바꿈 없음, 빈 파일·UTF8 오류,
1MB 및 10000행 제한, 출력 폴더 충돌, 동일 경로 재실행,
새 출력에서 결정적 재실행, 원본 SHA 보존, 입력 트리 내부 출력 거부,
CSV 논리 레코드와 물리행 구별, 원시행 대장 삭제·중복·변경·스택 유실 검출,
단독 CR·BOM 거부, 입력 최종 심볼릭 링크 거부, 수식형 보류 원문 JSON 보존

복구
보류: 원문 번호와 실제 제품 형식 문서를 대조하세요. 없는 시각·코드·원인을 만들지 않습니다.
UTF8 오류: ignore/replace를 사용하지 않습니다. 내보내기 인코딩을 확인해 별도 UTF8 사본을 준비합니다.
빈 입력: 수집 범위와 저장 여부 확인, 새 입력 준비
한도 초과: 출처·범위를 남긴 통제된 작은 오프라인 사본에서 연습
입력 최대 1,000,000바이트 · 10,000행 · 행 본문 20,000바이트, BOM 없는 UTF-8
지원: LF / CRLF / 마지막 줄 줄바꿈 없음
단독 CR과 탭을 제외한 U+0000~U+001F 제어문자는 중단합니다.
DEL(U+007F)과 C1 영역(U+0080~U+009F)은 거부하지 않으며 원문으로 보존합니다.
기존 출력: 덮어쓰거나 지우지 않고 새 외부 폴더 이름 지정
아래 첫 명령은 앞의 정상 실행 후 exit1이며 기존 결과가 그대로 남아야 합니다.
python3 group_logs.py --input fixtures/normal.log --out-dir ../p22_run_01
새 폴더로 재실행:
python3 group_logs.py --input fixtures/normal.log --out-dir ../p22_run_02
쓰기/재읽기 실패: 부분 출력 유지, STOP/exit1 확인, 완료 결과로 사용하지 않음
완료 판정: 실행 exit0 + 정상 JSON 파싱되는 summary.json + CSV 재읽기/원시행 커버리지 검산
inspect_result.py는 요약의 수치와 원본 SHA-256도 실제 입력/CSV와 대조합니다.
CSV 파일의 존재만으로 완료라고 판단하지 않습니다.
원본과 부분 결과 검토 후 다른 새 폴더로 다시 실행합니다.
권한·보안 설정 변경 기능은 포함하지 않습니다.
원본 수정·삭제·이동 기능은 포함하지 않습니다.

한계
입력 최종 심볼릭 링크는 거부하지만 resolve()는 조상 경로의 링크를 따라갈 수 있습니다.
조상 링크나 Windows junction을 모두 거부하지 않으며 junction은 실행 시험하지 않았습니다.
직접 만든 링크 없는 고정 연습 폴더에서 실행하세요.
입력을 두 번 제한 크기로 읽어 관찰된 변경은 감지하지만 파일을 잠그지 않습니다.
동시 파일·디렉터리 수정 경쟁에 대한 완전 안전을 보장하지 않습니다.
쓰기가 끝난 오프라인 사본을 통제된 개인 폴더에 놓고 연습하세요.
완전 커버리지는 읽은 바이트와 행의 대응 확인이며 실제 사건 동일성·원인 증명이 아닙니다.
서로 다른 스레드가 섞인 로그는 제품의 연결 ID 기준이 별도로 필요합니다.

파일 해시
SHA256SUMS.txt는 자기 자신을 제외한 제공 파일의 SHA-256입니다.
python3 verify_package.py로 목록과 파일 바이트의 일치를 확인합니다.
해시 목록 일치는 파일 비교용이며 제작자 인증을 보장하지 않습니다.

공식 Python 3.12 문서
https://docs.python.org/3.12/library/re.html#re.fullmatch
https://docs.python.org/3.12/library/datetime.html#datetime.datetime.strptime
https://docs.python.org/3.12/library/csv.html#csv.writer
https://docs.python.org/3.12/library/csv.html#csv.csvreader.line_num
https://docs.python.org/3.12/library/pathlib.html#pathlib.Path.mkdir
https://docs.python.org/3.12/library/functions.html#open
https://docs.python.org/3.12/library/io.html#io.BufferedIOBase.read
https://docs.python.org/3.12/library/hashlib.html#hashlib.sha256
https://docs.python.org/3.12/library/json.html#json.dumps
https://docs.python.org/3.12/library/unittest.html#command-line-interface
