-
Notifications
You must be signed in to change notification settings - Fork 76
Prometheus compatible histograms #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
hagen1778
merged 10 commits into
VictoriaMetrics:master
from
clementnuss:feat/otlp_histogram
Jun 13, 2025
+593
−4
Merged
Changes from 5 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d1c7648
feat: implement prometheus histograms
clementnuss 2a25c80
chore: implement custom buckets (with validation) and bug fixes
clementnuss 5592cc9
chore: complete PromHistogram.Merge() implementation
clementnuss 9c49c82
test(promHistogram): cover merge edge cases
clementnuss 67ff519
chore(promHistogram): remove Merge implementation and add LinearBuckets
clementnuss 0cfd41a
prometheus_histogram.go: fix typos and erroneous doc
clementnuss d313324
Update prometheus_histogram_test.go
clementnuss f96d9d1
prometheus_histogram: implement PR suggestions
clementnuss 95d307d
review updates
hagen1778 5bfbe76
add test for empty histogram rendering
hagen1778 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| package metrics | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "io" | ||
| "math" | ||
| "sync" | ||
| ) | ||
|
|
||
| var PrometheusHistogramDefaultBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} | ||
|
|
||
| // Prometheus Histogram is a histogram for non-negative values with pre-defined buckets | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // | ||
| // Each bucket contains a counter for values in the given range. | ||
| // Each bucket is exposed via the following metric: | ||
clementnuss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // | ||
| // <metric_name>_bucket{<optional_tags>,le="upper_bound"} <counter> | ||
| // | ||
| // Where: | ||
| // | ||
| // - <metric_name> is the metric name passed to NewHistogram | ||
| // - <optional_tags> is optional tags for the <metric_name>, which are passed to NewHistogram | ||
| // - <upper_bound> - upper bound of the current bucket. all samples <= upper_bound are in that bucket | ||
| // - <counter> - the number of hits to the given bucket during Update* calls | ||
| // | ||
| // Zero histogram is usable. | ||
| type PrometheusHistogram struct { | ||
| // Mu gurantees synchronous update for all the counters and sum. | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // | ||
| // Do not use sync.RWMutex, since it has zero sense from performance PoV. | ||
| // It only complicates the code. | ||
| mu sync.Mutex | ||
|
|
||
| upperBounds []float64 | ||
| buckets []uint64 | ||
clementnuss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // count is the counter for all observations on this histogram | ||
| count uint64 | ||
|
|
||
| // sum is the sum of all the values put into Histogram | ||
| sum float64 | ||
| } | ||
|
|
||
| // Reset resets the given histogram. | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| func (h *PrometheusHistogram) Reset() { | ||
| h.mu.Lock() | ||
| for i := range h.buckets { | ||
| h.buckets[i] = 0 | ||
| } | ||
|
|
||
| h.sum = 0 | ||
| h.count = 0 | ||
| h.mu.Unlock() | ||
| } | ||
|
|
||
| // Update updates h with v. | ||
| // | ||
| // Negative values and NaNs are ignored. | ||
| func (h *PrometheusHistogram) Update(v float64) { | ||
| if math.IsNaN(v) || v < 0 { | ||
| // Skip NaNs and negative values. | ||
| return | ||
| } | ||
| bucketIdx := -1 | ||
| for i, ub := range h.upperBounds { | ||
| if v <= ub { | ||
| bucketIdx = i | ||
| break | ||
| } | ||
| } | ||
| h.mu.Lock() | ||
| defer h.mu.Unlock() | ||
| h.sum += v | ||
| h.count++ | ||
| if bucketIdx == -1 { | ||
| // +Inf, nothing to do, already accounted for in the total sum | ||
| return | ||
| } | ||
| h.buckets[bucketIdx]++ | ||
| } | ||
|
|
||
| // NewPrometheusHistogram creates and returns new prometheus histogram with the given name. | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // | ||
| // name must be valid Prometheus-compatible metric with possible labels. | ||
| // For instance, | ||
| // | ||
| // - foo | ||
| // - foo{bar="baz"} | ||
| // - foo{bar="baz",aaa="b"} | ||
| // | ||
| // The returned histogram is safe to use from concurrent goroutines. | ||
| func NewPrometheusHistogram(name string) *PrometheusHistogram { | ||
| return defaultSet.NewPrometheusHistogram(name) | ||
| } | ||
|
|
||
| // NewPrometheusHistogram creates and returns new prometheus histogram with the given name. | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // | ||
| // name must be valid Prometheus-compatible metric with possible labels. | ||
| // For instance, | ||
| // | ||
| // - foo | ||
| // - foo{bar="baz"} | ||
| // - foo{bar="baz",aaa="b"} | ||
| // | ||
| // The returned histogram is safe to use from concurrent goroutines. | ||
| func NewPrometheusHistogramExt(name string, upperBounds []float64) *PrometheusHistogram { | ||
hagen1778 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return defaultSet.NewPrometheusHistogramExt(name, upperBounds) | ||
| } | ||
|
|
||
| // GetOrCreateHistogram returns registered histogram with the given name | ||
| // or creates new histogram if the registry doesn't contain histogram with | ||
| // the given name. | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // | ||
| // name must be valid Prometheus-compatible metric with possible labels. | ||
| // For instance, | ||
| // | ||
| // - foo | ||
| // - foo{bar="baz"} | ||
| // - foo{bar="baz",aaa="b"} | ||
| // | ||
| // The returned histogram is safe to use from concurrent goroutines. | ||
| // | ||
| // Performance tip: prefer NewHistogram instead of GetOrCreateHistogram. | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| func GetOrCreatePrometheusHistogram(name string) *PrometheusHistogram { | ||
| return defaultSet.GetOrCreatePrometheusHistogram(name) | ||
| } | ||
|
|
||
| // GetOrCreateHistogramExt returns registered histogram with the given name and | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // upperBounds or creates new histogram if the registry doesn't contain histogram | ||
| // with the given name. | ||
| // | ||
| // name must be valid Prometheus-compatible metric with possible labels. | ||
| // For instance, | ||
| // | ||
| // - foo | ||
| // - foo{bar="baz"} | ||
| // - foo{bar="baz",aaa="b"} | ||
| // | ||
| // The returned histogram is safe to use from concurrent goroutines. | ||
| // | ||
| // Performance tip: prefer NewHistogram instead of GetOrCreateHistogram. | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| func GetOrCreatePrometheusHistogramExt(name string, upperBounds []float64) *PrometheusHistogram { | ||
| return defaultSet.GetOrCreatePrometheusHistogramExt(name, upperBounds) | ||
| } | ||
|
|
||
| func newPrometheusHistogram(upperBounds []float64) *PrometheusHistogram { | ||
| mustValidateBuckets(upperBounds) | ||
| last := len(upperBounds) - 1 | ||
| if math.IsInf(upperBounds[last], +1) { | ||
| upperBounds = upperBounds[:last] // ignore +Inf bucket as it is covered anyways | ||
| } | ||
| h := PrometheusHistogram{ | ||
| upperBounds: upperBounds, | ||
| buckets: make([]uint64, len(upperBounds)), | ||
| } | ||
|
|
||
| return &h | ||
| } | ||
|
|
||
| func mustValidateBuckets(upperBounds []float64) { | ||
| if err := ValidateBuckets(upperBounds); err != nil { | ||
| panic(err) | ||
| } | ||
| } | ||
|
|
||
| func ValidateBuckets(upperBounds []float64) error { | ||
| if len(upperBounds) == 0 { | ||
| return fmt.Errorf("no upper bounds were given for the buckets") | ||
clementnuss marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| for i := 0; i < len(upperBounds)-1; i++ { | ||
| if upperBounds[i] >= upperBounds[i+1] { | ||
| return fmt.Errorf("upper bounds for the buckets must be strictly increasing") | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func LinearBuckets(start, width float64, count int) []float64 { | ||
clementnuss marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if count < 1 { | ||
| panic("LinearBuckets needs a positive count") | ||
| } | ||
| upperBounds := make([]float64, count) | ||
| for i := range upperBounds { | ||
| upperBounds[i] = start | ||
| start += width | ||
| } | ||
| return upperBounds | ||
| } | ||
|
|
||
| func (h *PrometheusHistogram) marshalTo(prefix string, w io.Writer) { | ||
| cumulativeSum := uint64(0) | ||
| h.mu.Lock() | ||
| count := h.count | ||
| sum := h.sum | ||
| if count == 0 { | ||
| h.mu.Unlock() | ||
hagen1778 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return | ||
| } | ||
| for i, ub := range h.upperBounds { | ||
| cumulativeSum += h.buckets[i] | ||
| tag := fmt.Sprintf(`le="%v"`, ub) | ||
| metricName := addTag(prefix, tag) | ||
| name, labels := splitMetricName(metricName) | ||
| fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, cumulativeSum) | ||
| } | ||
| h.mu.Unlock() | ||
|
|
||
| tag := fmt.Sprintf("le=%q", "+Inf") | ||
| metricName := addTag(prefix, tag) | ||
| name, labels := splitMetricName(metricName) | ||
| fmt.Fprintf(w, "%s_bucket%s %d\n", name, labels, count) | ||
|
|
||
| name, labels = splitMetricName(prefix) | ||
| if float64(int64(sum)) == sum { | ||
| fmt.Fprintf(w, "%s_sum%s %d\n", name, labels, int64(sum)) | ||
| } else { | ||
| fmt.Fprintf(w, "%s_sum%s %g\n", name, labels, sum) | ||
| } | ||
| fmt.Fprintf(w, "%s_count%s %d\n", name, labels, count) | ||
| } | ||
|
|
||
| func (h *PrometheusHistogram) metricType() string { | ||
| return "histogram" | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.