codingstairs
노트에듀라이프연락
⌕검색⌘K
koen

Navigation

  • Intro
  • Blog
  • Life

연락하기

로그인 없이도 보낼 수 있어요. 답변이 필요하면 이메일을 함께 적어 주세요.

  • 익명 폼으로 의견 남기기 →
  • ✉ warragon112@gmail.com
  • 카카오톡 오픈채팅 ↗

© 2026 codingstairs

  • 노트
  • 에듀
  • 검색
  • 라이프
  • 연락
  • 약관
  • RSS
  • GitHub
에듀›공공데이터 크롤러 만들기›6단계

6단계

관측 · 알림

0회 조회

관측 · 알림

크롤러는 조용히 망가집니다. 사이트 구조 변경 · 차단 · 네트워크 장애. 대시보드 · 알림이 없으면 몇 주 동안 데이터 누락을 모름.

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

← 5단계

증분 수집 · 중복 해소

🎉 공공데이터 크롤러 만들기 완주를 축하해요

이어서 어떤 걸 배워 볼까요?

다음: 모노레포 · SSOT · 계층 분리 사고 →전체 강좌 둘러보기