diff --git a/Cargo.lock b/Cargo.lock index 701cf64f..28b0f5fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,6 +76,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto_ops" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7460f7dd8e100147b82a63afca1a20eb6c231ee36b90ba7272e14951cb58af59" + [[package]] name = "autocfg" version = "1.5.0" @@ -277,6 +283,7 @@ dependencies = [ "fastrand", "isocountry", "miniz_oxide", + "openmetrics-parser", "papaya", "pbjson", "pbjson-build", @@ -1240,6 +1247,17 @@ dependencies = [ "rust-guest", ] +[[package]] +name = "openmetrics-parser" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40a68c62e09c5dfec2f6472af3bd5e8ddf506fcf14c78ece23794ffbb874eca" +dependencies = [ + "auto_ops", + "pest", + "pest_derive", +] + [[package]] name = "openssl-probe" version = "0.2.0" @@ -1328,6 +1346,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -2556,6 +2617,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.18" diff --git a/confidence-resolver/Cargo.toml b/confidence-resolver/Cargo.toml index ec0d72c8..2c451753 100644 --- a/confidence-resolver/Cargo.toml +++ b/confidence-resolver/Cargo.toml @@ -43,6 +43,7 @@ serde_json = { version = "1.0.107", optional = true } isocountry = "0.3.2" [dev-dependencies] +openmetrics-parser = "0.4.4" regex = "1.10.2" [build-dependencies] diff --git a/confidence-resolver/src/telemetry.rs b/confidence-resolver/src/telemetry.rs index 77d33000..4d9e658c 100644 --- a/confidence-resolver/src/telemetry.rs +++ b/confidence-resolver/src/telemetry.rs @@ -119,6 +119,28 @@ pub struct HistogramSnapshot { pub buckets: Vec, } +/// Configuration for Prometheus text format rendering. +pub struct PrometheusConfig { + /// Number of histogram buckets per decimal order of magnitude. + /// The internal histogram always stores 174 buckets (18 per decade). + /// This controls how many are emitted in the text output by striding. + /// Valid range: 1..=18. Values outside this range are clamped. + /// A value of 0 is treated as the default (18 — no striding). + pub buckets_per_decade: u32, + /// When true, emit OpenMetrics text format instead of Prometheus exposition format. + /// Differences: integer values rendered as floats, `# EOF` appended. + pub openmetrics: bool, +} + +impl Default for PrometheusConfig { + fn default() -> Self { + PrometheusConfig { + buckets_per_decade: 18, + openmetrics: false, + } + } +} + impl TelemetrySnapshot { /// Format the snapshot as Prometheus exposition text. /// @@ -126,70 +148,127 @@ impl TelemetrySnapshot { /// The histogram uses cumulative `le` buckets derived from the exponential /// bucket boundaries. /// - /// `instance` is included as an `instance="..."` label on every metric, + /// `resolver_id` is included as a `resolver_id="..."` label on every metric, /// allowing outputs from multiple WASM instances to be concatenated into /// a single scrape endpoint. - pub fn to_prometheus(&self, instance: &str) -> String { + pub fn to_prometheus(&self, resolver_id: &str, config: &PrometheusConfig) -> String { let mut out = String::new(); // fmt::Write for String is infallible; ignore the Ok. - let _ = self.write_prometheus(&mut out, instance); + let _ = self.write_prometheus(&mut out, resolver_id, config); out } - fn write_prometheus(&self, w: &mut dyn fmt::Write, instance: &str) -> fmt::Result { - self.write_histogram(w, instance)?; - self.write_resolve_rates(w, instance)?; - self.write_memory(w, instance) + fn write_prometheus( + &self, + w: &mut dyn fmt::Write, + resolver_id: &str, + config: &PrometheusConfig, + ) -> fmt::Result { + self.write_histogram(w, resolver_id, config)?; + self.write_resolve_rates(w, resolver_id, config)?; + self.write_memory(w, resolver_id, config)?; + if config.openmetrics { + writeln!(w, "# EOF")?; + } + Ok(()) } - fn write_histogram(&self, w: &mut dyn fmt::Write, instance: &str) -> fmt::Result { + fn write_histogram( + &self, + w: &mut dyn fmt::Write, + resolver_id: &str, + config: &PrometheusConfig, + ) -> fmt::Result { if self.latency.count == 0 { return Ok(()); } + let bpd = if config.buckets_per_decade == 0 { + 18usize + } else { + config.buckets_per_decade.clamp(1, 18) as usize + }; + // Ceiling division: emit at most bpd buckets per decade. + // bpd is in 1..=18, so checked_div cannot return None. + let stride = 18usize + .saturating_add(bpd) + .saturating_sub(1) + .checked_div(bpd) + .unwrap_or(1); + + writeln!( + w, + "# HELP confidence_resolve_latency_microseconds Latency of flag resolve operations in microseconds." + )?; writeln!( w, "# TYPE confidence_resolve_latency_microseconds histogram" )?; let mut cumulative: u64 = 0; + // Track the next bucket index to emit (stride-1, stride*2-1, ...). + // Advance next_emit at every stride boundary regardless of data; + // only skip the actual write when cumulative is still zero. + let mut next_emit = stride.wrapping_sub(1); + // OpenMetrics requires integer values to be rendered as floats. + let suffix = if config.openmetrics { ".0" } else { "" }; for (i, &count) in self.latency.buckets.iter().enumerate() { cumulative = cumulative.wrapping_add(count); + if i != next_emit { + continue; + } + next_emit = next_emit.saturating_add(stride); if cumulative == 0 { continue; } let upper = ((i as f64 + 1.0) * LN_RATIO).exp(); writeln!( w, - "confidence_resolve_latency_microseconds_bucket{{instance=\"{instance}\",le=\"{upper:.6e}\"}} {cumulative}" + "confidence_resolve_latency_microseconds_bucket{{resolver_id=\"{resolver_id}\",le=\"{upper:.6e}\"}} {cumulative}{suffix}" )?; } writeln!( w, - "confidence_resolve_latency_microseconds_bucket{{instance=\"{instance}\",le=\"+Inf\"}} {}", + "confidence_resolve_latency_microseconds_bucket{{resolver_id=\"{resolver_id}\",le=\"+Inf\"}} {}{suffix}", self.latency.count )?; writeln!( w, - "confidence_resolve_latency_microseconds_sum{{instance=\"{instance}\"}} {}", + "confidence_resolve_latency_microseconds_sum{{resolver_id=\"{resolver_id}\"}} {}{suffix}", self.latency.sum )?; writeln!( w, - "confidence_resolve_latency_microseconds_count{{instance=\"{instance}\"}} {}", + "confidence_resolve_latency_microseconds_count{{resolver_id=\"{resolver_id}\"}} {}{suffix}", self.latency.count )?; Ok(()) } - fn write_resolve_rates(&self, w: &mut dyn fmt::Write, instance: &str) -> fmt::Result { + fn write_resolve_rates( + &self, + w: &mut dyn fmt::Write, + resolver_id: &str, + config: &PrometheusConfig, + ) -> fmt::Result { let has_any = self.resolve_rates.iter().any(|&c| c > 0); if !has_any { return Ok(()); } - writeln!(w, "# TYPE confidence_resolves_total counter")?; + let suffix = if config.openmetrics { ".0" } else { "" }; + // OpenMetrics: TYPE/HELP use the base name; sample lines keep _total. + let type_name = if config.openmetrics { + "confidence_resolves" + } else { + "confidence_resolves_total" + }; + writeln!( + w, + "# HELP {type_name} Total number of flag resolve operations." + )?; + writeln!(w, "# TYPE {type_name} counter")?; for (i, &count) in self.resolve_rates.iter().enumerate() { if count == 0 { continue; @@ -199,21 +278,31 @@ impl TelemetrySnapshot { .unwrap_or("UNKNOWN"); writeln!( w, - "confidence_resolves_total{{instance=\"{instance}\",reason=\"{label}\"}} {count}" + "confidence_resolves_total{{resolver_id=\"{resolver_id}\",reason=\"{label}\"}} {count}{suffix}" )?; } Ok(()) } - fn write_memory(&self, w: &mut dyn fmt::Write, instance: &str) -> fmt::Result { + fn write_memory( + &self, + w: &mut dyn fmt::Write, + resolver_id: &str, + config: &PrometheusConfig, + ) -> fmt::Result { if self.memory_bytes == 0 { return Ok(()); } + let suffix = if config.openmetrics { ".0" } else { "" }; + writeln!( + w, + "# HELP confidence_memory_bytes Current memory usage of the resolver in bytes." + )?; writeln!(w, "# TYPE confidence_memory_bytes gauge")?; writeln!( w, - "confidence_memory_bytes{{instance=\"{instance}\"}} {}", + "confidence_memory_bytes{{resolver_id=\"{resolver_id}\"}} {}{suffix}", self.memory_bytes ) } @@ -633,7 +722,7 @@ mod tests { #[test] fn prometheus_empty_snapshot() { let snap = TelemetrySnapshot::default(); - assert_eq!(snap.to_prometheus("w0"), ""); + assert_eq!(snap.to_prometheus("w0", &PrometheusConfig::default()), ""); } #[test] @@ -645,25 +734,32 @@ mod tests { tel.mark_resolve(ResolveReason::Match); tel.mark_resolve(ResolveReason::NoSegmentMatch); - let prom = tel.snapshot().to_prometheus("w0"); + let config = PrometheusConfig::default(); + let prom = tel.snapshot().to_prometheus("w0", &config); - // Histogram header + // HELP and TYPE headers + assert!(prom.contains("# HELP confidence_resolve_latency_microseconds")); assert!(prom.contains("# TYPE confidence_resolve_latency_microseconds histogram")); - // Must have +Inf bucket with instance label + // Must have +Inf bucket with resolver_id label assert!(prom.contains( - r#"confidence_resolve_latency_microseconds_bucket{instance="w0",le="+Inf"} 2"# + r#"confidence_resolve_latency_microseconds_bucket{resolver_id="w0",le="+Inf"} 2"# )); - // Sum and count with instance label - assert!(prom.contains(r#"confidence_resolve_latency_microseconds_sum{instance="w0"} 600"#)); - assert!(prom.contains(r#"confidence_resolve_latency_microseconds_count{instance="w0"} 2"#)); + // Sum and count with resolver_id label + assert!( + prom.contains(r#"confidence_resolve_latency_microseconds_sum{resolver_id="w0"} 600"#) + ); + assert!( + prom.contains(r#"confidence_resolve_latency_microseconds_count{resolver_id="w0"} 2"#) + ); - // Counters with instance label + // Counters with resolver_id label + assert!(prom.contains("# HELP confidence_resolves_total")); assert!(prom.contains("# TYPE confidence_resolves_total counter")); assert!(prom.contains( - r#"confidence_resolves_total{instance="w0",reason="RESOLVE_REASON_MATCH"} 2"# + r#"confidence_resolves_total{resolver_id="w0",reason="RESOLVE_REASON_MATCH"} 2"# )); assert!(prom.contains( - r#"confidence_resolves_total{instance="w0",reason="RESOLVE_REASON_NO_SEGMENT_MATCH"} 1"# + r#"confidence_resolves_total{resolver_id="w0",reason="RESOLVE_REASON_NO_SEGMENT_MATCH"} 1"# )); // Zero-count reasons should be omitted assert!(!prom.contains("FLAG_ARCHIVED")); @@ -676,7 +772,8 @@ mod tests { tel.record_latency_us(10); tel.record_latency_us(10_000); - let prom = tel.snapshot().to_prometheus("w0"); + let config = PrometheusConfig::default(); + let prom = tel.snapshot().to_prometheus("w0", &config); // Parse all le buckets and verify they are monotonically non-decreasing let bucket_counts: Vec = prom @@ -702,11 +799,144 @@ mod tests { tel.mark_resolve(ResolveReason::Match); let snap = tel.snapshot(); - let mut combined = snap.to_prometheus("w0"); - combined.push_str(&snap.to_prometheus("w1")); + let config = PrometheusConfig::default(); + let mut combined = snap.to_prometheus("w0", &config); + combined.push_str(&snap.to_prometheus("w1", &config)); + + // Both resolver_ids present + assert!(combined.contains(r#"resolver_id="w0""#)); + assert!(combined.contains(r#"resolver_id="w1""#)); + } + + #[test] + fn prometheus_bucket_striding() { + let tel = Telemetry::new(); + tel.record_latency_us(100); + tel.record_latency_us(10_000); + tel.record_latency_us(1_000_000); + + let snap = tel.snapshot(); + + // Default (18 per decade) — count all le= lines + let full = snap.to_prometheus( + "w0", + &PrometheusConfig { + buckets_per_decade: 18, + ..PrometheusConfig::default() + }, + ); + let full_count = full.lines().filter(|l| l.contains("le=\"")).count(); + + // 9 per decade — should produce roughly half the bucket lines + let half = snap.to_prometheus( + "w0", + &PrometheusConfig { + buckets_per_decade: 9, + ..PrometheusConfig::default() + }, + ); + let half_count = half.lines().filter(|l| l.contains("le=\"")).count(); + + // 6 per decade — should produce roughly a third + let third = snap.to_prometheus( + "w0", + &PrometheusConfig { + buckets_per_decade: 6, + ..PrometheusConfig::default() + }, + ); + let third_count = third.lines().filter(|l| l.contains("le=\"")).count(); + + assert!( + half_count < full_count, + "9 bpd ({half_count}) should produce fewer buckets than 18 bpd ({full_count})" + ); + assert!( + third_count < half_count, + "6 bpd ({third_count}) should produce fewer buckets than 9 bpd ({half_count})" + ); + + // +Inf must always be present + assert!(half.contains(r#"le="+Inf""#)); + assert!(third.contains(r#"le="+Inf""#)); + + // Cumulative correctness: last non-Inf bucket count should equal +Inf count + // (since all observations are within u32 range) + let inf_line = half.lines().find(|l| l.contains("+Inf")).unwrap(); + let inf_count: u64 = inf_line.rsplit_once(' ').unwrap().1.parse().unwrap(); + assert_eq!(inf_count, 3); + } + + #[test] + fn openmetrics_parses_successfully() { + let tel = Telemetry::with_memory_provider(|| 1_048_576); + tel.record_latency_us(100); + tel.record_latency_us(500); + tel.mark_resolve(ResolveReason::Match); + tel.mark_resolve(ResolveReason::NoSegmentMatch); + + let config = PrometheusConfig { + openmetrics: true, + ..PrometheusConfig::default() + }; + let output = tel.snapshot().to_prometheus("w0", &config); + + // Must end with # EOF + assert!(output.trim_end().ends_with("# EOF")); + + // Must contain float-formatted values + assert!(output.contains(".0\n")); + + // Must parse with a strict OpenMetrics parser + let result = openmetrics_parser::openmetrics::parse_openmetrics(&output); + assert!( + result.is_ok(), + "OpenMetrics parser rejected output: {:?}\n\nRaw:\n{output}", + result.err() + ); + } + + #[test] + fn openmetrics_with_bucket_striding() { + let tel = Telemetry::with_memory_provider(|| 1_048_576); + tel.record_latency_us(100); + tel.record_latency_us(10_000); + tel.mark_resolve(ResolveReason::Match); - // Both instances present - assert!(combined.contains(r#"instance="w0""#)); - assert!(combined.contains(r#"instance="w1""#)); + let config = PrometheusConfig { + buckets_per_decade: 9, + openmetrics: true, + }; + let output = tel.snapshot().to_prometheus("w0", &config); + + let result = openmetrics_parser::openmetrics::parse_openmetrics(&output); + assert!( + result.is_ok(), + "OpenMetrics parser rejected strided output: {:?}\n\nRaw:\n{output}", + result.err() + ); + } + + #[test] + fn prometheus_bucket_stride_zero_means_default() { + let tel = Telemetry::new(); + tel.record_latency_us(100); + + let snap = tel.snapshot(); + let default_out = snap.to_prometheus( + "w0", + &PrometheusConfig { + buckets_per_decade: 18, + ..PrometheusConfig::default() + }, + ); + let zero_out = snap.to_prometheus( + "w0", + &PrometheusConfig { + buckets_per_decade: 0, + ..PrometheusConfig::default() + }, + ); + assert_eq!(default_out, zero_out); } } diff --git a/openfeature-provider/go/confidence/integration_test.go b/openfeature-provider/go/confidence/integration_test.go index d9ecfbaf..93c46ef5 100644 --- a/openfeature-provider/go/confidence/integration_test.go +++ b/openfeature-provider/go/confidence/integration_test.go @@ -11,6 +11,8 @@ import ( "time" "github.com/open-feature/go-sdk/openfeature" + "github.com/prometheus/common/expfmt" + "github.com/prometheus/common/model" fl "github.com/spotify/confidence-resolver/openfeature-provider/go/confidence/internal/flag_logger" lr "github.com/spotify/confidence-resolver/openfeature-provider/go/confidence/internal/local_resolver" resolverv1 "github.com/spotify/confidence-resolver/openfeature-provider/go/confidence/internal/proto/resolverinternal" @@ -741,11 +743,64 @@ func TestIntegration_GetPrometheusMetrics(t *testing.T) { t.Fatal("GetPrometheusMetrics() returned empty string, expected metrics output") } - if !strings.Contains(metrics, "confidence_resolve_latency") { - t.Errorf("Expected metrics to contain 'confidence_resolve_latency', got:\n%s", metrics) + t.Logf("Prometheus metrics output:\n%s", metrics) + + // Parse with prometheus/common/expfmt — this is the library the customer uses. + // v0.67+ is stricter about duplicate # TYPE headers, which was the reported bug. + parser := expfmt.NewTextParser(model.UTF8Validation) + families, err := parser.TextToMetricFamilies(strings.NewReader(metrics)) + if err != nil { + t.Fatalf("expfmt.TextParser failed to parse metrics output: %v\n\nRaw output:\n%s", err, metrics) } - t.Logf("Prometheus metrics output:\n%s", metrics) + // Verify expected metric families were parsed + latencyFamily, ok := families["confidence_resolve_latency_microseconds"] + if !ok { + t.Fatal("Parsed output missing metric family 'confidence_resolve_latency_microseconds'") + } + if _, ok := families["confidence_resolves_total"]; !ok { + t.Fatal("Parsed output missing metric family 'confidence_resolves_total'") + } + + // Verify resolver_id label is present and instance label is not + for _, m := range latencyFamily.GetMetric() { + hasResolverID := false + for _, lp := range m.GetLabel() { + if lp.GetName() == "resolver_id" { + hasResolverID = true + } + if lp.GetName() == "instance" { + t.Error("Metrics should not contain 'instance' label (renamed to resolver_id)") + } + } + if !hasResolverID { + t.Error("Expected 'resolver_id' label on latency metric") + } + } + + // Verify # HELP text was parsed (expfmt populates Help field) + if latencyFamily.GetHelp() == "" { + t.Error("Expected HELP text for latency histogram") + } + + // Verify BucketsPerDecade reduces output + fullMetrics := provider.GetPrometheusMetrics(SnapshotConfig{BucketsPerDecade: 18}) + halfMetrics := provider.GetPrometheusMetrics(SnapshotConfig{BucketsPerDecade: 9}) + + // Both must also parse cleanly + if _, err := parser.TextToMetricFamilies(strings.NewReader(fullMetrics)); err != nil { + t.Fatalf("BucketsPerDecade=18 output failed to parse: %v", err) + } + if _, err := parser.TextToMetricFamilies(strings.NewReader(halfMetrics)); err != nil { + t.Fatalf("BucketsPerDecade=9 output failed to parse: %v", err) + } + + fullBuckets := strings.Count(fullMetrics, `le="`) + halfBuckets := strings.Count(halfMetrics, `le="`) + if halfBuckets >= fullBuckets { + t.Errorf("BucketsPerDecade=9 (%d buckets) should produce fewer buckets than 18 (%d buckets)", + halfBuckets, fullBuckets) + } // Cleanup openfeature.Shutdown() 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 13ca2da2..c8f1e88c 100755 Binary files a/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm and b/openfeature-provider/go/confidence/internal/local_resolver/assets/confidence_resolver.wasm differ diff --git a/openfeature-provider/go/confidence/internal/local_resolver/local_resolver.go b/openfeature-provider/go/confidence/internal/local_resolver/local_resolver.go index df59c705..1e7675db 100644 --- a/openfeature-provider/go/confidence/internal/local_resolver/local_resolver.go +++ b/openfeature-provider/go/confidence/internal/local_resolver/local_resolver.go @@ -24,7 +24,10 @@ type LocalResolver interface { ApplyFlags(*resolver.ApplyFlagsRequest) error FlushAllLogs() error FlushAssignLogs() error - PrometheusSnapshot() string + // PrometheusSnapshot returns Prometheus/OpenMetrics text-format metrics. + // bucketsPerDecade controls histogram bucket density (1-18, 0 = default 18). + // openmetrics switches output to OpenMetrics text format. + PrometheusSnapshot(bucketsPerDecade uint32, openmetrics bool) string Close(context.Context) error } diff --git a/openfeature-provider/go/confidence/internal/local_resolver/pool.go b/openfeature-provider/go/confidence/internal/local_resolver/pool.go index 58b23831..f264f932 100644 --- a/openfeature-provider/go/confidence/internal/local_resolver/pool.go +++ b/openfeature-provider/go/confidence/internal/local_resolver/pool.go @@ -123,12 +123,54 @@ func (s *PooledResolver) FlushAssignLogs() error { } // PrometheusSnapshot implements LocalResolver. -func (s *PooledResolver) PrometheusSnapshot() string { - var b strings.Builder +// Outputs from multiple pool instances are concatenated with duplicate +// # TYPE and # HELP meta lines removed so the result is valid Prometheus +// exposition text. +func (s *PooledResolver) PrometheusSnapshot(bucketsPerDecade uint32, openmetrics bool) string { + var fragments []string s.maintenance(func(lr LocalResolver) error { - b.WriteString(lr.PrometheusSnapshot()) + text := lr.PrometheusSnapshot(bucketsPerDecade, openmetrics) + if text != "" { + fragments = append(fragments, text) + } return nil }) + if len(fragments) <= 1 { + if len(fragments) == 1 { + return fragments[0] + } + return "" + } + return deduplicateMetaLines(fragments) +} + +// deduplicateMetaLines merges multiple Prometheus/OpenMetrics text fragments, +// keeping only the first occurrence of each # TYPE / # HELP line. +// Any # EOF lines are stripped from fragments and a single # EOF is appended +// at the end if one was present (required for OpenMetrics). +func deduplicateMetaLines(fragments []string) string { + seen := make(map[string]bool) + hasEOF := false + var b strings.Builder + for _, frag := range fragments { + for _, line := range strings.Split(frag, "\n") { + if line == "# EOF" { + hasEOF = true + continue + } + if strings.HasPrefix(line, "# ") { + if seen[line] { + continue + } + seen[line] = true + } + b.WriteString(line) + b.WriteByte('\n') + } + } + if hasEOF { + b.WriteString("# EOF\n") + } return b.String() } diff --git a/openfeature-provider/go/confidence/internal/local_resolver/recover.go b/openfeature-provider/go/confidence/internal/local_resolver/recover.go index 784e745c..97513205 100644 --- a/openfeature-provider/go/confidence/internal/local_resolver/recover.go +++ b/openfeature-provider/go/confidence/internal/local_resolver/recover.go @@ -141,7 +141,7 @@ func (r *RecoveringResolver) FlushAssignLogs() (err error) { return } -func (r *RecoveringResolver) PrometheusSnapshot() string { +func (r *RecoveringResolver) PrometheusSnapshot(bucketsPerDecade uint32, openmetrics bool) string { defer func() { if rec := recover(); rec != nil { slog.Warn("PrometheusSnapshot panicked, ignoring", "error", rec) @@ -151,7 +151,7 @@ func (r *RecoveringResolver) PrometheusSnapshot() string { return "" } lr := r.get() - return lr.PrometheusSnapshot() + return lr.PrometheusSnapshot(bucketsPerDecade, openmetrics) } func (r *RecoveringResolver) Close(ctx context.Context) error { diff --git a/openfeature-provider/go/confidence/internal/local_resolver/recover_test.go b/openfeature-provider/go/confidence/internal/local_resolver/recover_test.go index d44eb104..02dcde7a 100644 --- a/openfeature-provider/go/confidence/internal/local_resolver/recover_test.go +++ b/openfeature-provider/go/confidence/internal/local_resolver/recover_test.go @@ -27,7 +27,7 @@ func (m *mockResolver) RegisterResolve(*wasm.RegisterResolveRequest) {} func (m *mockResolver) ApplyFlags(*resolver.ApplyFlagsRequest) error { return nil } func (m *mockResolver) FlushAllLogs() error { return nil } func (m *mockResolver) FlushAssignLogs() error { return nil } -func (m *mockResolver) PrometheusSnapshot() string { return "" } +func (m *mockResolver) PrometheusSnapshot(_ uint32, _ bool) string { return "" } func (m *mockResolver) Close(context.Context) error { return nil } // mockFactory returns a panicking mockResolver on the first call and diff --git a/openfeature-provider/go/confidence/internal/local_resolver/wasm.go b/openfeature-provider/go/confidence/internal/local_resolver/wasm.go index 325819b5..07862b11 100644 --- a/openfeature-provider/go/confidence/internal/local_resolver/wasm.go +++ b/openfeature-provider/go/confidence/internal/local_resolver/wasm.go @@ -85,9 +85,11 @@ func (r *WasmResolver) FlushAssignLogs() error { return err } -func (r *WasmResolver) PrometheusSnapshot() string { +func (r *WasmResolver) PrometheusSnapshot(bucketsPerDecade uint32, openmetrics bool) string { req := &wasm.PrometheusSnapshotRequest{ - Instance: r.instanceID, + Instance: r.instanceID, + BucketsPerDecade: bucketsPerDecade, + Openmetrics: openmetrics, } resp := &wasm.PrometheusSnapshotResponse{} if err := r.call("wasm_msg_guest_prometheus_snapshot", req, resp); err != nil { diff --git a/openfeature-provider/go/confidence/internal/proto/admin/resolver.pb.go b/openfeature-provider/go/confidence/internal/proto/admin/resolver.pb.go index 7027d97e..ca3370ef 100644 --- a/openfeature-provider/go/confidence/internal/proto/admin/resolver.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/admin/resolver.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v5.27.3 // source: confidence/flags/admin/v1/resolver.proto package admin diff --git a/openfeature-provider/go/confidence/internal/proto/resolver/api.pb.go b/openfeature-provider/go/confidence/internal/proto/resolver/api.pb.go index 31626ca5..84e43f59 100644 --- a/openfeature-provider/go/confidence/internal/proto/resolver/api.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/resolver/api.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v5.27.3 // source: confidence/flags/resolver/v1/api.proto package resolver diff --git a/openfeature-provider/go/confidence/internal/proto/resolver/api_grpc.pb.go b/openfeature-provider/go/confidence/internal/proto/resolver/api_grpc.pb.go index 8954a668..875eef04 100644 --- a/openfeature-provider/go/confidence/internal/proto/resolver/api_grpc.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/resolver/api_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v5.27.3 // source: confidence/flags/resolver/v1/api.proto package resolver diff --git a/openfeature-provider/go/confidence/internal/proto/resolver/types.pb.go b/openfeature-provider/go/confidence/internal/proto/resolver/types.pb.go index c2ed9bc6..99be39ba 100644 --- a/openfeature-provider/go/confidence/internal/proto/resolver/types.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/resolver/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v5.27.3 // source: confidence/flags/resolver/v1/types.proto package resolver diff --git a/openfeature-provider/go/confidence/internal/proto/resolverinternal/internal_api.pb.go b/openfeature-provider/go/confidence/internal/proto/resolverinternal/internal_api.pb.go index 3ca0b257..d91c0391 100644 --- a/openfeature-provider/go/confidence/internal/proto/resolverinternal/internal_api.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/resolverinternal/internal_api.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v5.27.3 // source: confidence/flags/resolver/v1/internal_api.proto // IMPORTANT: Package name must match backend expectation for gRPC service discovery diff --git a/openfeature-provider/go/confidence/internal/proto/resolverinternal/internal_api_grpc.pb.go b/openfeature-provider/go/confidence/internal/proto/resolverinternal/internal_api_grpc.pb.go index 7374215d..a36965c2 100644 --- a/openfeature-provider/go/confidence/internal/proto/resolverinternal/internal_api_grpc.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/resolverinternal/internal_api_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 -// - protoc v5.29.3 +// - protoc v5.27.3 // source: confidence/flags/resolver/v1/internal_api.proto // IMPORTANT: Package name must match backend expectation for gRPC service discovery diff --git a/openfeature-provider/go/confidence/internal/proto/types/target.pb.go b/openfeature-provider/go/confidence/internal/proto/types/target.pb.go index 340b8a20..408bce9d 100644 --- a/openfeature-provider/go/confidence/internal/proto/types/target.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/types/target.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v5.27.3 // source: confidence/flags/types/v1/target.proto package types diff --git a/openfeature-provider/go/confidence/internal/proto/types/types.pb.go b/openfeature-provider/go/confidence/internal/proto/types/types.pb.go index d33c31b2..c0efd0ef 100644 --- a/openfeature-provider/go/confidence/internal/proto/types/types.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/types/types.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v5.27.3 // source: confidence/flags/types/v1/types.proto package types diff --git a/openfeature-provider/go/confidence/internal/proto/wasm/messages.pb.go b/openfeature-provider/go/confidence/internal/proto/wasm/messages.pb.go index b2ed8ff6..d855e31a 100644 --- a/openfeature-provider/go/confidence/internal/proto/wasm/messages.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/wasm/messages.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v5.27.3 // source: confidence/wasm/messages.proto package wasm @@ -245,10 +245,12 @@ func (*Response_Data) isResponse_Result() {} func (*Response_Error) isResponse_Result() {} type PrometheusSnapshotRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Instance string `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Instance string `protobuf:"bytes,1,opt,name=instance,proto3" json:"instance,omitempty"` + BucketsPerDecade uint32 `protobuf:"varint,2,opt,name=buckets_per_decade,json=bucketsPerDecade,proto3" json:"buckets_per_decade,omitempty"` + Openmetrics bool `protobuf:"varint,3,opt,name=openmetrics,proto3" json:"openmetrics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PrometheusSnapshotRequest) Reset() { @@ -288,6 +290,20 @@ func (x *PrometheusSnapshotRequest) GetInstance() string { return "" } +func (x *PrometheusSnapshotRequest) GetBucketsPerDecade() uint32 { + if x != nil { + return x.BucketsPerDecade + } + return 0 +} + +func (x *PrometheusSnapshotRequest) GetOpenmetrics() bool { + if x != nil { + return x.Openmetrics + } + return false +} + type PrometheusSnapshotResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` @@ -348,9 +364,11 @@ const file_confidence_wasm_messages_proto_rawDesc = "" + "\bResponse\x12\x14\n" + "\x04data\x18\x01 \x01(\fH\x00R\x04data\x12\x16\n" + "\x05error\x18\x02 \x01(\tH\x00R\x05errorB\b\n" + - "\x06result\"7\n" + + "\x06result\"\x87\x01\n" + "\x19PrometheusSnapshotRequest\x12\x1a\n" + - "\binstance\x18\x01 \x01(\tR\binstance\"0\n" + + "\binstance\x18\x01 \x01(\tR\binstance\x12,\n" + + "\x12buckets_per_decade\x18\x02 \x01(\rR\x10bucketsPerDecade\x12 \n" + + "\vopenmetrics\x18\x03 \x01(\bR\vopenmetrics\"0\n" + "\x1aPrometheusSnapshotResponse\x12\x12\n" + "\x04text\x18\x01 \x01(\tR\x04textB\x8c\x01\n" + "\x1fcom.spotify.confidence.sdk.wasmB\bMessagesP\x00Z]github.com/spotify/confidence-resolver/openfeature-provider/go/confidence/internal/proto/wasmb\x06proto3" diff --git a/openfeature-provider/go/confidence/internal/proto/wasm/wasm_api.pb.go b/openfeature-provider/go/confidence/internal/proto/wasm/wasm_api.pb.go index 12fac671..b62fb247 100644 --- a/openfeature-provider/go/confidence/internal/proto/wasm/wasm_api.pb.go +++ b/openfeature-provider/go/confidence/internal/proto/wasm/wasm_api.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 -// protoc v5.29.3 +// protoc-gen-go v1.36.6 +// protoc v5.27.3 // source: confidence/wasm/wasm_api.proto package wasm diff --git a/openfeature-provider/go/confidence/internal/testutil/helpers.go b/openfeature-provider/go/confidence/internal/testutil/helpers.go index 60eb428d..4edb0836 100644 --- a/openfeature-provider/go/confidence/internal/testutil/helpers.go +++ b/openfeature-provider/go/confidence/internal/testutil/helpers.go @@ -428,7 +428,7 @@ func (m *MockedLocalResolver) ResolveProcess(*wasm.ResolveProcessRequest) (*wasm return m.Response, m.Err } func (m MockedLocalResolver) SetResolverState(*wasm.SetResolverStateRequest) error { return nil } -func (m MockedLocalResolver) PrometheusSnapshot() string { return "" } +func (m MockedLocalResolver) PrometheusSnapshot(_ uint32, _ bool) string { return "" } func (m MockedLocalResolver) RegisterResolve(*wasm.RegisterResolveRequest) {} func (m MockedLocalResolver) ApplyFlags(*resolver.ApplyFlagsRequest) error { return nil } diff --git a/openfeature-provider/go/confidence/materialization.go b/openfeature-provider/go/confidence/materialization.go index 31dc8b54..118fa593 100644 --- a/openfeature-provider/go/confidence/materialization.go +++ b/openfeature-provider/go/confidence/materialization.go @@ -189,8 +189,8 @@ func (m *materializationSupportedResolver) SetResolverState(request *wasm.SetRes return m.current.SetResolverState(request) } -func (m *materializationSupportedResolver) PrometheusSnapshot() string { - return m.current.PrometheusSnapshot() +func (m *materializationSupportedResolver) PrometheusSnapshot(bucketsPerDecade uint32, openmetrics bool) string { + return m.current.PrometheusSnapshot(bucketsPerDecade, openmetrics) } func (m *materializationSupportedResolver) Close(ctx context.Context) error { diff --git a/openfeature-provider/go/confidence/provider.go b/openfeature-provider/go/confidence/provider.go index 56d887a3..432e1cfc 100644 --- a/openfeature-provider/go/confidence/provider.go +++ b/openfeature-provider/go/confidence/provider.go @@ -353,17 +353,26 @@ func evaluate[T any]( // SnapshotConfig holds options for GetPrometheusMetrics. // // Experimental: this API is subject to change. -type SnapshotConfig struct{} +type SnapshotConfig struct { + // BucketsPerDecade controls histogram bucket density in the output. + // The internal histogram always stores 18 buckets per decimal order + // of magnitude. This controls how many are emitted by striding. + // Valid range: 1-18. Zero means default (18 — all buckets). + BucketsPerDecade uint32 + // OpenMetrics switches output to OpenMetrics text format. + // When false (default), Prometheus exposition format is used. + OpenMetrics bool +} // GetPrometheusMetrics returns a Prometheus text-format metrics snapshot // aggregated from all pooled resolver instances. // // Experimental: this API is subject to change. -func (p *LocalResolverProvider) GetPrometheusMetrics(_ SnapshotConfig) string { +func (p *LocalResolverProvider) GetPrometheusMetrics(config SnapshotConfig) string { if p.resolver == nil { return "" } - return p.resolver.PrometheusSnapshot() + return p.resolver.PrometheusSnapshot(config.BucketsPerDecade, config.OpenMetrics) } // Resolve resolves multiple flags for the given context. If flagNames is empty, diff --git a/openfeature-provider/go/confidence/provider_test.go b/openfeature-provider/go/confidence/provider_test.go index 58c5548c..1219c156 100644 --- a/openfeature-provider/go/confidence/provider_test.go +++ b/openfeature-provider/go/confidence/provider_test.go @@ -499,7 +499,7 @@ func (m *mockResolverAPIForInit) FlushAssignLogs() error { return nil } -func (m *mockResolverAPIForInit) PrometheusSnapshot() string { +func (m *mockResolverAPIForInit) PrometheusSnapshot(_ uint32, _ bool) string { return "" } diff --git a/openfeature-provider/go/go.mod b/openfeature-provider/go/go.mod index 1309c33e..f2343c9b 100644 --- a/openfeature-provider/go/go.mod +++ b/openfeature-provider/go/go.mod @@ -7,13 +7,18 @@ require github.com/open-feature/go-sdk v1.16.0 require github.com/tetratelabs/wazero v1.9.0 require ( + github.com/prometheus/common v0.67.5 google.golang.org/grpc v1.79.3 - google.golang.org/protobuf v1.36.10 + google.golang.org/protobuf v1.36.11 ) require ( github.com/go-logr/logr v1.4.3 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect go.uber.org/mock v0.6.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/text v0.32.0 // indirect diff --git a/openfeature-provider/go/go.sum b/openfeature-provider/go/go.sum index 19800881..896b9769 100644 --- a/openfeature-provider/go/go.sum +++ b/openfeature-provider/go/go.sum @@ -1,5 +1,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -10,8 +13,26 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/open-feature/go-sdk v1.16.0 h1:5NCHYv5slvNBIZhYXAzAufo0OI59OACZ5tczVqSE+Tg= github.com/open-feature/go-sdk v1.16.0/go.mod h1:EIF40QcoYT1VbQkMPy2ZJH4kvZeY+qGUXAorzSWgKSo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= @@ -28,6 +49,8 @@ go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6 go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= @@ -40,5 +63,10 @@ google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1: google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/openfeature-provider/js/src/WasmResolver.ts b/openfeature-provider/js/src/WasmResolver.ts index f1434420..2ff61f88 100644 --- a/openfeature-provider/js/src/WasmResolver.ts +++ b/openfeature-provider/js/src/WasmResolver.ts @@ -116,7 +116,10 @@ export class UnsafeWasmResolver implements LocalResolver { } prometheusSnapshot(instance: string): string { - const reqPtr = this.transferRequest({ instance }, PrometheusSnapshotRequest); + const reqPtr = this.transferRequest( + { instance, bucketsPerDecade: 0, openmetrics: false }, + PrometheusSnapshotRequest, + ); const resPtr = this.exports.wasm_msg_guest_prometheus_snapshot(reqPtr); return this.consumeResponse(resPtr, PrometheusSnapshotResponse).text; } diff --git a/openfeature-provider/proto/confidence/wasm/messages.proto b/openfeature-provider/proto/confidence/wasm/messages.proto index 17b4927d..b7dd1541 100644 --- a/openfeature-provider/proto/confidence/wasm/messages.proto +++ b/openfeature-provider/proto/confidence/wasm/messages.proto @@ -30,6 +30,8 @@ message Response { message PrometheusSnapshotRequest { string instance = 1; + uint32 buckets_per_decade = 2; + bool openmetrics = 3; } message PrometheusSnapshotResponse { diff --git a/wasm/proto/messages.proto b/wasm/proto/messages.proto index c602f364..8e8e9803 100644 --- a/wasm/proto/messages.proto +++ b/wasm/proto/messages.proto @@ -41,6 +41,8 @@ message Response { message PrometheusSnapshotRequest { string instance = 1; + uint32 buckets_per_decade = 2; + bool openmetrics = 3; } message PrometheusSnapshotResponse { diff --git a/wasm/rust-guest/src/lib.rs b/wasm/rust-guest/src/lib.rs index e4f255be..50a8988e 100644 --- a/wasm/rust-guest/src/lib.rs +++ b/wasm/rust-guest/src/lib.rs @@ -194,7 +194,11 @@ wasm_msg_guest! { } fn prometheus_snapshot(request: proto::PrometheusSnapshotRequest) -> WasmResult { - let text = TELEMETRY.snapshot().to_prometheus(&request.instance); + let config = confidence_resolver::telemetry::PrometheusConfig { + buckets_per_decade: request.buckets_per_decade, + openmetrics: request.openmetrics, + }; + let text = TELEMETRY.snapshot().to_prometheus(&request.instance, &config); Ok(proto::PrometheusSnapshotResponse { text }) }