From 87157dad4069bf3fdc04072ab330d99fa79a8c3a Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Thu, 7 May 2026 11:16:39 +0200 Subject: [PATCH 01/36] feat(cloudflare): add telemetry collection and /metrics endpoint Add Prometheus-compatible telemetry to the Cloudflare resolver, matching the same metric names as the WASM providers so they can share dashboards. - Collect per-flag resolve latency and reason in the fetch handler, deferred to ctx.wait_until to keep the hot path clean - Include telemetry deltas in WriteFlagLogsRequest via checkpoint() - Queue consumer accumulates cross-isolate deltas into a cumulative TelemetrySnapshot persisted in KV - Serve /metrics endpoint reading Prometheus text from KV - Add serde derives to TelemetrySnapshot and accumulate_delta() method for reconstructing flat histograms from compressed BucketSpans - Deployer auto-creates KV namespace (same pattern as queue creation) Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 2 + confidence-cloudflare-resolver/Cargo.toml | 2 + .../deployer/script.sh | 49 +++++++ confidence-cloudflare-resolver/src/lib.rs | 127 ++++++++++++++++-- confidence-cloudflare-resolver/wrangler.toml | 3 + confidence-resolver/src/telemetry.rs | 37 +++++ 6 files changed, 211 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79f2c9b1..494d140d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -258,10 +258,12 @@ dependencies = [ name = "confidence-cloudflare-resolver" version = "0.9.0" dependencies = [ + "arc-swap", "base64 0.22.1", "bytes", "confidence_resolver", "getrandom 0.3.3", + "js-sys", "once_cell", "prost 0.13.5", "serde", diff --git a/confidence-cloudflare-resolver/Cargo.toml b/confidence-cloudflare-resolver/Cargo.toml index 7a55790a..67a7915d 100644 --- a/confidence-cloudflare-resolver/Cargo.toml +++ b/confidence-cloudflare-resolver/Cargo.toml @@ -28,5 +28,7 @@ worker = { version= "0.6.1", features=['queue'] } base64 = "0.22.1" once_cell = "1.19" prost = "0.13" +arc-swap = "1" +js-sys = "0.3" serde = { version = "1.0.219" } serde_json = "1.0.85" \ No newline at end of file diff --git a/confidence-cloudflare-resolver/deployer/script.sh b/confidence-cloudflare-resolver/deployer/script.sh index 42d18ea8..5e75caee 100755 --- a/confidence-cloudflare-resolver/deployer/script.sh +++ b/confidence-cloudflare-resolver/deployer/script.sh @@ -366,6 +366,55 @@ else echo "⚠ïļ Could not check queue status (HTTP $QUEUE_STATUS)" fi +# Create KV namespace for /metrics endpoint if it doesn't exist +if [ -n "$WORKER_NAME_PREFIX" ]; then + KV_NAMESPACE_TITLE="${WORKER_NAME_PREFIX}-resolver-metrics" +else + KV_NAMESPACE_TITLE="resolver-metrics" +fi + +echo "🔍 Checking if KV namespace '$KV_NAMESPACE_TITLE' exists..." +KV_LIST=$(curl -sS -w "%{http_code}" \ + -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ + "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/storage/kv/namespaces?per_page=100") +KV_LIST_STATUS="${KV_LIST: -3}" +KV_LIST_BODY="${KV_LIST%???}" + +KV_NAMESPACE_ID="" +if [ "$KV_LIST_STATUS" = "200" ]; then + KV_NAMESPACE_ID=$(printf "%s" "$KV_LIST_BODY" | jq -r ".result[] | select(.title == \"${KV_NAMESPACE_TITLE}\") | .id" 2>/dev/null || true) +fi + +if [ -z "$KV_NAMESPACE_ID" ]; then + echo "ðŸ“Ķ KV namespace '$KV_NAMESPACE_TITLE' not found, creating..." + KV_CREATE_RESP=$(curl -sS -w "%{http_code}" -X POST \ + -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"title\": \"${KV_NAMESPACE_TITLE}\"}" \ + "https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/storage/kv/namespaces") + KV_CREATE_STATUS="${KV_CREATE_RESP: -3}" + KV_CREATE_BODY="${KV_CREATE_RESP%???}" + if [ "$KV_CREATE_STATUS" = "200" ] || [ "$KV_CREATE_STATUS" = "201" ]; then + KV_NAMESPACE_ID=$(printf "%s" "$KV_CREATE_BODY" | jq -r '.result.id') + echo "✅ KV namespace '$KV_NAMESPACE_TITLE' created (id: $KV_NAMESPACE_ID)" + else + echo "⚠ïļ Failed to create KV namespace (HTTP $KV_CREATE_STATUS), /metrics will be unavailable" + fi +else + echo "✅ KV namespace '$KV_NAMESPACE_TITLE' already exists (id: $KV_NAMESPACE_ID)" +fi + +# Append KV binding to wrangler.toml if namespace was created +if [ -n "$KV_NAMESPACE_ID" ]; then + cat >> wrangler.toml <> = LazyLock::new(ResolveLogger::new); static ASSIGN_LOGGER: LazyLock = LazyLock::new(AssignLogger::new); +static TELEMETRY: LazyLock = LazyLock::new(Telemetry::new); +static LAST_FLUSHED: LazyLock> = + LazyLock::new(|| ArcSwap::from_pointee(TelemetrySnapshot::default())); use confidence_resolver::Client; use once_cell::sync::Lazy; +use std::cell::RefCell; + +/// Per-request resolve metrics captured in the hot path, recorded in wait_until. +struct ResolveMetrics { + elapsed_us: u32, + reasons: Vec, +} + +thread_local! { + static PENDING_METRICS: RefCell> = const { RefCell::new(Vec::new()) }; +} /// SetResolverStateRequest message from the CDN. /// This matches the protobuf message format returned by the CDN. @@ -141,6 +157,18 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { let router = Router::new(); let response = router + .get_async("/metrics", |_req, ctx| { + async move { + let text = match ctx.env.kv("METRICS_KV") { + Ok(kv) => kv.get("prometheus").text().await.unwrap_or(None), + Err(_) => None, + }; + let body = text.unwrap_or_default(); + let headers = Headers::new(); + headers.set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")?; + Ok(Response::ok(body)?.with_headers(headers)) + } + }) // GET endpoint to expose the current deployment state etag and resolver version .get_async("/v1/state:etag", |_req, _ctx| { let allowed_origin = allowed_origin_env.clone(); @@ -180,6 +208,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .evaluation_context .clone() .unwrap_or_default(); + let start = js_sys::Date::now(); match state.get_resolver::( &resolver_request.client_secret, evaluation_context, @@ -192,23 +221,56 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { ); match resolver.resolve_flags(process_request) { Ok(process_response) => { + let elapsed_us = ((js_sys::Date::now() - start) * 1000.0) as u32; match process_response.into_resolved() { Some((response, _writes)) => { + let reasons: Vec = response + .resolved_flags + .iter() + .map(|f| f.reason()) + .collect(); + PENDING_METRICS.with(|m| { + m.borrow_mut().push(ResolveMetrics { elapsed_us, reasons }); + }); Response::from_json(&response)? .with_cors_headers(&allowed_origin) } - None => Response::error( - "Unexpected suspended response", - 500, - )? - .with_cors_headers(&allowed_origin), + None => { + PENDING_METRICS.with(|m| { + m.borrow_mut().push(ResolveMetrics { + elapsed_us, + reasons: vec![ResolveReason::Error], + }); + }); + Response::error( + "Unexpected suspended response", + 500, + )? + .with_cors_headers(&allowed_origin) + } } } - Err(msg) => Response::error(msg, 500)? - .with_cors_headers(&allowed_origin), + Err(msg) => { + let elapsed_us = ((js_sys::Date::now() - start) * 1000.0) as u32; + PENDING_METRICS.with(|m| { + m.borrow_mut().push(ResolveMetrics { + elapsed_us, + reasons: vec![ResolveReason::Error], + }); + }); + Response::error(msg, 500)? + .with_cors_headers(&allowed_origin) + } } } Err(msg) => { + let elapsed_us = ((js_sys::Date::now() - start) * 1000.0) as u32; + PENDING_METRICS.with(|m| { + m.borrow_mut().push(ResolveMetrics { + elapsed_us, + reasons: vec![ResolveReason::Error], + }); + }); Response::error(msg, 500)?.with_cors_headers(&allowed_origin) } } @@ -249,8 +311,18 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .run(req, env) .await; - // Use ctx.waitUntil to run logging after response is returned + // Use ctx.waitUntil to run logging and telemetry after response is returned ctx.wait_until(async move { + // Record pending resolve metrics into the telemetry counters + PENDING_METRICS.with(|m| { + for metrics in m.borrow_mut().drain(..) { + TELEMETRY.record_latency_us(metrics.elapsed_us); + for reason in metrics.reasons { + TELEMETRY.mark_resolve(reason); + } + } + }); + let aggregated: confidence_resolver::proto::confidence::flags::resolver::v1::WriteFlagLogsRequest = checkpoint(); if let Ok(converted) = serde_json::to_string(&aggregated) { @@ -283,6 +355,12 @@ pub async fn consume_flag_logs_queue( client_resolve_info: v.client_resolve_info, }) .collect(); + + // Accumulate telemetry deltas into KV-backed cumulative snapshot for /metrics + if let Ok(kv) = env.kv("METRICS_KV") { + let _ = update_prometheus_kv(&kv, &logs).await; + } + let req = flag_logger::aggregate_batch(logs); send_flags_logs(CONFIDENCE_CLIENT_SECRET.get().unwrap().as_str(), req).await?; } @@ -292,10 +370,41 @@ pub async fn consume_flag_logs_queue( fn checkpoint() -> WriteFlagLogsRequest { let mut req = RESOLVE_LOGGER.checkpoint(); + req.telemetry_data = Some(TELEMETRY.delta_snapshot(&LAST_FLUSHED)); ASSIGN_LOGGER.checkpoint_fill(&mut req); req } +/// Accumulate telemetry deltas from all isolates into a cumulative +/// `TelemetrySnapshot` stored in KV, then write its Prometheus text +/// representation for the /metrics endpoint. +async fn update_prometheus_kv(kv: &kv::KvStore, logs: &[WriteFlagLogsRequest]) { + let mut cumulative = match kv.get("snapshot").text().await { + Ok(Some(text)) => serde_json::from_str::(&text).unwrap_or_default(), + _ => TelemetrySnapshot::default(), + }; + + for log in logs { + if let Some(td) = &log.telemetry_data { + cumulative.accumulate_delta(td); + } + } + + let prom_text = cumulative.to_prometheus( + "cf-resolver", + &confidence_resolver::telemetry::PrometheusConfig::default(), + ); + + let _ = kv + .put("snapshot", serde_json::to_string(&cumulative).unwrap_or_default()) + .map(|b| b.execute()) + .ok(); + let _ = kv + .put("prometheus", prom_text) + .map(|b| b.execute()) + .ok(); +} + async fn send_flags_logs(client_secret: &str, message: WriteFlagLogsRequest) -> Result { let resolve_url = "https://resolver.confidence.dev/v1/clientFlagLogs:write"; let mut init = RequestInit::new(); diff --git a/confidence-cloudflare-resolver/wrangler.toml b/confidence-cloudflare-resolver/wrangler.toml index 47660cce..30bd64ac 100644 --- a/confidence-cloudflare-resolver/wrangler.toml +++ b/confidence-cloudflare-resolver/wrangler.toml @@ -14,5 +14,8 @@ max_batch_timeout = 10 # seconds queue = "flag-logs-queue" binding = "flag_logs_queue" +# KV namespace for /metrics endpoint is created and injected by the deployer. +# See deployer/script.sh for auto-creation of the METRICS_KV binding. + [vars] CONFIDENCE_CLIENT_SECRET = "SECRET" diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index 4d9e658c..6db00599 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -106,6 +106,7 @@ impl Histogram { /// Used for delta computation between flushes and as the future intermediate /// representation for Prometheus text format serialization. #[derive(Clone, Default)] +#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))] pub struct TelemetrySnapshot { pub latency: HistogramSnapshot, pub resolve_rates: Vec, @@ -113,6 +114,7 @@ pub struct TelemetrySnapshot { } #[derive(Clone, Default)] +#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))] pub struct HistogramSnapshot { pub sum: u64, pub count: u64, @@ -142,6 +144,41 @@ impl Default for PrometheusConfig { } impl TelemetrySnapshot { + /// Accumulate a `TelemetryData` delta into this cumulative snapshot. + /// + /// Expands compressed `BucketSpan`s back into the flat bucket array and + /// adds all counters. Gauge fields (memory_bytes) are replaced with the + /// latest value. + pub fn accumulate_delta(&mut self, td: &pb::TelemetryData) { + if let Some(latency) = &td.resolve_latency { + self.latency.sum = self.latency.sum.wrapping_add(latency.sum as u64); + self.latency.count = self.latency.count.wrapping_add(latency.count as u64); + + // Expand BucketSpans into flat bucket array + for span in &latency.buckets { + for (i, &count) in span.counts.iter().enumerate() { + let idx = span.offset as usize + i; + if idx >= self.latency.buckets.len() { + self.latency.buckets.resize(idx + 1, 0); + } + self.latency.buckets[idx] = self.latency.buckets[idx].wrapping_add(count as u64); + } + } + } + + for rate in &td.resolve_rate { + let idx = rate.reason as usize; + if idx >= self.resolve_rates.len() { + self.resolve_rates.resize(idx + 1, 0); + } + self.resolve_rates[idx] = self.resolve_rates[idx].wrapping_add(rate.count as u64); + } + + if td.memory_bytes > 0 { + self.memory_bytes = td.memory_bytes; + } + } + /// Format the snapshot as Prometheus exposition text. /// /// All values are cumulative counters, matching what Prometheus expects. From 30050f25b08f897827971e91b35ad5a5a43d61d0 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Thu, 7 May 2026 11:19:46 +0200 Subject: [PATCH 02/36] fix: await KV puts, guard malformed BucketSpan offsets, add tests - Await KV put operations in update_prometheus_kv (were fire-and-forget) - Guard against negative/oversized BucketSpan offsets in accumulate_delta - Add race condition comment on KV read-modify-write - Add CORS headers to /metrics endpoint for consistency - Add unit tests for accumulate_delta: basic, negative offset, oversized Co-Authored-By: Claude Opus 4.6 --- confidence-cloudflare-resolver/src/lib.rs | 20 +++-- confidence-resolver/src/telemetry.rs | 102 +++++++++++++++++++++- 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 6b9cce85..6c5d7103 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -158,6 +158,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { let response = router .get_async("/metrics", |_req, ctx| { + let allowed_origin = allowed_origin_env.clone(); async move { let text = match ctx.env.kv("METRICS_KV") { Ok(kv) => kv.get("prometheus").text().await.unwrap_or(None), @@ -166,7 +167,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { let body = text.unwrap_or_default(); let headers = Headers::new(); headers.set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")?; - Ok(Response::ok(body)?.with_headers(headers)) + Response::ok(body)?.with_headers(headers).with_cors_headers(&allowed_origin) } }) // GET endpoint to expose the current deployment state etag and resolver version @@ -378,6 +379,9 @@ fn checkpoint() -> WriteFlagLogsRequest { /// Accumulate telemetry deltas from all isolates into a cumulative /// `TelemetrySnapshot` stored in KV, then write its Prometheus text /// representation for the /metrics endpoint. +/// +/// Note: concurrent queue consumer invocations can race on KV read-modify-write. +/// Acceptable for metrics — at worst one batch's deltas are lost, not cumulative state. async fn update_prometheus_kv(kv: &kv::KvStore, logs: &[WriteFlagLogsRequest]) { let mut cumulative = match kv.get("snapshot").text().await { Ok(Some(text)) => serde_json::from_str::(&text).unwrap_or_default(), @@ -395,14 +399,12 @@ async fn update_prometheus_kv(kv: &kv::KvStore, logs: &[WriteFlagLogsRequest]) { &confidence_resolver::telemetry::PrometheusConfig::default(), ); - let _ = kv - .put("snapshot", serde_json::to_string(&cumulative).unwrap_or_default()) - .map(|b| b.execute()) - .ok(); - let _ = kv - .put("prometheus", prom_text) - .map(|b| b.execute()) - .ok(); + if let Ok(builder) = kv.put("snapshot", serde_json::to_string(&cumulative).unwrap_or_default()) { + let _ = builder.execute().await; + } + if let Ok(builder) = kv.put("prometheus", prom_text) { + let _ = builder.execute().await; + } } async fn send_flags_logs(client_secret: &str, message: WriteFlagLogsRequest) -> Result { diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index 6db00599..93d8e9fe 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -156,8 +156,15 @@ impl TelemetrySnapshot { // Expand BucketSpans into flat bucket array for span in &latency.buckets { + let base = match usize::try_from(span.offset) { + Ok(b) if b < BUCKET_COUNT => b, + _ => continue, // skip malformed span + }; for (i, &count) in span.counts.iter().enumerate() { - let idx = span.offset as usize + i; + let idx = base.saturating_add(i); + if idx >= BUCKET_COUNT { + break; + } if idx >= self.latency.buckets.len() { self.latency.buckets.resize(idx + 1, 0); } @@ -976,4 +983,97 @@ mod tests { ); assert_eq!(default_out, zero_out); } + + #[test] + fn accumulate_delta_basic() { + let mut snap = TelemetrySnapshot::default(); + let td = pb::TelemetryData { + resolve_latency: Some(pb::ResolveLatency { + sum: 500, + count: 2, + buckets: vec![pb::BucketSpan { + offset: 5, + counts: vec![1, 1], + }], + ln_ratio: LN_RATIO, + }), + resolve_rate: vec![pb::ResolveRate { + reason: ResolveReason::Match as i32, + count: 3, + }], + memory_bytes: 4096, + ..Default::default() + }; + + snap.accumulate_delta(&td); + assert_eq!(snap.latency.sum, 500); + assert_eq!(snap.latency.count, 2); + assert_eq!(snap.latency.buckets[5], 1); + assert_eq!(snap.latency.buckets[6], 1); + assert_eq!(snap.resolve_rates[ResolveReason::Match as usize], 3); + assert_eq!(snap.memory_bytes, 4096); + + // Second delta accumulates counters, replaces gauge + snap.accumulate_delta(&td); + assert_eq!(snap.latency.sum, 1000); + assert_eq!(snap.latency.count, 4); + assert_eq!(snap.latency.buckets[5], 2); + assert_eq!(snap.resolve_rates[ResolveReason::Match as usize], 6); + assert_eq!(snap.memory_bytes, 4096); + } + + #[test] + fn accumulate_delta_negative_offset_skipped() { + let mut snap = TelemetrySnapshot::default(); + let td = pb::TelemetryData { + resolve_latency: Some(pb::ResolveLatency { + sum: 100, + count: 1, + buckets: vec![ + pb::BucketSpan { + offset: -1, + counts: vec![99], + }, + pb::BucketSpan { + offset: 3, + counts: vec![1], + }, + ], + ln_ratio: LN_RATIO, + }), + ..Default::default() + }; + + snap.accumulate_delta(&td); + // Negative offset span skipped, valid span applied + assert_eq!(snap.latency.count, 1); + assert_eq!(snap.latency.buckets.get(3).copied().unwrap_or(0), 1); + // Bucket from negative offset should not exist + let total: u64 = snap.latency.buckets.iter().sum(); + assert_eq!(total, 1); + } + + #[test] + fn accumulate_delta_oversized_offset_skipped() { + let mut snap = TelemetrySnapshot::default(); + let td = pb::TelemetryData { + resolve_latency: Some(pb::ResolveLatency { + sum: 100, + count: 1, + buckets: vec![pb::BucketSpan { + offset: BUCKET_COUNT as i32 + 10, + counts: vec![1], + }], + ln_ratio: LN_RATIO, + }), + ..Default::default() + }; + + snap.accumulate_delta(&td); + // Oversized offset span skipped, sum/count still accumulated + assert_eq!(snap.latency.count, 1); + assert_eq!(snap.latency.sum, 100); + let total: u64 = snap.latency.buckets.iter().sum(); + assert_eq!(total, 0); + } } From d71af3f384c2d134ba450ea57772c00135c6f198 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Thu, 7 May 2026 11:36:57 +0200 Subject: [PATCH 03/36] chore: sync WASM module for Go provider Co-Authored-By: Claude Opus 4.6 --- .../assets/confidence_resolver.wasm | Bin 483040 -> 483141 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm b/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm index 5a5ad27b55f1e8fb4adf2eace1a5589e096c5a7d..e09bdb0d2a474e83aa4722c29ec502c78c5c502b 100755 GIT binary patch delta 83628 zcmd?S349dA_AfrCW(}Dn9mu}UBmu$_2>T9c1O!=>D{@g30XGb&An0{L680^u4HU?( zh@k9?L1hU$A}T0X0a+CV5fBxxpostPsqUFf2;Td<@BiNC^FE(Pw0pX`>QvRKQ)jDF z)u*?l?%tX@wOiz{VFd*=thb{8|EOSE->%c%t82Fx44Za-OPn{t?QuKZ5l#>89;ef3 zx4S$M#%5|rUgKwafFg`8qEgU;g`6pNq6qx)Kd0U0c9X}I>aw|GG>7JlNkf9&X&31gqHNqRyUVGe zHh{JMb9sFHJ1UABhZfjf_~&%G{Cr3CPLIv)%5a7|qry_%ZlQB?>~?pA1Nr$64`_=W z{c_=dQ~&^OdqfIA*izy%`HhoY9s)Fe?{*to>@}j(&^wRK>9)HacFh5-8`teE8b-LB zHfID7A*F|C~~FK1!ILPgT6ODai!2D<08_3FrwX6 z=(^F^U73C}dbv}@O}p`myHUa4$58(Bqv;*`g3gGYt{-TJd#8Jsd$)U!d$0Q@EwQh$ zueQ(jyz2Q@{EG_hH)w}*rG1g>YxgntY4-{D4g2ekRgP

yF&IPV*u7dsU1McJQQ|_CY^{n%( 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 MXEY{ws`)|?k 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 JW$ScJ{|{z5_+J13 From d149fe5450e7b290220f123bfba734b4f9fb6942 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Thu, 7 May 2026 11:44:07 +0200 Subject: [PATCH 04/36] fix: satisfy strict clippy lints in accumulate_delta Allow indexing_slicing and arithmetic_side_effects on the method since bounds are checked before every index. Use saturating_add for resize. Re-sync Go WASM module. Co-Authored-By: Claude Opus 4.6 --- confidence-resolver/src/telemetry.rs | 7 +++++-- .../assets/confidence_resolver.wasm | Bin 483141 -> 483141 bytes 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index 93d8e9fe..756fcd80 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -149,6 +149,7 @@ impl TelemetrySnapshot { /// Expands compressed `BucketSpan`s back into the flat bucket array and /// adds all counters. Gauge fields (memory_bytes) are replaced with the /// latest value. + #[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)] pub fn accumulate_delta(&mut self, td: &pb::TelemetryData) { if let Some(latency) = &td.resolve_latency { self.latency.sum = self.latency.sum.wrapping_add(latency.sum as u64); @@ -166,8 +167,9 @@ impl TelemetrySnapshot { break; } if idx >= self.latency.buckets.len() { - self.latency.buckets.resize(idx + 1, 0); + self.latency.buckets.resize(idx.saturating_add(1), 0); } + // Safety: idx < BUCKET_COUNT and we just resized to at least idx+1 self.latency.buckets[idx] = self.latency.buckets[idx].wrapping_add(count as u64); } } @@ -176,8 +178,9 @@ impl TelemetrySnapshot { for rate in &td.resolve_rate { let idx = rate.reason as usize; if idx >= self.resolve_rates.len() { - self.resolve_rates.resize(idx + 1, 0); + self.resolve_rates.resize(idx.saturating_add(1), 0); } + // Safety: we just resized to at least idx+1 self.resolve_rates[idx] = self.resolve_rates[idx].wrapping_add(rate.count as u64); } diff --git a/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm b/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm index e09bdb0d2a474e83aa4722c29ec502c78c5c502b..68f5d3ea48f3cc653f77d6c451966ea7af86d37e 100755 GIT binary patch delta 33 pcmX@QPxk0O*@hOz7N!>F7M2#)Eo^fn85`T@O0sRAE6KjX6#(263;6&5 delta 33 pcmX@QPxk0O*@hOz7N!>F7M2#)Eo^fn8SC2TO0sRAE6KjX6#(1w3-$m2 From 58cdfc9e1780dfff33c0f34fddcecb6330562cc4 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Thu, 7 May 2026 13:30:11 +0200 Subject: [PATCH 05/36] style: fix rustfmt line length in accumulate_delta Co-Authored-By: Claude Opus 4.6 --- confidence-resolver/src/telemetry.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index 756fcd80..b74e9e85 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -170,7 +170,8 @@ impl TelemetrySnapshot { self.latency.buckets.resize(idx.saturating_add(1), 0); } // Safety: idx < BUCKET_COUNT and we just resized to at least idx+1 - self.latency.buckets[idx] = self.latency.buckets[idx].wrapping_add(count as u64); + self.latency.buckets[idx] = + self.latency.buckets[idx].wrapping_add(count as u64); } } } From d241fa7736c1a9bb8811af32270946ecc7b80421 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Thu, 7 May 2026 13:37:02 +0200 Subject: [PATCH 06/36] chore: re-sync WASM module for Go provider after fmt fix Co-Authored-By: Claude Opus 4.6 --- .../assets/confidence_resolver.wasm | Bin 483141 -> 483141 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm b/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm index 68f5d3ea48f3cc653f77d6c451966ea7af86d37e..02ed19fd941693fc9eb710dfbf74c8075e78d918 100755 GIT binary patch delta 33 pcmX@QPxk0O*@hOz7N!>F7M2#)Eo^fn8JpVYO0sRAE6KjX6#(2H3;F;6 delta 33 pcmX@QPxk0O*@hOz7N!>F7M2#)Eo^fn85`T@O0sRAE6KjX6#(263;6&5 From 6d5f02bbacacc84e655bf6ab34459fd68f3c2e84 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Thu, 7 May 2026 13:54:23 +0200 Subject: [PATCH 07/36] fix: aggregate telemetry deltas in flag_logger::aggregate_batch Previously aggregate_batch discarded all telemetry data except the SDK field from the first message. Now it merges latency histograms, resolve rate counters, and gauge fields across all messages in the batch, so the Confidence backend receives aggregated telemetry matching what the WASM providers send. Co-Authored-By: Claude Opus 4.6 --- confidence-resolver/src/flag_logger.rs | 68 ++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/confidence-resolver/src/flag_logger.rs b/confidence-resolver/src/flag_logger.rs index 307653f9..04acfd5a 100644 --- a/confidence-resolver/src/flag_logger.rs +++ b/confidence-resolver/src/flag_logger.rs @@ -4,6 +4,7 @@ use crate::proto::confidence::flags::admin::v1::flag_resolve_info::{ }; use crate::proto::confidence::flags::admin::v1::{ClientResolveInfo, FlagResolveInfo}; use crate::proto::confidence::flags::resolver::v1::events::FlagAssigned; +use crate::proto::confidence::flags::resolver::v1::telemetry_data::ResolveRate; use crate::proto::confidence::flags::resolver::v1::{TelemetryData, WriteFlagLogsRequest}; use std::collections::{HashMap, HashSet}; @@ -14,12 +15,14 @@ pub fn aggregate_batch(message_batch: Vec) -> WriteFlagLog let mut flag_resolve_map: HashMap = HashMap::new(); let mut flag_assigned: Vec = vec![]; let mut first_sdk: Option = None; + let mut agg_telemetry: Option = None; for flag_logs_message in message_batch { if let Some(td) = &flag_logs_message.telemetry_data { if first_sdk.is_none() && td.sdk.is_some() { first_sdk = td.sdk.clone(); } + agg_telemetry = Some(merge_telemetry(agg_telemetry.take(), td)); } for c in &flag_logs_message.client_resolve_info { @@ -98,10 +101,20 @@ pub fn aggregate_batch(message_batch: Vec) -> WriteFlagLog }) } - let telemetry_data = first_sdk.map(|sdk| TelemetryData { - sdk: Some(sdk), - ..Default::default() - }); + // Attach SDK info to the aggregated telemetry + let telemetry_data = match (agg_telemetry, first_sdk) { + (Some(mut td), sdk) => { + if td.sdk.is_none() { + td.sdk = sdk; + } + Some(td) + } + (None, Some(sdk)) => Some(TelemetryData { + sdk: Some(sdk), + ..Default::default() + }), + (None, None) => None, + }; WriteFlagLogsRequest { telemetry_data, @@ -111,6 +124,53 @@ pub fn aggregate_batch(message_batch: Vec) -> WriteFlagLog } } +/// Merge a telemetry delta into an accumulator. +/// Both are deltas, so counters are summed and gauges take the latest non-zero value. +fn merge_telemetry(acc: Option, delta: &TelemetryData) -> TelemetryData { + let mut acc = acc.unwrap_or_default(); + + // Merge resolve latency + match (&mut acc.resolve_latency, &delta.resolve_latency) { + (Some(a), Some(d)) => { + a.sum = a.sum.wrapping_add(d.sum); + a.count = a.count.wrapping_add(d.count); + a.buckets.extend(d.buckets.iter().cloned()); + if a.ln_ratio == 0.0 { + a.ln_ratio = d.ln_ratio; + } + } + (None, Some(d)) => { + acc.resolve_latency = Some(d.clone()); + } + _ => {} + } + + // Merge resolve rates by reason + for dr in &delta.resolve_rate { + if let Some(ar) = acc.resolve_rate.iter_mut().find(|r| r.reason == dr.reason) { + ar.count = ar.count.wrapping_add(dr.count); + } else { + acc.resolve_rate.push(ResolveRate { + count: dr.count, + reason: dr.reason, + }); + } + } + + // Gauges: take latest non-zero + if let Some(sa) = &delta.state_age { + acc.state_age = Some(sa.clone()); + } + if delta.memory_bytes > 0 { + acc.memory_bytes = delta.memory_bytes; + } + if !delta.resolver_version.is_empty() { + acc.resolver_version = delta.resolver_version.clone(); + } + + acc +} + struct SchemaItem { pub client: String, pub schemas: HashSet, From c686dd3b5ce0287dafbb418dba631462c5d280fd Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Fri, 8 May 2026 16:54:42 +0200 Subject: [PATCH 08/36] fix(cloudflare): address PR review feedback - Use performance.now() instead of Date.now() for better timing resolution - Rename METRICS_KV binding to CONFIDENCE_METRICS_KV - Fallback to Date.now() if performance API is unavailable Co-Authored-By: Claude Opus 4.6 (1M context) --- .../deployer/script.sh | 4 ++-- confidence-cloudflare-resolver/src/lib.rs | 22 ++++++++++++++----- confidence-cloudflare-resolver/wrangler.toml | 2 +- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/confidence-cloudflare-resolver/deployer/script.sh b/confidence-cloudflare-resolver/deployer/script.sh index 5e75caee..e5d9aaa0 100755 --- a/confidence-cloudflare-resolver/deployer/script.sh +++ b/confidence-cloudflare-resolver/deployer/script.sh @@ -409,10 +409,10 @@ if [ -n "$KV_NAMESPACE_ID" ]; then cat >> wrangler.toml < f64 { + js_sys::Reflect::get(&js_sys::global(), &"performance".into()) + .ok() + .and_then(|p| js_sys::Reflect::get(&p, &"now".into()).ok()) + .and_then(|f| js_sys::Function::from(f).call0(&js_sys::global()).ok()) + .and_then(|v| v.as_f64()) + .unwrap_or_else(|| js_sys::Date::now()) +} + /// Per-request resolve metrics captured in the hot path, recorded in wait_until. struct ResolveMetrics { elapsed_us: u32, @@ -160,7 +170,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .get_async("/metrics", |_req, ctx| { let allowed_origin = allowed_origin_env.clone(); async move { - let text = match ctx.env.kv("METRICS_KV") { + let text = match ctx.env.kv("CONFIDENCE_METRICS_KV") { Ok(kv) => kv.get("prometheus").text().await.unwrap_or(None), Err(_) => None, }; @@ -209,7 +219,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .evaluation_context .clone() .unwrap_or_default(); - let start = js_sys::Date::now(); + let start = performance_now(); match state.get_resolver::( &resolver_request.client_secret, evaluation_context, @@ -222,7 +232,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { ); match resolver.resolve_flags(process_request) { Ok(process_response) => { - let elapsed_us = ((js_sys::Date::now() - start) * 1000.0) as u32; + let elapsed_us = ((performance_now() - start) * 1000.0) as u32; match process_response.into_resolved() { Some((response, _writes)) => { let reasons: Vec = response @@ -252,7 +262,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { } } Err(msg) => { - let elapsed_us = ((js_sys::Date::now() - start) * 1000.0) as u32; + let elapsed_us = ((performance_now() - start) * 1000.0) as u32; PENDING_METRICS.with(|m| { m.borrow_mut().push(ResolveMetrics { elapsed_us, @@ -265,7 +275,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { } } Err(msg) => { - let elapsed_us = ((js_sys::Date::now() - start) * 1000.0) as u32; + let elapsed_us = ((performance_now() - start) * 1000.0) as u32; PENDING_METRICS.with(|m| { m.borrow_mut().push(ResolveMetrics { elapsed_us, @@ -358,7 +368,7 @@ pub async fn consume_flag_logs_queue( .collect(); // Accumulate telemetry deltas into KV-backed cumulative snapshot for /metrics - if let Ok(kv) = env.kv("METRICS_KV") { + if let Ok(kv) = env.kv("CONFIDENCE_METRICS_KV") { let _ = update_prometheus_kv(&kv, &logs).await; } diff --git a/confidence-cloudflare-resolver/wrangler.toml b/confidence-cloudflare-resolver/wrangler.toml index 30bd64ac..76bbdcaa 100644 --- a/confidence-cloudflare-resolver/wrangler.toml +++ b/confidence-cloudflare-resolver/wrangler.toml @@ -15,7 +15,7 @@ queue = "flag-logs-queue" binding = "flag_logs_queue" # KV namespace for /metrics endpoint is created and injected by the deployer. -# See deployer/script.sh for auto-creation of the METRICS_KV binding. +# See deployer/script.sh for auto-creation of the CONFIDENCE_METRICS_KV binding. [vars] CONFIDENCE_CLIENT_SECRET = "SECRET" From f5a745e0b1e7cb385721b6eb81381115b2ce729b Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Fri, 8 May 2026 17:08:26 +0200 Subject: [PATCH 09/36] fix: resolve clippy redundant closure warning --- confidence-cloudflare-resolver/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 0cdfc746..e6e43e96 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -35,7 +35,7 @@ fn performance_now() -> f64 { .and_then(|p| js_sys::Reflect::get(&p, &"now".into()).ok()) .and_then(|f| js_sys::Function::from(f).call0(&js_sys::global()).ok()) .and_then(|v| v.as_f64()) - .unwrap_or_else(|| js_sys::Date::now()) + .unwrap_or_else(js_sys::Date::now) } /// Per-request resolve metrics captured in the hot path, recorded in wait_until. From 86fb301cd64a9bbdba5410e37246362028b67f91 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Mon, 11 May 2026 13:03:56 +0200 Subject: [PATCH 10/36] perf(cloudflare): replace Reflect with web-sys typed bindings for performance.now() Use web_sys::WorkerGlobalScope::performance() instead of js_sys::Reflect::get() to avoid dynamic JS lookups on the hot path. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 1 + confidence-cloudflare-resolver/Cargo.toml | 1 + confidence-cloudflare-resolver/src/lib.rs | 11 +++++------ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 494d140d..90d9a6ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,6 +268,7 @@ dependencies = [ "prost 0.13.5", "serde", "serde_json", + "web-sys", "worker", ] diff --git a/confidence-cloudflare-resolver/Cargo.toml b/confidence-cloudflare-resolver/Cargo.toml index 67a7915d..0aeca901 100644 --- a/confidence-cloudflare-resolver/Cargo.toml +++ b/confidence-cloudflare-resolver/Cargo.toml @@ -30,5 +30,6 @@ once_cell = "1.19" prost = "0.13" arc-swap = "1" js-sys = "0.3" +web-sys = { version = "0.3", features = ["WorkerGlobalScope", "Performance"] } serde = { version = "1.0.219" } serde_json = "1.0.85" \ No newline at end of file diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index e6e43e96..aad44776 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -30,12 +30,11 @@ use std::cell::RefCell; /// High-resolution timestamp in milliseconds via `performance.now()`. fn performance_now() -> f64 { - js_sys::Reflect::get(&js_sys::global(), &"performance".into()) - .ok() - .and_then(|p| js_sys::Reflect::get(&p, &"now".into()).ok()) - .and_then(|f| js_sys::Function::from(f).call0(&js_sys::global()).ok()) - .and_then(|v| v.as_f64()) - .unwrap_or_else(js_sys::Date::now) + use js_sys::wasm_bindgen::JsCast; + js_sys::global() + .unchecked_into::() + .performance() + .map_or_else(|| js_sys::Date::now(), |p| p.now()) } /// Per-request resolve metrics captured in the hot path, recorded in wait_until. From cc4ee3f5577faa01666841d5db251172ff74836f Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Mon, 11 May 2026 13:05:21 +0200 Subject: [PATCH 11/36] docs: explain why js_sys::global() cast is needed for WorkerGlobalScope Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index aad44776..78a62f09 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -29,6 +29,8 @@ use once_cell::sync::Lazy; use std::cell::RefCell; /// High-resolution timestamp in milliseconds via `performance.now()`. +/// Uses `js_sys::global()` + cast because `web_sys::window()` is `None` in +/// Workers and there is no `web_sys::worker_global_scope()` accessor. fn performance_now() -> f64 { use js_sys::wasm_bindgen::JsCast; js_sys::global() From eb2183b8d89e8ef503c0d02cbd7aadb83b043810 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Mon, 11 May 2026 15:09:56 +0200 Subject: [PATCH 12/36] feat(cloudflare): source resolve latency from CF analytics API Cloudflare Workers freeze all timer APIs (Date.now, performance.now) during synchronous CPU work as a Spectre mitigation, making it impossible to measure resolve latency internally. Instead, we query Cloudflare's GraphQL analytics API from the queue consumer to get real CPU time percentiles (p25/p50/p75/p90/p99) and distribute them into the same exponential histogram buckets used by all other providers. This ensures: - Same metric names (confidence_resolve_latency_microseconds) - Same histogram format (compatible with shared Grafana dashboards) - Real CPU time data (~1-2ms p50 for in-memory flag evaluation) The queue consumer uses a cursor stored in KV (cpu_time_cursor) to avoid double-counting across batches. The deployer now passes CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as Worker env vars. Also removes the broken internal timer (performance.now/Date.now) and the web-sys dependency that was added for it. Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 1 - confidence-cloudflare-resolver/Cargo.toml | 1 - .../deployer/script.sh | 6 +- confidence-cloudflare-resolver/src/lib.rs | 190 +++++++++++++++--- 4 files changed, 172 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90d9a6ee..494d140d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -268,7 +268,6 @@ dependencies = [ "prost 0.13.5", "serde", "serde_json", - "web-sys", "worker", ] diff --git a/confidence-cloudflare-resolver/Cargo.toml b/confidence-cloudflare-resolver/Cargo.toml index 0aeca901..67a7915d 100644 --- a/confidence-cloudflare-resolver/Cargo.toml +++ b/confidence-cloudflare-resolver/Cargo.toml @@ -30,6 +30,5 @@ once_cell = "1.19" prost = "0.13" arc-swap = "1" js-sys = "0.3" -web-sys = { version = "0.3", features = ["WorkerGlobalScope", "Performance"] } serde = { version = "1.0.219" } serde_json = "1.0.85" \ No newline at end of file diff --git a/confidence-cloudflare-resolver/deployer/script.sh b/confidence-cloudflare-resolver/deployer/script.sh index e5d9aaa0..2da8fe3f 100755 --- a/confidence-cloudflare-resolver/deployer/script.sh +++ b/confidence-cloudflare-resolver/deployer/script.sh @@ -442,7 +442,9 @@ if [ -n "$ALLOWED_ORIGIN_TOML" ] || [ -n "$ETAG_TOML" ] || [ -n "$DEPLOYER_VERSI sed -i.tmp '/^RESOLVER_VERSION *= *.*$/d' wrangler.toml || true sed -i.tmp '/^DEPLOYER_VERSION *= *.*$/d' wrangler.toml || true sed -i.tmp '/^CONFIDENCE_CLIENT_SECRET *= *.*$/d' wrangler.toml || true - awk -v allowed="${ALLOWED_ORIGIN_TOML}" -v etag="${ETAG_TOML}" -v version="${DEPLOYER_VERSION}" -v client_secret="${CLIENT_SECRET_TOML}" ' + sed -i.tmp '/^CLOUDFLARE_API_TOKEN *= *.*$/d' wrangler.toml || true + sed -i.tmp '/^CLOUDFLARE_ACCOUNT_ID *= *.*$/d' wrangler.toml || true + awk -v allowed="${ALLOWED_ORIGIN_TOML}" -v etag="${ETAG_TOML}" -v version="${DEPLOYER_VERSION}" -v client_secret="${CLIENT_SECRET_TOML}" -v cf_token="${CLOUDFLARE_API_TOKEN}" -v cf_account="${CLOUDFLARE_ACCOUNT_ID}" ' BEGIN{inserted=0} { print $0 @@ -451,6 +453,8 @@ if [ -n "$ALLOWED_ORIGIN_TOML" ] || [ -n "$ETAG_TOML" ] || [ -n "$DEPLOYER_VERSI if (etag != "") print "RESOLVER_STATE_ETAG = \"" etag "\"" if (version != "") print "DEPLOYER_VERSION = \"" version "\"" if (client_secret != "") print "CONFIDENCE_CLIENT_SECRET = \"" client_secret "\"" + if (cf_token != "") print "CLOUDFLARE_API_TOKEN = \"" cf_token "\"" + if (cf_account != "") print "CLOUDFLARE_ACCOUNT_ID = \"" cf_account "\"" inserted=1 } } diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 78a62f09..50bf7853 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -28,20 +28,11 @@ use confidence_resolver::Client; use once_cell::sync::Lazy; use std::cell::RefCell; -/// High-resolution timestamp in milliseconds via `performance.now()`. -/// Uses `js_sys::global()` + cast because `web_sys::window()` is `None` in -/// Workers and there is no `web_sys::worker_global_scope()` accessor. -fn performance_now() -> f64 { - use js_sys::wasm_bindgen::JsCast; - js_sys::global() - .unchecked_into::() - .performance() - .map_or_else(|| js_sys::Date::now(), |p| p.now()) -} - /// Per-request resolve metrics captured in the hot path, recorded in wait_until. +/// Note: CF Workers freeze all timer APIs during sync CPU work (Spectre mitigation), +/// so we cannot measure resolve latency internally. CPU time is sourced from +/// Cloudflare's analytics API in the queue consumer instead. struct ResolveMetrics { - elapsed_us: u32, reasons: Vec, } @@ -220,7 +211,6 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .evaluation_context .clone() .unwrap_or_default(); - let start = performance_now(); match state.get_resolver::( &resolver_request.client_secret, evaluation_context, @@ -233,7 +223,6 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { ); match resolver.resolve_flags(process_request) { Ok(process_response) => { - let elapsed_us = ((performance_now() - start) * 1000.0) as u32; match process_response.into_resolved() { Some((response, _writes)) => { let reasons: Vec = response @@ -242,7 +231,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .map(|f| f.reason()) .collect(); PENDING_METRICS.with(|m| { - m.borrow_mut().push(ResolveMetrics { elapsed_us, reasons }); + m.borrow_mut().push(ResolveMetrics { reasons }); }); Response::from_json(&response)? .with_cors_headers(&allowed_origin) @@ -250,7 +239,6 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { None => { PENDING_METRICS.with(|m| { m.borrow_mut().push(ResolveMetrics { - elapsed_us, reasons: vec![ResolveReason::Error], }); }); @@ -263,10 +251,8 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { } } Err(msg) => { - let elapsed_us = ((performance_now() - start) * 1000.0) as u32; PENDING_METRICS.with(|m| { m.borrow_mut().push(ResolveMetrics { - elapsed_us, reasons: vec![ResolveReason::Error], }); }); @@ -276,10 +262,8 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { } } Err(msg) => { - let elapsed_us = ((performance_now() - start) * 1000.0) as u32; PENDING_METRICS.with(|m| { m.borrow_mut().push(ResolveMetrics { - elapsed_us, reasons: vec![ResolveReason::Error], }); }); @@ -323,12 +307,13 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .run(req, env) .await; - // Use ctx.waitUntil to run logging and telemetry after response is returned + // Use ctx.waitUntil to run logging and telemetry after response is returned. + // Note: resolve latency cannot be measured inside Workers (timer APIs are + // frozen during sync CPU work). CPU time is sourced from Cloudflare's + // analytics API in the queue consumer instead. ctx.wait_until(async move { - // Record pending resolve metrics into the telemetry counters PENDING_METRICS.with(|m| { for metrics in m.borrow_mut().drain(..) { - TELEMETRY.record_latency_us(metrics.elapsed_us); for reason in metrics.reasons { TELEMETRY.mark_resolve(reason); } @@ -373,6 +358,20 @@ pub async fn consume_flag_logs_queue( let _ = update_prometheus_kv(&kv, &logs).await; } + // Fetch real CPU time from CF analytics API and append to Prometheus KV + let cf_token = env.var("CLOUDFLARE_API_TOKEN").ok().map(|v| v.to_string()); + let cf_account = env.var("CLOUDFLARE_ACCOUNT_ID").ok().map(|v| v.to_string()); + let cf_script = env + .var("CF_SCRIPT_NAME") + .ok() + .map(|v| v.to_string()) + .unwrap_or_else(|| "confidence-cloudflare-resolver".to_string()); + if let (Some(token), Some(account)) = (cf_token, cf_account) { + if let Ok(kv) = env.kv("CONFIDENCE_METRICS_KV") { + let _ = update_cpu_time_kv(&kv, &token, &account, &cf_script).await; + } + } + let req = flag_logger::aggregate_batch(logs); send_flags_logs(CONFIDENCE_CLIENT_SECRET.get().unwrap().as_str(), req).await?; } @@ -418,6 +417,151 @@ async fn update_prometheus_kv(kv: &kv::KvStore, logs: &[WriteFlagLogsRequest]) { } } +/// Query Cloudflare's analytics GraphQL API for Worker CPU time and write +/// the percentiles as Prometheus gauge metrics to KV. +/// Return an ISO-8601 timestamp `seconds_ago` in the past. +fn since_iso8601(seconds_ago: u64) -> String { + let now_ms = js_sys::Date::now() as u64; + let then_ms = now_ms.saturating_sub(seconds_ago.saturating_mul(1000)); + let d = js_sys::Date::new_0(); + d.set_time(then_ms as f64); + // Use Date.toISOString() for proper formatting + d.to_iso_string().into() +} + +/// Query recent CPU time from Cloudflare analytics and update the cumulative +/// `TelemetrySnapshot` in KV so the Prometheus output uses the same metric +/// names as all other providers (`confidence_resolve_latency_microseconds`). +/// +/// Since CF Workers freeze timers during sync CPU work, we can't measure +/// latency internally. Instead, we use CF's own analytics (p50 × requests) +/// as an approximation for the histogram's `_sum` and `_count`. +/// +/// To avoid double-counting, we store the last queried timestamp in KV +/// (`cpu_time_cursor`) and only process data points newer than that. +async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script: &str) { + // Read cursor: the last datetime we processed + let cursor = match kv.get("cpu_time_cursor").text().await { + Ok(Some(c)) if !c.is_empty() => c, + _ => since_iso8601(300), // bootstrap: last 5 minutes + }; + + let gql = format!( + "{{ viewer {{ accounts(filter: {{accountTag: \"{account}\"}}) {{ workersInvocationsAdaptive(limit: 50, filter: {{scriptName: \"{script}\", datetime_gt: \"{cursor}\"}}, orderBy: [datetime_ASC]) {{ dimensions {{ datetime }} quantiles {{ cpuTimeP25 cpuTimeP50 cpuTimeP75 cpuTimeP90 cpuTimeP99 }} sum {{ requests }} }} }} }} }}" + ); + let query = serde_json::to_string(&json!({ "query": gql })).unwrap_or_default(); + + let url = "https://api.cloudflare.com/client/v4/graphql"; + let mut init = RequestInit::new(); + let headers = Headers::new(); + let _ = headers.set("Authorization", &format!("Bearer {token}")); + let _ = headers.set("Content-Type", "application/json"); + init.with_headers(headers); + init.with_method(Method::Post); + init.with_body(Some(query.into())); + + let Ok(request) = Request::new_with_init(url, &init) else { return }; + let Ok(mut resp) = Fetch::Request(request).send().await else { return }; + let Ok(body) = resp.text().await else { return }; + let Ok(data) = serde_json::from_str::(&body) else { return }; + + let Some(entries) = data + .pointer("/data/viewer/accounts/0/workersInvocationsAdaptive") + .and_then(|v| v.as_array()) + else { return }; + + if entries.is_empty() { return } + + // Aggregate only NEW data points (after cursor) + let mut total_requests: u64 = 0; + let mut weighted_sum: u64 = 0; + let mut bucket_adds: Vec<(u64, u64)> = Vec::new(); // (Ξs_value, count) + let mut latest_datetime = String::new(); + + for entry in entries { + let reqs = entry.pointer("/sum/requests").and_then(|v| v.as_u64()).unwrap_or(0); + if reqs == 0 { continue } + let q = entry.pointer("/quantiles"); + let p25 = q.and_then(|q| q.get("cpuTimeP25")).and_then(|v| v.as_u64()).unwrap_or(0); + let p50 = q.and_then(|q| q.get("cpuTimeP50")).and_then(|v| v.as_u64()).unwrap_or(0); + let p75 = q.and_then(|q| q.get("cpuTimeP75")).and_then(|v| v.as_u64()).unwrap_or(0); + let p90 = q.and_then(|q| q.get("cpuTimeP90")).and_then(|v| v.as_u64()).unwrap_or(0); + let p99 = q.and_then(|q| q.get("cpuTimeP99")).and_then(|v| v.as_u64()).unwrap_or(0); + + // Distribute requests across percentile bands into synthetic observations. + // Each band gets its share of requests placed at the percentile value. + // Bands: [0-25%], (25-50%], (50-75%], (75-90%], (90-99%], (99-100%] + // Fractions: 0.25, 0.25, 0.25, 0.15, 0.09, 0.01 + let bands: &[(f64, u64)] = &[ + (0.25, p25), (0.25, p50), (0.25, p75), (0.15, p90), (0.09, p99), (0.01, p99), + ]; + let mut assigned: u64 = 0; + for (i, &(frac, value)) in bands.iter().enumerate() { + let count = if i == bands.len() - 1 { + reqs.saturating_sub(assigned) + } else { + (reqs as f64 * frac).round() as u64 + }; + if count > 0 && value > 0 { + bucket_adds.push((value, count)); + weighted_sum = weighted_sum.saturating_add(value.saturating_mul(count)); + } + assigned = assigned.saturating_add(count); + } + total_requests = total_requests.saturating_add(reqs); + + if let Some(dt) = entry.pointer("/dimensions/datetime").and_then(|v| v.as_str()) { + if dt > latest_datetime.as_str() { + latest_datetime = dt.to_string(); + } + } + } + + if total_requests == 0 || latest_datetime.is_empty() { return } + + // Update cursor to latest processed timestamp + if let Ok(builder) = kv.put("cpu_time_cursor", &latest_datetime) { + let _ = builder.execute().await; + } + + // Update the cumulative snapshot with histogram buckets, sum, and count + let mut cumulative = match kv.get("snapshot").text().await { + Ok(Some(text)) => serde_json::from_str::(&text).unwrap_or_default(), + _ => TelemetrySnapshot::default(), + }; + + cumulative.latency.sum = cumulative.latency.sum.saturating_add(weighted_sum); + cumulative.latency.count = cumulative.latency.count.saturating_add(total_requests); + + // Place observations into exponential histogram buckets (same as other providers) + let ln_ratio: f64 = core::f64::consts::LN_10 / 18.0; + for (us_value, count) in bucket_adds { + let idx = if us_value == 0 { + 0 + } else { + ((us_value as f64).ln() / ln_ratio).floor() as usize + }; + if idx >= cumulative.latency.buckets.len() { + cumulative.latency.buckets.resize(idx.saturating_add(1), 0); + } + if let Some(b) = cumulative.latency.buckets.get_mut(idx) { + *b = b.saturating_add(count); + } + } + + let prom_text = cumulative.to_prometheus( + "cf-resolver", + &confidence_resolver::telemetry::PrometheusConfig::default(), + ); + + if let Ok(builder) = kv.put("snapshot", serde_json::to_string(&cumulative).unwrap_or_default()) { + let _ = builder.execute().await; + } + if let Ok(builder) = kv.put("prometheus", prom_text) { + let _ = builder.execute().await; + } +} + async fn send_flags_logs(client_secret: &str, message: WriteFlagLogsRequest) -> Result { let resolve_url = "https://resolver.confidence.dev/v1/clientFlagLogs:write"; let mut init = RequestInit::new(); From 3ee03968dd7ba56ccab64f4c65199a3e896131c3 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Mon, 11 May 2026 15:28:39 +0200 Subject: [PATCH 13/36] fix(cloudflare): fix histogram bucket overflow and distribution rounding - Use floor() instead of round() for percentile band allocation to prevent assigning more observations than requests - Track actual observations placed into buckets for _count to ensure +Inf bucket >= all cumulative bucket counts - Revert sum to use weighted distribution across all percentile bands (correct representation of the latency distribution) Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 25 +++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 50bf7853..a56669cf 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -473,7 +473,7 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script if entries.is_empty() { return } // Aggregate only NEW data points (after cursor) - let mut total_requests: u64 = 0; + let mut total_observations: u64 = 0; let mut weighted_sum: u64 = 0; let mut bucket_adds: Vec<(u64, u64)> = Vec::new(); // (Ξs_value, count) let mut latest_datetime = String::new(); @@ -488,27 +488,30 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script let p90 = q.and_then(|q| q.get("cpuTimeP90")).and_then(|v| v.as_u64()).unwrap_or(0); let p99 = q.and_then(|q| q.get("cpuTimeP99")).and_then(|v| v.as_u64()).unwrap_or(0); - // Distribute requests across percentile bands into synthetic observations. - // Each band gets its share of requests placed at the percentile value. + // Distribute requests across percentile bands into synthetic histogram + // observations. Each band's requests are placed at the band's percentile + // value for bucket distribution. + // // Bands: [0-25%], (25-50%], (50-75%], (75-90%], (90-99%], (99-100%] - // Fractions: 0.25, 0.25, 0.25, 0.15, 0.09, 0.01 + // Use floor() to avoid over-counting; remainder goes to last band. let bands: &[(f64, u64)] = &[ (0.25, p25), (0.25, p50), (0.25, p75), (0.15, p90), (0.09, p99), (0.01, p99), ]; - let mut assigned: u64 = 0; + let mut remaining = reqs; for (i, &(frac, value)) in bands.iter().enumerate() { let count = if i == bands.len() - 1 { - reqs.saturating_sub(assigned) + remaining } else { - (reqs as f64 * frac).round() as u64 + let c = (reqs as f64 * frac) as u64; // floor + c.min(remaining) }; + remaining = remaining.saturating_sub(count); if count > 0 && value > 0 { bucket_adds.push((value, count)); weighted_sum = weighted_sum.saturating_add(value.saturating_mul(count)); + total_observations = total_observations.saturating_add(count); } - assigned = assigned.saturating_add(count); } - total_requests = total_requests.saturating_add(reqs); if let Some(dt) = entry.pointer("/dimensions/datetime").and_then(|v| v.as_str()) { if dt > latest_datetime.as_str() { @@ -517,7 +520,7 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script } } - if total_requests == 0 || latest_datetime.is_empty() { return } + if total_observations == 0 || latest_datetime.is_empty() { return } // Update cursor to latest processed timestamp if let Ok(builder) = kv.put("cpu_time_cursor", &latest_datetime) { @@ -531,7 +534,7 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script }; cumulative.latency.sum = cumulative.latency.sum.saturating_add(weighted_sum); - cumulative.latency.count = cumulative.latency.count.saturating_add(total_requests); + cumulative.latency.count = cumulative.latency.count.saturating_add(total_observations); // Place observations into exponential histogram buckets (same as other providers) let ln_ratio: f64 = core::f64::consts::LN_10 / 18.0; From b857fcc2afee3a974707ae5fa59cc252f087473d Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Mon, 11 May 2026 15:47:32 +0200 Subject: [PATCH 14/36] fix(cloudflare): handle single-request analytics data points Single-request CF analytics data points have all percentiles identical (p25=p50=p75=p90=p99). Spreading them across 6 bands inflates the histogram. Now place a single observation at p50 for these cases. Also fix rounding: use floor() and track remaining to prevent over-allocation across bands. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 49 +++++++++++++---------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index a56669cf..8ee286f0 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -488,28 +488,33 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script let p90 = q.and_then(|q| q.get("cpuTimeP90")).and_then(|v| v.as_u64()).unwrap_or(0); let p99 = q.and_then(|q| q.get("cpuTimeP99")).and_then(|v| v.as_u64()).unwrap_or(0); - // Distribute requests across percentile bands into synthetic histogram - // observations. Each band's requests are placed at the band's percentile - // value for bucket distribution. - // - // Bands: [0-25%], (25-50%], (50-75%], (75-90%], (90-99%], (99-100%] - // Use floor() to avoid over-counting; remainder goes to last band. - let bands: &[(f64, u64)] = &[ - (0.25, p25), (0.25, p50), (0.25, p75), (0.15, p90), (0.09, p99), (0.01, p99), - ]; - let mut remaining = reqs; - for (i, &(frac, value)) in bands.iter().enumerate() { - let count = if i == bands.len() - 1 { - remaining - } else { - let c = (reqs as f64 * frac) as u64; // floor - c.min(remaining) - }; - remaining = remaining.saturating_sub(count); - if count > 0 && value > 0 { - bucket_adds.push((value, count)); - weighted_sum = weighted_sum.saturating_add(value.saturating_mul(count)); - total_observations = total_observations.saturating_add(count); + // Distribute requests into histogram buckets using CF percentiles. + // For single-request data points all percentiles are identical, so + // we just place 1 observation at p50. For multi-request points we + // spread across bands: [0-25%] p25, (25-50%] p50, (50-75%] p75, + // (75-90%] p90, (90-99%] p99, (99-100%] p99. + if reqs == 1 { + bucket_adds.push((p50, 1)); + weighted_sum = weighted_sum.saturating_add(p50); + total_observations = total_observations.saturating_add(1); + } else { + let bands: &[(f64, u64)] = &[ + (0.25, p25), (0.25, p50), (0.25, p75), (0.15, p90), (0.09, p99), (0.01, p99), + ]; + let mut remaining = reqs; + for (i, &(frac, value)) in bands.iter().enumerate() { + let count = if i == bands.len() - 1 { + remaining + } else { + let c = (reqs as f64 * frac) as u64; + c.min(remaining) + }; + remaining = remaining.saturating_sub(count); + if count > 0 && value > 0 { + bucket_adds.push((value, count)); + weighted_sum = weighted_sum.saturating_add(value.saturating_mul(count)); + total_observations = total_observations.saturating_add(count); + } } } From b087861ae0669566dcebc5d64080dbede914ce49 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 10:47:28 +0200 Subject: [PATCH 15/36] feat(cloudflare): rate-limit CF analytics queries to minimum 10s intervals Store last fetch timestamp in KV (cpu_time_last_fetch_ms) and skip the GraphQL query if less than 10 seconds have elapsed. Prevents excessive API calls when queue batches arrive faster than analytics data updates. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 8ee286f0..bae71642 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -440,6 +440,16 @@ fn since_iso8601(seconds_ago: u64) -> String { /// To avoid double-counting, we store the last queried timestamp in KV /// (`cpu_time_cursor`) and only process data points newer than that. async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script: &str) { + // Rate-limit: skip if less than 10 seconds since last fetch + let now_ms = js_sys::Date::now() as u64; + if let Ok(Some(last)) = kv.get("cpu_time_last_fetch_ms").text().await { + if let Ok(last_ms) = last.parse::() { + if now_ms.saturating_sub(last_ms) < 10_000 { + return; + } + } + } + // Read cursor: the last datetime we processed let cursor = match kv.get("cpu_time_cursor").text().await { Ok(Some(c)) if !c.is_empty() => c, @@ -531,6 +541,10 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script if let Ok(builder) = kv.put("cpu_time_cursor", &latest_datetime) { let _ = builder.execute().await; } + // Record fetch timestamp for rate-limiting + if let Ok(builder) = kv.put("cpu_time_last_fetch_ms", now_ms.to_string()) { + let _ = builder.execute().await; + } // Update the cumulative snapshot with histogram buckets, sum, and count let mut cumulative = match kv.get("snapshot").text().await { From ba0c117310e73cbcffa38f699d28df1ad82e6ba3 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 10:54:16 +0200 Subject: [PATCH 16/36] refactor(cloudflare): use cursor datetime for rate-limiting instead of extra KV key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compare cursor timestamp to now — skip CF analytics call if cursor is less than 10s old. No extra KV reads/writes needed; the cursor we already store doubles as the rate-limit check. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 24 +++++++++-------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index bae71642..7444bdb3 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -440,22 +440,20 @@ fn since_iso8601(seconds_ago: u64) -> String { /// To avoid double-counting, we store the last queried timestamp in KV /// (`cpu_time_cursor`) and only process data points newer than that. async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script: &str) { - // Rate-limit: skip if less than 10 seconds since last fetch - let now_ms = js_sys::Date::now() as u64; - if let Ok(Some(last)) = kv.get("cpu_time_last_fetch_ms").text().await { - if let Ok(last_ms) = last.parse::() { - if now_ms.saturating_sub(last_ms) < 10_000 { - return; - } - } - } - - // Read cursor: the last datetime we processed + // Read cursor: the last datetime we processed (also serves as rate-limit) let cursor = match kv.get("cpu_time_cursor").text().await { Ok(Some(c)) if !c.is_empty() => c, _ => since_iso8601(300), // bootstrap: last 5 minutes }; + // Rate-limit: skip if cursor is less than 10 seconds old. + // Cursor is an ISO-8601 datetime from CF analytics; parse via JS Date. + let cursor_ms = js_sys::Date::parse(&cursor); + let now_ms = js_sys::Date::now(); + if (now_ms - cursor_ms) < 10_000.0 { + return; + } + let gql = format!( "{{ viewer {{ accounts(filter: {{accountTag: \"{account}\"}}) {{ workersInvocationsAdaptive(limit: 50, filter: {{scriptName: \"{script}\", datetime_gt: \"{cursor}\"}}, orderBy: [datetime_ASC]) {{ dimensions {{ datetime }} quantiles {{ cpuTimeP25 cpuTimeP50 cpuTimeP75 cpuTimeP90 cpuTimeP99 }} sum {{ requests }} }} }} }} }}" ); @@ -541,10 +539,6 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script if let Ok(builder) = kv.put("cpu_time_cursor", &latest_datetime) { let _ = builder.execute().await; } - // Record fetch timestamp for rate-limiting - if let Ok(builder) = kv.put("cpu_time_last_fetch_ms", now_ms.to_string()) { - let _ = builder.execute().await; - } // Update the cumulative snapshot with histogram buckets, sum, and count let mut cumulative = match kv.get("snapshot").text().await { From e724283b1168e80d687254cb7333e28c4b025caa Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 11:03:12 +0200 Subject: [PATCH 17/36] feat(cloudflare): include CF analytics latency in WriteFlagLogsRequest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the queue consumer fetches new CPU time data from CF analytics (rate-limited to every 10s via cursor age), the latency histogram delta is now included in the WriteFlagLogsRequest sent to the Confidence backend. When no new latency data is available (rate limit or no traffic), only resolve reasons are sent — no stale latency data is re-sent. This ensures the backend's VictoriaMetrics telemetry consumer receives the same latency distribution data as the /metrics endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 105 +++++++++++++++++----- 1 file changed, 82 insertions(+), 23 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 7444bdb3..8a9b9958 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -17,6 +17,10 @@ use serde_json::json; use confidence::flags::resolver::v1::{ApplyFlagsRequest, ApplyFlagsResponse, ResolveFlagsRequest}; use confidence_resolver::proto::confidence::flags::resolver::v1::{ResolveProcessRequest, ResolveReason}; +use confidence_resolver::proto::confidence::flags::resolver::v1::{ + TelemetryData, + telemetry_data::{BucketSpan, ResolveLatency}, +}; static RESOLVE_LOGGER: LazyLock> = LazyLock::new(ResolveLogger::new); static ASSIGN_LOGGER: LazyLock = LazyLock::new(AssignLogger::new); @@ -358,21 +362,37 @@ pub async fn consume_flag_logs_queue( let _ = update_prometheus_kv(&kv, &logs).await; } - // Fetch real CPU time from CF analytics API and append to Prometheus KV - let cf_token = env.var("CLOUDFLARE_API_TOKEN").ok().map(|v| v.to_string()); - let cf_account = env.var("CLOUDFLARE_ACCOUNT_ID").ok().map(|v| v.to_string()); - let cf_script = env - .var("CF_SCRIPT_NAME") - .ok() - .map(|v| v.to_string()) - .unwrap_or_else(|| "confidence-cloudflare-resolver".to_string()); - if let (Some(token), Some(account)) = (cf_token, cf_account) { - if let Ok(kv) = env.kv("CONFIDENCE_METRICS_KV") { - let _ = update_cpu_time_kv(&kv, &token, &account, &cf_script).await; + // Fetch real CPU time from CF analytics API and append to Prometheus KV. + // Returns a TelemetryData delta when new latency data is available. + let cf_latency = { + let cf_token = env.var("CLOUDFLARE_API_TOKEN").ok().map(|v| v.to_string()); + let cf_account = env.var("CLOUDFLARE_ACCOUNT_ID").ok().map(|v| v.to_string()); + let cf_script = env + .var("CF_SCRIPT_NAME") + .ok() + .map(|v| v.to_string()) + .unwrap_or_else(|| "confidence-cloudflare-resolver".to_string()); + match (cf_token, cf_account) { + (Some(token), Some(account)) => match env.kv("CONFIDENCE_METRICS_KV") { + Ok(kv) => update_cpu_time_kv(&kv, &token, &account, &cf_script).await, + Err(_) => None, + }, + _ => None, } - } + }; - let req = flag_logger::aggregate_batch(logs); + let mut req = flag_logger::aggregate_batch(logs); + // Inject CF analytics latency into the telemetry sent to the backend + if let Some(latency_td) = cf_latency { + match &mut req.telemetry_data { + Some(td) => { + td.resolve_latency = latency_td.resolve_latency; + } + None => { + req.telemetry_data = Some(latency_td); + } + } + } send_flags_logs(CONFIDENCE_CLIENT_SECRET.get().unwrap().as_str(), req).await?; } @@ -439,7 +459,7 @@ fn since_iso8601(seconds_ago: u64) -> String { /// /// To avoid double-counting, we store the last queried timestamp in KV /// (`cpu_time_cursor`) and only process data points newer than that. -async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script: &str) { +async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script: &str) -> Option { // Read cursor: the last datetime we processed (also serves as rate-limit) let cursor = match kv.get("cpu_time_cursor").text().await { Ok(Some(c)) if !c.is_empty() => c, @@ -451,7 +471,7 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script let cursor_ms = js_sys::Date::parse(&cursor); let now_ms = js_sys::Date::now(); if (now_ms - cursor_ms) < 10_000.0 { - return; + return None; } let gql = format!( @@ -468,17 +488,17 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script init.with_method(Method::Post); init.with_body(Some(query.into())); - let Ok(request) = Request::new_with_init(url, &init) else { return }; - let Ok(mut resp) = Fetch::Request(request).send().await else { return }; - let Ok(body) = resp.text().await else { return }; - let Ok(data) = serde_json::from_str::(&body) else { return }; + let Ok(request) = Request::new_with_init(url, &init) else { return None }; + let Ok(mut resp) = Fetch::Request(request).send().await else { return None }; + let Ok(body) = resp.text().await else { return None }; + let Ok(data) = serde_json::from_str::(&body) else { return None }; let Some(entries) = data .pointer("/data/viewer/accounts/0/workersInvocationsAdaptive") .and_then(|v| v.as_array()) - else { return }; + else { return None }; - if entries.is_empty() { return } + if entries.is_empty() { return None } // Aggregate only NEW data points (after cursor) let mut total_observations: u64 = 0; @@ -533,7 +553,7 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script } } - if total_observations == 0 || latest_datetime.is_empty() { return } + if total_observations == 0 || latest_datetime.is_empty() { return None } // Update cursor to latest processed timestamp if let Ok(builder) = kv.put("cpu_time_cursor", &latest_datetime) { @@ -551,7 +571,7 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script // Place observations into exponential histogram buckets (same as other providers) let ln_ratio: f64 = core::f64::consts::LN_10 / 18.0; - for (us_value, count) in bucket_adds { + for &(us_value, count) in &bucket_adds { let idx = if us_value == 0 { 0 } else { @@ -576,6 +596,45 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script if let Ok(builder) = kv.put("prometheus", prom_text) { let _ = builder.execute().await; } + + // Build BucketSpans for the TelemetryData delta sent to the backend. + let spans = { + let mut flat: Vec = Vec::new(); + for &(us_value, count) in &bucket_adds { + let idx = if us_value == 0 { 0 } else { + ((us_value as f64).ln() / ln_ratio).floor() as usize + }; + if idx >= flat.len() { flat.resize(idx + 1, 0); } + if let Some(b) = flat.get_mut(idx) { *b = b.saturating_add(count as u32); } + } + // Compress into BucketSpans (contiguous non-zero runs) + let mut spans: Vec = Vec::new(); + let mut current: Option<(i32, Vec)> = None; + for (i, &v) in flat.iter().enumerate() { + if v > 0 { + match &mut current { + Some((_, counts)) => counts.push(v), + None => current = Some((i as i32, vec![v])), + } + } else if let Some((offset, counts)) = current.take() { + spans.push(BucketSpan { offset, counts }); + } + } + if let Some((offset, counts)) = current { + spans.push(BucketSpan { offset, counts }); + } + spans + }; + + Some(TelemetryData { + resolve_latency: Some(ResolveLatency { + sum: weighted_sum as u32, + count: total_observations as u32, + buckets: spans, + ln_ratio, + }), + ..Default::default() + }) } async fn send_flags_logs(client_secret: &str, message: WriteFlagLogsRequest) -> Result { From 096769c4f6c929d7a36b7ba0dff547c270eb55df Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 11:13:34 +0200 Subject: [PATCH 18/36] feat: add SDK_ID_CLOUDFLARE_RESOLVER (25) to SdkId enum Add new SDK ID for the Cloudflare resolver to all proto definitions and set it in the telemetry data sent by the CF resolver. Also syncs missing IDs 23 (PYTHON_LOCAL_PROVIDER) and 24 (RUST_LOCAL_PROVIDER) from epx-flags-resolver. Without this, the backend receives telemetry with sdk=null, making it invisible on the SDK telemetry Grafana dashboard. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 9 ++++++++- .../protos/confidence/flags/resolver/v1/types.proto | 3 +++ openfeature-provider/js/proto/test-only.proto | 1 + .../proto/confidence/flags/resolver/v1/types.proto | 3 +++ wasm/proto/types.proto | 1 + 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 8a9b9958..a3eef0c4 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -401,7 +401,14 @@ pub async fn consume_flag_logs_queue( fn checkpoint() -> WriteFlagLogsRequest { let mut req = RESOLVE_LOGGER.checkpoint(); - req.telemetry_data = Some(TELEMETRY.delta_snapshot(&LAST_FLUSHED)); + let mut td = TELEMETRY.delta_snapshot(&LAST_FLUSHED); + td.sdk = Some(confidence::flags::resolver::v1::Sdk { + sdk: Some(confidence::flags::resolver::v1::sdk::Sdk::Id( + confidence::flags::resolver::v1::SdkId::CloudflareResolver as i32, + )), + version: env!("CARGO_PKG_VERSION").to_string(), + }); + req.telemetry_data = Some(td); ASSIGN_LOGGER.checkpoint_fill(&mut req); req } diff --git a/confidence-resolver/protos/confidence/flags/resolver/v1/types.proto b/confidence-resolver/protos/confidence/flags/resolver/v1/types.proto index bc1ffc0d..60091464 100644 --- a/confidence-resolver/protos/confidence/flags/resolver/v1/types.proto +++ b/confidence-resolver/protos/confidence/flags/resolver/v1/types.proto @@ -75,4 +75,7 @@ enum SdkId { SDK_ID_GO_LOCAL_PROVIDER = 20; SDK_ID_JAVA_LOCAL_PROVIDER = 21; SDK_ID_JS_LOCAL_SERVER_PROVIDER = 22; + SDK_ID_PYTHON_LOCAL_PROVIDER = 23; + SDK_ID_RUST_LOCAL_PROVIDER = 24; + SDK_ID_CLOUDFLARE_RESOLVER = 25; } diff --git a/openfeature-provider/js/proto/test-only.proto b/openfeature-provider/js/proto/test-only.proto index 12774a8f..99f26668 100644 --- a/openfeature-provider/js/proto/test-only.proto +++ b/openfeature-provider/js/proto/test-only.proto @@ -20,6 +20,7 @@ message Sdk { enum SdkId { SDK_ID_UNSPECIFIED = 0; SDK_ID_JS_LOCAL_SERVER_PROVIDER = 22; + SDK_ID_CLOUDFLARE_RESOLVER = 25; } message TelemetryData { diff --git a/openfeature-provider/proto/confidence/flags/resolver/v1/types.proto b/openfeature-provider/proto/confidence/flags/resolver/v1/types.proto index e831bc4a..bec0aae5 100644 --- a/openfeature-provider/proto/confidence/flags/resolver/v1/types.proto +++ b/openfeature-provider/proto/confidence/flags/resolver/v1/types.proto @@ -73,4 +73,7 @@ enum SdkId { SDK_ID_GO_LOCAL_PROVIDER = 20; SDK_ID_JAVA_LOCAL_PROVIDER = 21; SDK_ID_JS_LOCAL_SERVER_PROVIDER = 22; + SDK_ID_PYTHON_LOCAL_PROVIDER = 23; + SDK_ID_RUST_LOCAL_PROVIDER = 24; + SDK_ID_CLOUDFLARE_RESOLVER = 25; } diff --git a/wasm/proto/types.proto b/wasm/proto/types.proto index 31ae0765..6aeeec62 100644 --- a/wasm/proto/types.proto +++ b/wasm/proto/types.proto @@ -42,6 +42,7 @@ message Sdk { // receives and passes through Sdk messages from the main confidence-resolver. enum SdkId { SDK_ID_UNSPECIFIED = 0; + SDK_ID_CLOUDFLARE_RESOLVER = 25; } message ResolvedValue { From 17fcf25a5ad0c7e54207322e8197403c207cc3b5 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 11:19:04 +0200 Subject: [PATCH 19/36] fix(cloudflare): set resolver_version in telemetry data The backend's VictoriaMetricsClient skips resolve_rate metrics when resolver_version is empty. Without this, resolve rates from the CF resolver are silently dropped and don't appear on Grafana dashboards. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index a3eef0c4..28539b0f 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -408,6 +408,7 @@ fn checkpoint() -> WriteFlagLogsRequest { )), version: env!("CARGO_PKG_VERSION").to_string(), }); + td.resolver_version = env!("CARGO_PKG_VERSION").to_string(); req.telemetry_data = Some(td); ASSIGN_LOGGER.checkpoint_fill(&mut req); req From 75aeae4967b3043b5a29f35ad6cea848463152f1 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 12:14:45 +0200 Subject: [PATCH 20/36] fix(cloudflare): address PR review findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move CLOUDFLARE_API_TOKEN from plaintext [vars] to encrypted Worker secret via `env.secret()` and `wrangler secret put` after deploy - Inject CF_SCRIPT_NAME from WORKER_NAME so prefixed deployments query the correct script in CF analytics - Merge update_prometheus_kv and update_cpu_time_kv into a single update_metrics_kv to avoid double KV read-modify-write of "snapshot" - Clamp u64→u32 truncation on sum/count with .min(u32::MAX as u64) - Cap histogram bucket index at BUCKET_COUNT-1 to prevent unbounded growth from unexpected CF analytics values - Add Cache-Control: no-store to /metrics response - Export BUCKET_COUNT as pub from telemetry module Co-Authored-By: Claude Opus 4.6 (1M context) --- .../deployer/script.sh | 14 +- confidence-cloudflare-resolver/src/lib.rs | 141 +++++++++--------- confidence-resolver/src/telemetry.rs | 2 +- 3 files changed, 82 insertions(+), 75 deletions(-) diff --git a/confidence-cloudflare-resolver/deployer/script.sh b/confidence-cloudflare-resolver/deployer/script.sh index 2da8fe3f..8e326ab6 100755 --- a/confidence-cloudflare-resolver/deployer/script.sh +++ b/confidence-cloudflare-resolver/deployer/script.sh @@ -442,9 +442,9 @@ if [ -n "$ALLOWED_ORIGIN_TOML" ] || [ -n "$ETAG_TOML" ] || [ -n "$DEPLOYER_VERSI sed -i.tmp '/^RESOLVER_VERSION *= *.*$/d' wrangler.toml || true sed -i.tmp '/^DEPLOYER_VERSION *= *.*$/d' wrangler.toml || true sed -i.tmp '/^CONFIDENCE_CLIENT_SECRET *= *.*$/d' wrangler.toml || true - sed -i.tmp '/^CLOUDFLARE_API_TOKEN *= *.*$/d' wrangler.toml || true sed -i.tmp '/^CLOUDFLARE_ACCOUNT_ID *= *.*$/d' wrangler.toml || true - awk -v allowed="${ALLOWED_ORIGIN_TOML}" -v etag="${ETAG_TOML}" -v version="${DEPLOYER_VERSION}" -v client_secret="${CLIENT_SECRET_TOML}" -v cf_token="${CLOUDFLARE_API_TOKEN}" -v cf_account="${CLOUDFLARE_ACCOUNT_ID}" ' + sed -i.tmp '/^CF_SCRIPT_NAME *= *.*$/d' wrangler.toml || true + awk -v allowed="${ALLOWED_ORIGIN_TOML}" -v etag="${ETAG_TOML}" -v version="${DEPLOYER_VERSION}" -v client_secret="${CLIENT_SECRET_TOML}" -v cf_account="${CLOUDFLARE_ACCOUNT_ID}" -v cf_script="${WORKER_NAME}" ' BEGIN{inserted=0} { print $0 @@ -453,8 +453,8 @@ if [ -n "$ALLOWED_ORIGIN_TOML" ] || [ -n "$ETAG_TOML" ] || [ -n "$DEPLOYER_VERSI if (etag != "") print "RESOLVER_STATE_ETAG = \"" etag "\"" if (version != "") print "DEPLOYER_VERSION = \"" version "\"" if (client_secret != "") print "CONFIDENCE_CLIENT_SECRET = \"" client_secret "\"" - if (cf_token != "") print "CLOUDFLARE_API_TOKEN = \"" cf_token "\"" if (cf_account != "") print "CLOUDFLARE_ACCOUNT_ID = \"" cf_account "\"" + if (cf_script != "") print "CF_SCRIPT_NAME = \"" cf_script "\"" inserted=1 } } @@ -532,6 +532,14 @@ add_wrangler_deploy_args_from_lines "WRANGLER_DEPLOY_ARGS" "$WRANGLER_DEPLOY_ARG # only deploy if NO_DEPLOY is not set if test -z "$NO_DEPLOY"; then wrangler deploy "${WRANGLER_DEPLOY_ARGS_ARRAY[@]}" + + # Set CLOUDFLARE_API_TOKEN as an encrypted secret (not a plaintext var) + if [ -n "$CLOUDFLARE_API_TOKEN" ]; then + echo "🔒 Setting CLOUDFLARE_API_TOKEN as encrypted Worker secret..." + echo "$CLOUDFLARE_API_TOKEN" | wrangler secret put CLOUDFLARE_API_TOKEN 2>/dev/null \ + && echo "✅ CLOUDFLARE_API_TOKEN set as encrypted secret" \ + || echo "⚠ïļ Failed to set CLOUDFLARE_API_TOKEN secret (non-fatal)" + fi else echo "NO_DEPLOY is set, skipping deploy" fi diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 28539b0f..bb9db0fd 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -2,7 +2,7 @@ use confidence_resolver::{ assign_logger::AssignLogger, flag_logger, proto::{confidence, google::Struct}, - telemetry::{Telemetry, TelemetrySnapshot}, + telemetry::{Telemetry, TelemetrySnapshot, BUCKET_COUNT}, FlagToApply, Host, ResolvedValue, ResolverState, }; use worker::*; @@ -173,6 +173,7 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { let body = text.unwrap_or_default(); let headers = Headers::new(); headers.set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")?; + headers.set("Cache-Control", "no-store")?; Response::ok(body)?.with_headers(headers).with_cors_headers(&allowed_origin) } }) @@ -357,28 +358,19 @@ pub async fn consume_flag_logs_queue( }) .collect(); - // Accumulate telemetry deltas into KV-backed cumulative snapshot for /metrics - if let Ok(kv) = env.kv("CONFIDENCE_METRICS_KV") { - let _ = update_prometheus_kv(&kv, &logs).await; - } - - // Fetch real CPU time from CF analytics API and append to Prometheus KV. - // Returns a TelemetryData delta when new latency data is available. - let cf_latency = { - let cf_token = env.var("CLOUDFLARE_API_TOKEN").ok().map(|v| v.to_string()); + // Unified KV update: accumulate telemetry deltas + CF analytics in one + // read-modify-write cycle to avoid the second write clobbering the first. + let cf_latency = if let Ok(kv) = env.kv("CONFIDENCE_METRICS_KV") { + let cf_token = env.secret("CLOUDFLARE_API_TOKEN").ok().map(|v| v.to_string()); let cf_account = env.var("CLOUDFLARE_ACCOUNT_ID").ok().map(|v| v.to_string()); let cf_script = env .var("CF_SCRIPT_NAME") .ok() .map(|v| v.to_string()) .unwrap_or_else(|| "confidence-cloudflare-resolver".to_string()); - match (cf_token, cf_account) { - (Some(token), Some(account)) => match env.kv("CONFIDENCE_METRICS_KV") { - Ok(kv) => update_cpu_time_kv(&kv, &token, &account, &cf_script).await, - Err(_) => None, - }, - _ => None, - } + update_metrics_kv(&kv, &logs, cf_token.as_deref(), cf_account.as_deref(), &cf_script).await + } else { + None }; let mut req = flag_logger::aggregate_batch(logs); @@ -414,24 +406,43 @@ fn checkpoint() -> WriteFlagLogsRequest { req } -/// Accumulate telemetry deltas from all isolates into a cumulative -/// `TelemetrySnapshot` stored in KV, then write its Prometheus text -/// representation for the /metrics endpoint. +/// Unified metrics update: accumulate telemetry deltas from the queue batch +/// AND CF analytics CPU time in a single KV read-modify-write cycle. +/// +/// Returns `Option` — the CF latency delta to inject into the +/// `WriteFlagLogsRequest` sent to the backend. `None` if no new latency data. /// /// Note: concurrent queue consumer invocations can race on KV read-modify-write. /// Acceptable for metrics — at worst one batch's deltas are lost, not cumulative state. -async fn update_prometheus_kv(kv: &kv::KvStore, logs: &[WriteFlagLogsRequest]) { +async fn update_metrics_kv( + kv: &kv::KvStore, + logs: &[WriteFlagLogsRequest], + token: Option<&str>, + account: Option<&str>, + script: &str, +) -> Option { + // 1. Read cumulative snapshot once let mut cumulative = match kv.get("snapshot").text().await { Ok(Some(text)) => serde_json::from_str::(&text).unwrap_or_default(), _ => TelemetrySnapshot::default(), }; + // 2. Accumulate telemetry deltas from queue batch for log in logs { if let Some(td) = &log.telemetry_data { cumulative.accumulate_delta(td); } } + // 3. Fetch and accumulate CF analytics CPU time (if credentials available) + let cf_latency = match (token, account) { + (Some(t), Some(a)) => { + apply_cpu_time_to_snapshot(&mut cumulative, kv, t, a, script).await + } + _ => None, + }; + + // 4. Write snapshot and prometheus text once let prom_text = cumulative.to_prometheus( "cf-resolver", &confidence_resolver::telemetry::PrometheusConfig::default(), @@ -443,31 +454,36 @@ async fn update_prometheus_kv(kv: &kv::KvStore, logs: &[WriteFlagLogsRequest]) { if let Ok(builder) = kv.put("prometheus", prom_text) { let _ = builder.execute().await; } + + cf_latency } -/// Query Cloudflare's analytics GraphQL API for Worker CPU time and write -/// the percentiles as Prometheus gauge metrics to KV. /// Return an ISO-8601 timestamp `seconds_ago` in the past. fn since_iso8601(seconds_ago: u64) -> String { let now_ms = js_sys::Date::now() as u64; let then_ms = now_ms.saturating_sub(seconds_ago.saturating_mul(1000)); let d = js_sys::Date::new_0(); d.set_time(then_ms as f64); - // Use Date.toISOString() for proper formatting d.to_iso_string().into() } -/// Query recent CPU time from Cloudflare analytics and update the cumulative -/// `TelemetrySnapshot` in KV so the Prometheus output uses the same metric -/// names as all other providers (`confidence_resolve_latency_microseconds`). +/// Query recent CPU time from Cloudflare analytics and apply it to the +/// cumulative snapshot in place. Returns the latency delta as `TelemetryData` +/// for the backend. /// -/// Since CF Workers freeze timers during sync CPU work, we can't measure -/// latency internally. Instead, we use CF's own analytics (p50 × requests) -/// as an approximation for the histogram's `_sum` and `_count`. +/// Since CF Workers freeze timers during sync CPU work (Spectre mitigation), +/// we can't measure latency internally. Instead, we use CF's own analytics +/// (percentiles × requests) as an approximation for the histogram. /// /// To avoid double-counting, we store the last queried timestamp in KV /// (`cpu_time_cursor`) and only process data points newer than that. -async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script: &str) -> Option { +async fn apply_cpu_time_to_snapshot( + cumulative: &mut TelemetrySnapshot, + kv: &kv::KvStore, + token: &str, + account: &str, + script: &str, +) -> Option { // Read cursor: the last datetime we processed (also serves as rate-limit) let cursor = match kv.get("cpu_time_cursor").text().await { Ok(Some(c)) if !c.is_empty() => c, @@ -475,7 +491,6 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script }; // Rate-limit: skip if cursor is less than 10 seconds old. - // Cursor is an ISO-8601 datetime from CF analytics; parse via JS Date. let cursor_ms = js_sys::Date::parse(&cursor); let now_ms = js_sys::Date::now(); if (now_ms - cursor_ms) < 10_000.0 { @@ -508,10 +523,12 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script if entries.is_empty() { return None } - // Aggregate only NEW data points (after cursor) + let ln_ratio: f64 = core::f64::consts::LN_10 / 18.0; + let max_idx = BUCKET_COUNT - 1; + let mut total_observations: u64 = 0; let mut weighted_sum: u64 = 0; - let mut bucket_adds: Vec<(u64, u64)> = Vec::new(); // (Ξs_value, count) + let mut bucket_adds: Vec<(usize, u64)> = Vec::new(); // (bucket_idx, count) let mut latest_datetime = String::new(); for entry in entries { @@ -524,13 +541,20 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script let p90 = q.and_then(|q| q.get("cpuTimeP90")).and_then(|v| v.as_u64()).unwrap_or(0); let p99 = q.and_then(|q| q.get("cpuTimeP99")).and_then(|v| v.as_u64()).unwrap_or(0); + // Helper: compute bucket index capped at BUCKET_COUNT - 1 + let bucket_idx = |us_value: u64| -> usize { + let idx = if us_value == 0 { 0 } + else { ((us_value as f64).ln() / ln_ratio).floor() as usize }; + idx.min(max_idx) + }; + // Distribute requests into histogram buckets using CF percentiles. // For single-request data points all percentiles are identical, so // we just place 1 observation at p50. For multi-request points we // spread across bands: [0-25%] p25, (25-50%] p50, (50-75%] p75, // (75-90%] p90, (90-99%] p99, (99-100%] p99. if reqs == 1 { - bucket_adds.push((p50, 1)); + bucket_adds.push((bucket_idx(p50), 1)); weighted_sum = weighted_sum.saturating_add(p50); total_observations = total_observations.saturating_add(1); } else { @@ -547,7 +571,7 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script }; remaining = remaining.saturating_sub(count); if count > 0 && value > 0 { - bucket_adds.push((value, count)); + bucket_adds.push((bucket_idx(value), count)); weighted_sum = weighted_sum.saturating_add(value.saturating_mul(count)); total_observations = total_observations.saturating_add(count); } @@ -568,23 +592,11 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script let _ = builder.execute().await; } - // Update the cumulative snapshot with histogram buckets, sum, and count - let mut cumulative = match kv.get("snapshot").text().await { - Ok(Some(text)) => serde_json::from_str::(&text).unwrap_or_default(), - _ => TelemetrySnapshot::default(), - }; - + // Apply to the cumulative snapshot (already read by caller) cumulative.latency.sum = cumulative.latency.sum.saturating_add(weighted_sum); cumulative.latency.count = cumulative.latency.count.saturating_add(total_observations); - // Place observations into exponential histogram buckets (same as other providers) - let ln_ratio: f64 = core::f64::consts::LN_10 / 18.0; - for &(us_value, count) in &bucket_adds { - let idx = if us_value == 0 { - 0 - } else { - ((us_value as f64).ln() / ln_ratio).floor() as usize - }; + for &(idx, count) in &bucket_adds { if idx >= cumulative.latency.buckets.len() { cumulative.latency.buckets.resize(idx.saturating_add(1), 0); } @@ -593,27 +605,13 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script } } - let prom_text = cumulative.to_prometheus( - "cf-resolver", - &confidence_resolver::telemetry::PrometheusConfig::default(), - ); - - if let Ok(builder) = kv.put("snapshot", serde_json::to_string(&cumulative).unwrap_or_default()) { - let _ = builder.execute().await; - } - if let Ok(builder) = kv.put("prometheus", prom_text) { - let _ = builder.execute().await; - } - // Build BucketSpans for the TelemetryData delta sent to the backend. let spans = { - let mut flat: Vec = Vec::new(); - for &(us_value, count) in &bucket_adds { - let idx = if us_value == 0 { 0 } else { - ((us_value as f64).ln() / ln_ratio).floor() as usize - }; - if idx >= flat.len() { flat.resize(idx + 1, 0); } - if let Some(b) = flat.get_mut(idx) { *b = b.saturating_add(count as u32); } + let mut flat: Vec = vec![0; max_idx + 1]; + for &(idx, count) in &bucket_adds { + if let Some(b) = flat.get_mut(idx) { + *b = b.saturating_add(count as u32); + } } // Compress into BucketSpans (contiguous non-zero runs) let mut spans: Vec = Vec::new(); @@ -634,10 +632,11 @@ async fn update_cpu_time_kv(kv: &kv::KvStore, token: &str, account: &str, script spans }; + // Proto field is uint32; clamp to avoid silent wraparound on high-traffic windows Some(TelemetryData { resolve_latency: Some(ResolveLatency { - sum: weighted_sum as u32, - count: total_observations as u32, + sum: weighted_sum.min(u32::MAX as u64) as u32, + count: total_observations.min(u32::MAX as u64) as u32, buckets: spans, ln_ratio, }), diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index b74e9e85..2a99a9cc 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -24,7 +24,7 @@ const LN_RATIO: f64 = core::f64::consts::LN_10 / 18.0; /// Number of buckets needed to cover the full u32 range: ceil(ln(2^32) / LN_RATIO) + 1. /// With LN_RATIO ≈ 0.128 this is 174 buckets (~1.4KB), so we always allocate for /// the entire u32 space — no clamping needed on the client side. -const BUCKET_COUNT: usize = 174; +pub const BUCKET_COUNT: usize = 174; /// Lock-free exponential histogram for positive integer observations. /// From c6e967667f52d4852c2a751efd0086dacc6e1c45 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 12:34:40 +0200 Subject: [PATCH 21/36] feat(cloudflare): use cached percentiles for continuous latency telemetry When CF analytics rate-limit fires, estimate latency from cached percentiles instead of sending nothing. This keeps the Grafana latency graph continuous between analytics fetches. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 124 +++++++++++++++++++++- 1 file changed, 121 insertions(+), 3 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index bb9db0fd..24847e0f 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -435,9 +435,26 @@ async fn update_metrics_kv( } // 3. Fetch and accumulate CF analytics CPU time (if credentials available) + // Count total resolves in this batch for estimated latency fallback. + let batch_resolves: u64 = logs.iter() + .filter_map(|l| l.telemetry_data.as_ref()) + .flat_map(|td| td.resolve_rate.iter()) + .map(|r| r.count as u64) + .sum(); + let cf_latency = match (token, account) { (Some(t), Some(a)) => { - apply_cpu_time_to_snapshot(&mut cumulative, kv, t, a, script).await + let fresh = apply_cpu_time_to_snapshot(&mut cumulative, kv, t, a, script).await; + match fresh { + Some(td) => Some(td), + // No fresh CF analytics — use cached percentiles to estimate + // latency from the batch's resolve count so the backend always + // gets latency data and the Grafana graph stays continuous. + None if batch_resolves > 0 => { + estimate_latency_from_cache(kv, batch_resolves).await + } + _ => None, + } } _ => None, }; @@ -490,10 +507,11 @@ async fn apply_cpu_time_to_snapshot( _ => since_iso8601(300), // bootstrap: last 5 minutes }; - // Rate-limit: skip if cursor is less than 10 seconds old. + // Rate-limit: skip if cursor is less than 60 seconds old. + // CF analytics has ~30s-2min lag, so querying more often just wastes API calls. let cursor_ms = js_sys::Date::parse(&cursor); let now_ms = js_sys::Date::now(); - if (now_ms - cursor_ms) < 10_000.0 { + if (now_ms - cursor_ms) < 60_000.0 { return None; } @@ -530,6 +548,11 @@ async fn apply_cpu_time_to_snapshot( let mut weighted_sum: u64 = 0; let mut bucket_adds: Vec<(usize, u64)> = Vec::new(); // (bucket_idx, count) let mut latest_datetime = String::new(); + let mut avg_p25: u64 = 0; + let mut avg_p50: u64 = 0; + let mut avg_p75: u64 = 0; + let mut avg_p90: u64 = 0; + let mut avg_p99: u64 = 0; for entry in entries { let reqs = entry.pointer("/sum/requests").and_then(|v| v.as_u64()).unwrap_or(0); @@ -540,6 +563,11 @@ async fn apply_cpu_time_to_snapshot( let p75 = q.and_then(|q| q.get("cpuTimeP75")).and_then(|v| v.as_u64()).unwrap_or(0); let p90 = q.and_then(|q| q.get("cpuTimeP90")).and_then(|v| v.as_u64()).unwrap_or(0); let p99 = q.and_then(|q| q.get("cpuTimeP99")).and_then(|v| v.as_u64()).unwrap_or(0); + avg_p25 = avg_p25.saturating_add(p25); + avg_p50 = avg_p50.saturating_add(p50); + avg_p75 = avg_p75.saturating_add(p75); + avg_p90 = avg_p90.saturating_add(p90); + avg_p99 = avg_p99.saturating_add(p99); // Helper: compute bucket index capped at BUCKET_COUNT - 1 let bucket_idx = |us_value: u64| -> usize { @@ -592,6 +620,20 @@ async fn apply_cpu_time_to_snapshot( let _ = builder.execute().await; } + // Cache the latest averaged percentiles for use when rate-limited. + // Average across all entries to get representative percentile values. + let num_entries = entries.len().max(1) as u64; + let cached = serde_json::to_string(&json!({ + "p25": avg_p25 / num_entries, + "p50": avg_p50 / num_entries, + "p75": avg_p75 / num_entries, + "p90": avg_p90 / num_entries, + "p99": avg_p99 / num_entries, + })).unwrap_or_default(); + if let Ok(builder) = kv.put("cf_percentiles", cached) { + let _ = builder.execute().await; + } + // Apply to the cumulative snapshot (already read by caller) cumulative.latency.sum = cumulative.latency.sum.saturating_add(weighted_sum); cumulative.latency.count = cumulative.latency.count.saturating_add(total_observations); @@ -644,6 +686,82 @@ async fn apply_cpu_time_to_snapshot( }) } +/// Construct estimated latency from cached CF percentiles and a known resolve count. +/// Used when the CF analytics rate-limit fires so the backend always gets latency data. +async fn estimate_latency_from_cache(kv: &kv::KvStore, resolve_count: u64) -> Option { + let text = kv.get("cf_percentiles").text().await.ok()??; + let cached: serde_json::Value = serde_json::from_str(&text).ok()?; + let p25 = cached.get("p25").and_then(|v| v.as_u64()).unwrap_or(0); + let p50 = cached.get("p50").and_then(|v| v.as_u64()).unwrap_or(0); + let p75 = cached.get("p75").and_then(|v| v.as_u64()).unwrap_or(0); + let p90 = cached.get("p90").and_then(|v| v.as_u64()).unwrap_or(0); + let p99 = cached.get("p99").and_then(|v| v.as_u64()).unwrap_or(0); + if p50 == 0 { return None } + + let ln_ratio: f64 = core::f64::consts::LN_10 / 18.0; + let max_idx = BUCKET_COUNT - 1; + let bucket_idx = |us: u64| -> usize { + let idx = if us == 0 { 0 } else { ((us as f64).ln() / ln_ratio).floor() as usize }; + idx.min(max_idx) + }; + + // Distribute resolve_count across percentile bands (same shape as fresh data) + let bands: &[(f64, u64)] = &[ + (0.25, p25), (0.25, p50), (0.25, p75), (0.15, p90), (0.09, p99), (0.01, p99), + ]; + let mut weighted_sum: u64 = 0; + let mut total: u64 = 0; + let mut flat: Vec = vec![0; max_idx + 1]; + let mut remaining = resolve_count; + + for (i, &(frac, value)) in bands.iter().enumerate() { + let count = if i == bands.len() - 1 { + remaining + } else { + let c = (resolve_count as f64 * frac) as u64; + c.min(remaining) + }; + remaining = remaining.saturating_sub(count); + if count > 0 && value > 0 { + let idx = bucket_idx(value); + if let Some(b) = flat.get_mut(idx) { + *b = b.saturating_add(count as u32); + } + weighted_sum = weighted_sum.saturating_add(value.saturating_mul(count)); + total = total.saturating_add(count); + } + } + + if total == 0 { return None } + + // Compress into BucketSpans + let mut spans: Vec = Vec::new(); + let mut current: Option<(i32, Vec)> = None; + for (i, &v) in flat.iter().enumerate() { + if v > 0 { + match &mut current { + Some((_, counts)) => counts.push(v), + None => current = Some((i as i32, vec![v])), + } + } else if let Some((offset, counts)) = current.take() { + spans.push(BucketSpan { offset, counts }); + } + } + if let Some((offset, counts)) = current { + spans.push(BucketSpan { offset, counts }); + } + + Some(TelemetryData { + resolve_latency: Some(ResolveLatency { + sum: weighted_sum.min(u32::MAX as u64) as u32, + count: total.min(u32::MAX as u64) as u32, + buckets: spans, + ln_ratio, + }), + ..Default::default() + }) +} + async fn send_flags_logs(client_secret: &str, message: WriteFlagLogsRequest) -> Result { let resolve_url = "https://resolver.confidence.dev/v1/clientFlagLogs:write"; let mut init = RequestInit::new(); From 545315d2b12cc707ed4959dfa942a63a79b809cf Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 12:49:23 +0200 Subject: [PATCH 22/36] feat(cloudflare): measure resolve latency inline via scheduler.wait(0) scheduler.wait(0) unfreezes CF Workers' Spectre-mitigated timers with zero overhead, allowing Date.now() to reflect actual CPU time. This enables inline latency measurement identical to other providers, removing the need for the CF GraphQL analytics API dependency. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 74 ++++++++++++++--------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 24847e0f..b394a385 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -33,11 +33,9 @@ use once_cell::sync::Lazy; use std::cell::RefCell; /// Per-request resolve metrics captured in the hot path, recorded in wait_until. -/// Note: CF Workers freeze all timer APIs during sync CPU work (Spectre mitigation), -/// so we cannot measure resolve latency internally. CPU time is sourced from -/// Cloudflare's analytics API in the queue consumer instead. struct ResolveMetrics { reasons: Vec, + elapsed_us: u64, } thread_local! { @@ -216,7 +214,12 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .evaluation_context .clone() .unwrap_or_default(); - match state.get_resolver::( + + // Start timer before resolve. CF Workers freeze timers + // during sync CPU, but scheduler.wait(0) unfreezes them. + let t0 = js_sys::Date::now(); + + let (reasons, resp) = match state.get_resolver::( &resolver_request.client_secret, evaluation_context, &Bytes::from(STANDARD.decode(ENCRYPTION_KEY_BASE64).unwrap()), @@ -235,46 +238,57 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .iter() .map(|f| f.reason()) .collect(); - PENDING_METRICS.with(|m| { - m.borrow_mut().push(ResolveMetrics { reasons }); - }); - Response::from_json(&response)? - .with_cors_headers(&allowed_origin) + (reasons, Response::from_json(&response)? + .with_cors_headers(&allowed_origin)) } None => { - PENDING_METRICS.with(|m| { - m.borrow_mut().push(ResolveMetrics { - reasons: vec![ResolveReason::Error], - }); - }); + (vec![ResolveReason::Error], Response::error( "Unexpected suspended response", 500, )? - .with_cors_headers(&allowed_origin) + .with_cors_headers(&allowed_origin)) } } } Err(msg) => { - PENDING_METRICS.with(|m| { - m.borrow_mut().push(ResolveMetrics { - reasons: vec![ResolveReason::Error], - }); - }); + (vec![ResolveReason::Error], Response::error(msg, 500)? - .with_cors_headers(&allowed_origin) + .with_cors_headers(&allowed_origin)) } } } Err(msg) => { - PENDING_METRICS.with(|m| { - m.borrow_mut().push(ResolveMetrics { - reasons: vec![ResolveReason::Error], - }); - }); - Response::error(msg, 500)?.with_cors_headers(&allowed_origin) + (vec![ResolveReason::Error], + Response::error(msg, 500)?.with_cors_headers(&allowed_origin)) + } + }; + + // Unfreeze timer: scheduler.wait(0) yields to the + // runtime with zero delay, advancing the clock. + { + let scheduler = js_sys::Reflect::get( + &js_sys::global(), &wasm_bindgen::JsValue::from_str("scheduler") + ).unwrap_or(wasm_bindgen::JsValue::UNDEFINED); + if !scheduler.is_undefined() { + let wait = js_sys::Reflect::get( + &scheduler, &wasm_bindgen::JsValue::from_str("wait") + ).unwrap_or(wasm_bindgen::JsValue::UNDEFINED); + if let Ok(promise) = js_sys::Function::from(wait) + .call1(&scheduler, &wasm_bindgen::JsValue::from(0)) + { + let _ = wasm_bindgen_futures::JsFuture::from( + js_sys::Promise::from(promise) + ).await; + } } } + let elapsed_us = ((js_sys::Date::now() - t0) * 1000.0).max(0.0) as u64; + + PENDING_METRICS.with(|m| { + m.borrow_mut().push(ResolveMetrics { reasons, elapsed_us }); + }); + resp } "flags:apply" => { let body_bytes: Vec = req.bytes().await?; @@ -313,12 +327,12 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .await; // Use ctx.waitUntil to run logging and telemetry after response is returned. - // Note: resolve latency cannot be measured inside Workers (timer APIs are - // frozen during sync CPU work). CPU time is sourced from Cloudflare's - // analytics API in the queue consumer instead. ctx.wait_until(async move { PENDING_METRICS.with(|m| { for metrics in m.borrow_mut().drain(..) { + if metrics.elapsed_us > 0 { + TELEMETRY.record_latency_us(metrics.elapsed_us.min(u32::MAX as u64) as u32); + } for reason in metrics.reasons { TELEMETRY.mark_resolve(reason); } From 84ca08817a8c4963f4444eb10e2828dec40ca09b Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 12:57:53 +0200 Subject: [PATCH 23/36] refactor(cloudflare): remove CF GraphQL analytics dependency Replace the entire Cloudflare GraphQL analytics API machinery with inline latency measurement via scheduler.wait(0). This zero-overhead yield unfreezes CF Workers' Spectre-mitigated timers, enabling Date.now()-based CPU time measurement identical to other providers. Removes ~360 lines: GraphQL query, cursor-based pagination, rate-limiting, percentile-to-histogram distribution, cached percentile estimation, and the CLOUDFLARE_API_TOKEN dependency. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 371 +--------------------- 1 file changed, 11 insertions(+), 360 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index b394a385..a368f5e5 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -2,7 +2,7 @@ use confidence_resolver::{ assign_logger::AssignLogger, flag_logger, proto::{confidence, google::Struct}, - telemetry::{Telemetry, TelemetrySnapshot, BUCKET_COUNT}, + telemetry::{Telemetry, TelemetrySnapshot}, FlagToApply, Host, ResolvedValue, ResolverState, }; use worker::*; @@ -17,10 +17,7 @@ use serde_json::json; use confidence::flags::resolver::v1::{ApplyFlagsRequest, ApplyFlagsResponse, ResolveFlagsRequest}; use confidence_resolver::proto::confidence::flags::resolver::v1::{ResolveProcessRequest, ResolveReason}; -use confidence_resolver::proto::confidence::flags::resolver::v1::{ - TelemetryData, - telemetry_data::{BucketSpan, ResolveLatency}, -}; + static RESOLVE_LOGGER: LazyLock> = LazyLock::new(ResolveLogger::new); static ASSIGN_LOGGER: LazyLock = LazyLock::new(AssignLogger::new); @@ -372,33 +369,12 @@ pub async fn consume_flag_logs_queue( }) .collect(); - // Unified KV update: accumulate telemetry deltas + CF analytics in one - // read-modify-write cycle to avoid the second write clobbering the first. - let cf_latency = if let Ok(kv) = env.kv("CONFIDENCE_METRICS_KV") { - let cf_token = env.secret("CLOUDFLARE_API_TOKEN").ok().map(|v| v.to_string()); - let cf_account = env.var("CLOUDFLARE_ACCOUNT_ID").ok().map(|v| v.to_string()); - let cf_script = env - .var("CF_SCRIPT_NAME") - .ok() - .map(|v| v.to_string()) - .unwrap_or_else(|| "confidence-cloudflare-resolver".to_string()); - update_metrics_kv(&kv, &logs, cf_token.as_deref(), cf_account.as_deref(), &cf_script).await - } else { - None - }; - - let mut req = flag_logger::aggregate_batch(logs); - // Inject CF analytics latency into the telemetry sent to the backend - if let Some(latency_td) = cf_latency { - match &mut req.telemetry_data { - Some(td) => { - td.resolve_latency = latency_td.resolve_latency; - } - None => { - req.telemetry_data = Some(latency_td); - } - } + // Accumulate telemetry deltas into KV-backed cumulative snapshot for /metrics. + if let Ok(kv) = env.kv("CONFIDENCE_METRICS_KV") { + update_prometheus_kv(&kv, &logs).await; } + + let req = flag_logger::aggregate_batch(logs); send_flags_logs(CONFIDENCE_CLIENT_SECRET.get().unwrap().as_str(), req).await?; } @@ -420,60 +396,24 @@ fn checkpoint() -> WriteFlagLogsRequest { req } -/// Unified metrics update: accumulate telemetry deltas from the queue batch -/// AND CF analytics CPU time in a single KV read-modify-write cycle. -/// -/// Returns `Option` — the CF latency delta to inject into the -/// `WriteFlagLogsRequest` sent to the backend. `None` if no new latency data. +/// Accumulate telemetry deltas from all isolates into a cumulative +/// `TelemetrySnapshot` stored in KV, then write its Prometheus text +/// representation for the /metrics endpoint. /// /// Note: concurrent queue consumer invocations can race on KV read-modify-write. /// Acceptable for metrics — at worst one batch's deltas are lost, not cumulative state. -async fn update_metrics_kv( - kv: &kv::KvStore, - logs: &[WriteFlagLogsRequest], - token: Option<&str>, - account: Option<&str>, - script: &str, -) -> Option { - // 1. Read cumulative snapshot once +async fn update_prometheus_kv(kv: &kv::KvStore, logs: &[WriteFlagLogsRequest]) { let mut cumulative = match kv.get("snapshot").text().await { Ok(Some(text)) => serde_json::from_str::(&text).unwrap_or_default(), _ => TelemetrySnapshot::default(), }; - // 2. Accumulate telemetry deltas from queue batch for log in logs { if let Some(td) = &log.telemetry_data { cumulative.accumulate_delta(td); } } - // 3. Fetch and accumulate CF analytics CPU time (if credentials available) - // Count total resolves in this batch for estimated latency fallback. - let batch_resolves: u64 = logs.iter() - .filter_map(|l| l.telemetry_data.as_ref()) - .flat_map(|td| td.resolve_rate.iter()) - .map(|r| r.count as u64) - .sum(); - - let cf_latency = match (token, account) { - (Some(t), Some(a)) => { - let fresh = apply_cpu_time_to_snapshot(&mut cumulative, kv, t, a, script).await; - match fresh { - Some(td) => Some(td), - // No fresh CF analytics — use cached percentiles to estimate - // latency from the batch's resolve count so the backend always - // gets latency data and the Grafana graph stays continuous. - None if batch_resolves > 0 => { - estimate_latency_from_cache(kv, batch_resolves).await - } - _ => None, - } - } - _ => None, - }; - - // 4. Write snapshot and prometheus text once let prom_text = cumulative.to_prometheus( "cf-resolver", &confidence_resolver::telemetry::PrometheusConfig::default(), @@ -485,295 +425,6 @@ async fn update_metrics_kv( if let Ok(builder) = kv.put("prometheus", prom_text) { let _ = builder.execute().await; } - - cf_latency -} - -/// Return an ISO-8601 timestamp `seconds_ago` in the past. -fn since_iso8601(seconds_ago: u64) -> String { - let now_ms = js_sys::Date::now() as u64; - let then_ms = now_ms.saturating_sub(seconds_ago.saturating_mul(1000)); - let d = js_sys::Date::new_0(); - d.set_time(then_ms as f64); - d.to_iso_string().into() -} - -/// Query recent CPU time from Cloudflare analytics and apply it to the -/// cumulative snapshot in place. Returns the latency delta as `TelemetryData` -/// for the backend. -/// -/// Since CF Workers freeze timers during sync CPU work (Spectre mitigation), -/// we can't measure latency internally. Instead, we use CF's own analytics -/// (percentiles × requests) as an approximation for the histogram. -/// -/// To avoid double-counting, we store the last queried timestamp in KV -/// (`cpu_time_cursor`) and only process data points newer than that. -async fn apply_cpu_time_to_snapshot( - cumulative: &mut TelemetrySnapshot, - kv: &kv::KvStore, - token: &str, - account: &str, - script: &str, -) -> Option { - // Read cursor: the last datetime we processed (also serves as rate-limit) - let cursor = match kv.get("cpu_time_cursor").text().await { - Ok(Some(c)) if !c.is_empty() => c, - _ => since_iso8601(300), // bootstrap: last 5 minutes - }; - - // Rate-limit: skip if cursor is less than 60 seconds old. - // CF analytics has ~30s-2min lag, so querying more often just wastes API calls. - let cursor_ms = js_sys::Date::parse(&cursor); - let now_ms = js_sys::Date::now(); - if (now_ms - cursor_ms) < 60_000.0 { - return None; - } - - let gql = format!( - "{{ viewer {{ accounts(filter: {{accountTag: \"{account}\"}}) {{ workersInvocationsAdaptive(limit: 50, filter: {{scriptName: \"{script}\", datetime_gt: \"{cursor}\"}}, orderBy: [datetime_ASC]) {{ dimensions {{ datetime }} quantiles {{ cpuTimeP25 cpuTimeP50 cpuTimeP75 cpuTimeP90 cpuTimeP99 }} sum {{ requests }} }} }} }} }}" - ); - let query = serde_json::to_string(&json!({ "query": gql })).unwrap_or_default(); - - let url = "https://api.cloudflare.com/client/v4/graphql"; - let mut init = RequestInit::new(); - let headers = Headers::new(); - let _ = headers.set("Authorization", &format!("Bearer {token}")); - let _ = headers.set("Content-Type", "application/json"); - init.with_headers(headers); - init.with_method(Method::Post); - init.with_body(Some(query.into())); - - let Ok(request) = Request::new_with_init(url, &init) else { return None }; - let Ok(mut resp) = Fetch::Request(request).send().await else { return None }; - let Ok(body) = resp.text().await else { return None }; - let Ok(data) = serde_json::from_str::(&body) else { return None }; - - let Some(entries) = data - .pointer("/data/viewer/accounts/0/workersInvocationsAdaptive") - .and_then(|v| v.as_array()) - else { return None }; - - if entries.is_empty() { return None } - - let ln_ratio: f64 = core::f64::consts::LN_10 / 18.0; - let max_idx = BUCKET_COUNT - 1; - - let mut total_observations: u64 = 0; - let mut weighted_sum: u64 = 0; - let mut bucket_adds: Vec<(usize, u64)> = Vec::new(); // (bucket_idx, count) - let mut latest_datetime = String::new(); - let mut avg_p25: u64 = 0; - let mut avg_p50: u64 = 0; - let mut avg_p75: u64 = 0; - let mut avg_p90: u64 = 0; - let mut avg_p99: u64 = 0; - - for entry in entries { - let reqs = entry.pointer("/sum/requests").and_then(|v| v.as_u64()).unwrap_or(0); - if reqs == 0 { continue } - let q = entry.pointer("/quantiles"); - let p25 = q.and_then(|q| q.get("cpuTimeP25")).and_then(|v| v.as_u64()).unwrap_or(0); - let p50 = q.and_then(|q| q.get("cpuTimeP50")).and_then(|v| v.as_u64()).unwrap_or(0); - let p75 = q.and_then(|q| q.get("cpuTimeP75")).and_then(|v| v.as_u64()).unwrap_or(0); - let p90 = q.and_then(|q| q.get("cpuTimeP90")).and_then(|v| v.as_u64()).unwrap_or(0); - let p99 = q.and_then(|q| q.get("cpuTimeP99")).and_then(|v| v.as_u64()).unwrap_or(0); - avg_p25 = avg_p25.saturating_add(p25); - avg_p50 = avg_p50.saturating_add(p50); - avg_p75 = avg_p75.saturating_add(p75); - avg_p90 = avg_p90.saturating_add(p90); - avg_p99 = avg_p99.saturating_add(p99); - - // Helper: compute bucket index capped at BUCKET_COUNT - 1 - let bucket_idx = |us_value: u64| -> usize { - let idx = if us_value == 0 { 0 } - else { ((us_value as f64).ln() / ln_ratio).floor() as usize }; - idx.min(max_idx) - }; - - // Distribute requests into histogram buckets using CF percentiles. - // For single-request data points all percentiles are identical, so - // we just place 1 observation at p50. For multi-request points we - // spread across bands: [0-25%] p25, (25-50%] p50, (50-75%] p75, - // (75-90%] p90, (90-99%] p99, (99-100%] p99. - if reqs == 1 { - bucket_adds.push((bucket_idx(p50), 1)); - weighted_sum = weighted_sum.saturating_add(p50); - total_observations = total_observations.saturating_add(1); - } else { - let bands: &[(f64, u64)] = &[ - (0.25, p25), (0.25, p50), (0.25, p75), (0.15, p90), (0.09, p99), (0.01, p99), - ]; - let mut remaining = reqs; - for (i, &(frac, value)) in bands.iter().enumerate() { - let count = if i == bands.len() - 1 { - remaining - } else { - let c = (reqs as f64 * frac) as u64; - c.min(remaining) - }; - remaining = remaining.saturating_sub(count); - if count > 0 && value > 0 { - bucket_adds.push((bucket_idx(value), count)); - weighted_sum = weighted_sum.saturating_add(value.saturating_mul(count)); - total_observations = total_observations.saturating_add(count); - } - } - } - - if let Some(dt) = entry.pointer("/dimensions/datetime").and_then(|v| v.as_str()) { - if dt > latest_datetime.as_str() { - latest_datetime = dt.to_string(); - } - } - } - - if total_observations == 0 || latest_datetime.is_empty() { return None } - - // Update cursor to latest processed timestamp - if let Ok(builder) = kv.put("cpu_time_cursor", &latest_datetime) { - let _ = builder.execute().await; - } - - // Cache the latest averaged percentiles for use when rate-limited. - // Average across all entries to get representative percentile values. - let num_entries = entries.len().max(1) as u64; - let cached = serde_json::to_string(&json!({ - "p25": avg_p25 / num_entries, - "p50": avg_p50 / num_entries, - "p75": avg_p75 / num_entries, - "p90": avg_p90 / num_entries, - "p99": avg_p99 / num_entries, - })).unwrap_or_default(); - if let Ok(builder) = kv.put("cf_percentiles", cached) { - let _ = builder.execute().await; - } - - // Apply to the cumulative snapshot (already read by caller) - cumulative.latency.sum = cumulative.latency.sum.saturating_add(weighted_sum); - cumulative.latency.count = cumulative.latency.count.saturating_add(total_observations); - - for &(idx, count) in &bucket_adds { - if idx >= cumulative.latency.buckets.len() { - cumulative.latency.buckets.resize(idx.saturating_add(1), 0); - } - if let Some(b) = cumulative.latency.buckets.get_mut(idx) { - *b = b.saturating_add(count); - } - } - - // Build BucketSpans for the TelemetryData delta sent to the backend. - let spans = { - let mut flat: Vec = vec![0; max_idx + 1]; - for &(idx, count) in &bucket_adds { - if let Some(b) = flat.get_mut(idx) { - *b = b.saturating_add(count as u32); - } - } - // Compress into BucketSpans (contiguous non-zero runs) - let mut spans: Vec = Vec::new(); - let mut current: Option<(i32, Vec)> = None; - for (i, &v) in flat.iter().enumerate() { - if v > 0 { - match &mut current { - Some((_, counts)) => counts.push(v), - None => current = Some((i as i32, vec![v])), - } - } else if let Some((offset, counts)) = current.take() { - spans.push(BucketSpan { offset, counts }); - } - } - if let Some((offset, counts)) = current { - spans.push(BucketSpan { offset, counts }); - } - spans - }; - - // Proto field is uint32; clamp to avoid silent wraparound on high-traffic windows - Some(TelemetryData { - resolve_latency: Some(ResolveLatency { - sum: weighted_sum.min(u32::MAX as u64) as u32, - count: total_observations.min(u32::MAX as u64) as u32, - buckets: spans, - ln_ratio, - }), - ..Default::default() - }) -} - -/// Construct estimated latency from cached CF percentiles and a known resolve count. -/// Used when the CF analytics rate-limit fires so the backend always gets latency data. -async fn estimate_latency_from_cache(kv: &kv::KvStore, resolve_count: u64) -> Option { - let text = kv.get("cf_percentiles").text().await.ok()??; - let cached: serde_json::Value = serde_json::from_str(&text).ok()?; - let p25 = cached.get("p25").and_then(|v| v.as_u64()).unwrap_or(0); - let p50 = cached.get("p50").and_then(|v| v.as_u64()).unwrap_or(0); - let p75 = cached.get("p75").and_then(|v| v.as_u64()).unwrap_or(0); - let p90 = cached.get("p90").and_then(|v| v.as_u64()).unwrap_or(0); - let p99 = cached.get("p99").and_then(|v| v.as_u64()).unwrap_or(0); - if p50 == 0 { return None } - - let ln_ratio: f64 = core::f64::consts::LN_10 / 18.0; - let max_idx = BUCKET_COUNT - 1; - let bucket_idx = |us: u64| -> usize { - let idx = if us == 0 { 0 } else { ((us as f64).ln() / ln_ratio).floor() as usize }; - idx.min(max_idx) - }; - - // Distribute resolve_count across percentile bands (same shape as fresh data) - let bands: &[(f64, u64)] = &[ - (0.25, p25), (0.25, p50), (0.25, p75), (0.15, p90), (0.09, p99), (0.01, p99), - ]; - let mut weighted_sum: u64 = 0; - let mut total: u64 = 0; - let mut flat: Vec = vec![0; max_idx + 1]; - let mut remaining = resolve_count; - - for (i, &(frac, value)) in bands.iter().enumerate() { - let count = if i == bands.len() - 1 { - remaining - } else { - let c = (resolve_count as f64 * frac) as u64; - c.min(remaining) - }; - remaining = remaining.saturating_sub(count); - if count > 0 && value > 0 { - let idx = bucket_idx(value); - if let Some(b) = flat.get_mut(idx) { - *b = b.saturating_add(count as u32); - } - weighted_sum = weighted_sum.saturating_add(value.saturating_mul(count)); - total = total.saturating_add(count); - } - } - - if total == 0 { return None } - - // Compress into BucketSpans - let mut spans: Vec = Vec::new(); - let mut current: Option<(i32, Vec)> = None; - for (i, &v) in flat.iter().enumerate() { - if v > 0 { - match &mut current { - Some((_, counts)) => counts.push(v), - None => current = Some((i as i32, vec![v])), - } - } else if let Some((offset, counts)) = current.take() { - spans.push(BucketSpan { offset, counts }); - } - } - if let Some((offset, counts)) = current { - spans.push(BucketSpan { offset, counts }); - } - - Some(TelemetryData { - resolve_latency: Some(ResolveLatency { - sum: weighted_sum.min(u32::MAX as u64) as u32, - count: total.min(u32::MAX as u64) as u32, - buckets: spans, - ln_ratio, - }), - ..Default::default() - }) } async fn send_flags_logs(client_secret: &str, message: WriteFlagLogsRequest) -> Result { From 5903d4e2e09a3d880a86af1ce22d38231dd295f5 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 13:08:03 +0200 Subject: [PATCH 24/36] chore(cloudflare): clean up after GraphQL removal Remove deployer vars (CLOUDFLARE_ACCOUNT_ID, CF_SCRIPT_NAME, CLOUDFLARE_API_TOKEN secret) that were only needed for the CF analytics API. Revert BUCKET_COUNT to non-pub. Remove unused imports. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/deployer/script.sh | 13 +------------ confidence-cloudflare-resolver/src/lib.rs | 1 - confidence-resolver/src/telemetry.rs | 2 +- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/confidence-cloudflare-resolver/deployer/script.sh b/confidence-cloudflare-resolver/deployer/script.sh index 8e326ab6..f965c088 100755 --- a/confidence-cloudflare-resolver/deployer/script.sh +++ b/confidence-cloudflare-resolver/deployer/script.sh @@ -442,9 +442,7 @@ if [ -n "$ALLOWED_ORIGIN_TOML" ] || [ -n "$ETAG_TOML" ] || [ -n "$DEPLOYER_VERSI sed -i.tmp '/^RESOLVER_VERSION *= *.*$/d' wrangler.toml || true sed -i.tmp '/^DEPLOYER_VERSION *= *.*$/d' wrangler.toml || true sed -i.tmp '/^CONFIDENCE_CLIENT_SECRET *= *.*$/d' wrangler.toml || true - sed -i.tmp '/^CLOUDFLARE_ACCOUNT_ID *= *.*$/d' wrangler.toml || true - sed -i.tmp '/^CF_SCRIPT_NAME *= *.*$/d' wrangler.toml || true - awk -v allowed="${ALLOWED_ORIGIN_TOML}" -v etag="${ETAG_TOML}" -v version="${DEPLOYER_VERSION}" -v client_secret="${CLIENT_SECRET_TOML}" -v cf_account="${CLOUDFLARE_ACCOUNT_ID}" -v cf_script="${WORKER_NAME}" ' + awk -v allowed="${ALLOWED_ORIGIN_TOML}" -v etag="${ETAG_TOML}" -v version="${DEPLOYER_VERSION}" -v client_secret="${CLIENT_SECRET_TOML}" ' BEGIN{inserted=0} { print $0 @@ -453,8 +451,6 @@ if [ -n "$ALLOWED_ORIGIN_TOML" ] || [ -n "$ETAG_TOML" ] || [ -n "$DEPLOYER_VERSI if (etag != "") print "RESOLVER_STATE_ETAG = \"" etag "\"" if (version != "") print "DEPLOYER_VERSION = \"" version "\"" if (client_secret != "") print "CONFIDENCE_CLIENT_SECRET = \"" client_secret "\"" - if (cf_account != "") print "CLOUDFLARE_ACCOUNT_ID = \"" cf_account "\"" - if (cf_script != "") print "CF_SCRIPT_NAME = \"" cf_script "\"" inserted=1 } } @@ -533,13 +529,6 @@ add_wrangler_deploy_args_from_lines "WRANGLER_DEPLOY_ARGS" "$WRANGLER_DEPLOY_ARG if test -z "$NO_DEPLOY"; then wrangler deploy "${WRANGLER_DEPLOY_ARGS_ARRAY[@]}" - # Set CLOUDFLARE_API_TOKEN as an encrypted secret (not a plaintext var) - if [ -n "$CLOUDFLARE_API_TOKEN" ]; then - echo "🔒 Setting CLOUDFLARE_API_TOKEN as encrypted Worker secret..." - echo "$CLOUDFLARE_API_TOKEN" | wrangler secret put CLOUDFLARE_API_TOKEN 2>/dev/null \ - && echo "✅ CLOUDFLARE_API_TOKEN set as encrypted secret" \ - || echo "⚠ïļ Failed to set CLOUDFLARE_API_TOKEN secret (non-fatal)" - fi else echo "NO_DEPLOY is set, skipping deploy" fi diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index a368f5e5..74c3d354 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -18,7 +18,6 @@ use serde_json::json; use confidence::flags::resolver::v1::{ApplyFlagsRequest, ApplyFlagsResponse, ResolveFlagsRequest}; use confidence_resolver::proto::confidence::flags::resolver::v1::{ResolveProcessRequest, ResolveReason}; - static RESOLVE_LOGGER: LazyLock> = LazyLock::new(ResolveLogger::new); static ASSIGN_LOGGER: LazyLock = LazyLock::new(AssignLogger::new); static TELEMETRY: LazyLock = LazyLock::new(Telemetry::new); diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index 2a99a9cc..b74e9e85 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -24,7 +24,7 @@ const LN_RATIO: f64 = core::f64::consts::LN_10 / 18.0; /// Number of buckets needed to cover the full u32 range: ceil(ln(2^32) / LN_RATIO) + 1. /// With LN_RATIO ≈ 0.128 this is 174 buckets (~1.4KB), so we always allocate for /// the entire u32 space — no clamping needed on the client side. -pub const BUCKET_COUNT: usize = 174; +const BUCKET_COUNT: usize = 174; /// Lock-free exponential histogram for positive integer observations. /// From 6afd9482243b71d7c7f1c1734ea99f92a37c0ba4 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 13:16:34 +0200 Subject: [PATCH 25/36] chore: re-sync WASM module for Go provider after rebase Co-Authored-By: Claude Opus 4.6 (1M context) --- .../assets/confidence_resolver.wasm | Bin 483141 -> 483040 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm b/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm index 02ed19fd941693fc9eb710dfbf74c8075e78d918..88ff1926ecd3a7f70e82d5b80ce591bcaf0afca1 100755 GIT binary patch delta 84916 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 MXEY{ws`)|?k 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

yF&IPV*u7dsU1McJQQ|_CY^{n%( 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 From 3c46b13e23e6c960fe961bbd650251f96f324384 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 14:10:04 +0200 Subject: [PATCH 26/36] fix(cloudflare): address review comments (#6-#9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auth-gate /metrics with ClientSecret header (#9) - Extract Prometheus content-type to named constant (#6) - Record 0Ξs sub-ms observations instead of dropping them (#7) - Use Option for elapsed_us: None when scheduler API unavailable, Some(0) for measured sub-ms resolves (#8) Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 36 ++++++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 74c3d354..89b77c6a 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -31,13 +31,18 @@ use std::cell::RefCell; /// Per-request resolve metrics captured in the hot path, recorded in wait_until. struct ResolveMetrics { reasons: Vec, - elapsed_us: u64, + /// `Some(Ξs)` when scheduler.wait(0) unfroze the timer, `None` if the + /// scheduler API was unavailable (latency not recorded, resolve unaffected). + elapsed_us: Option, } thread_local! { static PENDING_METRICS: RefCell> = const { RefCell::new(Vec::new()) }; } +/// Prometheus exposition format content type (version 0.0.4). +const PROMETHEUS_CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8"; + /// SetResolverStateRequest message from the CDN. /// This matches the protobuf message format returned by the CDN. #[derive(Clone, PartialEq, Message)] @@ -157,16 +162,26 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { let router = Router::new(); let response = router - .get_async("/metrics", |_req, ctx| { + .get_async("/metrics", |req, ctx| { let allowed_origin = allowed_origin_env.clone(); async move { + // Require client secret — metrics are not public. + if let Some(expected) = CONFIDENCE_CLIENT_SECRET.get() { + let authorized = req.headers().get("Authorization").ok().flatten() + .map(|v| v.strip_prefix("ClientSecret ").unwrap_or("") == expected.as_str()) + .unwrap_or(false); + if !authorized { + return Response::error("Unauthorized", 401)? + .with_cors_headers(&allowed_origin); + } + } let text = match ctx.env.kv("CONFIDENCE_METRICS_KV") { Ok(kv) => kv.get("prometheus").text().await.unwrap_or(None), Err(_) => None, }; let body = text.unwrap_or_default(); let headers = Headers::new(); - headers.set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")?; + headers.set("Content-Type", PROMETHEUS_CONTENT_TYPE)?; headers.set("Cache-Control", "no-store")?; Response::ok(body)?.with_headers(headers).with_cors_headers(&allowed_origin) } @@ -262,7 +277,10 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { // Unfreeze timer: scheduler.wait(0) yields to the // runtime with zero delay, advancing the clock. - { + // If CF removes or changes the scheduler API, all + // lookups fall through gracefully: elapsed_us is None, + // the resolve still succeeds, we just lose latency data. + let elapsed_us = { let scheduler = js_sys::Reflect::get( &js_sys::global(), &wasm_bindgen::JsValue::from_str("scheduler") ).unwrap_or(wasm_bindgen::JsValue::UNDEFINED); @@ -277,9 +295,11 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { js_sys::Promise::from(promise) ).await; } + Some(((js_sys::Date::now() - t0) * 1000.0).max(0.0) as u32) + } else { + None } - } - let elapsed_us = ((js_sys::Date::now() - t0) * 1000.0).max(0.0) as u64; + }; PENDING_METRICS.with(|m| { m.borrow_mut().push(ResolveMetrics { reasons, elapsed_us }); @@ -326,8 +346,8 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { ctx.wait_until(async move { PENDING_METRICS.with(|m| { for metrics in m.borrow_mut().drain(..) { - if metrics.elapsed_us > 0 { - TELEMETRY.record_latency_us(metrics.elapsed_us.min(u32::MAX as u64) as u32); + if let Some(us) = metrics.elapsed_us { + TELEMETRY.record_latency_us(us); } for reason in metrics.reasons { TELEMETRY.mark_resolve(reason); From e5be0182e9769c368c71f6c958a8a1988265d37b Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 15:27:59 +0200 Subject: [PATCH 27/36] docs(cloudflare): add telemetry docs and DISABLE_METRICS option - Document latency measurement (scheduler.wait(0), 1ms precision) - Document /metrics endpoint auth (ClientSecret header) - Document KV store usage and DISABLE_METRICS deployer option - Add DISABLE_METRICS env var to skip KV creation when scraping not needed Co-Authored-By: Claude Opus 4.6 (1M context) --- .../deployer/README.md | 28 +++++++++++++++++++ .../deployer/script.sh | 8 ++++++ 2 files changed, 36 insertions(+) diff --git a/confidence-cloudflare-resolver/deployer/README.md b/confidence-cloudflare-resolver/deployer/README.md index f3592282..9e8bb74f 100644 --- a/confidence-cloudflare-resolver/deployer/README.md +++ b/confidence-cloudflare-resolver/deployer/README.md @@ -63,6 +63,7 @@ The deployer automatically: | `WRANGLER_DEPLOY_MESSAGE` | Value passed to `wrangler deploy --message` | | `WRANGLER_DEPLOY_ARGS` | Additional newline-separated arguments passed to `wrangler deploy` | | `WRANGLER_DEPLOY_ARGS_FILE` | Path to a file containing additional `wrangler deploy` arguments, one argument per line | +| `DISABLE_METRICS` | Skip KV namespace creation and disable the `/metrics` endpoint. Reduces resource usage when Prometheus scraping is not needed | ### Extending Wrangler Configuration @@ -120,6 +121,33 @@ When integrating with the Cloudflare resolver, you have two options: For more details on integration, including code examples using the [`@spotify-confidence/sdk`](https://github.com/spotify/confidence-sdk-js), see the [Confidence documentation](https://confidence.spotify.com/docs/sdks/edge/cloudflare#cloudflare-workers). +## Telemetry & Metrics + +The resolver collects telemetry and exposes a Prometheus-compatible `/metrics` endpoint using the same metric names as all other Confidence providers (`confidence_resolve_latency_microseconds`, `confidence_resolves_total`), enabling shared Grafana dashboards. + +### How latency is measured + +Cloudflare Workers freeze `Date.now()` and `performance.now()` during synchronous CPU work (Spectre mitigation). The resolver uses `scheduler.wait(0)` — a zero-delay yield to the runtime — to unfreeze the clock after each resolve. This provides 1ms resolution with no measurable overhead. Grafana's `histogram_quantile()` interpolates between histogram buckets to produce sub-millisecond reporting. + +### `/metrics` endpoint + +Requires authentication: + +```bash +curl -H "Authorization: ClientSecret " \ + https://.workers.dev/metrics +``` + +Returns Prometheus exposition format with: +- `confidence_resolve_latency_microseconds` — histogram (sum, count, cumulative `le` buckets) +- `confidence_resolves_total` — counter by resolve reason + +Metrics are accumulated in a KV namespace (`CONFIDENCE_METRICS_KV`) created automatically by the deployer. Set `DISABLE_METRICS` to skip KV creation and disable the endpoint when Prometheus scraping is not needed. + +### Backend telemetry + +Resolve rates and latency are also sent to the Confidence backend via `WriteFlagLogsRequest`, appearing on the shared Grafana dashboard alongside other providers. + ## Limitations * **Sticky assignments**: Not currently supported with the Cloudflare resolver. Flags with sticky assignment rules will return "flag not found". diff --git a/confidence-cloudflare-resolver/deployer/script.sh b/confidence-cloudflare-resolver/deployer/script.sh index f965c088..9090f54e 100755 --- a/confidence-cloudflare-resolver/deployer/script.sh +++ b/confidence-cloudflare-resolver/deployer/script.sh @@ -373,6 +373,12 @@ else KV_NAMESPACE_TITLE="resolver-metrics" fi +DISABLE_METRICS=${DISABLE_METRICS:=} +if [ -n "$DISABLE_METRICS" ]; then + echo "â„đïļ DISABLE_METRICS is set; skipping KV namespace creation and /metrics endpoint" + KV_NAMESPACE_ID="" +else + echo "🔍 Checking if KV namespace '$KV_NAMESPACE_TITLE' exists..." KV_LIST=$(curl -sS -w "%{http_code}" \ -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ @@ -415,6 +421,8 @@ EOF echo "✅ Added CONFIDENCE_METRICS_KV binding to wrangler.toml" fi +fi # end DISABLE_METRICS check + # Update worker name and queue name in wrangler.toml if using prefix if [ -n "$WORKER_NAME_PREFIX" ]; then sed -i.tmp "s/^name = .*/name = \"$WORKER_NAME\"/" wrangler.toml From dd15df5ab1d28363ad6f13e5b191c6c3e2965278 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 15:30:19 +0200 Subject: [PATCH 28/36] docs(cloudflare): clarify backend telemetry is independent of DISABLE_METRICS Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/deployer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confidence-cloudflare-resolver/deployer/README.md b/confidence-cloudflare-resolver/deployer/README.md index 9e8bb74f..14f09aa4 100644 --- a/confidence-cloudflare-resolver/deployer/README.md +++ b/confidence-cloudflare-resolver/deployer/README.md @@ -146,7 +146,7 @@ Metrics are accumulated in a KV namespace (`CONFIDENCE_METRICS_KV`) created auto ### Backend telemetry -Resolve rates and latency are also sent to the Confidence backend via `WriteFlagLogsRequest`, appearing on the shared Grafana dashboard alongside other providers. +Resolve rates and latency are always sent to the Confidence backend via `WriteFlagLogsRequest`, regardless of the `DISABLE_METRICS` setting. This data appears on the shared Grafana dashboard alongside other providers. The `/metrics` endpoint and KV store are only needed for direct Prometheus scraping — backend telemetry flows through the queue consumer independently. ## Limitations From e081a8427ceeaed81062972a10a06384b01d0b06 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 15:31:11 +0200 Subject: [PATCH 29/36] docs(cloudflare): remove Grafana references from deployer docs Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/deployer/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/confidence-cloudflare-resolver/deployer/README.md b/confidence-cloudflare-resolver/deployer/README.md index 14f09aa4..804e3353 100644 --- a/confidence-cloudflare-resolver/deployer/README.md +++ b/confidence-cloudflare-resolver/deployer/README.md @@ -123,11 +123,11 @@ For more details on integration, including code examples using the [`@spotify-co ## Telemetry & Metrics -The resolver collects telemetry and exposes a Prometheus-compatible `/metrics` endpoint using the same metric names as all other Confidence providers (`confidence_resolve_latency_microseconds`, `confidence_resolves_total`), enabling shared Grafana dashboards. +The resolver collects telemetry and exposes a Prometheus-compatible `/metrics` endpoint using the same metric names as all other Confidence providers (`confidence_resolve_latency_microseconds`, `confidence_resolves_total`). ### How latency is measured -Cloudflare Workers freeze `Date.now()` and `performance.now()` during synchronous CPU work (Spectre mitigation). The resolver uses `scheduler.wait(0)` — a zero-delay yield to the runtime — to unfreeze the clock after each resolve. This provides 1ms resolution with no measurable overhead. Grafana's `histogram_quantile()` interpolates between histogram buckets to produce sub-millisecond reporting. +Cloudflare Workers freeze `Date.now()` and `performance.now()` during synchronous CPU work (Spectre mitigation). The resolver uses `scheduler.wait(0)` — a zero-delay yield to the runtime — to unfreeze the clock after each resolve. This provides 1ms resolution with no measurable overhead. ### `/metrics` endpoint @@ -146,7 +146,7 @@ Metrics are accumulated in a KV namespace (`CONFIDENCE_METRICS_KV`) created auto ### Backend telemetry -Resolve rates and latency are always sent to the Confidence backend via `WriteFlagLogsRequest`, regardless of the `DISABLE_METRICS` setting. This data appears on the shared Grafana dashboard alongside other providers. The `/metrics` endpoint and KV store are only needed for direct Prometheus scraping — backend telemetry flows through the queue consumer independently. +Resolve rates and latency are always sent to the Confidence backend via `WriteFlagLogsRequest`, regardless of the `DISABLE_METRICS` setting. The `/metrics` endpoint and KV store are only needed for direct Prometheus scraping — backend telemetry flows through the queue consumer independently. ## Limitations From aeadc9fc5979ab549f5d3295fcfed5f821ba2a17 Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Tue, 12 May 2026 15:41:08 +0200 Subject: [PATCH 30/36] docs(cloudflare): link KV pricing in metrics docs Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/deployer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confidence-cloudflare-resolver/deployer/README.md b/confidence-cloudflare-resolver/deployer/README.md index 804e3353..b7b4e95a 100644 --- a/confidence-cloudflare-resolver/deployer/README.md +++ b/confidence-cloudflare-resolver/deployer/README.md @@ -142,7 +142,7 @@ Returns Prometheus exposition format with: - `confidence_resolve_latency_microseconds` — histogram (sum, count, cumulative `le` buckets) - `confidence_resolves_total` — counter by resolve reason -Metrics are accumulated in a KV namespace (`CONFIDENCE_METRICS_KV`) created automatically by the deployer. Set `DISABLE_METRICS` to skip KV creation and disable the endpoint when Prometheus scraping is not needed. +Metrics are accumulated in a [KV namespace](https://developers.cloudflare.com/kv/platform/pricing/) (`CONFIDENCE_METRICS_KV`) created automatically by the deployer. Set `DISABLE_METRICS` to skip KV creation and disable the endpoint when Prometheus scraping is not needed. ### Backend telemetry From 1fb034cf8b1cbc0e1cec720ddf64726b8ab95acb Mon Sep 17 00:00:00 2001 From: vahidlazio Date: Wed, 13 May 2026 10:05:48 +0200 Subject: [PATCH 31/36] refactor(cloudflare): change /metrics from opt-out to opt-in Replace DISABLE_METRICS with ENABLE_METRICS. The /metrics endpoint and KV store are now only created when explicitly enabled, reducing default resource usage for customers who don't need Prometheus scraping. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/deployer/README.md | 4 ++-- confidence-cloudflare-resolver/deployer/script.sh | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/confidence-cloudflare-resolver/deployer/README.md b/confidence-cloudflare-resolver/deployer/README.md index b7b4e95a..317db9fc 100644 --- a/confidence-cloudflare-resolver/deployer/README.md +++ b/confidence-cloudflare-resolver/deployer/README.md @@ -63,7 +63,7 @@ The deployer automatically: | `WRANGLER_DEPLOY_MESSAGE` | Value passed to `wrangler deploy --message` | | `WRANGLER_DEPLOY_ARGS` | Additional newline-separated arguments passed to `wrangler deploy` | | `WRANGLER_DEPLOY_ARGS_FILE` | Path to a file containing additional `wrangler deploy` arguments, one argument per line | -| `DISABLE_METRICS` | Skip KV namespace creation and disable the `/metrics` endpoint. Reduces resource usage when Prometheus scraping is not needed | +| `ENABLE_METRICS` | Set to create a KV namespace and enable the `/metrics` Prometheus endpoint. Requires a [KV store](https://developers.cloudflare.com/kv/platform/pricing/) | ### Extending Wrangler Configuration @@ -142,7 +142,7 @@ Returns Prometheus exposition format with: - `confidence_resolve_latency_microseconds` — histogram (sum, count, cumulative `le` buckets) - `confidence_resolves_total` — counter by resolve reason -Metrics are accumulated in a [KV namespace](https://developers.cloudflare.com/kv/platform/pricing/) (`CONFIDENCE_METRICS_KV`) created automatically by the deployer. Set `DISABLE_METRICS` to skip KV creation and disable the endpoint when Prometheus scraping is not needed. +Metrics are accumulated in a [KV namespace](https://developers.cloudflare.com/kv/platform/pricing/) (`CONFIDENCE_METRICS_KV`). Set `ENABLE_METRICS` to have the deployer create the KV namespace and bind it to the Worker. Without it, the `/metrics` endpoint returns empty and no KV writes occur. ### Backend telemetry diff --git a/confidence-cloudflare-resolver/deployer/script.sh b/confidence-cloudflare-resolver/deployer/script.sh index 9090f54e..b9017cd5 100755 --- a/confidence-cloudflare-resolver/deployer/script.sh +++ b/confidence-cloudflare-resolver/deployer/script.sh @@ -373,9 +373,9 @@ else KV_NAMESPACE_TITLE="resolver-metrics" fi -DISABLE_METRICS=${DISABLE_METRICS:=} -if [ -n "$DISABLE_METRICS" ]; then - echo "â„đïļ DISABLE_METRICS is set; skipping KV namespace creation and /metrics endpoint" +ENABLE_METRICS=${ENABLE_METRICS:=} +if [ -z "$ENABLE_METRICS" ]; then + echo "â„đïļ ENABLE_METRICS not set; skipping KV namespace creation (/metrics endpoint disabled)" KV_NAMESPACE_ID="" else @@ -421,7 +421,7 @@ EOF echo "✅ Added CONFIDENCE_METRICS_KV binding to wrangler.toml" fi -fi # end DISABLE_METRICS check +fi # end ENABLE_METRICS check # Update worker name and queue name in wrangler.toml if using prefix if [ -n "$WORKER_NAME_PREFIX" ]; then From 133496977f64491701d72304b848a06825fa8f7a Mon Sep 17 00:00:00 2001 From: Andreas Karlsson Date: Wed, 13 May 2026 11:17:31 +0200 Subject: [PATCH 32/36] refactor(cloudflare): replace static loggers with per-request FLAG_LOG Replace RESOLVE_LOGGER, ASSIGN_LOGGER, TELEMETRY, and LAST_FLUSHED statics with a single thread-local WriteFlagLogsRequest populated per-request. Host callbacks append directly via new public builder functions (build_flag_assigned, build_resolve_log, build_request_telemetry). All cross-request aggregation now happens in the queue consumer. Co-Authored-By: Claude Opus 4.6 (1M context) --- confidence-cloudflare-resolver/src/lib.rs | 152 ++++++++++------------ confidence-resolver/src/assign_logger.rs | 124 ++++++++++-------- confidence-resolver/src/resolve_logger.rs | 70 ++++++++++ confidence-resolver/src/telemetry.rs | 40 ++++++ 4 files changed, 247 insertions(+), 139 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index 89b77c6a..f41031e5 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -1,47 +1,27 @@ use confidence_resolver::{ - assign_logger::AssignLogger, - flag_logger, + assign_logger, flag_logger, proto::{confidence, google::Struct}, - telemetry::{Telemetry, TelemetrySnapshot}, + resolve_logger, + telemetry::{self, TelemetrySnapshot}, FlagToApply, Host, ResolvedValue, ResolverState, }; use worker::*; -use arc_swap::ArcSwap; use base64::engine::general_purpose::STANDARD; use base64::Engine; use bytes::Bytes; use prost::Message; use serde_json::from_slice; use serde_json::json; +use std::cell::RefCell; use confidence::flags::resolver::v1::{ApplyFlagsRequest, ApplyFlagsResponse, ResolveFlagsRequest}; -use confidence_resolver::proto::confidence::flags::resolver::v1::{ResolveProcessRequest, ResolveReason}; - -static RESOLVE_LOGGER: LazyLock> = LazyLock::new(ResolveLogger::new); -static ASSIGN_LOGGER: LazyLock = LazyLock::new(AssignLogger::new); -static TELEMETRY: LazyLock = LazyLock::new(Telemetry::new); -static LAST_FLUSHED: LazyLock> = - LazyLock::new(|| ArcSwap::from_pointee(TelemetrySnapshot::default())); +use confidence_resolver::proto::confidence::flags::resolver::v1::{ + ResolveProcessRequest, ResolveReason, +}; use confidence_resolver::Client; use once_cell::sync::Lazy; -use std::cell::RefCell; - -/// Per-request resolve metrics captured in the hot path, recorded in wait_until. -struct ResolveMetrics { - reasons: Vec, - /// `Some(Ξs)` when scheduler.wait(0) unfroze the timer, `None` if the - /// scheduler API was unavailable (latency not recorded, resolve unaffected). - elapsed_us: Option, -} - -thread_local! { - static PENDING_METRICS: RefCell> = const { RefCell::new(Vec::new()) }; -} - -/// Prometheus exposition format content type (version 0.0.4). -const PROMETHEUS_CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8"; /// SetResolverStateRequest message from the CDN. /// This matches the protobuf message format returned by the CDN. @@ -59,8 +39,14 @@ const ENCRYPTION_KEY_BASE64: &str = include_str!("../../data/encryption_key"); use confidence::flags::resolver::v1::Sdk; use confidence_resolver::proto::confidence::flags::resolver::v1::WriteFlagLogsRequest; -use confidence_resolver::resolve_logger::ResolveLogger; -use std::sync::{LazyLock, OnceLock}; +use std::sync::OnceLock; + +thread_local! { + static FLAG_LOG: RefCell> = RefCell::new(None); +} + +/// Prometheus exposition format content type (version 0.0.4). +const PROMETHEUS_CONTENT_TYPE: &str = "text/plain; version=0.0.4; charset=utf-8"; static FLAGS_LOGS_QUEUE: OnceLock = OnceLock::new(); @@ -92,18 +78,22 @@ struct H {} impl Host for H { fn log_resolve( - resolve_id: &str, + _resolve_id: &str, evaluation_context: &Struct, values: &[ResolvedValue<'_>], client: &Client, ) { - RESOLVE_LOGGER.log_resolve( - resolve_id, - evaluation_context, - client.client_credential_name.as_str(), - values, - client, - ); + FLAG_LOG.with(|f| { + if let Some(req) = f.borrow_mut().as_mut() { + let (flag_infos, client_info) = resolve_logger::build_resolve_log( + evaluation_context, + client.client_credential_name.as_str(), + values, + ); + req.flag_resolve_info.extend(flag_infos); + req.client_resolve_info.push(client_info); + } + }); } fn log_assign( @@ -112,7 +102,17 @@ impl Host for H { client: &Client, sdk: &Option, ) { - ASSIGN_LOGGER.log_assigns(resolve_id, assigned_flags, client, sdk); + FLAG_LOG.with(|f| { + if let Some(req) = f.borrow_mut().as_mut() { + req.flag_assigned + .push(assign_logger::build_flag_assigned( + resolve_id, + assigned_flags, + client, + sdk, + )); + } + }); } } @@ -124,6 +124,15 @@ fn set_client_secret(env: &Env) { } } +fn sdk_info() -> Sdk { + Sdk { + sdk: Some(confidence::flags::resolver::v1::sdk::Sdk::Id( + confidence::flags::resolver::v1::SdkId::CloudflareResolver as i32, + )), + version: env!("CARGO_PKG_VERSION").to_string(), + } +} + #[event(fetch)] pub async fn main(req: Request, env: Env, ctx: Context) -> Result { match env.queue("flag_logs_queue") { @@ -158,6 +167,8 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { return Response::ok("")?.with_cors_headers(&allowed_origin_env); } + FLAG_LOG.with(|f| *f.borrow_mut() = Some(WriteFlagLogsRequest::default())); + let state = &RESOLVER_STATE; let router = Router::new(); @@ -301,9 +312,14 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { } }; - PENDING_METRICS.with(|m| { - m.borrow_mut().push(ResolveMetrics { reasons, elapsed_us }); + let mut td = telemetry::build_request_telemetry(elapsed_us, &reasons); + td.sdk = Some(sdk_info()); + FLAG_LOG.with(|f| { + if let Some(req) = f.borrow_mut().as_mut() { + req.telemetry_data = Some(td); + } }); + resp } "flags:apply" => { @@ -343,23 +359,13 @@ pub async fn main(req: Request, env: Env, ctx: Context) -> Result { .await; // Use ctx.waitUntil to run logging and telemetry after response is returned. + let flag_log = FLAG_LOG.with(|f| f.borrow_mut().take()); ctx.wait_until(async move { - PENDING_METRICS.with(|m| { - for metrics in m.borrow_mut().drain(..) { - if let Some(us) = metrics.elapsed_us { - TELEMETRY.record_latency_us(us); + if let Some(req) = flag_log { + if let Ok(json) = serde_json::to_string(&req) { + if let Some(queue) = FLAGS_LOGS_QUEUE.get() { + let _ = queue.send(json).await; } - for reason in metrics.reasons { - TELEMETRY.mark_resolve(reason); - } - } - }); - - let aggregated: confidence_resolver::proto::confidence::flags::resolver::v1::WriteFlagLogsRequest - = checkpoint(); - if let Ok(converted) = serde_json::to_string(&aggregated) { - if let Some(queue) = FLAGS_LOGS_QUEUE.get() { - let _ = queue.send(converted).await; } } }); @@ -379,58 +385,36 @@ pub async fn consume_flag_logs_queue( let logs: Vec = messages .iter() .map(|m| m.body().clone()) - .map(|s| serde_json::from_str::(s.as_str()).unwrap()) - .map(|v| WriteFlagLogsRequest { - telemetry_data: v.telemetry_data, - flag_resolve_info: v.flag_resolve_info, - flag_assigned: v.flag_assigned, - client_resolve_info: v.client_resolve_info, - }) + .map(|s| serde_json::from_str::(s.as_str()).unwrap()) .collect(); + let req = flag_logger::aggregate_batch(logs); + // Accumulate telemetry deltas into KV-backed cumulative snapshot for /metrics. if let Ok(kv) = env.kv("CONFIDENCE_METRICS_KV") { - update_prometheus_kv(&kv, &logs).await; + update_prometheus_kv(&kv, &req).await; } - let req = flag_logger::aggregate_batch(logs); send_flags_logs(CONFIDENCE_CLIENT_SECRET.get().unwrap().as_str(), req).await?; } Ok(()) } -fn checkpoint() -> WriteFlagLogsRequest { - let mut req = RESOLVE_LOGGER.checkpoint(); - let mut td = TELEMETRY.delta_snapshot(&LAST_FLUSHED); - td.sdk = Some(confidence::flags::resolver::v1::Sdk { - sdk: Some(confidence::flags::resolver::v1::sdk::Sdk::Id( - confidence::flags::resolver::v1::SdkId::CloudflareResolver as i32, - )), - version: env!("CARGO_PKG_VERSION").to_string(), - }); - td.resolver_version = env!("CARGO_PKG_VERSION").to_string(); - req.telemetry_data = Some(td); - ASSIGN_LOGGER.checkpoint_fill(&mut req); - req -} - /// Accumulate telemetry deltas from all isolates into a cumulative /// `TelemetrySnapshot` stored in KV, then write its Prometheus text /// representation for the /metrics endpoint. /// /// Note: concurrent queue consumer invocations can race on KV read-modify-write. /// Acceptable for metrics — at worst one batch's deltas are lost, not cumulative state. -async fn update_prometheus_kv(kv: &kv::KvStore, logs: &[WriteFlagLogsRequest]) { +async fn update_prometheus_kv(kv: &kv::KvStore, req: &WriteFlagLogsRequest) { let mut cumulative = match kv.get("snapshot").text().await { Ok(Some(text)) => serde_json::from_str::(&text).unwrap_or_default(), _ => TelemetrySnapshot::default(), }; - for log in logs { - if let Some(td) = &log.telemetry_data { - cumulative.accumulate_delta(td); - } + if let Some(td) = &req.telemetry_data { + cumulative.accumulate_delta(td); } let prom_text = cumulative.to_prometheus( diff --git a/confidence-resolver/src/assign_logger.rs b/confidence-resolver/src/assign_logger.rs index 06f602b4..7048dd7e 100644 --- a/confidence-resolver/src/assign_logger.rs +++ b/confidence-resolver/src/assign_logger.rs @@ -42,61 +42,12 @@ impl AssignLogger { client: &crate::Client, sdk: &Option, ) { - let client_info = Some(pb::ClientInfo { - client: client.client_name.to_string(), - client_credential: client.client_credential_name.to_string(), - sdk: sdk.clone(), - }); - let flags = assigned_flags - .iter() - .map( - |FlagToApply { - assigned_flag: f, - skew_adjusted_applied_time, - }| { - let assignment = if !f.variant.is_empty() { - let assignment_info = pb::AssignmentInfo { - segment: f.segment.clone(), - variant: f.variant.clone(), - }; - Some(pb::Assignment::AssignmentInfo(assignment_info)) - } else { - let default_reason: pb::DefaultAssignmentReason = - match pb::ResolveReason::try_from(f.reason) { - Ok(pb::ResolveReason::NoSegmentMatch) => { - pb::DefaultAssignmentReason::NoSegmentMatch - } - Ok(pb::ResolveReason::NoTreatmentMatch) => { - pb::DefaultAssignmentReason::NoTreatmentMatch - } - Ok(pb::ResolveReason::FlagArchived) => { - pb::DefaultAssignmentReason::FlagArchived - } - _ => pb::DefaultAssignmentReason::Unspecified, - }; - Some(pb::Assignment::DefaultAssignment(pb::DefaultAssignment { - reason: default_reason.into(), - })) - }; - pb::AppliedFlag { - flag: f.flag.clone(), - targeting_key: f.targeting_key.clone(), - targeting_key_selector: f.targeting_key_selector.clone(), - assignment_id: f.assignment_id.clone(), - rule: f.rule.clone(), - fallthrough_assignments: f.fallthrough_assignments.clone(), - apply_time: Some(skew_adjusted_applied_time.clone()), - assignment, - } - }, - ) - .collect(); - - self.assigned.push(pb::FlagAssigned { - resolve_id: resolve_id.to_string(), - client_info, - flags, - }); + self.assigned.push(build_flag_assigned( + resolve_id, + assigned_flags, + client, + sdk, + )); } pub fn checkpoint(&self) -> WriteFlagLogsRequest { @@ -164,6 +115,69 @@ impl AssignLogger { } } +pub fn build_flag_assigned( + resolve_id: &str, + assigned_flags: &[FlagToApply], + client: &crate::Client, + sdk: &Option, +) -> pb::FlagAssigned { + let client_info = Some(pb::ClientInfo { + client: client.client_name.to_string(), + client_credential: client.client_credential_name.to_string(), + sdk: sdk.clone(), + }); + let flags = assigned_flags + .iter() + .map( + |FlagToApply { + assigned_flag: f, + skew_adjusted_applied_time, + }| { + let assignment = if !f.variant.is_empty() { + let assignment_info = pb::AssignmentInfo { + segment: f.segment.clone(), + variant: f.variant.clone(), + }; + Some(pb::Assignment::AssignmentInfo(assignment_info)) + } else { + let default_reason: pb::DefaultAssignmentReason = + match pb::ResolveReason::try_from(f.reason) { + Ok(pb::ResolveReason::NoSegmentMatch) => { + pb::DefaultAssignmentReason::NoSegmentMatch + } + Ok(pb::ResolveReason::NoTreatmentMatch) => { + pb::DefaultAssignmentReason::NoTreatmentMatch + } + Ok(pb::ResolveReason::FlagArchived) => { + pb::DefaultAssignmentReason::FlagArchived + } + _ => pb::DefaultAssignmentReason::Unspecified, + }; + Some(pb::Assignment::DefaultAssignment(pb::DefaultAssignment { + reason: default_reason.into(), + })) + }; + pb::AppliedFlag { + flag: f.flag.clone(), + targeting_key: f.targeting_key.clone(), + targeting_key_selector: f.targeting_key_selector.clone(), + assignment_id: f.assignment_id.clone(), + rule: f.rule.clone(), + fallthrough_assignments: f.fallthrough_assignments.clone(), + apply_time: Some(skew_adjusted_applied_time.clone()), + assignment, + } + }, + ) + .collect(); + + pb::FlagAssigned { + resolve_id: resolve_id.to_string(), + client_info, + flags, + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/confidence-resolver/src/resolve_logger.rs b/confidence-resolver/src/resolve_logger.rs index b3d8d986..db8e4dac 100644 --- a/confidence-resolver/src/resolve_logger.rs +++ b/confidence-resolver/src/resolve_logger.rs @@ -145,6 +145,76 @@ impl ResolveLogger { } } +pub fn build_resolve_log( + evaluation_context: &pb::Struct, + client_credential: &str, + values: &[crate::ResolvedValue<'_>], +) -> (Vec, pb::ClientResolveInfo) { + let schema = SchemaFromEvaluationContext::get_schema(evaluation_context); + let client_info = pb::ClientResolveInfo { + client: extract_client(client_credential), + client_credential: client_credential.to_string(), + schema: vec![to_pb_schema_instance(&schema)], + }; + + let flag_infos = values + .iter() + .map(|value| { + let af = &value.inner; + let mut variant_resolve_info = Vec::new(); + let mut rule_resolve_info: Vec = Vec::new(); + + for fallthrough in &af.fallthrough_assignments { + rule_resolve_info.push(pb::flag_resolve_info::RuleResolveInfo { + rule: fallthrough.rule.clone(), + count: 1, + assignment_resolve_info: vec![ + pb::flag_resolve_info::AssignmentResolveInfo { + assignment_id: fallthrough.assignment_id.clone(), + count: 1, + }, + ], + }); + } + + if !af.rule.is_empty() { + let variant_key = if af.variant.is_empty() { + String::new() + } else { + af.variant.clone() + }; + variant_resolve_info.push(pb::flag_resolve_info::VariantResolveInfo { + variant: variant_key, + count: 1, + }); + rule_resolve_info.push(pb::flag_resolve_info::RuleResolveInfo { + rule: af.rule.clone(), + count: 1, + assignment_resolve_info: vec![ + pb::flag_resolve_info::AssignmentResolveInfo { + assignment_id: af.assignment_id.clone(), + count: 1, + }, + ], + }); + } else { + variant_resolve_info.push(pb::flag_resolve_info::VariantResolveInfo { + variant: String::new(), + count: 1, + }); + } + + pb::FlagResolveInfo { + flag: af.flag.clone(), + variant_resolve_info, + rule_resolve_info, + } + }) + .collect(); + + (flag_infos, client_info) +} + #[derive(Debug, Default)] struct RuleResolveInfo { count: AtomicU32, diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index b74e9e85..7b5a3300 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -508,6 +508,46 @@ impl Default for Telemetry { } } +pub fn build_request_telemetry( + latency_us: Option, + reasons: &[ResolveReason], +) -> pb::TelemetryData { + let resolve_latency = latency_us.map(|us| { + let idx = if us == 0 { + 0 + } else { + let k = ((us as f64).ln() / LN_RATIO).floor() as usize; + k.min(BUCKET_COUNT.saturating_sub(1)) + }; + pb::ResolveLatency { + sum: us, + count: 1, + buckets: vec![pb::BucketSpan { + offset: idx as i32, + counts: vec![1], + }], + ln_ratio: LN_RATIO, + } + }); + + let mut reason_counts: Vec = Vec::new(); + for reason in reasons { + let r = *reason as i32; + if let Some(entry) = reason_counts.iter_mut().find(|e| e.reason == r) { + entry.count = entry.count.saturating_add(1); + } else { + reason_counts.push(pb::ResolveRate { count: 1, reason: r }); + } + } + + pb::TelemetryData { + resolve_latency, + resolve_rate: reason_counts, + resolver_version: crate::version::VERSION.to_string(), + ..Default::default() + } +} + #[cfg(test)] mod tests { use super::*; From 9e4d07b9552fc954ad5f7c5e8a6ad7052211dce6 Mon Sep 17 00:00:00 2001 From: "andreas.karlsson" Date: Wed, 13 May 2026 13:26:32 +0200 Subject: [PATCH 33/36] Update confidence-cloudflare-resolver/deployer/README.md Co-authored-by: Nicklas Lundin --- confidence-cloudflare-resolver/deployer/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/confidence-cloudflare-resolver/deployer/README.md b/confidence-cloudflare-resolver/deployer/README.md index 317db9fc..1c2b4c08 100644 --- a/confidence-cloudflare-resolver/deployer/README.md +++ b/confidence-cloudflare-resolver/deployer/README.md @@ -146,7 +146,7 @@ Metrics are accumulated in a [KV namespace](https://developers.cloudflare.com/kv ### Backend telemetry -Resolve rates and latency are always sent to the Confidence backend via `WriteFlagLogsRequest`, regardless of the `DISABLE_METRICS` setting. The `/metrics` endpoint and KV store are only needed for direct Prometheus scraping — backend telemetry flows through the queue consumer independently. +Resolve rates and latency are always sent to the Confidence backend via `WriteFlagLogsRequest`, regardless of the `ENABLE_METRICS` setting. The `/metrics` endpoint and KV store are only needed for direct Prometheus scraping — backend telemetry flows through the queue consumer independently. ## Limitations From 5ae3403ca8a4fd1a8e8050835147346838df09e0 Mon Sep 17 00:00:00 2001 From: Andreas Karlsson Date: Wed, 13 May 2026 15:20:42 +0200 Subject: [PATCH 34/36] fixup! refactor(cloudflare): replace static loggers with per-request FLAG_LOG --- .../assets/confidence_resolver.wasm | Bin 483040 -> 482963 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm b/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm index 88ff1926ecd3a7f70e82d5b80ce591bcaf0afca1..27aca2915db1fa5615526a8d78fc7e115d28a231 100755 GIT binary patch delta 4036 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 Date: Wed, 13 May 2026 15:51:21 +0200 Subject: [PATCH 35/36] fix: formatting and linting --- confidence-cloudflare-resolver/src/lib.rs | 2 +- confidence-resolver/src/assign_logger.rs | 8 ++------ confidence-resolver/src/resolve_logger.rs | 20 ++++++++------------ confidence-resolver/src/telemetry.rs | 5 ++++- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/confidence-cloudflare-resolver/src/lib.rs b/confidence-cloudflare-resolver/src/lib.rs index f41031e5..63202785 100644 --- a/confidence-cloudflare-resolver/src/lib.rs +++ b/confidence-cloudflare-resolver/src/lib.rs @@ -42,7 +42,7 @@ use confidence_resolver::proto::confidence::flags::resolver::v1::WriteFlagLogsRe use std::sync::OnceLock; thread_local! { - static FLAG_LOG: RefCell> = RefCell::new(None); + static FLAG_LOG: RefCell> = const { RefCell::new(None) }; } /// Prometheus exposition format content type (version 0.0.4). diff --git a/confidence-resolver/src/assign_logger.rs b/confidence-resolver/src/assign_logger.rs index 7048dd7e..74634ea1 100644 --- a/confidence-resolver/src/assign_logger.rs +++ b/confidence-resolver/src/assign_logger.rs @@ -42,12 +42,8 @@ impl AssignLogger { client: &crate::Client, sdk: &Option, ) { - self.assigned.push(build_flag_assigned( - resolve_id, - assigned_flags, - client, - sdk, - )); + self.assigned + .push(build_flag_assigned(resolve_id, assigned_flags, client, sdk)); } pub fn checkpoint(&self) -> WriteFlagLogsRequest { diff --git a/confidence-resolver/src/resolve_logger.rs b/confidence-resolver/src/resolve_logger.rs index db8e4dac..d461e092 100644 --- a/confidence-resolver/src/resolve_logger.rs +++ b/confidence-resolver/src/resolve_logger.rs @@ -168,12 +168,10 @@ pub fn build_resolve_log( rule_resolve_info.push(pb::flag_resolve_info::RuleResolveInfo { rule: fallthrough.rule.clone(), count: 1, - assignment_resolve_info: vec![ - pb::flag_resolve_info::AssignmentResolveInfo { - assignment_id: fallthrough.assignment_id.clone(), - count: 1, - }, - ], + assignment_resolve_info: vec![pb::flag_resolve_info::AssignmentResolveInfo { + assignment_id: fallthrough.assignment_id.clone(), + count: 1, + }], }); } @@ -190,12 +188,10 @@ pub fn build_resolve_log( rule_resolve_info.push(pb::flag_resolve_info::RuleResolveInfo { rule: af.rule.clone(), count: 1, - assignment_resolve_info: vec![ - pb::flag_resolve_info::AssignmentResolveInfo { - assignment_id: af.assignment_id.clone(), - count: 1, - }, - ], + assignment_resolve_info: vec![pb::flag_resolve_info::AssignmentResolveInfo { + assignment_id: af.assignment_id.clone(), + count: 1, + }], }); } else { variant_resolve_info.push(pb::flag_resolve_info::VariantResolveInfo { diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index 7b5a3300..e075953c 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -536,7 +536,10 @@ pub fn build_request_telemetry( if let Some(entry) = reason_counts.iter_mut().find(|e| e.reason == r) { entry.count = entry.count.saturating_add(1); } else { - reason_counts.push(pb::ResolveRate { count: 1, reason: r }); + reason_counts.push(pb::ResolveRate { + count: 1, + reason: r, + }); } } From 123add6b3f2f001c7db0b5204dd2334f69b1eae4 Mon Sep 17 00:00:00 2001 From: Andreas Karlsson Date: Wed, 13 May 2026 16:06:23 +0200 Subject: [PATCH 36/36] fix: wasm go.... --- .../assets/confidence_resolver.wasm | Bin 482963 -> 482963 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm b/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm index 27aca2915db1fa5615526a8d78fc7e115d28a231..dea9f7b0f78d3fff395cae196ed279ff2e282c43 100755 GIT binary patch delta 49 zcmbQdS9bDV*@hOz7N!>F7M2#)Eo^Q!g24<74Dvv1$u7X40i+Y!!)@5Mhug5*C;F7M2#)Eo^Q!g5eAd4Dvv1$u7X40i;vf!)@5Mhug5*C;