지속 메트릭 로그¶
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_secondsretention_dayscompaction.enabled와compaction.schedule
보존 기간 변경은 진행 중인 스냅샷을 중단하지 않고 prune 컷오프만 원자적으로 다시 계산합니다.
backend, path를 바꾸거나 enabled를 토글하려면 재시작이 필요합니다.
디스크 사용량¶
각 스냅샷 틱은 전체 레지스트리를 수집하지만, 시리즈의 행은 다음 경우에만 저장합니다.
- 값 변경: 해당 시리즈에 마지막으로 저장된 값과 다릅니다.
- 기준선: 재시작 후 첫 스냅샷은 모든 시리즈를 저장합니다.
- 1시간 주기 유지 행: 값이 바뀌지 않은 시리즈도 한 시간에 한 번 다시 저장합니다. 따라서 보존 기간 정리로 유휴 시리즈의 유일한 행이 지워지지 않고, 1시간 이상의 구간에서는 항상 값을 얻을 수 있습니다.
히스토그램과 서머리는 한 시리즈가 여러 행(sum, count, 버킷별 또는 분위수별)으로 풀리며, 각 행이 이 규칙을 따로 적용받습니다. 카운터와 게이지는 시리즈당 한 행만 발생합니다. GET /admin/metrics/history를 사용하는 쪽은 행 사이 구간에 마지막 값을 그대로 이어서 사용해야 합니다.
공식¶
메트릭이 대부분의 틱에서 움직이는 워크로드의 대략적인 추정식은 다음과 같습니다:
유휴 워크로드는 기준선 행과 실제로 발생한 변경만 저장하므로, 사용량은 이 식보다 훨씬 낮습니다. 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일 보존 환경이라면:
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집계 카운터를 별도로 재시작 너머까지 보존합니다. 저장 형태와 읽기 경로가 이 기능과는 다릅니다.