Quipu-Log 교과서
파트 8 · 분산·운영·확장

35 · 서버·클라이언트·MCP·관측성

이 챕터는 책의 마지막입니다. 지금까지 배운 저장 엔진, 무결성, 기밀성, 파이프라인이 실제로 서비스되는 층을 한눈에 살펴봅니다. 서버 모드, 클라이언트, MCP 감사관, 그리고 운영 가시성 — 각각 개관 위주로, 핵심 결정만 짚겠습니다.

서버 모드: HTTP/JSON 위의 단일 스토어

임베디드 모드는 감사 로그를 직접 프로세스 안에 심는 방식입니다. 서버 모드는 그 엔진을 HTTP 데몬으로 감싸서, 어떤 언어로 작성된 서비스든 bearer 토큰 하나로 감사 기록을 보내고 조회할 수 있게 합니다.

서비스 A (Go) 서비스 B (Python) 감사관/SIEM Bearer 토큰 HTTP/JSON quipu-server RBAC (deny-by-default) bounded queue + writer 파일 락 (단일 프로세스) write-only 가능 스토어 logs/ registry/<type>/ dlq/ access/ syslog / SIEM UDP 미러 (best-effort)
quipu-server는 여러 언어의 서비스가 하나의 감사 데몬에 HTTP로 연결하는 구조입니다. 스토어는 단일 프로세스가 파일 락으로 독점합니다.

서버의 핵심 설계 포인트 몇 가지만 짚겠습니다.

토큰 인증과 RBAC. 모든 엔드포인트는 Authorization: Bearer <token>을 요구합니다(healthz 제외). 토큰은 설정 파일에 SHA-256 해시로 저장하므로, 설정 파일 자체가 자격증명이 되지 않습니다. 각 토큰에 역할(role)을 부여하고, 역할에 동작(emit·query·administer)을 허용합니다. deny-by-default — 명시적으로 허용하지 않은 것은 거부됩니다. SIGHUP으로 토큰을 무중단 핫 리로드할 수 있습니다.

write-only 배포. 27장 쓰기 전용 배포에서 자세히 다뤘듯, 서버는 RSA 개인키 없이도 기동할 수 있습니다. 그러면 RSA 보호 필드는 복호 불가 암호문으로 반환됩니다 — 서버가 털려도 평문을 볼 수 없습니다.

쿼리 동시성 상한. 쿼리는 풀 스캔이므로 동시 쿼리가 CPU를 독점할 수 있습니다. auth.max_concurrent_queries로 토큰당 동시 쿼리 수를 제한하고, 한도를 넘으면 429로 즉시 거부합니다.

NDJSON 내보내기. POST /v1/logs/export는 같은 LogQuery를 받아 application/x-ndjson으로 응답합니다 — SIEM 파이프라인과 감사관 핸드오프용입니다.

MCP: LLM을 감사관으로

quipu-mcp는 Model Context Protocol(MCP) 서버입니다 — LLM 에이전트가 자연어로 감사 로그를 조회하고 검증할 수 있게 합니다. "지난 주에 alice가 어떤 문서를 삭제했나요?" 같은 질문을 에이전트가 직접 로그에서 찾을 수 있습니다.

왜 이렇게?

MCP 서버를 임베디드 스토어로 만들면, 이미 돌고 있는 quipu-server와 파일 락 충돌이 납니다 — 단일 writer 설계 때문입니다. 그래서 quipu-mcp는 임베디드가 아니라 HTTP 클라이언트입니다. 이 선택으로 RBAC, 쿼리 동시성 상한, 키 경계, 메타 감사를 전부 quipu-server에서 재사용합니다. MCP는 새로운 보안 표면을 전혀 추가하지 않습니다.

에이전트에게 노출되는 도구는 세 가지입니다.

도구하는 일필요 권한
query_logsLogQuery로 감사 로그 검색query
get_entity_history엔티티의 전체 버전 이력query
verify_store_integrity체인 무결성 검증 실행administer

도구 실패는 JSON-RPC 프로토콜 에러가 아니라 isError: true인 정상 결과로 반환됩니다. 에이전트가 에러를 텍스트로 읽고 추론을 조정할 수 있게 하려는 것입니다 — 세션이 죽지 않습니다.

그리고 중요한 속성: 에이전트의 조회도 메타 감사에 기록됩니다. 모든 tool 호출은 quipu-server의 query 경로를 타기 때문에, access_log가 켜져 있으면 "AI가 언제 무엇을 조회했는지"가 자동으로 남습니다. MCP 크레이트가 별도 처리 없이 얻는 공짜 속성입니다.

crates/quipu-mcp/src/lib.rs — 핵심 설계 결정// The agent talks MCP to this server; this server talks the
// ordinary token-authenticated HTTP API to quipu-server.
// Access is audited for free. Every tool call is an HTTP query
// against the server, so the agent's own reads land in the ledger
// through the same path — without this crate doing anything special.

관측성: 보이지 않으면 운영할 수 없다

내려가는 서버, 디스크가 차오르는 것, 이벤트가 DLQ에 쌓이는 것 — 이것들을 제때 알아야 운영할 수 있습니다. Quipu-Log는 Prometheus, healthz, syslog로 이를 지원합니다.

Prometheus: GET /metrics

메트릭은 writer 스레드와 분리된 락 프리(lock-free) atomic 스냅샷으로 제공됩니다. writer가 죽어도 /metrics는 살아 있습니다 — 가장 중요한 순간에 가장 필요한 정보입니다.

crates/quipu-middleware/src/metrics.rs — lock-free 히스토그램pub struct PipelineMetrics {
    queue_depth: AtomicI64,
    writes_ok: AtomicU64,
    writes_parked: AtomicU64,   // DLQ로 간 이벤트
    events_lost: AtomicU64,     // 스토어+DLQ 둘 다 실패
    latency_buckets: [AtomicU64; LATENCY_BUCKETS_SECS.len()], // 히스토그램
    latency_sum_micros: AtomicU64,
}

주요 메트릭과 권장 알람입니다.

메트릭타입알람 기준
quipu_writer_alivegauge== 0 이면 즉시 페이지
quipu_disk_fullgauge== 1 이면 즉시 페이지
quipu_events_lost_totalcounterrate > 0 이면 즉시 페이지
quipu_dlq_entriesgauge> 0 이 몇 분 지속되면 경고
quipu_disk_lowgauge== 1 이면 경고
quipu_write_latency_secondshistogramp99 상승 추세 모니터링

GET /v1/healthz: 세 가지 상태

healthz는 JSON {"status": "ok"/"degraded"/"unhealthy", "reasons": [...]}를 반환합니다.

  • unhealthy (HTTP 503) — writer 스레드 사망, ENOSPC 래치 세팅, 또는 스토어 루트 프로브 실패. 이벤트가 지속되지 않고 있습니다.
  • degraded (HTTP 200) — 쓰기는 작동하나 디스크 여유가 임계치 아래. 200인 것이 포인트입니다 — 로드 밸런서가 이 인스턴스를 빼면 안 됩니다. 경보는 상태 코드가 아니라 status 필드로 걸어야 합니다.
  • ok (HTTP 200) — 모든 것 정상.
crates/quipu-middleware/src/health.rs — HealthStatepub struct HealthState {
    writer_alive: AtomicBool,
    disk_full: AtomicBool,    // ENOSPC 래치 — 쓰기 성공 시 자동 해제
    low_disk: AtomicBool,     // 사전 경보 — 디스크 임계치 아래
    // 모두 atomic — writer가 죽어도 healthz는 읽을 수 있다
}

syslog / SIEM 미러

설정에 "sink": {"syslog_udp": "10.0.0.5:514"}를 추가하면, 감사된 모든 이벤트가 RFC 5424 syslog UDP로 SIEM에 미러됩니다. 미러는 best-effort입니다 — 백로그가 꽉 차면 라인을 드롭하고, 쓰기 경로를 절대 막지 않습니다. 스토어가 시스템 오브 레코드이고 SIEM은 skeleton 복사본입니다.

보안 포인트

GET /metrics는 administer 토큰을 요구합니다. 메트릭에는 큐 깊이, DLQ 상태 등 내부 동작 정보가 담겨 있어 공개하면 공격 표면이 될 수 있습니다. Prometheus 스크레이프 설정에 반드시 bearer 토큰을 포함하세요.

책을 닫으며: 파일 위에 DB를 짓는다는 것

이 책은 하나의 질문으로 시작했습니다 — "DB가 공짜로 주던 것들을, 파일 위에서 직접 만들려면 어떻게 해야 할까?" 우리는 append-only 파일로 WAL을 만들고, CRC로 torn write를 탐지하고, fsync 정책으로 내구성을 조율하고, 파일 락으로 단일 writer를 강제했습니다. 인덱스를 메모리에 직접 쌓고, 스냅샷으로 읽기-쓰기를 비차단했습니다. 여기에 머클 트리로 변조 증명을 더하고, AEAD와 블라인드 인덱스로 평문 없이 검색했습니다. 그리고 마지막으로, 이 단순한 단일 writer 설계를 — 복잡하게 바꾸지 않고 — 클라이언트 스풀과 샤딩으로 확장했습니다. 각 층에서 한 가지 원칙이 일관되게 지켜졌습니다: 단순함을 유지하고, 복잡성을 더하지 않아도 되는 곳에 더하지 않는다. 그것이 Quipu-Log가, 그리고 파일시스템 위에 짓는 모든 저장 엔진이 가르쳐주는 가장 깊은 교훈입니다.

스스로 확인

quipu-mcp가 임베디드가 아니라 HTTP 클라이언트인 이유는 무엇인가요? 단일 writer 설계와 어떻게 연결되나요?
/v1/healthz가 "degraded"일 때 HTTP 200을 반환하는 이유는 무엇인가요? 503을 반환하면 어떤 운영 문제가 생길까요?
③ 이 책 전체를 돌아보며, "DB에서 공짜로 주던 것"을 "파일로 직접 구현"한 사례를 세 가지 골라 설명해보세요.