Devin.KR

측정 로그를 근거로 요약하기

70분 안팎

학습 목표

센서 기록기 로그에서 오류 수·최대 지연·복구 성공 수를 정해진 형식으로 집계합니다.

개념

집계는 관찰과 주장을 연결하는 코드입니다

좋은 측정 보고서는 사람의 눈으로 수천 줄을 세어 만든 표가 아닙니다. 같은 입력 로그를 다른 사람이 실행해 같은 요약을 얻어야 합니다. 이번 레슨은 센서 기록기의 사건별 로그에서 오류 수·최대 지연·복구 성공 수를 계산하는 Python 함수와 CLI를 작성합니다. 값의 의미와 입력 범위를 먼저 정하고 빈 입력·잘못된 형식·같은 시각의 여러 사건을 다룹니다. 파일 회전이나 복잡한 정규식은 서재의 더 읽기로 보내고 우리 로그 계약에 집중합니다.

브라우저 과제는 첫 줄에 사건 수 n, 이어 n줄에 event latency_ms를 받습니다. n은 0 이상 10000 이하의 정수입니다. event는 OK·ERROR·RECOVERED 셋이고 latency_ms는 0 이상 1000000 이하의 정수입니다. 출력은 errors=정수 max_latency_ms=정수 recovered=정수 한 줄입니다. 비어 있는 표준 입력·누락 행·추가 행·알 수 없는 event·음수 또는 비정수 지연은 INVALID 한 줄로 거부합니다. 첫 줄 0만 있는 정상 무관찰 입력은 유효합니다.

ERROR는 오류가 관찰된 사건 한 줄이고 RECOVERED는 이미 복구에 성공했다고 판정한 사건 한 줄입니다. 이 과제는 로그를 요약할 뿐 오류와 복구를 같은 장애 ID로 연결하지 않습니다. 그러므로 recovered가 errors보다 많아도 자동으로 입력 오류는 아닙니다. 로그가 이전 시간 구간의 고장에서 복구한 기록으로 시작할 수 있습니다. 실제 복구율을 구하려면 고유 장애 ID와 관찰 구간을 추가해야 합니다.

집계 전에 각 행의 계약을 검사합니다

splitlines로 행 경계를 유지하고 첫 줄의 n을 읽습니다. len(lines)가 n+1인지 먼저 확인합니다. 전체 문자열을 split해서 토큰만 세면 한 행의 세 토큰과 다음 행의 한 토큰을 정상 두 사건처럼 합칠 수 있습니다. 행마다 split 결과가 정확히 두 필드인지 검사합니다. 필드 사이 여러 공백은 허용하되 입력 끝의 빈 행이 사건으로 들어오지 않도록 계약에 맞추어 판단합니다. 아래 과제는 실제 추가 빈 행도 추가 행으로 거부합니다.

숫자 변환은 int를 사용하고 ValueError를 잡습니다. 1.5 같은 소수 문자열은 이 형식에서 거부합니다. 정수 값의 범위를 검사하므로 +2처럼 int가 읽을 수 있는 표현은 허용합니다. 지연 단위는 밀리초이며 2라는 정수를 2.0으로 바꾸지 않습니다. 형식이 깨졌을 때 해당 행을 건너뛰고 정상 요약만 출력하면 관찰 누락을 숨길 수 있어 전체 입력을 INVALID로 처리합니다.

errors·maximum·recovered를 0으로 초기화하고 유효 행마다 세 값을 갱신합니다. event가 ERROR일 때 errors만, RECOVERED일 때 recovered만 올립니다. 최대 지연은 세 종류의 모든 사건을 대상으로 합니다. 오류 처리 자체가 긴 지연일 수도 있기 때문입니다. 정상 사건의 최대값을 별도로 요구한다면 새로운 열과 테스트를 추가해야 하며 현재 명세를 임의로 바꾸지 않습니다.

빈 관찰과 반복 오류를 다르게 해석합니다

n이 0이면 최대 지연도 0으로 출력합니다. 이것은 관찰 데이터가 없다는 약속이며 실제 장치가 즉시 반응한다는 뜻이 아닙니다. 보고서에는 샘플 수를 함께 붙여 최대값 0의 의미를 알려야 합니다. n이 양수인데 모든 지연이 0인 입력과 출력 일부가 같을 수 있어 요약 세 숫자만으로 관찰 충실도를 판단하지 않습니다. 실무 증거에는 전체 건수와 버린 행 수도 포함하는 편이 좋습니다.

같은 내용의 ERROR가 연달아 두 번 있으면 오류 수는 2입니다. 이 로그 형식에는 사건 ID가 없어 두 줄이 재전송인지 서로 다른 고장인지 구별할 수 없습니다. 문자열이 같다는 이유로 set에 넣어 중복 제거하면 오류 빈도가 줄어듭니다. 중복 제거가 요구되는 제품에서는 ID 생성과 재전송 의미부터 바꾸고 원래 건수도 남깁니다. 본 실습의 집계기는 입력 행 수에 충실하게 계산합니다.

순서가 다른 로그라도 사건 종류 수와 최대 지연이 같으면 요약은 같습니다. 이 함수는 상태 전이 순서를 검증하지 않습니다. RECOVERED를 어디에 넣어도 오류 수·최대 지연이 같을 수 있으므로 집계 통과를 복구 상태 머신 검증으로 소개하지 않습니다. 앞 레슨의 fixture 상태·valid·history 검사가 그 역할을 맡습니다. 한 가지 도구가 무엇을 볼 수 있고 무엇을 잃는지 설명하는 것이 근거 요약의 일부입니다.

따라하기와 채점 테스트를 연결합니다

처음에는 4행을 손으로 표로 만들고 ERROR 개수와 최대값을 표시합니다. OK 2, ERROR 20, RECOVERED 7, ERROR 3이면 errors 2·max 20·recovered 1입니다. 다음으로 함수에서 같은 값을 계산하고 formatter가 정확한 키 순서를 사용하는지 확인합니다. print 안에 진단 문장을 함께 넣으면 채점 기대 출력이 달라지므로 정상 결과는 한 줄만 출력합니다. 입력 오류도 INVALID 외의 설명을 표준 출력에 섞지 않습니다.

오류를 디버깅할 때 KeyError가 나오면 허용 event를 검사하지 않고 사전에 접근했는지 봅니다. max의 빈 목록 오류는 n 0의 초기값 계약을 빠뜨린 경우입니다. ValueError는 소수·단위 문자열·잘못된 첫 줄을 정수로 읽은 경로를 확인합니다. 예외를 전부 잡아 0을 반환하면 결함과 잘못된 입력을 구별하지 못합니다. 파서에서 예상하는 ValueError만 입력 거부로 바꾸고 작성 실수의 예외는 고칩니다.

테스트에는 정상 혼합·0건·최대 허용 지연·복구만 존재·음수·잘못된 종류·행 누락·추가 행을 넣습니다. 4행 정상 하나만 통과하는 함수는 입력 계약을 완성한 것이 아닙니다. starter는 모든 정상 결과를 0으로 출력하므로 무관찰 입력은 통과하고 혼합 입력은 실패합니다. solution은 동일 테스트 전체를 통과합니다. 출력 숫자가 아니라 event 의미와 범위 검사의 이유를 말할 수 있어야 합니다.

미션의 raw.csv는 이 과제의 사건 행과 형식이 다릅니다. 시간 블록의 samples·errors·recovered는 블록 합계이고 digest·dropped는 누적 열입니다. release_check.py는 CSV 전용 집계로 samples와 errors를 더하고 마지막 dropped를 읽습니다. browser solution에 그 파일을 그대로 넣지 않습니다. 로그 형식이 다르면 같은 최대값 연산을 재사용하더라도 파서와 누적 의미를 다시 정의합니다. 요약 코드를 선택할 때 입력 형식도 증거에 포함합니다.

최종 문장은 범위·건수·단위·한계를 포함합니다. 예를 들어 사건 로그 4줄에서 관찰 오류 2회와 최대 지연 20ms, 복구 성공 기록 1회를 집계했다고 적습니다. 전체 장치의 복구율 50%라고 확장하지 않습니다. 정상 하루 재생의 recovered 0도 복구 실패 수가 아니라 복구 사건을 주입하지 않은 관찰입니다. 숫자 계산이 맞아도 설명이 틀리면 다음 담당자가 잘못된 릴리스 결정을 내릴 수 있습니다.

따라하기

손으로 센 사건을 코드와 대조하기

사건4개에서 두 오류·한 복구와 최대20ms를 구합니다. 최대값 대상은 모든 사건입니다.

events = [("OK", 2), ("ERROR", 20), ("RECOVERED", 7), ("ERROR", 3)]
errors = sum(event == "ERROR" for event, _ in events)
recovered = sum(event == "RECOVERED" for event, _ in events)
maximum = max(value for _, value in events)
print(f"errors={errors} max_latency_ms={maximum} recovered={recovered}")

실행 결과

errors=2 max_latency_ms=20 recovered=1

빈 관찰의 초기값 적용

같은 집계 규칙을 빈 목록에 적용합니다. 0은 무관찰의 표시입니다.

events = []
errors = sum(event == "ERROR" for event, _ in events)
recovered = sum(event == "RECOVERED" for event, _ in events)
maximum = max((value for _, value in events), default=0)
print(f"errors={errors} max_latency_ms={maximum} recovered={recovered}")

실행 결과

errors=0 max_latency_ms=0 recovered=0

행 경계를 보존하여 누락 검출

선언2건인데 실제1행인 입력을 거부합니다. 전체 token 수만 세지 않습니다.

text = "2\nOK 1\n"
lines = text.splitlines()
n = int(lines[0])
print("VALID" if len(lines) == n + 1 else "INVALID")

실행 결과

INVALID

완성 함수로 경계 결과 비교

아래 함수는 완성 파서입니다. 제출 전 정상·추가행·음수 조건이 서로 다른 결과인지 확인합니다.

import sys

def summarize(text):
    lines = text.splitlines()
    try:
        if not lines:
            return "INVALID"
        n = int(lines[0])
        if not 0 <= n <= 10000 or len(lines) != n + 1:
            return "INVALID"
        errors = maximum = recovered = 0
        for line in lines[1:]:
            fields = line.split()
            if len(fields) != 2 or fields[0] not in {"OK", "ERROR", "RECOVERED"}:
                return "INVALID"
            event, value = fields
            latency = int(value)
            if not 0 <= latency <= 1000000:
                return "INVALID"
            errors += event == "ERROR"
            recovered += event == "RECOVERED"
            maximum = max(maximum, latency)
        return f"errors={errors} max_latency_ms={maximum} recovered={recovered}"
    except ValueError:
        return "INVALID"

for text in ["0\n", "1\nOK 0\n\n", "1\nERROR -1\n"]:
    print(summarize(text))

실행 결과

errors=0 max_latency_ms=0 recovered=0
INVALID
INVALID

확인 문제

실습

첫 줄 n(0..10000), 이어 정확히 n줄의 event latency_ms를 읽습니다. event는 OK·ERROR·RECOVERED이고 latency_ms는 정수0..1000000입니다. 모든 사건의 최대 지연을 구하여 errors=N max_latency_ms=N recovered=N 한 줄을 출력합니다. 0건은 세 값0입니다. 잘못된 숫자·종류·범위·행 수와 빈 입력은 INVALID 한 줄로 거부합니다. 동일 ERROR 두 행은2건이며 사건ID가 없어 중복 제거하지 않습니다. int가 읽을 수 있는 +2 등 표현과 필드 사이 여러 공백은 허용합니다. 추가 빈 행은 거부합니다.

모범 답안
import sys

def summarize(text):
    lines = text.splitlines()
    try:
        if not lines:
            return "INVALID"
        n = int(lines[0])
        if not 0 <= n <= 10000 or len(lines) != n + 1:
            return "INVALID"
        errors = maximum = recovered = 0
        for line in lines[1:]:
            fields = line.split()
            if len(fields) != 2 or fields[0] not in {"OK", "ERROR", "RECOVERED"}:
                return "INVALID"
            event, value = fields
            latency = int(value)
            if not 0 <= latency <= 1000000:
                return "INVALID"
            errors += event == "ERROR"
            recovered += event == "RECOVERED"
            maximum = max(maximum, latency)
        return f"errors={errors} max_latency_ms={maximum} recovered={recovered}"
    except ValueError:
        return "INVALID"

if __name__ == "__main__":
    print(summarize(sys.stdin.read()))

더 읽기

면접 질문

  • 센서가 응답하지 않을 때 점검할 순서를 설명해 주시면 됩니다.