Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
f41gh7 committed Nov 24, 2023
1 parent 972ae4e commit 6945973
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 62 deletions.
139 changes: 77 additions & 62 deletions go_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:

Check warning on line 81 in go_metrics.go

View check run for this annotation

Codecov / codecov/patch

go_metrics.go#L81

Added line #L81 was not covered by tests
// not supported sample kind by current runtime version
return
case runtime_metrics.KindUint64:
fmt.Fprintf(w, "%s %d\n", name, sample.Value.Uint64())

Check warning on line 85 in go_metrics.go

View check run for this annotation

Codecov / codecov/patch

go_metrics.go#L83-L85

Added lines #L83 - L85 were not covered by tests
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)
}
}
88 changes: 88 additions & 0 deletions go_metrics_test.go
Original file line number Diff line number Diff line change
@@ -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)},
})
}

0 comments on commit 6945973

Please sign in to comment.