6단계
관측 · 알림
25 분
관측 · 알림
크롤러는 조용히 망가집니다. 사이트 구조 변경 · 차단 · 네트워크 장애. 대시보드 · 알림이 없으면 몇 주 동안 데이터 누락을 모름.
1. 수집해야 할 지표
- 성공률 — 200 / (200 + 4xx + 5xx)
- latency — p50 · p95 · p99
- 수집 행 수 — 일별 · 소스별
- 차단 지표 — 403 · 429 · CAPTCHA 비율
- 큐 lag — 대기 중 URL 수
2. 구조화 로깅
import logging
import json
logger = logging.getLogger("crawler")
def log(level: str, event: str, **fields):
logger.log(getattr(logging, level.upper()), json.dumps({
"event": event, "ts": time.time(), **fields,
}))
log("info", "fetch_ok", url=url, status=200, latency_ms=320, bytes=10_240)
log("warn", "fetch_blocked", url=url, status=429)
JSON 한 줄 로그. 나중에 jq · Loki · Elasticsearch 로 집계.
3. PostgreSQL 대시보드 테이블
CREATE TABLE crawl_events (
id BIGSERIAL PRIMARY KEY,
source VARCHAR NOT NULL,
status INT NOT NULL,
latency_ms INT,
rows_inserted INT DEFAULT 0,
error_type VARCHAR,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX ON crawl_events (source, created_at DESC);
APScheduler 가 돌 때마다 INSERT. 관리자 UI 가 최근 24 시간 집계.
4. 집계 쿼리
-- 오늘 성공률
SELECT source,
count(*) FILTER (WHERE status = 200) * 100.0 / NULLIF(count(*), 0) AS success_rate,
count(*) AS total,
avg(latency_ms) AS avg_latency
FROM crawl_events
WHERE created_at > now() - interval '24 hours'
GROUP BY source;
5. 알림 — 실패 시
if success_rate < 0.8:
await send_slack(f"⚠️ {source} 성공률 {success_rate:.1%} (24h)")
async def send_slack(text: str):
webhook = os.environ["SLACK_WEBHOOK_URL"]
async with aiohttp.ClientSession() as s:
await s.post(webhook, json={"text": text})
이메일 · 카카오 · PagerDuty 도 같은 패턴. webhook 한 줄.
6. 알림 지침 (운영 실수 방지)
- 모든 것에 알림 금지 — 알림 피로가 가장 큰 적
- repeating 억제 — 같은 알림 10분 내 재발송 X
- 심각도 구분 — INFO · WARN · CRITICAL
- 야간 · 주말 정책 — CRITICAL 만 즉시
7. 일일 요약 리포트
@scheduler.scheduled_job("cron", hour=9, minute=0, timezone="Asia/Seoul")
async def daily_summary():
stats = await fetch_yesterday_stats()
report = format_report(stats)
await send_slack(report)
매일 아침 "어제 수집: NPS 12k · DART 3k · HIRA 5k · 에러 2건" 한 줄. 장애를 미리 감지하기에 충분.
8. Prometheus · Grafana (선택)
from prometheus_client import Counter, Histogram
fetch_total = Counter("crawler_fetch_total", "requests", ["source", "status"])
fetch_latency = Histogram("crawler_fetch_latency_seconds", "latency", ["source"])
with fetch_latency.labels(source="nps").time():
resp = await session.get(url)
fetch_total.labels(source="nps", status=resp.status).inc()
/metrics endpoint 노출 → Prometheus scrape → Grafana 대시보드.
오버엔지니어링 가능성. 10+ 크롤러 운영 시부터 가치.
9. 에러 트래킹 — Sentry
import sentry_sdk
sentry_sdk.init(dsn=os.environ["SENTRY_DSN"])
try:
await crawl_job()
except Exception as e:
sentry_sdk.capture_exception(e)
raise
스택 트레이스 · 요청 컨텍스트 자동 수집. 무료 티어 5k 이벤트/월.
10. 헬스체크
@app.get("/health/crawler")
async def health():
last_success = await db.fetchval(
"SELECT MAX(created_at) FROM crawl_events WHERE status = 200"
)
age_hours = (now() - last_success).total_seconds() / 3600
if age_hours > 25:
raise HTTPException(503, "crawler stale")
return {"status": "ok", "last_success": last_success}
}
외부 uptime 모니터 (UptimeRobot · Better Uptime) 가 이 endpoint 1분 폴링 → 다운 시 알림.
11. 자주 걸리는 자리
- 알림 너무 많음 — 피로로 무시하게 됨
- 알림 없음 — 며칠 후 데이터 누락 발견
- INFO 도 페이저 — 야간 수면 방해
- 에러 로그만 보고 성공 패턴 안 봄 — 점진적 저하 놓침
하고픈 말
일일 요약 한 줄 Slack 이 운영 대시보드의 진짜 가치. 화려한 Grafana 보다 "어제 어땠어?" 한 줄이 꾸준히 읽힘.
Next
- security/06-headers-and-cors
- quality/03-observability-minimal
🎉 공공데이터 크롤러 만들기 완주를 축하해요
이어서 어떤 걸 배워 볼까요?