콘텐츠로 이동

지속 메트릭 로그

Continuum Router는 Prometheus 레지스트리를 로컬 저장소에 주기적으로 기록해, 재시작 후에도 최근 메트릭 이력을 잃지 않도록 할 수 있습니다. 외부 시계열 데이터베이스가 따로 필요하지 않고 라우터 안에 내장된 기능입니다.

이 문서에서는 지속 로그가 무엇이고 어떻게 설정하는지, 디스크 사용량 관계와 관리자 API를 통한 이력 질의 방법을 설명합니다.

개요

/metrics 엔드포인트가 노출하는 Prometheus 카운터와 게이지는 보통 프로세스 메모리에만 존재하기 때문에, 바이너리를 재시작하면 모두 사라집니다. 지속 메트릭 로그는 설정된 간격으로 레지스트리를 디스크에 기록하되 시리즈당 값 변경마다 한 행을 저장하고, 별도의 관리자 엔드포인트로 최근 이력을 질의할 수 있게 해주는 기능입니다.

이 기능이 아닌 것

  • Prometheus / Grafana / Thanos 같은 외부 TSDB의 대체재가 아닙니다. 이미 TSDB를 운영하고 있다면 그쪽이 더 적합합니다. 이 로그는 단일 노드 배포와 '재시작 직전에 무슨 일이 있었는가'를 들여다보는 진단 용도를 노립니다.
  • 시작 시점에 카운터·게이지 값을 라이브 레지스트리로 복원하지 않습니다. Prometheus는 카운터의 단조 증가를 가정하고 클라이언트가 리셋을 감지하기 때문입니다. 지속 로그는 GET /admin/metrics/history로만 읽히는 별도의 읽기 경로입니다.
  • PromQL이 아닙니다. 질의 인터페이스는 단순한 시간 범위 필터입니다.

기본 동작

metrics-persistence Cargo 피처가 빌드에 포함된 경우(기본 full 빌드에 포함) 기본값은 enabled: true입니다. 런타임에서 끄려면 metrics.persistence.enabled: false로 설정하면 됩니다.

로그는 설정에 metrics: 섹션이 있을 때만 기록됩니다. 섹션이 없는 설정도 기본 설정으로 /metrics를 제공하지만 로그는 기록하지 않습니다.

설정

config.yaml의 metrics: 아래 persistence 블록을 둡니다:

metrics:
  persistence:
    enabled: true
    backend: sqlite
    path: ./data/metrics.db
    snapshot_interval_seconds: 60
    retention_days: 15
    compaction:
      enabled: true
      schedule: "0 3 * * *"

필드 정의

필드 기본값 설명
enabled true false로 두면 DB를 열거나 백그라운드 태스크를 띄우지 않습니다.
backend sqlite 지원되는 백엔드는 sqlite뿐이고 redb와 duckdb는 예약된 값입니다.
path ./data/metrics.db 상위 디렉터리는 자동으로 생성됩니다.
snapshot_interval_seconds 60 통상적인 Prometheus 스크레이프 주기에 맞춥니다. 범위는 1..=86_400입니다.
retention_days 15 범위는 1..=365이고 디스크 사용량 공식은 아래를 참고하세요.
compaction.enabled true 백엔드별 컴팩션(SQLite의 VACUUM) 활성화 여부입니다. VACUUM은 별도 연결에서 백그라운드로 실행되므로, 그동안에도 스냅샷과 이력 질의가 계속 동작합니다.
compaction.schedule "0 3 * * *" cron의 부분 구문만 인식하고 minute hour * * * 형식만 유효합니다.

핫 리로드

다음 필드는 src/admin_config/ 경로로 핫 리로드됩니다:

  • snapshot_interval_seconds
  • retention_days
  • compaction.enabled와 compaction.schedule

보존 기간 변경은 진행 중인 스냅샷을 중단하지 않고 prune 컷오프만 원자적으로 다시 계산합니다.

backend, path를 바꾸거나 enabled를 토글하려면 재시작이 필요합니다.

디스크 사용량

각 스냅샷 틱은 전체 레지스트리를 수집하지만, 시리즈의 행은 다음 경우에만 저장합니다.

  • 값 변경: 해당 시리즈에 마지막으로 저장된 값과 다릅니다.
  • 기준선: 재시작 후 첫 스냅샷은 모든 시리즈를 저장합니다.
  • 1시간 주기 유지 행: 값이 바뀌지 않은 시리즈도 한 시간에 한 번 다시 저장합니다. 따라서 보존 기간 정리로 유휴 시리즈의 유일한 행이 지워지지 않고, 1시간 이상의 구간에서는 항상 값을 얻을 수 있습니다.

히스토그램과 서머리는 한 시리즈가 여러 행(sum, count, 버킷별 또는 분위수별)으로 풀리며, 각 행이 이 규칙을 따로 적용받습니다. 카운터와 게이지는 시리즈당 한 행만 발생합니다. GET /admin/metrics/history를 사용하는 쪽은 행 사이 구간에 마지막 값을 그대로 이어서 사용해야 합니다.

공식

메트릭이 대부분의 틱에서 움직이는 워크로드의 대략적인 추정식은 다음과 같습니다:

bytes ≈ series × (86_400 / snapshot_interval_seconds) × retention_days × bytes_per_sample

유휴 워크로드는 기준선 행과 실제로 발생한 변경만 저장하므로, 사용량은 이 식보다 훨씬 낮습니다. SQLite 백엔드에서 측정해보면 WAL 체크포인트 이후 bytes_per_sample은 70~120바이트 범위에 들어옵니다. 100 시리즈 × 10 스냅샷의 합성 워크로드에서 측정한 값이고 자세한 측정 코드는 tests/server/metrics_persistence_test.rs::disk_usage_smoke_check_under_synthetic_load에 있습니다. 실제 워크로드는 레이블 집합 크기에 따라 이 범위 안에서 변동합니다.

적용 예제

활성 시리즈 5,000개, 60초 스냅샷, 15일 보존 환경이라면:

5_000 × (86_400 / 60) × 15 × 100 = ~10.8 GB

15일이 과하면 retention_days를 7로 낮추면 됩니다(같은 시리즈 수에서 약 5GB 수준). 스냅샷 주기가 디스크 비용보다 덜 중요하면 snapshot_interval_seconds를 120이나 300으로 늘리는 것도 방법입니다.

관리자 엔드포인트

GET /admin/metrics/history

특정 메트릭의 지속 로그를 시간 범위로 질의합니다.

쿼리 파라미터:

  • metric (필수): 메트릭 패밀리 이름(예: http_requests_total). 히스토그램과 서머리는 패밀리마다 여러 kind 행이 반환됩니다(아래 참고).
  • from (선택): 포함 하한값으로, Unix 밀리초(정수) 또는 RFC 3339(2026-05-11T00:00:00Z) 형식입니다. 기본값은 24시간 전입니다.
  • to (선택): 미포함 상한값으로, from과 같은 인코딩입니다. 기본값은 현재 시각입니다.
  • limit (선택): 반환 행 수 상한. 기본값은 10,000이고 하드 한도는 100,000입니다.

요청 예제

# 지난 24시간의 http_requests_total
curl -s 'http://localhost:8080/admin/metrics/history?metric=http_requests_total' | jq .

# 특정 RFC 3339 범위, 최대 500행
curl -s 'http://localhost:8080/admin/metrics/history?metric=model_tokens_processed&from=2026-05-10T00:00:00Z&to=2026-05-11T00:00:00Z&limit=500' | jq .

# Unix 밀리초 범위
curl -s 'http://localhost:8080/admin/metrics/history?metric=errors_total&from=1715385600000&to=1715472000000' | jq .

응답 형식

{
  "metric": "http_requests_total",
  "from_ms": 1715385600000,
  "to_ms": 1715472000000,
  "row_count": 2,
  "limit": 10000,
  "samples": [
    {
      "ts_ms": 1715385600000,
      "labels": {"backend": "openai", "endpoint": "/v1/chat/completions"},
      "value": 42.0,
      "kind": "counter"
    }
  ]
}

샘플 종류

kind 출처 설명
counter Counter 패밀리 단조 증가하는 누적값입니다.
gauge Gauge 패밀리 순간값입니다.
histogram_sum Histogram 패밀리 모든 관측값의 합입니다.
histogram_count Histogram 패밀리 모든 관측값의 개수입니다.
histogram_bucket Histogram 패밀리 버킷별 누적 개수입니다. 상한은 labels.le에 들어갑니다.
summary_sum Summary 패밀리 모든 관측값의 합입니다.
summary_count Summary 패밀리 모든 관측값의 개수입니다.
summary_quantile Summary 패밀리 분위수별 값이고 q는 labels.quantile에 들어갑니다.
untyped 타입이 없거나 인식할 수 없는 종류 범용 표현입니다.

오류 응답

  • 400 Bad Request: metric이 누락·비어 있음·과대 크기이거나 시간 범위가 0 이하인 경우입니다.
  • 404 Not Found: 지속 기능이 비활성화된 경우입니다(metrics.persistence.enabled: false).
  • 500 Internal Server Error: 저장소 오류이고 자세한 내용은 라우터 로그에서 확인할 수 있습니다.
  • 503 Service Unavailable: metrics-persistence 피처가 빌드에 포함되지 않은 경우입니다.

저장소 레이아웃

SQLite 스키마는 다른 스토리지 엔진으로도 옮겨 쓸 수 있는 행 구조를 유지하도록 의도적으로 최소화되어 있습니다:

CREATE TABLE IF NOT EXISTS metric_samples (
    ts     INTEGER NOT NULL,   -- Unix 밀리초
    metric TEXT    NOT NULL,   -- 메트릭 패밀리 이름
    labels TEXT    NOT NULL,   -- 키 정렬된 정규화 JSON
    value  REAL    NOT NULL,   -- 샘플 값 (버킷은 누적값)
    kind   TEXT    NOT NULL    -- counter | gauge | histogram_* | summary_* | untyped
);
CREATE INDEX IF NOT EXISTS idx_metric_samples_metric_ts
    ON metric_samples (metric, ts);
CREATE INDEX IF NOT EXISTS idx_metric_samples_ts
    ON metric_samples (ts);

labels 컬럼은 레이블 집합을 키 정렬된 정규화 JSON(예: {"backend":"openai","model":"gpt-5.6-sol"})으로 저장합니다. 그래서 (metric, labels)가 임의의 조인에 쓸 수 있는 결정적 동등성 키가 됩니다.

PRAGMA journal_mode = WAL이 적용되어 있어 관리자 엔드포인트가 스냅샷 라이터와 동시에 읽을 수 있습니다.

운영 메모

  • 첫 실행 권한: 프로세스가 path의 상위 디렉터리를 생성할 수 있어야 합니다. 그렇지 못하면 라우터는 에러 로그를 남기고 이력 없이 /metrics 제공만 이어 갑니다.
  • 백업: PRAGMA journal_mode = WAL 상태에서는 .db, .db-wal, .db-shm 세 파일을 함께 복사하거나 sqlite3 metrics.db .backup을 돌려야 일관된 스냅샷이 됩니다.
  • 상태 확인: sqlite3 ./data/metrics.db 'select count(*) from metric_samples;'은 라이브 라우터에 대고 돌려도 안전합니다.
  • 런타임 비활성화: metrics.persistence.enabled: false로 핫 리로드해도 스냅샷 태스크가 즉시 정리되지는 않으니, 파일 핸들까지 풀어야 한다면 재시작과 함께 적용하세요.

관련 문서

  • 메트릭 및 모니터링: 라이브 /metrics 엔드포인트와 Grafana/Prometheus 통합을 다루고 이 지속 레이어 덕분에 장기 분석이 가능해지는 API 키별 토큰 사용량 메트릭도 여기에서 설명합니다.
  • 관리자 통계 스냅샷(관리자 API의 admin.stats.persistence)은 /admin/stats 집계 카운터를 별도로 재시작 너머까지 보존합니다. 저장 형태와 읽기 경로가 이 기능과는 다릅니다.