지속 메트릭 로그¶
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로 설정하면 됩니다.
설정¶
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) 활성화 여부입니다. |
compaction.schedule |
"0 3 * * *" |
cron의 부분 구문만 인식하고 minute hour * * * 형식만 유효합니다. |
핫 리로드¶
다음 필드는 src/admin_config/ 경로로 핫 리로드됩니다:
snapshot_interval_secondsretention_dayscompaction.enabled와compaction.schedule
보존 기간 변경은 진행 중인 스냅샷을 중단하지 않고 prune 컷오프만 원자적으로 다시 계산합니다.
backend, path를 바꾸거나 enabled를 토글하려면 재시작이 필요합니다.
디스크 사용량¶
지속 로그는 스냅샷 틱마다 샘플 하나당 한 행을 씁니다. 히스토그램과 서머리는 한 시리즈가 여러 행(sum, count, 버킷별 또는 분위수별)으로 풀리는 반면, 카운터와 게이지는 시리즈당 한 행만 발생합니다.
공식¶
대략적인 추정식은 다음과 같습니다:
SQLite 백엔드에서 측정해보면 WAL 체크포인트 이후 bytes_per_sample은 70~120바이트 범위에 들어옵니다. 100 시리즈 × 10 스냅샷의 합성 워크로드에서 측정한 값이고, 자세한 측정 코드는 tests/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 |
알 수 없거나 새로 추가된 종류 | 향후 호환을 위한 catch-all입니다. |
오류 응답¶
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-4o"})으로 저장합니다. 그래서 (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집계 카운터를 별도로 재시작 너머까지 보존합니다. 저장 형태와 읽기 경로가 이 기능과는 다릅니다.