From 6945973d8c4d187e5df63c524348df1d056cea13 Mon Sep 17 00:00:00 2001 From: f41gh7 Date: Fri, 24 Nov 2023 17:15:23 +0100 Subject: [PATCH] add tests --- go_metrics.go | 139 +++++++++++++++++++++++++-------------------- go_metrics_test.go | 88 ++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 62 deletions(-) create mode 100644 go_metrics_test.go diff --git a/go_metrics.go b/go_metrics.go index 90b5bbd..e6364ee 100644 --- a/go_metrics.go +++ b/go_metrics.go @@ -4,80 +4,22 @@ import ( "fmt" "io" "log" + "math" "runtime" runtime_metrics "runtime/metrics" "github.com/valyala/histogram" ) -func writeRuntimeFloat64Metric(w io.Writer, name string, sample runtime_metrics.Sample) { - if sample.Value.Kind() == runtime_metrics.KindBad { - // skip not supported metric - return - } - fmt.Fprintf(w, "%s %g\n", name, sample.Value.Float64()) -} - -func writeRuntimeHistogramMetric(w io.Writer, name string, sample runtime_metrics.Sample) { - if sample.Value.Kind() == runtime_metrics.KindBad { - // skip not supported metric - return - } - // it's unsafe to modify histogram - h := sample.Value.Float64Histogram() - if len(h.Buckets) == 0 { - return - } - // sanity check - if len(h.Buckets) < len(h.Counts) { - log.Printf("ERROR: runtime_metrics.histogram: %q bad format for histogram, expected buckets to be less then counts, got: bucket %d: counts: %d", name, len(h.Buckets), len(h.Counts)) - return - } - var sum uint64 - // filter empty bins and convert histogram to cumulative - for _, weight := range h.Counts { - if weight == 0 { - continue - } - sum += weight - } - quantile := func(phi float64) float64 { - switch phi { - case 0: - return h.Buckets[0] - case 1: - // its guaranteed that histogram bucket has at least three values -inf, bound and + inf - return h.Buckets[len(h.Buckets)-2] - } - reqValue := phi * float64(sum) - prevIdx := 0 - cumulativeWeight := uint64(0) - for idx, weight := range h.Counts { - cumulativeWeight += weight - if reqValue < float64(cumulativeWeight) { - prevIdx = idx - continue - } - break - } - - return h.Buckets[prevIdx] - } - phis := []float64{0, 0.25, 0.5, 0.75, 1} - for _, phi := range phis { - q := quantile(phi) - fmt.Fprintf(w, `%s{quantile="%g"} %g`+"\n", name, phi, q) - } -} - func writeGoMetrics(w io.Writer) { + // https://pkg.go.dev/runtime/metrics#hdr-Supported_metrics runtimeMetricSamples := [2]runtime_metrics.Sample{ {Name: "/sched/latencies:seconds"}, {Name: "/sync/mutex/wait/total:seconds"}, } runtime_metrics.Read(runtimeMetricSamples[:]) - writeRuntimeHistogramMetric(w, "go_sched_latency_seconds", runtimeMetricSamples[0]) - writeRuntimeFloat64Metric(w, "go_mutex_wait_total_seconds", runtimeMetricSamples[1]) + writeRuntimeMetric(w, "go_sched_latency_seconds", runtimeMetricSamples[0]) + writeRuntimeMetric(w, "go_mutex_wait_total_seconds", runtimeMetricSamples[1]) var ms runtime.MemStats runtime.ReadMemStats(&ms) @@ -133,3 +75,76 @@ func writeGoMetrics(w io.Writer) { fmt.Fprintf(w, "go_info_ext{compiler=%q, GOARCH=%q, GOOS=%q, GOROOT=%q} 1\n", runtime.Compiler, runtime.GOARCH, runtime.GOOS, runtime.GOROOT()) } + +func writeRuntimeMetric(w io.Writer, name string, sample runtime_metrics.Sample) { + switch sample.Value.Kind() { + case runtime_metrics.KindBad: + // not supported sample kind by current runtime version + return + case runtime_metrics.KindUint64: + fmt.Fprintf(w, "%s %d\n", name, sample.Value.Uint64()) + case runtime_metrics.KindFloat64: + fmt.Fprintf(w, "%s %g\n", name, sample.Value.Float64()) + case runtime_metrics.KindFloat64Histogram: + writeRuntimeHistogramMetric(w, name, sample.Value.Float64Histogram()) + } +} + +func writeRuntimeHistogramMetric(w io.Writer, name string, h *runtime_metrics.Float64Histogram) { + // it's unsafe to modify histogram + if len(h.Buckets) == 0 { + return + } + // sanity check + if len(h.Buckets) < len(h.Counts) { + log.Printf("ERROR: runtime_metrics.histogram: %q bad format for histogram, expected buckets to be less then counts, got: bucket %d: counts: %d", name, len(h.Buckets), len(h.Counts)) + return + } + var sum uint64 + // filter empty bins and convert histogram to cumulative + for _, weight := range h.Counts { + if weight == 0 { + continue + } + sum += weight + } + var lastNonInf float64 + for i := len(h.Buckets) - 1; i > 0; i-- { + if !math.IsInf(h.Buckets[i], 0) { + lastNonInf = h.Buckets[i] + break + } + } + quantile := func(phi float64) float64 { + switch phi { + case 0: + return h.Buckets[0] + case 1: + return lastNonInf + } + reqValue := phi * float64(sum) + upperBoundIdx := 0 + cumulativeWeight := uint64(0) + for idx, weight := range h.Counts { + cumulativeWeight += weight + if float64(cumulativeWeight) > reqValue { + upperBoundIdx = idx + break + } + } + // the first bucket is inclusive + if upperBoundIdx > 0 { + upperBoundIdx++ + } + // last bucket may have an inf value, return last non inf in this case + if upperBoundIdx >= len(h.Buckets)-1 { + return lastNonInf + } + return h.Buckets[upperBoundIdx] + } + phis := []float64{0, 0.25, 0.5, 0.75, 0.95, 1} + for _, phi := range phis { + q := quantile(phi) + fmt.Fprintf(w, `%s{quantile="%g"} %g`+"\n", name, phi, q) + } +} diff --git a/go_metrics_test.go b/go_metrics_test.go new file mode 100644 index 0000000..916077c --- /dev/null +++ b/go_metrics_test.go @@ -0,0 +1,88 @@ +package metrics + +import ( + "math" + runtime_metrics "runtime/metrics" + "strings" + "testing" +) + +func TestWriteRuntimeHistogramMetricOk(t *testing.T) { + f := func(expected string, metricName string, h runtime_metrics.Float64Histogram) { + t.Helper() + var wOut strings.Builder + writeRuntimeHistogramMetric(&wOut, metricName, &h) + got := wOut.String() + if got != expected { + t.Fatalf("got out: \n%s\nwant: \n%s", got, expected) + } + + } + + f(`runtime_latency_seconds{quantile="0"} 1 +runtime_latency_seconds{quantile="0.25"} 3 +runtime_latency_seconds{quantile="0.5"} 4 +runtime_latency_seconds{quantile="0.75"} 4 +runtime_latency_seconds{quantile="0.95"} 4 +runtime_latency_seconds{quantile="1"} 4 +`, + `runtime_latency_seconds`, runtime_metrics.Float64Histogram{ + Counts: []uint64{1, 2, 3}, + Buckets: []float64{1.0, 2.0, 3.0, 4.0}, + }) + f(`runtime_latency_seconds{quantile="0"} 1 +runtime_latency_seconds{quantile="0.25"} 3 +runtime_latency_seconds{quantile="0.5"} 3 +runtime_latency_seconds{quantile="0.75"} 3 +runtime_latency_seconds{quantile="0.95"} 4 +runtime_latency_seconds{quantile="1"} 4 +`, + `runtime_latency_seconds`, runtime_metrics.Float64Histogram{ + Counts: []uint64{0, 25, 1, 3, 0}, + Buckets: []float64{1.0, 2.0, 3.0, 4.0, math.Inf(1)}, + }) + f(`runtime_latency_seconds{quantile="0"} 1 +runtime_latency_seconds{quantile="0.25"} 7 +runtime_latency_seconds{quantile="0.5"} 9 +runtime_latency_seconds{quantile="0.75"} 9 +runtime_latency_seconds{quantile="0.95"} 10 +runtime_latency_seconds{quantile="1"} 10 +`, + `runtime_latency_seconds`, runtime_metrics.Float64Histogram{ + Counts: []uint64{0, 25, 1, 3, 0, 44, 15, 132, 10, 11}, + Buckets: []float64{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, math.Inf(1)}, + }) + f(`runtime_latency_seconds{quantile="0"} -Inf +runtime_latency_seconds{quantile="0.25"} 4 +runtime_latency_seconds{quantile="0.5"} 4 +runtime_latency_seconds{quantile="0.75"} 4 +runtime_latency_seconds{quantile="0.95"} 4 +runtime_latency_seconds{quantile="1"} 4 +`, + `runtime_latency_seconds`, runtime_metrics.Float64Histogram{ + Counts: []uint64{1, 5}, + Buckets: []float64{math.Inf(-1), 4.0, math.Inf(1)}, + }) +} + +func TestWriteRuntimeHistogramMetricFail(t *testing.T) { + f := func(h runtime_metrics.Float64Histogram) { + t.Helper() + var wOut strings.Builder + writeRuntimeHistogramMetric(&wOut, ``, &h) + got := wOut.String() + if got != "" { + t.Fatalf("expected empty output, got out: \n%s", got) + } + + } + + f(runtime_metrics.Float64Histogram{ + Counts: []uint64{}, + Buckets: []float64{}, + }) + f(runtime_metrics.Float64Histogram{ + Counts: []uint64{0, 25, 1, 3, 0, 12, 12}, + Buckets: []float64{1.0, 2.0, 3.0, 4.0, math.Inf(1)}, + }) +}