Skip to content

Commit f5822e1

Browse files
hyperpolymathclaude
andcommitted
feat(api): W10+W11 — Prometheus metrics + per-prover temporal trend
GET /api/v1/proof_attempts/metrics — Prometheus text format: verisim_cert_proven_total (gauge) verisim_cert_pending_total (gauge) verisim_cert_sanctified_total (gauge) verisim_cert_revoked_total (gauge — from W9 revocation view) Scrape interval-able. Current snapshot: proven=8, pending=13, sanctified=2, revoked=0. GET /api/v1/proof_attempts/trend?class=X&prover=Y&bucket_hours=N&limit=M — per-prover success-rate time series. Uses ClickHouse's toStartOfInterval() to bucket by N hours (default 24, max 168 = 1 week). Returns [{bucket_start, success_count, total_attempts, success_rate}]. Together these enable drift dashboards (trend per prover) and the PanLL StrategyDrift panel's timeline view. Verified live: safety/z3 daily → 74/75 success on 2026-04-05 bucket (98.7%) metrics scrape → 4 gauges, valid Prometheus exposition format Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 58aaec4 commit f5822e1

2 files changed

Lines changed: 149 additions & 0 deletions

File tree

rust-core/verisim-api/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,14 @@ pub fn build_router(state: AppState) -> Router {
849849
"/api/v1/proof_attempts/coverage",
850850
get(proof_attempts::coverage_handler),
851851
)
852+
.route(
853+
"/api/v1/proof_attempts/trend",
854+
get(proof_attempts::trend_handler),
855+
)
856+
.route(
857+
"/api/v1/proof_attempts/metrics",
858+
get(proof_attempts::cert_metrics_handler),
859+
)
852860
// Authentication middleware layer
853861
.layer(axum_middleware::from_fn_with_state(
854862
auth_state,

rust-core/verisim-api/src/proof_attempts.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,34 @@ pub struct CoverageResponse {
309309
pub coverage: Vec<CoverageRow>,
310310
}
311311

312+
/// Query parameters for `GET /api/v1/proof_attempts/trend`.
313+
#[derive(Debug, Deserialize)]
314+
pub struct TrendQuery {
315+
#[serde(rename = "class")]
316+
pub obligation_class: String,
317+
pub prover: String,
318+
/// Bucket size in hours (default 24, max 168 = 1 week).
319+
pub bucket_hours: Option<u32>,
320+
/// Max buckets to return (default 90, max 500).
321+
pub limit: Option<u32>,
322+
}
323+
324+
#[derive(Debug, Serialize)]
325+
pub struct TrendPoint {
326+
pub bucket_start: String,
327+
pub success_count: u64,
328+
pub total_attempts: u64,
329+
pub success_rate: f64,
330+
}
331+
332+
#[derive(Debug, Serialize)]
333+
pub struct TrendResponse {
334+
pub obligation_class: String,
335+
pub prover: String,
336+
pub bucket_hours: u32,
337+
pub points: Vec<TrendPoint>,
338+
}
339+
312340
// ---------------------------------------------------------------------------
313341
// V4: Certificate types
314342
// ---------------------------------------------------------------------------
@@ -637,6 +665,119 @@ pub async fn list_attempts_handler(
637665
}))
638666
}
639667

668+
/// `GET /api/v1/proof_attempts/trend?class=X&prover=Y&bucket_hours=24`
669+
///
670+
/// W11: per-prover success-rate trend over time (hourly or daily buckets).
671+
/// Returns a time series so UIs can chart prover drift.
672+
#[instrument]
673+
pub async fn trend_handler(
674+
Query(params): Query<TrendQuery>,
675+
) -> Result<Json<TrendResponse>, HandlerError> {
676+
require_non_empty(&params.obligation_class, "class")?;
677+
require_non_empty(&params.prover, "prover")?;
678+
let bucket_hours = params.bucket_hours.unwrap_or(24).min(168);
679+
let limit = params.limit.unwrap_or(90).min(500);
680+
681+
let sql = format!(
682+
"SELECT \
683+
toString(toStartOfInterval(started_at, INTERVAL {} HOUR)) AS bucket_start, \
684+
countIf(outcome = 'success') AS success_count, \
685+
count() AS total_attempts \
686+
FROM verisim.proof_attempts \
687+
WHERE obligation_class = '{}' AND prover_used = '{}' \
688+
GROUP BY bucket_start \
689+
ORDER BY bucket_start DESC \
690+
LIMIT {} \
691+
FORMAT JSONEachRow",
692+
bucket_hours,
693+
escape_sql_str(&params.obligation_class),
694+
escape_sql_str(&params.prover),
695+
limit
696+
);
697+
698+
let body = send_clickhouse_read(&sql).await?;
699+
let points: Vec<TrendPoint> = body
700+
.lines()
701+
.filter(|l| !l.trim().is_empty())
702+
.filter_map(|line| {
703+
let v: serde_json::Value = serde_json::from_str(line).ok()?;
704+
let n = u64_from_json(&v["total_attempts"])?;
705+
let s = u64_from_json(&v["success_count"])?;
706+
Some(TrendPoint {
707+
bucket_start: v["bucket_start"]
708+
.as_str()?
709+
.replacen(' ', "T", 1),
710+
success_count: s,
711+
total_attempts: n,
712+
success_rate: if n > 0 { (s as f64) / (n as f64) } else { 0.0 },
713+
})
714+
})
715+
.collect();
716+
717+
Ok(Json(TrendResponse {
718+
obligation_class: params.obligation_class,
719+
prover: params.prover,
720+
bucket_hours,
721+
points,
722+
}))
723+
}
724+
725+
/// `GET /api/v1/proof_attempts/metrics` — Prometheus-format cert events.
726+
///
727+
/// W10: exposes cert counters in Prometheus text format. Call from a
728+
/// Prometheus scrape target every N seconds to time-series the
729+
/// mint/revoke cycle.
730+
#[instrument]
731+
pub async fn cert_metrics_handler() -> Result<String, HandlerError> {
732+
let mut output = String::new();
733+
734+
// Count proven certs
735+
let proven_count_sql = "SELECT count() FROM verisim.mv_proven_certificates \
736+
WHERE status = 'proven' FORMAT TSV";
737+
let proven_body = send_clickhouse_read(proven_count_sql)
738+
.await
739+
.unwrap_or_default();
740+
let proven_n: u64 = proven_body.trim().parse().unwrap_or(0);
741+
742+
// Count pending certs
743+
let pending_count_sql = "SELECT count() FROM verisim.mv_proven_certificates \
744+
WHERE status = 'pending' FORMAT TSV";
745+
let pending_body = send_clickhouse_read(pending_count_sql)
746+
.await
747+
.unwrap_or_default();
748+
let pending_n: u64 = pending_body.trim().parse().unwrap_or(0);
749+
750+
// Count sanctified classes
751+
let sanctified_count_sql = "SELECT count() FROM verisim.mv_sanctify_certificates \
752+
WHERE status = 'sanctified' FORMAT TSV";
753+
let sanctified_body = send_clickhouse_read(sanctified_count_sql)
754+
.await
755+
.unwrap_or_default();
756+
let sanctified_n: u64 = sanctified_body.trim().parse().unwrap_or(0);
757+
758+
// Count revocations
759+
let revoked_count_sql = "SELECT count() FROM verisim.mv_cert_revocations FORMAT TSV";
760+
let revoked_body = send_clickhouse_read(revoked_count_sql)
761+
.await
762+
.unwrap_or_default();
763+
let revoked_n: u64 = revoked_body.trim().parse().unwrap_or(0);
764+
765+
output.push_str("# HELP verisim_cert_proven_total Total PROVEN certificates\n");
766+
output.push_str("# TYPE verisim_cert_proven_total gauge\n");
767+
output.push_str(&format!("verisim_cert_proven_total {}\n", proven_n));
768+
output.push_str("# HELP verisim_cert_pending_total Total pending certificates\n");
769+
output.push_str("# TYPE verisim_cert_pending_total gauge\n");
770+
output.push_str(&format!("verisim_cert_pending_total {}\n", pending_n));
771+
output.push_str("# HELP verisim_cert_sanctified_total Total SANCTIFIED classes\n");
772+
output.push_str("# TYPE verisim_cert_sanctified_total gauge\n");
773+
output.push_str(&format!("verisim_cert_sanctified_total {}\n", sanctified_n));
774+
output.push_str("# HELP verisim_cert_revoked_total Certs proven all-time but pending in 30-day window\n");
775+
output.push_str("# TYPE verisim_cert_revoked_total gauge\n");
776+
output.push_str(&format!("verisim_cert_revoked_total {}\n", revoked_n));
777+
778+
Ok(output)
779+
}
780+
640781
/// `GET /api/v1/proof_attempts/coverage`
641782
///
642783
/// Returns cross-repo coverage stats per obligation_class: how many distinct

0 commit comments

Comments
 (0)