Devin.KR

기동 로그 집계

90분 안팎

학습 목표

표준 입력 로그에서 서비스별 오류 횟수를 집계하고 정렬된 결과를 출력합니다.

개념

집계는 다음에 읽을 원문을 고르는 도구입니다

여러 서비스의 기동 기록이 섞이면 어느 유닛에서 오류가 반복되는지 눈으로 세기 어렵습니다. 이번에는 표준 입력을 한 줄씩 읽고 서비스별 ERROR 횟수를 계산합니다. 결과는 원인을 확정하는 진단서가 아니라 자세히 볼 유닛을 선택하는 근거입니다. 한 서비스가 여러 번 실패했다면 줄 수가 늘지만 서로 다른 장애가 그만큼 있었다고 단정할 수 없습니다. 중복 제거 정책 없이 반복 시도도 각각 한 줄로 셉니다.

실습의 입력은 교육용 정규화 형식이며 journalctl의 화면 출력을 그대로 파싱하지 않습니다. 한 줄은 LEVEL SERVICE MESSAGE 순서이고 LEVEL은 INFO·WARN·ERROR 중 하나입니다. SERVICE는 영문 소문자로 시작하며 영문 소문자·숫자·하이픈이 이어지고 .service로 끝납니다. MESSAGE는 공백을 포함할 수 있지만 비어 있으면 안 됩니다. 날짜·PID·호스트는 이 과제 범위에서 제외해 집계의 핵심을 먼저 구현합니다.

줄의 계약을 먼저 정의합니다

각 줄의 앞뒤 공백은 제거하고 공백뿐인 줄은 집계하지 않습니다. 연속 공백이나 탭은 필드 구분자로 받아들입니다. split(maxsplit=2)를 쓰면 레벨과 서비스 두 필드를 나누고 나머지 메시지는 한 덩어리로 유지합니다. split을 제한 없이 한 뒤 세 조각인지 검사하면 메시지가 여러 단어일 때 정상 줄도 거부합니다. 입력 형식을 문서화하지 않으면 서로 다른 구현이 같은 로그에 다른 숫자를 반환합니다.

필드가 모자라거나 알 수 없는 LEVEL, 대문자 서비스 이름, 잘못된 접미사가 있는 비어 있지 않은 줄은 invalid를 하나 늘립니다. 대소문자를 자동 교정하지 않습니다. ERROR 대신 error라고 쓰인 줄이 많으면 생산자의 계약 위반을 발견해야 하므로 오류 줄로 세지 않고 invalid로 남깁니다. 잘못된 줄을 조용히 버리면 집계에서 오류가 줄어든 것을 실제 복구로 오해할 수 있습니다.

문자열 포함과 필드 판정은 다릅니다

INFO bootcamp-systems.service ERROR word in message는 정상 INFO 로그입니다. 문자열 전체에 ERROR가 포함됐다고 세면 정상 이벤트를 오류로 잘못 분류합니다. 레벨 필드가 정확히 ERROR일 때만 해당 서비스 카운터를 올립니다. WARN도 이번 계약에서는 오류 집계에 포함하지 않습니다. invalid는 파서가 거부한 줄의 수이며 앱 오류 횟수와 분모가 다르므로 별도의 마지막 줄로 출력합니다.

서비스 이름 검사에는 re.fullmatch를 사용합니다. 정규 표현식의 점은 임의 문자라는 뜻이므로 .service의 점은 이스케이프해 실제 마침표를 요구합니다. 시작과 끝의 전체 일치가 필요하며 search로 부분 문자열만 찾아 정상으로 받아들이지 않습니다. bootcampXservice 같은 이름과 정상 이름 뒤에 쓰레기 문자가 붙은 사례가 이 차이를 보여 줍니다. 정규식은 메시지 내용을 진단하지 않고 식별자 계약만 확인합니다.

키마다 누적 값을 둡니다

counts는 서비스 이름을 키, 오류 횟수를 정수 값으로 저장하는 딕셔너리입니다. 처음 보는 이름은 0에서 시작해 1을 더합니다. 같은 서비스의 두 번째 ERROR가 왔을 때 새 딕셔너리를 만들면 앞 결과를 잃습니다. 루프 전에 counts와 invalid를 만들고 줄 처리 중에는 필요한 값만 바꿉니다. 이 규모에서는 기본 딕셔너리의 get으로 충분하며 Counter의 다양한 활용은 더 읽기에서 확장합니다.

입력 전체를 read로 가져오는 대신 for raw in sys.stdin으로 순회합니다. 메모리에는 전체 로그가 아니라 서비스별 카운터와 현재 줄만 남습니다. 서비스 이름의 종류가 매우 많으면 그 수만큼 메모리가 늘어나는 한계도 설명합니다. 파일 인자나 외부 명령을 사용하지 않아 브라우저 채점기의 표준 입력으로 동일한 코드를 실행할 수 있습니다. print 디버깅은 정답 출력에 섞지 않습니다.

정렬 기준은 출력 계약입니다

결과는 오류 횟수 내림차순이고 횟수가 같으면 서비스 이름 오름차순입니다. sorted(counts.items(), key=lambda pair: (-pair[1], pair[0]))에서 첫 값은 음수 횟수, 둘째 값은 이름입니다. sorted의 기본 오름차순에서 큰 횟수가 먼저 오도록 부호를 바꿉니다. reverse=True로 둘을 함께 뒤집으면 이름도 내림차순이 되어 동률 계약을 어깁니다. 입력에서 먼저 본 서비스 순서에 결과를 맡기지 않습니다.

서비스 결과 한 줄은 이름과 정수 횟수를 한 칸 공백으로 나눕니다. 합계는 별도로 출력하지 않습니다. 마지막에는 invalid와 정수 개수를 출력합니다. 오류가 하나도 없어도 invalid 0은 출력하므로 빈 입력에도 한 줄이 나옵니다. INFO만 있는 서비스는 0이라는 결과 행을 만들지 않습니다. 이 규칙을 지키면 자동화 도구가 끝 줄을 확인해 거부된 입력이 있는지 판단하기 쉽습니다.

작은 사례에서 실제 값을 확인합니다

처음에는 서비스 하나의 두 오류로 누적이 되는지 확인하고 다음에는 동률 두 이름으로 정렬 기준을 확인합니다. 이어 INFO 메시지에 ERROR라는 단어를 넣어 필드 분리가 맞는지 검사합니다. 마지막으로 빈 줄과 불완전한 줄을 섞어 invalid 정책을 확인합니다. 정답 코드가 대표 로그에 맞는 것과 모든 경계 조건에 맞는 것은 다르므로 각각을 독립된 tests로 둡니다.

실습 starter는 표준 입력 루프와 TODO를 제공하며 정상 집계를 아직 만들지 않습니다. 구현 후 채점에서는 정상 누적·동률·빈 입력·잘못된 레벨·잘못된 서비스·메시지 공백·마지막 개행 없는 입력을 확인합니다. 브라우저에 출력 안내나 프롬프트 문장을 추가하면 숫자가 맞아도 계약을 어깁니다. 입력은 채점기가 전달하며 input으로 사용자 질문을 출력할 이유가 없습니다.

오류 메시지에서 구현의 빈틈을 찾습니다

ValueError: not enough values to unpack은 필드가 모자란 줄을 바로 세 변수에 대입했는지 확인할 단서입니다. 먼저 조각 수를 검사하면 invalid로 처리할 수 있습니다. KeyError는 초기 카운터가 없는 이름에 바로 접근했는지 확인합니다. 정렬된 목록에서 횟수와 이름의 자리를 뒤집으면 TypeError나 예상과 다른 결과가 날 수 있으므로 딕셔너리 items가 이름·횟수 쌍이라는 사실을 작은 예제로 확인합니다.

NameError는 sys나 re를 import했는지, 변수 이름 철자가 같은지 확인합니다. 한 테스트만 고치려 이름을 하드코딩하면 다른 유닛 입력에서 바로 깨집니다. 정렬 결과가 동률에서만 틀렸다면 누적 로직 전체를 바꾸지 않고 key의 두 번째 기준을 읽습니다. invalid가 많으면 예외를 숨기는 try 블록부터 늘리기보다 어떤 계약을 거부했는지 사례를 출력 밖에서 따로 조사합니다.

숫자를 운영 판단으로 연결합니다

실제 저널을 분석할 때는 날짜·유닛·우선순위 같은 구조 필드를 가진 내보내기 형식을 선택하고 이 교육용 입력으로 변환하는 별도 단계를 둡니다. 시간 범위가 다른 두 집계의 수치를 비교하면 차이를 잘못 해석할 수 있습니다. 같은 기간과 같은 필터를 기록한 뒤 증가한 서비스의 원문으로 돌아갑니다. 오류 횟수와 사용자 요청 실패율은 분모가 달라 같은 지표가 아닙니다.

집계 보고에는 입력 계약·기간·오류 기준·invalid 수·정렬 기준을 함께 적습니다. invalid가 0이라는 사실은 로그가 누락되지 않았음을 증명하지 않습니다. 파일 수집 자체가 실패했거나 빈 범위가 선택됐을 가능성을 별도로 확인합니다. 이 레슨의 완료 기준은 주어진 문자열에 대해 재현 가능한 결과를 내는 것입니다. 정규식·압축 로그·코드별 분석은 서재의 로그 분석 장에서 이어 읽습니다.

따라하기

세 필드로 자르기

메시지 안의 공백을 유지하고 레벨만 판정합니다.

line='INFO bootcamp-systems.service ERROR word in message'
fields=line.split(maxsplit=2)
print(fields)
print('is_error='+str(fields[0]=='ERROR'))

실행 결과

['INFO', 'bootcamp-systems.service', 'ERROR word in message']
is_error=False

누적과 동률 정렬

입력 순서와 상관없는 횟수·이름 정렬을 실제 실행합니다.

counts={'svc-b.service':2,'svc-c.service':3,'svc-a.service':2}
for name,count in sorted(counts.items(),key=lambda x:(-x[1],x[0])):print(name,count)

실행 결과

svc-c.service 3
svc-a.service 2
svc-b.service 2

빈 줄과 불완전한 줄

필드 개수 확인으로 unpack 예외를 예방합니다. 서비스 이름과 레벨의 검사는 실습에서 더합니다.

lines=['','   ','ERROR svc-a.service','ERROR svc-a.service read denied']
invalid=0
for line in lines:
    if not line.strip():continue
    fields=line.split(maxsplit=2)
    if len(fields)!=3:invalid+=1
print('invalid',invalid)

실행 결과

invalid 1

전체 계약 시험

브라우저 실습에서 아래 작은 입력을 먼저 실행합니다. 실제 실행 결과입니다.

import re
from collections import Counter
lines=['ERROR svc-b.service read failed','INFO svc-a.service ready','ERROR svc-a.service path failed','bad']
counts=Counter();invalid=0
for line in lines:
    f=line.split(maxsplit=2)
    if len(f)!=3 or f[0] not in {'INFO','WARN','ERROR'} or not re.fullmatch(r'[a-z][a-z0-9-]*\.service',f[1]):
        invalid+=1;continue
    if f[0]=='ERROR':counts[f[1]]+=1
for name,count in sorted(counts.items(),key=lambda x:(-x[1],x[0])):print(name,count)
print('invalid',invalid)

실행 결과

svc-a.service 1
svc-b.service 1
invalid 1

확인 문제

실습

표준 입력의 LEVEL SERVICE MESSAGE를 읽습니다. LEVEL은 INFO·WARN·ERROR, SERVICE는 [a-z][a-z0-9-]* 뒤에 .service, MESSAGE는 비어 있지 않은 나머지 문자열입니다. 앞뒤 공백을 제거하며 빈 줄은 무시합니다. 형식 위반 줄은 invalid를 늘리고 ERROR만 서비스별로 셉니다. 횟수 내림차순·동률 이름 오름차순으로 이름 횟수를 출력하고 마지막에 invalid 개수를 출력합니다. 오류가 없는 서비스는 출력하지 않습니다.

모범 답안
import sys,re
counts={}
invalid=0
for raw in sys.stdin:
    line=raw.strip()
    if not line:
        continue
    fields=line.split(maxsplit=2)
    if len(fields)!=3 or fields[0] not in {'INFO','WARN','ERROR'} or not re.fullmatch(r'[a-z][a-z0-9-]*\.service',fields[1]):
        invalid+=1
        continue
    level,name,message=fields
    if level=='ERROR':
        counts[name]=counts.get(name,0)+1
for name,count in sorted(counts.items(),key=lambda pair:(-pair[1],pair[0])):
    print(name,count)
print('invalid',invalid)

더 읽기

면접 질문

  • 프로세스와 systemd 서비스 상태의 차이를 설명합니다.