Skip to content

Commit 330e8fb

Browse files
committed
support native histograms
Signed-off-by: Jan Fajerski <[email protected]>
1 parent 3db0564 commit 330e8fb

File tree

8 files changed

+317
-1712
lines changed

8 files changed

+317
-1712
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ executors:
66
# Whenever the Go version is updated here, .promu.yml should also be updated.
77
golang:
88
docker:
9-
- image: cimg/go:1.20
9+
- image: cimg/go:1.21
1010
jobs:
1111
test:
1212
executor: golang

.promu.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
go:
33
# Whenever the Go version is updated here
44
# .circle/config.yml should also be updated.
5-
version: 1.20
5+
version: 1.21
66
repository:
77
path: github.com/prometheus/prom2json
88
build:

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ Example input from stdin:
8282
Note that all numbers are encoded as strings. Some parsers want it
8383
that way. Also, Prometheus allows sample values like `NaN` or `+Inf`,
8484
which cannot be encoded as JSON numbers.
85+
Native histograms are formated similarly as [the query
86+
API](https://prometheus.io/docs/prometheus/latest/querying/api/#native-histograms)
87+
would return.
8588

8689
```json
8790
[
@@ -129,6 +132,40 @@ which cannot be encoded as JSON numbers.
129132
"value": "1063110"
130133
}
131134
]
135+
},
136+
{
137+
"name": "http_request_duration_seconds",
138+
"type": "HISTOGRAM",
139+
"help": "More HTTP request latencies in seconds.",
140+
"metrics": [
141+
{
142+
"labels": {
143+
"method": "GET",
144+
},
145+
"buckets": [
146+
[
147+
0,
148+
"17.448123722644123",
149+
"19.027313840043536",
150+
"139"
151+
],
152+
[
153+
0,
154+
"19.027313840043536",
155+
"20.749432874416154",
156+
"85"
157+
],
158+
[
159+
0,
160+
"20.749432874416154",
161+
"22.62741699796952",
162+
"70"
163+
],
164+
],
165+
"count": "1000",
166+
"sum": "29969.50000000001"
167+
}
168+
]
132169
}
133170
]
134171
```

go.mod

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
module github.com/prometheus/prom2json
22

3-
go 1.17
3+
go 1.21
44

55
require (
6-
github.com/davecgh/go-spew v1.1.1
6+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
77
github.com/matttproud/golang_protobuf_extensions v1.0.4
88
github.com/prometheus/client_model v0.6.1
9-
github.com/prometheus/common v0.53.0
9+
github.com/prometheus/common v0.54.0
10+
github.com/prometheus/prometheus v0.53.0
1011
)
1112

1213
require (
13-
github.com/golang/protobuf v1.5.3 // indirect
14-
google.golang.org/protobuf v1.33.0 // indirect
14+
github.com/golang/protobuf v1.5.4 // indirect
15+
google.golang.org/protobuf v1.34.1 // indirect
1516
)

go.sum

Lines changed: 15 additions & 1695 deletions
Large diffs are not rendered by default.

histogram/prometheus_model.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright 2020 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
package histogram
14+
15+
import (
16+
"fmt"
17+
18+
dto "github.com/prometheus/client_model/go"
19+
model "github.com/prometheus/prometheus/model/histogram"
20+
)
21+
22+
type APIBucket[BC model.BucketCount] struct {
23+
Boundaries uint64
24+
Lower, Upper float64
25+
Count BC
26+
}
27+
28+
func NewModelHistogram(ch *dto.Histogram) (*model.Histogram, *model.FloatHistogram) {
29+
if ch.GetSampleCountFloat() > 0 || ch.GetZeroCountFloat() > 0 {
30+
// It is a float histogram.
31+
fh := model.FloatHistogram{
32+
Count: ch.GetSampleCountFloat(),
33+
Sum: ch.GetSampleSum(),
34+
ZeroThreshold: ch.GetZeroThreshold(),
35+
ZeroCount: ch.GetZeroCountFloat(),
36+
Schema: ch.GetSchema(),
37+
PositiveSpans: make([]model.Span, len(ch.GetPositiveSpan())),
38+
PositiveBuckets: ch.GetPositiveCount(),
39+
NegativeSpans: make([]model.Span, len(ch.GetNegativeSpan())),
40+
NegativeBuckets: ch.GetNegativeCount(),
41+
}
42+
for i, span := range ch.GetPositiveSpan() {
43+
fh.PositiveSpans[i].Offset = span.GetOffset()
44+
fh.PositiveSpans[i].Length = span.GetLength()
45+
}
46+
for i, span := range ch.GetNegativeSpan() {
47+
fh.NegativeSpans[i].Offset = span.GetOffset()
48+
fh.NegativeSpans[i].Length = span.GetLength()
49+
}
50+
return nil, &fh
51+
}
52+
h := model.Histogram{
53+
Count: ch.GetSampleCount(),
54+
Sum: ch.GetSampleSum(),
55+
ZeroThreshold: ch.GetZeroThreshold(),
56+
ZeroCount: ch.GetZeroCount(),
57+
Schema: ch.GetSchema(),
58+
PositiveSpans: make([]model.Span, len(ch.GetPositiveSpan())),
59+
PositiveBuckets: ch.GetPositiveDelta(),
60+
NegativeSpans: make([]model.Span, len(ch.GetNegativeSpan())),
61+
NegativeBuckets: ch.GetNegativeDelta(),
62+
}
63+
for i, span := range ch.GetPositiveSpan() {
64+
h.PositiveSpans[i].Offset = span.GetOffset()
65+
h.PositiveSpans[i].Length = span.GetLength()
66+
}
67+
for i, span := range ch.GetNegativeSpan() {
68+
h.NegativeSpans[i].Offset = span.GetOffset()
69+
h.NegativeSpans[i].Length = span.GetLength()
70+
}
71+
return &h, nil
72+
}
73+
74+
func BucketsAsJson[BC model.BucketCount](buckets []APIBucket[BC]) [][]interface{} {
75+
ret := make([][]interface{}, len(buckets))
76+
for i, b := range buckets {
77+
ret[i] = []interface{}{b.Boundaries, fmt.Sprintf("%v", b.Lower), fmt.Sprintf("%v", b.Upper), fmt.Sprintf("%v", b.Count)}
78+
}
79+
return ret
80+
}
81+
82+
func GetAPIBuckets(h *model.Histogram) []APIBucket[uint64] {
83+
var apiBuckets []APIBucket[uint64]
84+
var nBuckets []model.Bucket[uint64]
85+
for it := h.NegativeBucketIterator(); it.Next(); {
86+
bucket := it.At()
87+
if bucket.Count != 0 {
88+
nBuckets = append(nBuckets, it.At())
89+
}
90+
}
91+
for i := len(nBuckets) - 1; i >= 0; i-- {
92+
apiBuckets = append(apiBuckets, makeBucket[uint64](nBuckets[i]))
93+
}
94+
95+
if h.ZeroCount != 0 {
96+
apiBuckets = append(apiBuckets, makeBucket[uint64](h.ZeroBucket()))
97+
}
98+
99+
for it := h.PositiveBucketIterator(); it.Next(); {
100+
bucket := it.At()
101+
if bucket.Count != 0 {
102+
apiBuckets = append(apiBuckets, makeBucket[uint64](bucket))
103+
}
104+
}
105+
return apiBuckets
106+
}
107+
108+
func GetAPIFloatBuckets(h *model.FloatHistogram) []APIBucket[float64] {
109+
var apiBuckets []APIBucket[float64]
110+
var nBuckets []model.Bucket[float64]
111+
for it := h.NegativeBucketIterator(); it.Next(); {
112+
bucket := it.At()
113+
if bucket.Count != 0 {
114+
nBuckets = append(nBuckets, it.At())
115+
}
116+
}
117+
for i := len(nBuckets) - 1; i >= 0; i-- {
118+
apiBuckets = append(apiBuckets, makeBucket[float64](nBuckets[i]))
119+
}
120+
121+
if h.ZeroCount != 0 {
122+
apiBuckets = append(apiBuckets, makeBucket[float64](h.ZeroBucket()))
123+
}
124+
125+
for it := h.PositiveBucketIterator(); it.Next(); {
126+
bucket := it.At()
127+
if bucket.Count != 0 {
128+
apiBuckets = append(apiBuckets, makeBucket[float64](bucket))
129+
}
130+
}
131+
return apiBuckets
132+
}
133+
134+
func makeBucket[BC model.BucketCount](bucket model.Bucket[BC]) APIBucket[BC] {
135+
boundaries := uint64(2) // () Exclusive on both sides AKA open interval.
136+
if bucket.LowerInclusive {
137+
if bucket.UpperInclusive {
138+
boundaries = 3 // [] Inclusive on both sides AKA closed interval.
139+
} else {
140+
boundaries = 1 // [) Inclusive only on lower end AKA right open.
141+
}
142+
} else {
143+
if bucket.UpperInclusive {
144+
boundaries = 0 // (] Inclusive only on upper end AKA left open.
145+
}
146+
}
147+
return APIBucket[BC]{
148+
Boundaries: boundaries,
149+
Lower: bucket.Lower,
150+
Upper: bucket.Upper,
151+
Count: bucket.Count,
152+
}
153+
}

prom2json.go

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/prometheus/common/expfmt"
2424

2525
dto "github.com/prometheus/client_model/go"
26+
"github.com/prometheus/prom2json/histogram"
2627
)
2728

2829
const acceptHeader = `application/vnd.google.protobuf;proto=io.prometheus.client.MetricFamily;encoding=delimited;q=0.7,text/plain;version=0.0.4;q=0.3`
@@ -56,7 +57,7 @@ type Summary struct {
5657
type Histogram struct {
5758
Labels map[string]string `json:"labels,omitempty"`
5859
TimestampMs string `json:"timestamp_ms,omitempty"`
59-
Buckets map[string]string `json:"buckets,omitempty"`
60+
Buckets interface{} `json:"buckets,omitempty"`
6061
Count string `json:"count"`
6162
Sum string `json:"sum"`
6263
}
@@ -81,13 +82,7 @@ func NewFamily(dtoMF *dto.MetricFamily) *Family {
8182
Sum: fmt.Sprint(m.GetSummary().GetSampleSum()),
8283
}
8384
case dto.MetricType_HISTOGRAM:
84-
mf.Metrics[i] = Histogram{
85-
Labels: makeLabels(m),
86-
TimestampMs: makeTimestamp(m),
87-
Buckets: makeBuckets(m),
88-
Count: fmt.Sprint(m.GetHistogram().GetSampleCount()),
89-
Sum: fmt.Sprint(m.GetHistogram().GetSampleSum()),
90-
}
85+
mf.Metrics[i] = makeHistogram(m)
9186
default:
9287
mf.Metrics[i] = Metric{
9388
Labels: makeLabels(m),
@@ -112,6 +107,27 @@ func getValue(m *dto.Metric) float64 {
112107
}
113108
}
114109

110+
func makeHistogram(m *dto.Metric) Histogram {
111+
hist := Histogram{
112+
Labels: makeLabels(m),
113+
TimestampMs: makeTimestamp(m),
114+
Count: fmt.Sprint(m.GetHistogram().GetSampleCount()),
115+
Sum: fmt.Sprint(m.GetHistogram().GetSampleSum()),
116+
}
117+
if b := makeBuckets(m); len(b) > 0 {
118+
hist.Buckets = b
119+
} else {
120+
h, fh := histogram.NewModelHistogram(m.GetHistogram())
121+
if h == nil {
122+
// float histogram
123+
hist.Buckets = histogram.BucketsAsJson[float64](histogram.GetAPIFloatBuckets(fh))
124+
} else {
125+
hist.Buckets = histogram.BucketsAsJson[uint64](histogram.GetAPIBuckets(h))
126+
}
127+
}
128+
return hist
129+
}
130+
115131
func makeLabels(m *dto.Metric) map[string]string {
116132
result := map[string]string{}
117133
for _, lp := range m.Label {

0 commit comments

Comments
 (0)