아무도 주인이 아니던 로그 82MB — 청소가 아니라 신호 추출이었다

AI 에이전트 생태계 구축기· 19 / 19
- 1 — 완료는 실측으로만 말한다
- 2 — 같은 함정, 세 번째는 다른 얼굴로
- 3 — 테스트가 통과했다고 완료는 아니다
- 4 — 속 빈 강정 — 함수는 있는데 왜 안 불렸나
- 5 — 하루 만에 지은 나의 두 번째 뇌
- 6 — 조직보다 자산 먼저 — 두 번 실패하고 남은 3원칙
- 7 — AI의 판단력은 복리로 자라지 않는다
- 8 — 큐는 비우고, raw는 지우지 않는다
- 9 — 하나의 비서를 세 개의 뇌로 나눈 이유
- 10 — 허위 적격 0 — 애매하면 통과시키는 필터
- 11 — 텔레그램 단일 채팅에서 슬랙 3직원 사무실로 — 에이전트 비서진 이사 기록
- 12 — 슬랙 카드 UI의 물리 법칙은 문서에 없다
- 13 — 렌더러가 제목을 지어내고 있었다 — 같은 결정을 두 곳에 구현한 대가
- 14 — 장부는 누가 쓰는가
- 15 — AI 블로그 작가에게 자기 그림 그리는 손을 달아준 이야기
- 16 — 작전회의 — AI 직원이 매일 아침 나에게 일을 제안하게 만들기
- 17 — AI 직원은 왜 거짓말을 하나 — 말로 시킬 수 없는 것과 배관으로 시킬 것
- 18 — 침묵하도록 설계한 자동화가 보고까지 삼켰다
- 19 — 아무도 주인이 아니던 로그 82MB — 청소가 아니라 신호 추출이었다
82MB보다 먼저 봐야 했던 것
상주 에이전트 넷을 돌리는 맥에서 로그를 훑다가 gateway.error.log 네 개가 9일 만에 82MB가 된 것을 봤다. 애니 파일 하나만 52MB였고, 하루 증가량은 5.8MB였다. 처음에는 최근 5MB만 남기는 주간 정리를 제안했다.
그 제안은 틀린 순서였다. 74만 줄 가운데 실제로 남길 신호는 938줄, 0.13%였다. 그 안에는 웹검색이 빈손으로 돌아왔다는 기록도 있었지만 아무도 읽지 않았다. 파일이 커지는 문제보다 먼저, 고장이 나도 내가 모르는 상태를 해결해야 했다.
로그는 같은 폴더에 있어도 같은 성격이 아니었다.
| 종류 | 예시 | 상태 |
|---|---|---|
| 앱이 직접 쓰는 로그 | agent.log, errors.log | 시각이 있고 자체 로테이션이 있다 |
| launchd가 받아 적는 로그 | gateway.error.log | 시각도 크기 상한도 없다 |
후자는 plist의 StandardErrorPath에 직결된 출력이었다. 앱이 관리하는 기록이 아니라 자동시작 배관이 흘린 부산물이라, 누구도 주인으로 삼지 않고 있었다.
로그를 잘랐다는 것과 고장을 알게 됐다는 것은 다른 일이다.
즉, 파일 용량만 줄이면 세 번째 문제만 고친다. 내가 다시 세운 문제의 순서는 아래와 같았다.
- 관측 부재: 검색 실패 같은 이상이 로그에만 남고 보고되지 않는다.
- 추적 불가: 줄마다 날짜와 시각이 없어 사고의 시점을 가를 수 없다.
- 용량 증가: 파일이 계속 커진다.
비우기 전에 기준을 다시 세웠다
처음에는 “7일 지난 줄을 버리자”고 생각했다. 하지만 이 로그에는 시간 정보가 없어서 어느 줄이 7일 전인지 알 수 없었다. 그다음 후보는 크기였다. 10MB를 넘으면 최근 5MB만 남기는 방식이다. 이것은 동작하지만 오래된 신호도 같이 버린다.
결국 기준은 기간이나 크기가 아니라 줄의 종류가 됐다.
| 기준 | 실측 결과 | 판정 |
|---|---|---|
| 기간: 7일 지난 것 제거 | — | 로그에 날짜가 없어 원리상 불가능 |
| 크기: 10MB 초과 시 최근 5MB 유지 | 52MB → 5MB | 신호까지 함께 유실 |
| 종류: 등급 줄만 남기고 재접속 소음 제외 | 52MB → 124KB | 신호 100% 보존 |
내가 던진 질문은 단순했다.
“그중에 중요 신호가 있을 수도 있는데, 그거 캐치하는 방법 없을까. 또한 아까 너가 실패 에러를 수십 번씩 로그가 쌓인다고 했잖아. 그 실패 로그가 그렇게 수십 개씩 발생한다는 건 결국 개선의 여지를 나타내는 건데, 이것도 방법 없나”
이 질문 뒤로 설계의 순서가 바뀌었다.
- 신호 추출: 등급으로 시작하는 줄만 남긴다.
- 소음 분리:
Failed to connect재접속 실패는 원본 신호에서 뺀다. - 폭주 요약: 제외한 재접속은 횟수와 최대 연속 횟수로 카드에 올린다.
- 원본 비우기: 보관과 요약이 끝난 뒤에만 원본을 줄인다.
DEBUG|INFO|WARNING|ERROR|CRITICAL 로 시작하는 줄만 남긴다
→ 트레이스백·코드 조각 55만 줄 제거
그중 'Failed to connect' 를 버린다
→ 재접속 실패 6만 줄 제거여기서도 한 번 더 걸렀다. 남은 938줄 중 750줄은 Socket Mode unhealthy; reconnecting이었다. 끊김 횟수는 통계로 남기면 되므로 이 줄까지 빼자 영구 보관 대상은 188줄이 됐다.
파일을 갈아끼우지 않은 이유
로그를 줄일 때 mv로 새 파일을 만들면 안 됐다. launchd는 파일 이름이 아니라 이미 열어 둔 파일을 계속 쓴다. 새 파일로 갈아끼우는 순간, 프로세스는 이름이 사라진 옛 파일에 계속 기록하고 나는 새 파일만 바라보게 된다.
그래서 파일을 바꾸지 않고 내용만 비웠다. 가장 작은 파일로 먼저 검증한 결과는 이랬다.
| 확인 항목 | 결과 |
|---|---|
| 파일 크기 | 51MB → 5.0MB |
| 줄 수 | 741,389줄 → 72,690줄 |
| 최근 기록 | 마지막 줄 동일 |
| inode | 29531966 → 29531966 |
inode가 같아야 파일의 신분이 유지된다. 추가로 열린 파일 서술자가 append 모드이며 오프셋과 디스크 블록이 0인지 확인했다. 즉, 앞을 비우되 새 파일과 옛 파일로 갈라지지 않는지를 실물로 확인한 셈이다.
재접속 6만 회를 다시 읽는 법
재접속 실패를 전부 버리면 문제가 사라지는 것은 아니다. 개별 줄을 읽는 대신 분포를 보면, 무엇을 개선해야 하는지 다른 방식으로 드러난다.
| 재시도 횟수 | 사건 수 | 해석 |
|---|---|---|
| 1~9회 | 679건, 87% | 잠깐 끊겼다가 바로 붙은 정상 범위 |
| 10~99회 | 82건 | 관찰 대상 |
| 100~999회 | 12건 | 긴 재접속 |
| 1,000회 이상 | 11건, 1.4% | 6만 회 대부분을 만든 폭주 |
총 784건에서 재시도는 61,571회였고, 한 사건의 최대 연속 횟수는 16,551회였다. 실패할수록 간격을 늘리는 백오프가 없거나 먹지 않는다는 신호로 읽을 수 있었다. 다만 원인을 손대는 것과 사실을 포착하는 것은 별개라서, 이번에는 후자만 맡겼다.
전수 실행 뒤 원본은 79MB에서 8KB가 됐고, 신호 5,879줄은 별도 보관했다. 네 에이전트의 끊김 통계도 카드로 보냈다. 애니는 784건·최대 16,551회, 피크는 661건·최대 4,345회였고, 라이너와 베르톨트는 최대 연속 2회였다.
청소기가 아니라 관측기
이번 일에서 남은 결론은 파일 정리 기술이 아니었다. 99%가 소음인 로그는 기록이 있어도 관측되지 않는 것과 같다. 버리기 전에 신호를 따로 남기고, 버린 소음은 통계로 압축해야 한다.
또 하나는 설명의 단위다. 처음에는 “4개를 정리한다”고 말했지만, 그 4가 파일 수인지 로그 종류 수인지 섞여 있었다. 최종 대상은 에이전트마다 하나씩 있는 gateway.error.log 네 파일이었다. 숫자에는 단위를 붙이고, 계획에는 누가 무엇을 하는지도 붙여야 했다.
⇒ 로그를 줄이는 작업은 보관 정책이지만, 신호를 꺼내 보고하는 작업은 관측 시스템이다. 이번에는 전자를 하려다 후자가 먼저 필요하다는 것을 배웠다.