mirror of
https://github.com/prometheus/prometheus.git
synced 2026-07-06 15:57:03 -04:00
feat(api): add histogram_format=native to query endpoints
Add a new optional histogram_format query parameter to /api/v1/query
and /api/v1/query_range. When set to native, native histograms in the
JSON response are emitted in a schema-aware shape.
For exponential schemas:
{
"count": "<count>",
"sum": "<sum>",
"schema": <int>,
"zero_threshold": "<threshold>",
"zero_count": "<count>", // when non-zero
"negative_buckets": [ [ <index>, "<count>" ], ... ], // when non-empty
"buckets": [ [ <index>, "<count>" ], ... ] // when non-empty
}
zero_threshold is always emitted because, together with schema, it
fully describes the bucket layout. The negative/positive bucket
arrays carry the schema-defined bucket index and count, so empty
buckets that fall between non-empty ones are not transmitted.
For custom-bucket histograms (schema -53):
{
"count": "<count>",
"sum": "<sum>",
"schema": -53,
"boundaries": [ "<upper_bound>", ... ],
"buckets": [ [ <index>, "<count>" ], ... ]
}
boundaries lists the bucket upper bounds; each bucket entry's index
is the 0-based index into boundaries.
This lets clients that already understand the schema (e.g. Grafana
rendering NHCB heatmaps) reconstruct every bucket including empty
custom-bucket slots, and is also substantially more compact than the
boundary-based representation for histograms with many buckets.
The existing boundary-based representation is unchanged when
histogram_format is absent or set to any other value.
Signed-off-by: György Krajcsovits <gyorgy.krajcsovits@grafana.com>
This commit is contained in:
parent
891e698992
commit
db46de3dcb
10 changed files with 606 additions and 17 deletions
|
|
@ -103,6 +103,11 @@ URL query parameters:
|
|||
- `limit=<number>`: Maximum number of returned series. Doesn't affect scalars or strings but truncates the number of series for matrices and vectors. Optional. 0 means disabled.
|
||||
- `lookback_delta=<duration | float>`: Override the [lookback period](#staleness) just for this query in `duration` format or float number of seconds. Optional.
|
||||
- `stats=<string>`: Include query statistics in the response. If set to `all`, includes detailed statistics. Optional.
|
||||
- `histogram_format=<string>`: Selects an alternative JSON representation for
|
||||
native histograms in the response. If set to `native`, each histogram is
|
||||
emitted using the schema-aware shape described in [Native histograms (native
|
||||
format)](#native-histograms-native-format) instead of the default
|
||||
boundary-based shape. Optional.
|
||||
|
||||
The current server time is used if the `time` parameter is omitted.
|
||||
|
||||
|
|
@ -177,6 +182,11 @@ URL query parameters:
|
|||
- `limit=<number>`: Maximum number of returned series. Optional. 0 means disabled.
|
||||
- `lookback_delta=<duration | float>`: Override the [lookback period](#staleness) just for this query in `duration` format or float number of seconds. Optional.
|
||||
- `stats=<string>`: Include query statistics in the response. If set to `all`, includes detailed statistics. Optional.
|
||||
- `histogram_format=<string>`: Selects an alternative JSON representation for
|
||||
native histograms in the response. If set to `native`, each histogram is
|
||||
emitted using the schema-aware shape described in [Native histograms (native
|
||||
format)](#native-histograms-native-format) instead of the default
|
||||
boundary-based shape. Optional.
|
||||
|
||||
You can URL-encode these parameters directly in the request body by using the `POST` method and
|
||||
`Content-Type: application/x-www-form-urlencoded` header. This is useful when specifying a large
|
||||
|
|
@ -796,6 +806,63 @@ Note that with the currently implemented bucket schemas, positive buckets are
|
|||
“open left”, negative buckets are “open right”, and the zero bucket (with a
|
||||
negative left boundary and a positive right boundary) is “closed both”.
|
||||
|
||||
### Native histograms (native format)
|
||||
|
||||
When the `histogram_format=native` query parameter is set on `/api/v1/query` or
|
||||
`/api/v1/query_range`, the `<histogram>` placeholder is rendered using a
|
||||
schema-aware shape that mirrors the native histogram data model more closely.
|
||||
The caller is expected to know how buckets are organised for the given schema
|
||||
(and, for custom-bucket histograms, the supplied bucket boundaries). The
|
||||
shape depends on the schema.
|
||||
|
||||
For exponential-schema histograms:
|
||||
|
||||
```json
|
||||
{
|
||||
"count": "<count_of_observations>",
|
||||
"sum": "<sum_of_observations>",
|
||||
"schema": <schema_number>,
|
||||
"zero_threshold": "<zero_bucket_threshold>",
|
||||
"zero_count": "<count_in_zero_bucket>",
|
||||
"negative_buckets": [ [ <index>, "<count_in_bucket>" ], ... ],
|
||||
"buckets": [ [ <index>, "<count_in_bucket>" ], ... ]
|
||||
}
|
||||
```
|
||||
|
||||
- `zero_threshold` is the zero-bucket threshold and is always present, because
|
||||
together with `schema` it fully describes the bucket layout.
|
||||
- `zero_count` is the observation count in the zero bucket; it is only present
|
||||
when non-zero.
|
||||
- `negative_buckets` and `buckets` (the positive buckets) are each optional and
|
||||
only present when at least one bucket on that side has a non-zero count. Each
|
||||
entry is a two-element array `[ <index>, <count> ]` where `<index>` is the
|
||||
schema-defined bucket index.
|
||||
|
||||
For custom-bucket histograms (schema `-53`):
|
||||
|
||||
```json
|
||||
{
|
||||
"count": "<count_of_observations>",
|
||||
"sum": "<sum_of_observations>",
|
||||
"schema": -53,
|
||||
"boundaries": [ "<bucket_upper_bound>", ... ],
|
||||
"buckets": [ [ <index>, "<count_in_bucket>" ], ... ]
|
||||
}
|
||||
```
|
||||
|
||||
- `boundaries` lists the bucket upper bounds.
|
||||
- Each entry in `buckets` is `[ <index>, <count> ]` where `<index>` is the
|
||||
0-based index into `boundaries` identifying the bucket's upper bound. The
|
||||
implicit overflow bucket `(boundaries[len-1], +Inf]` has index `len(boundaries)`
|
||||
and is emitted just like any other bucket; `+Inf` itself never appears in
|
||||
the response.
|
||||
|
||||
For exponential schemas, observations of ±Inf land in a regular bucket whose
|
||||
schema-defined upper bound is `+Inf`, one index past the last finite-bound
|
||||
bucket. It is emitted as any other `[ <index>, <count> ]` entry.
|
||||
|
||||
Empty buckets are omitted in both representations.
|
||||
|
||||
## Scrape pools
|
||||
|
||||
The following endpoint returns a list of all configured scrape pools:
|
||||
|
|
|
|||
|
|
@ -135,3 +135,113 @@ func MarshalHistogram(h *histogram.FloatHistogram, stream *jsoniter.Stream) {
|
|||
}
|
||||
stream.WriteObjectEnd()
|
||||
}
|
||||
|
||||
// MarshalHistogramNative marshals a histogram in a format that mirrors the
|
||||
// native histogram data model closely. It is intended for callers that already
|
||||
// understand how buckets are organised given the schema (and, for custom-bucket
|
||||
// histograms, the bucket boundaries).
|
||||
//
|
||||
// The shape depends on the schema. For exponential schemas it looks like:
|
||||
//
|
||||
// {
|
||||
// "count": "42",
|
||||
// "sum": "34593.34",
|
||||
// "schema": 2,
|
||||
// "zero_threshold": "0.001",
|
||||
// "zero_count": "12",
|
||||
// "negative_buckets": [ [ -1, "3" ], [ 0, "2" ] ],
|
||||
// "buckets": [ [ 0, "5" ], [ 1, "8" ] ]
|
||||
// }
|
||||
//
|
||||
// "zero_threshold" is always emitted for exponential schemas because it is
|
||||
// part of the schema definition. "zero_count" is only emitted when the zero
|
||||
// bucket has a non-zero count. "negative_buckets" and "buckets" are each
|
||||
// optional and only emitted when at least one bucket on that side has a
|
||||
// non-zero count. The bucket entries are [index, count] pairs where index is
|
||||
// the schema-defined bucket index.
|
||||
//
|
||||
// For custom-bucket histograms (schema -53) it looks like:
|
||||
//
|
||||
// {
|
||||
// "count": "6",
|
||||
// "sum": "7",
|
||||
// "schema": -53,
|
||||
// "boundaries": [ "0.5", "1", "2", "5" ],
|
||||
// "buckets": [ [ 1, "2" ], [ 2, "3" ], [ 3, "1" ] ]
|
||||
// }
|
||||
//
|
||||
// "boundaries" lists the bucket upper bounds. Each bucket entry is
|
||||
// [index, count] where index is the 0-based index into "boundaries"
|
||||
// identifying the bucket's upper bound.
|
||||
//
|
||||
// Counts are encoded as strings for NaN/Inf safety. Empty buckets are
|
||||
// skipped.
|
||||
func MarshalHistogramNative(h *histogram.FloatHistogram, stream *jsoniter.Stream) {
|
||||
stream.WriteObjectStart()
|
||||
stream.WriteObjectField(`count`)
|
||||
MarshalFloat(h.Count, stream)
|
||||
stream.WriteMore()
|
||||
stream.WriteObjectField(`sum`)
|
||||
MarshalFloat(h.Sum, stream)
|
||||
stream.WriteMore()
|
||||
stream.WriteObjectField(`schema`)
|
||||
stream.WriteInt32(h.Schema)
|
||||
|
||||
if histogram.IsCustomBucketsSchema(h.Schema) {
|
||||
stream.WriteMore()
|
||||
stream.WriteObjectField(`boundaries`)
|
||||
stream.WriteArrayStart()
|
||||
for i, v := range h.CustomValues {
|
||||
if i != 0 {
|
||||
stream.WriteMore()
|
||||
}
|
||||
MarshalFloat(v, stream)
|
||||
}
|
||||
stream.WriteArrayEnd()
|
||||
|
||||
writeIndexedBuckets(stream, `buckets`, h.PositiveBucketIterator())
|
||||
stream.WriteObjectEnd()
|
||||
return
|
||||
}
|
||||
|
||||
stream.WriteMore()
|
||||
stream.WriteObjectField(`zero_threshold`)
|
||||
MarshalFloat(h.ZeroThreshold, stream)
|
||||
if h.ZeroCount != 0 {
|
||||
stream.WriteMore()
|
||||
stream.WriteObjectField(`zero_count`)
|
||||
MarshalFloat(h.ZeroCount, stream)
|
||||
}
|
||||
writeIndexedBuckets(stream, `negative_buckets`, h.NegativeBucketIterator())
|
||||
writeIndexedBuckets(stream, `buckets`, h.PositiveBucketIterator())
|
||||
stream.WriteObjectEnd()
|
||||
}
|
||||
|
||||
// writeIndexedBuckets writes a "<field>: [ [index, count], ... ]" entry from
|
||||
// the iterator, skipping empty buckets. If every bucket is empty, nothing is
|
||||
// written (so the field is omitted from the parent object).
|
||||
func writeIndexedBuckets(stream *jsoniter.Stream, field string, it histogram.BucketIterator[float64]) {
|
||||
started := false
|
||||
for it.Next() {
|
||||
b := it.At()
|
||||
if b.Count == 0 {
|
||||
continue
|
||||
}
|
||||
if !started {
|
||||
stream.WriteMore()
|
||||
stream.WriteObjectField(field)
|
||||
stream.WriteArrayStart()
|
||||
started = true
|
||||
} else {
|
||||
stream.WriteMore()
|
||||
}
|
||||
stream.WriteArrayStart()
|
||||
stream.WriteInt32(b.Index)
|
||||
stream.WriteMore()
|
||||
MarshalFloat(b.Count, stream)
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
if started {
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -580,7 +580,7 @@ func (api *API) query(r *http.Request) (result apiFuncResult) {
|
|||
|
||||
return apiFuncResult{&QueryData{
|
||||
ResultType: res.Value.Type(),
|
||||
Result: res.Value,
|
||||
Result: applyHistogramFormat(res.Value, r.FormValue("histogram_format")),
|
||||
Stats: qs,
|
||||
}, nil, warnings, qry.Close}
|
||||
}
|
||||
|
|
@ -705,7 +705,7 @@ func (api *API) queryRange(r *http.Request) (result apiFuncResult) {
|
|||
|
||||
return apiFuncResult{&QueryData{
|
||||
ResultType: res.Value.Type(),
|
||||
Result: res.Value,
|
||||
Result: applyHistogramFormat(res.Value, r.FormValue("histogram_format")),
|
||||
Stats: qs,
|
||||
}, nil, warnings, qry.Close}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -416,6 +416,92 @@ func TestAPIWithNativeHistograms(t *testing.T) {
|
|||
RequireJSONArray("$.data").
|
||||
RequireLenAtLeast("$.data", 1)
|
||||
})
|
||||
|
||||
t.Run("GET /api/v1/query with histogram_format=native returns raw buckets", func(t *testing.T) {
|
||||
testhelpers.GET(t, api, "/api/v1/query",
|
||||
"query", "test_histogram",
|
||||
"histogram_format", "native").
|
||||
RequireSuccess().
|
||||
ValidateOpenAPI().
|
||||
RequireEquals("$.data.resultType", "vector").
|
||||
RequireLenAtLeast("$.data.result", 1).
|
||||
RequireSome("$.data.result", func(item any) bool {
|
||||
sample, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
h, ok := sample["histogram"].([]any)
|
||||
if !ok || len(h) != 2 {
|
||||
return false
|
||||
}
|
||||
obj, ok := h[1].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hasNativeHistogramShape(obj)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("GET /api/v1/query_range with histogram_format=native returns raw buckets", func(t *testing.T) {
|
||||
// Use end=now+60 so the fixture's millisecond-precision timestamps
|
||||
// (just after the integer-second boundary) reliably land before the
|
||||
// last evaluation timestamp.
|
||||
now := time.Now().Unix()
|
||||
testhelpers.GET(t, api, "/api/v1/query_range",
|
||||
"query", "test_histogram",
|
||||
"start", strconv.FormatInt(now-60, 10),
|
||||
"end", strconv.FormatInt(now+60, 10),
|
||||
"step", "60",
|
||||
"histogram_format", "native").
|
||||
RequireSuccess().
|
||||
ValidateOpenAPI().
|
||||
RequireEquals("$.data.resultType", "matrix").
|
||||
RequireSome("$.data.result", func(item any) bool {
|
||||
series, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
hs, ok := series["histograms"].([]any)
|
||||
if !ok || len(hs) == 0 {
|
||||
return false
|
||||
}
|
||||
pair, ok := hs[0].([]any)
|
||||
if !ok || len(pair) != 2 {
|
||||
return false
|
||||
}
|
||||
obj, ok := pair[1].(map[string]any)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hasNativeHistogramShape(obj)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// hasNativeHistogramShape asserts that the given histogram object follows
|
||||
// the schema-aware native format for an exponential-schema histogram: it
|
||||
// must carry schema, zero_threshold, and at least one of the [index, count]
|
||||
// bucket arrays expected for the fixture data (negative_buckets, buckets).
|
||||
func hasNativeHistogramShape(obj map[string]any) bool {
|
||||
if _, ok := obj["schema"]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := obj["zero_threshold"]; !ok {
|
||||
return false
|
||||
}
|
||||
if buckets, ok := obj["buckets"].([]any); ok && len(buckets) > 0 {
|
||||
first, ok := buckets[0].([]any)
|
||||
if !ok || len(first) != 2 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if neg, ok := obj["negative_buckets"].([]any); ok && len(neg) > 0 {
|
||||
first, ok := neg[0].([]any)
|
||||
if !ok || len(first) != 2 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TestAPIWithStats tests the API with the stats query parameter.
|
||||
|
|
|
|||
155
web/api/v1/json_codec_native_histogram.go
Normal file
155
web/api/v1/json_codec_native_histogram.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
// Copyright The Prometheus Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package v1
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
|
||||
"github.com/prometheus/prometheus/promql"
|
||||
"github.com/prometheus/prometheus/promql/parser"
|
||||
"github.com/prometheus/prometheus/util/jsonutil"
|
||||
)
|
||||
|
||||
// nativeHistogramVector wraps a promql.Vector so that histogram samples are
|
||||
// marshaled using the native histogram JSON shape (see
|
||||
// jsonutil.MarshalHistogramNative). It is used to opt in to the alternative
|
||||
// format on a per-request basis without affecting the default encoders.
|
||||
type nativeHistogramVector promql.Vector
|
||||
|
||||
// nativeHistogramMatrix wraps a promql.Matrix for the same reason as
|
||||
// nativeHistogramVector.
|
||||
type nativeHistogramMatrix promql.Matrix
|
||||
|
||||
// Type satisfies the parser.Value interface so the wrapper can stand in for
|
||||
// the underlying value type in QueryData.Result.
|
||||
func (nativeHistogramVector) Type() parser.ValueType { return parser.ValueTypeVector }
|
||||
|
||||
// Type satisfies the parser.Value interface so the wrapper can stand in for
|
||||
// the underlying value type in QueryData.Result.
|
||||
func (nativeHistogramMatrix) Type() parser.ValueType { return parser.ValueTypeMatrix }
|
||||
|
||||
// String delegates to the wrapped promql.Vector so debug output matches.
|
||||
func (v nativeHistogramVector) String() string { return promql.Vector(v).String() }
|
||||
|
||||
// String delegates to the wrapped promql.Matrix so debug output matches.
|
||||
func (m nativeHistogramMatrix) String() string { return promql.Matrix(m).String() }
|
||||
|
||||
// histogramFormatNative is the histogram_format query parameter value that
|
||||
// selects the native histogram JSON shape.
|
||||
const histogramFormatNative = "native"
|
||||
|
||||
// applyHistogramFormat returns v wrapped so it is encoded according to the
|
||||
// requested histogram_format value, or v unchanged when the format is empty
|
||||
// or not recognised. Non-histogram-bearing values are returned unchanged.
|
||||
func applyHistogramFormat(v parser.Value, format string) parser.Value {
|
||||
if format != histogramFormatNative {
|
||||
return v
|
||||
}
|
||||
switch x := v.(type) {
|
||||
case promql.Vector:
|
||||
return nativeHistogramVector(x)
|
||||
case promql.Matrix:
|
||||
return nativeHistogramMatrix(x)
|
||||
default:
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
jsoniter.RegisterTypeEncoderFunc("v1.nativeHistogramVector", unsafeMarshalNativeHistogramVectorJSON, neverEmpty)
|
||||
jsoniter.RegisterTypeEncoderFunc("v1.nativeHistogramMatrix", unsafeMarshalNativeHistogramMatrixJSON, neverEmpty)
|
||||
}
|
||||
|
||||
func unsafeMarshalNativeHistogramVectorJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
v := *((*nativeHistogramVector)(ptr))
|
||||
stream.WriteArrayStart()
|
||||
for i, s := range v {
|
||||
marshalNativeHistogramSampleJSON(s, stream)
|
||||
if i != len(v)-1 {
|
||||
stream.WriteMore()
|
||||
}
|
||||
}
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
|
||||
func unsafeMarshalNativeHistogramMatrixJSON(ptr unsafe.Pointer, stream *jsoniter.Stream) {
|
||||
m := *((*nativeHistogramMatrix)(ptr))
|
||||
stream.WriteArrayStart()
|
||||
for i, s := range m {
|
||||
marshalNativeHistogramSeriesJSON(s, stream)
|
||||
if i != len(m)-1 {
|
||||
stream.WriteMore()
|
||||
}
|
||||
}
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
|
||||
func marshalNativeHistogramSampleJSON(s promql.Sample, stream *jsoniter.Stream) {
|
||||
stream.WriteObjectStart()
|
||||
stream.WriteObjectField(`metric`)
|
||||
marshalLabelsJSON(s.Metric, stream)
|
||||
stream.WriteMore()
|
||||
if s.H == nil {
|
||||
stream.WriteObjectField(`value`)
|
||||
stream.WriteArrayStart()
|
||||
jsonutil.MarshalTimestamp(s.T, stream)
|
||||
stream.WriteMore()
|
||||
jsonutil.MarshalFloat(s.F, stream)
|
||||
stream.WriteArrayEnd()
|
||||
} else {
|
||||
stream.WriteObjectField(`histogram`)
|
||||
stream.WriteArrayStart()
|
||||
jsonutil.MarshalTimestamp(s.T, stream)
|
||||
stream.WriteMore()
|
||||
jsonutil.MarshalHistogramNative(s.H, stream)
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
stream.WriteObjectEnd()
|
||||
}
|
||||
|
||||
func marshalNativeHistogramSeriesJSON(s promql.Series, stream *jsoniter.Stream) {
|
||||
stream.WriteObjectStart()
|
||||
stream.WriteObjectField(`metric`)
|
||||
marshalLabelsJSON(s.Metric, stream)
|
||||
|
||||
for i, p := range s.Floats {
|
||||
stream.WriteMore()
|
||||
if i == 0 {
|
||||
stream.WriteObjectField(`values`)
|
||||
stream.WriteArrayStart()
|
||||
}
|
||||
marshalFPointJSON(p, stream)
|
||||
}
|
||||
if len(s.Floats) > 0 {
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
for i, p := range s.Histograms {
|
||||
stream.WriteMore()
|
||||
if i == 0 {
|
||||
stream.WriteObjectField(`histograms`)
|
||||
stream.WriteArrayStart()
|
||||
}
|
||||
stream.WriteArrayStart()
|
||||
jsonutil.MarshalTimestamp(p.T, stream)
|
||||
stream.WriteMore()
|
||||
jsonutil.MarshalHistogramNative(p.H, stream)
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
if len(s.Histograms) > 0 {
|
||||
stream.WriteArrayEnd()
|
||||
}
|
||||
stream.WriteObjectEnd()
|
||||
}
|
||||
|
|
@ -102,6 +102,53 @@ func TestJsonCodec_Encode(t *testing.T) {
|
|||
},
|
||||
expected: `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"foo"},"histograms":[[1,{"count":"10","sum":"20","buckets":[[1,"-1.6817928305074288","-1.414213562373095","1"],[1,"-1.414213562373095","-1.189207115002721","2"],[3,"-0.001","0.001","12"],[0,"1.414213562373095","1.6817928305074288","1"],[0,"1.6817928305074288","2","2"],[0,"2.378414230005442","2.82842712474619","2"],[0,"2.82842712474619","3.3635856610148576","1"],[0,"3.3635856610148576","4","1"]]}]]}]}}`,
|
||||
},
|
||||
{
|
||||
response: &QueryData{
|
||||
ResultType: parser.ValueTypeMatrix,
|
||||
Result: nativeHistogramMatrix{
|
||||
promql.Series{
|
||||
Histograms: []promql.HPoint{{H: &histogram.FloatHistogram{
|
||||
Schema: 2,
|
||||
ZeroThreshold: 0.001,
|
||||
ZeroCount: 12,
|
||||
Count: 10,
|
||||
Sum: 20,
|
||||
PositiveSpans: []histogram.Span{
|
||||
{Offset: 3, Length: 2},
|
||||
{Offset: 1, Length: 3},
|
||||
},
|
||||
NegativeSpans: []histogram.Span{
|
||||
{Offset: 2, Length: 2},
|
||||
},
|
||||
PositiveBuckets: []float64{1, 2, 2, 1, 1},
|
||||
NegativeBuckets: []float64{2, 1},
|
||||
}, T: 1000}},
|
||||
Metric: labels.FromStrings("__name__", "foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `{"status":"success","data":{"resultType":"matrix","result":[{"metric":{"__name__":"foo"},"histograms":[[1,{"count":"10","sum":"20","schema":2,"zero_threshold":"0.001","zero_count":"12","negative_buckets":[[2,"2"],[3,"1"]],"buckets":[[3,"1"],[4,"2"],[6,"2"],[7,"1"],[8,"1"]]}]]}]}}`,
|
||||
},
|
||||
{
|
||||
response: &QueryData{
|
||||
ResultType: parser.ValueTypeVector,
|
||||
Result: nativeHistogramVector{
|
||||
promql.Sample{
|
||||
Metric: labels.FromStrings("__name__", "nhcb"),
|
||||
T: 1000,
|
||||
H: &histogram.FloatHistogram{
|
||||
Schema: histogram.CustomBucketsSchema,
|
||||
Count: 6,
|
||||
Sum: 7,
|
||||
CustomValues: []float64{0.5, 1, 2, 5},
|
||||
PositiveSpans: []histogram.Span{{Offset: 1, Length: 3}},
|
||||
PositiveBuckets: []float64{2, 3, 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"nhcb"},"histogram":[1,{"count":"6","sum":"7","schema":-53,"boundaries":["0.5","1","2","5"],"buckets":[[1,"2"],[2,"3"],[3,"1"]]}]}]}}`,
|
||||
},
|
||||
{
|
||||
response: promql.FPoint{F: 0, T: 0},
|
||||
expected: `{"status":"success","data":[0,"0"]}`,
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ func (*OpenAPIBuilder) queryPath() *v3.PathItem {
|
|||
queryParamWithExample("timeout", "Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.", false, durationSchema(), []example{{"duration", "1m30s"}, {"number", "90"}}),
|
||||
queryParamWithExample("lookback_delta", "Override the lookback period for this query. Optional.", false, durationSchema(), []example{{"duration", "5m"}, {"number", "300"}}),
|
||||
queryParamWithExample("stats", "When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.", false, stringSchema(), []example{{"example", "all"}}),
|
||||
queryParamWithExample("histogram_format", "Request an alternative JSON representation for native histograms in the response. The only supported value is 'native', which emits raw bucket indices instead of bucket boundaries. Optional.", false, stringSchema(), []example{{"example", "native"}}),
|
||||
}
|
||||
return &v3.PathItem{
|
||||
Get: &v3.Operation{
|
||||
|
|
@ -62,6 +63,7 @@ func (*OpenAPIBuilder) queryRangePath() *v3.PathItem {
|
|||
queryParamWithExample("timeout", "Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.", false, durationSchema(), []example{{"duration", "1m30s"}, {"number", "90"}}),
|
||||
queryParamWithExample("lookback_delta", "Override the lookback period for this query. Optional.", false, durationSchema(), []example{{"duration", "5m"}, {"number", "300"}}),
|
||||
queryParamWithExample("stats", "When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.", false, stringSchema(), []example{{"example", "all"}}),
|
||||
queryParamWithExample("histogram_format", "Request an alternative JSON representation for native histograms in the response. The only supported value is 'native', which emits raw bucket indices instead of bucket boundaries. Optional.", false, stringSchema(), []example{{"example", "native"}}),
|
||||
}
|
||||
return &v3.PathItem{
|
||||
Get: &v3.Operation{
|
||||
|
|
|
|||
|
|
@ -332,26 +332,44 @@ func (*OpenAPIBuilder) floatSampleSchema() *base.SchemaProxy {
|
|||
}
|
||||
|
||||
func (*OpenAPIBuilder) histogramValueSchema() *base.SchemaProxy {
|
||||
indexCountArray := base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"array"},
|
||||
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
|
||||
OneOf: []*base.SchemaProxy{
|
||||
base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}),
|
||||
stringSchema(),
|
||||
},
|
||||
})},
|
||||
})
|
||||
|
||||
props := orderedmap.New[string, *base.SchemaProxy]()
|
||||
props.Set("count", stringSchemaWithDescription("Total count of observations."))
|
||||
props.Set("sum", stringSchemaWithDescription("Sum of all observed values."))
|
||||
props.Set("buckets", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"array"},
|
||||
Description: "Histogram buckets as [boundary_rule, lower, upper, count].",
|
||||
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"array"},
|
||||
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
|
||||
OneOf: []*base.SchemaProxy{
|
||||
base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}}),
|
||||
stringSchema(),
|
||||
},
|
||||
})},
|
||||
})},
|
||||
Description: "Histogram buckets. The default representation uses [boundary_rule, lower, upper, count] entries; when histogram_format=native was requested each entry is [index, count] for the positive buckets only.",
|
||||
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: indexCountArray},
|
||||
}))
|
||||
props.Set("schema", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"integer"},
|
||||
Description: "Bucket schema number. Only present when histogram_format=native was requested.",
|
||||
}))
|
||||
props.Set("zero_threshold", stringSchemaWithDescription("Zero-bucket threshold for exponential-schema histograms. Always present when histogram_format=native was requested for an exponential-schema histogram, because it is part of the schema definition."))
|
||||
props.Set("zero_count", stringSchemaWithDescription("Observation count in the zero bucket for exponential-schema histograms. Only present when histogram_format=native was requested and the zero-bucket count is non-zero."))
|
||||
props.Set("negative_buckets", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"array"},
|
||||
Description: "Negative buckets for exponential-schema histograms as [index, count] pairs. Only present when histogram_format=native was requested.",
|
||||
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: indexCountArray},
|
||||
}))
|
||||
props.Set("boundaries", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"array"},
|
||||
Description: "Bucket upper bounds for custom-bucket histograms (schema -53). Only present when histogram_format=native was requested.",
|
||||
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
|
||||
}))
|
||||
|
||||
return base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"object"},
|
||||
Description: "Native histogram value representation.",
|
||||
Description: "Native histogram value representation. The default response uses [boundary_rule, lower, upper, count] bucket entries. With histogram_format=native exponential histograms expose zero_threshold, optional zero_count, and optional negative_buckets/buckets ([index, count] pairs); custom-bucket histograms (schema -53) expose boundaries plus buckets.",
|
||||
Required: []string{"count", "sum"},
|
||||
AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
|
||||
Properties: props,
|
||||
|
|
@ -620,6 +638,7 @@ func (*OpenAPIBuilder) queryPostInputBodySchema() *base.SchemaProxy {
|
|||
props.Set("timeout", stringSchemaWithDescriptionAndExample("Form field: Evaluation timeout (optional, defaults to and is capped by the value of the -query.timeout flag).", "30s"))
|
||||
props.Set("lookback_delta", stringSchemaWithDescriptionAndExample("Form field: Override the lookback period for this query (optional).", "5m"))
|
||||
props.Set("stats", stringSchemaWithDescriptionAndExample("Form field: When provided, include query statistics in the response (the special value 'all' enables more comprehensive statistics).", "all"))
|
||||
props.Set("histogram_format", stringSchemaWithDescriptionAndExample("Form field: Alternative JSON representation for native histograms. Set to 'native' to emit raw bucket indices instead of bucket boundaries (optional).", "native"))
|
||||
|
||||
return base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"object"},
|
||||
|
|
@ -640,6 +659,7 @@ func (*OpenAPIBuilder) queryRangePostInputBodySchema() *base.SchemaProxy {
|
|||
props.Set("timeout", stringSchemaWithDescriptionAndExample("Form field: Evaluation timeout (optional, defaults to and is capped by the value of the -query.timeout flag).", "30s"))
|
||||
props.Set("lookback_delta", stringSchemaWithDescriptionAndExample("Form field: Override the lookback period for this query (optional).", "5m"))
|
||||
props.Set("stats", stringSchemaWithDescriptionAndExample("Form field: When provided, include query statistics in the response (the special value 'all' enables more comprehensive statistics).", "all"))
|
||||
props.Set("histogram_format", stringSchemaWithDescriptionAndExample("Form field: Alternative JSON representation for native histograms. Set to 'native' to emit raw bucket indices instead of bucket boundaries (optional).", "native"))
|
||||
|
||||
return base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"object"},
|
||||
|
|
|
|||
55
web/api/v1/testdata/openapi_3.1_golden.yaml
vendored
55
web/api/v1/testdata/openapi_3.1_golden.yaml
vendored
|
|
@ -104,6 +104,16 @@ paths:
|
|||
examples:
|
||||
example:
|
||||
value: all
|
||||
- name: histogram_format
|
||||
in: query
|
||||
description: Request an alternative JSON representation for native histograms in the response. The only supported value is 'native', which emits raw bucket indices instead of bucket boundaries. Optional.
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
examples:
|
||||
example:
|
||||
value: native
|
||||
responses:
|
||||
"200":
|
||||
description: Query executed successfully.
|
||||
|
|
@ -337,6 +347,16 @@ paths:
|
|||
examples:
|
||||
example:
|
||||
value: all
|
||||
- name: histogram_format
|
||||
in: query
|
||||
description: Request an alternative JSON representation for native histograms in the response. The only supported value is 'native', which emits raw bucket indices instead of bucket boundaries. Optional.
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
examples:
|
||||
example:
|
||||
value: native
|
||||
responses:
|
||||
"200":
|
||||
description: Range query executed successfully.
|
||||
|
|
@ -3469,6 +3489,10 @@ components:
|
|||
type: string
|
||||
description: 'Form field: When provided, include query statistics in the response (the special value ''all'' enables more comprehensive statistics).'
|
||||
example: all
|
||||
histogram_format:
|
||||
type: string
|
||||
description: 'Form field: Alternative JSON representation for native histograms. Set to ''native'' to emit raw bucket indices instead of bucket boundaries (optional).'
|
||||
example: native
|
||||
required:
|
||||
- query
|
||||
additionalProperties: false
|
||||
|
|
@ -3509,6 +3533,10 @@ components:
|
|||
type: string
|
||||
description: 'Form field: When provided, include query statistics in the response (the special value ''all'' enables more comprehensive statistics).'
|
||||
example: all
|
||||
histogram_format:
|
||||
type: string
|
||||
description: 'Form field: Alternative JSON representation for native histograms. Set to ''native'' to emit raw bucket indices instead of bucket boundaries (optional).'
|
||||
example: native
|
||||
required:
|
||||
- query
|
||||
- start
|
||||
|
|
@ -3883,12 +3911,35 @@ components:
|
|||
oneOf:
|
||||
- type: number
|
||||
- type: string
|
||||
description: Histogram buckets as [boundary_rule, lower, upper, count].
|
||||
description: Histogram buckets. The default representation uses [boundary_rule, lower, upper, count] entries; when histogram_format=native was requested each entry is [index, count] for the positive buckets only.
|
||||
schema:
|
||||
type: integer
|
||||
description: Bucket schema number. Only present when histogram_format=native was requested.
|
||||
zero_threshold:
|
||||
type: string
|
||||
description: Zero-bucket threshold for exponential-schema histograms. Always present when histogram_format=native was requested for an exponential-schema histogram, because it is part of the schema definition.
|
||||
zero_count:
|
||||
type: string
|
||||
description: Observation count in the zero bucket for exponential-schema histograms. Only present when histogram_format=native was requested and the zero-bucket count is non-zero.
|
||||
negative_buckets:
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
oneOf:
|
||||
- type: number
|
||||
- type: string
|
||||
description: Negative buckets for exponential-schema histograms as [index, count] pairs. Only present when histogram_format=native was requested.
|
||||
boundaries:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Bucket upper bounds for custom-bucket histograms (schema -53). Only present when histogram_format=native was requested.
|
||||
required:
|
||||
- count
|
||||
- sum
|
||||
additionalProperties: false
|
||||
description: Native histogram value representation.
|
||||
description: Native histogram value representation. The default response uses [boundary_rule, lower, upper, count] bucket entries. With histogram_format=native exponential histograms expose zero_threshold, optional zero_count, and optional negative_buckets/buckets ([index, count] pairs); custom-bucket histograms (schema -53) expose boundaries plus buckets.
|
||||
LabelsOutputBody:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
55
web/api/v1/testdata/openapi_3.2_golden.yaml
vendored
55
web/api/v1/testdata/openapi_3.2_golden.yaml
vendored
|
|
@ -104,6 +104,16 @@ paths:
|
|||
examples:
|
||||
example:
|
||||
value: all
|
||||
- name: histogram_format
|
||||
in: query
|
||||
description: Request an alternative JSON representation for native histograms in the response. The only supported value is 'native', which emits raw bucket indices instead of bucket boundaries. Optional.
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
examples:
|
||||
example:
|
||||
value: native
|
||||
responses:
|
||||
"200":
|
||||
description: Query executed successfully.
|
||||
|
|
@ -337,6 +347,16 @@ paths:
|
|||
examples:
|
||||
example:
|
||||
value: all
|
||||
- name: histogram_format
|
||||
in: query
|
||||
description: Request an alternative JSON representation for native histograms in the response. The only supported value is 'native', which emits raw bucket indices instead of bucket boundaries. Optional.
|
||||
required: false
|
||||
explode: false
|
||||
schema:
|
||||
type: string
|
||||
examples:
|
||||
example:
|
||||
value: native
|
||||
responses:
|
||||
"200":
|
||||
description: Range query executed successfully.
|
||||
|
|
@ -3507,6 +3527,10 @@ components:
|
|||
type: string
|
||||
description: 'Form field: When provided, include query statistics in the response (the special value ''all'' enables more comprehensive statistics).'
|
||||
example: all
|
||||
histogram_format:
|
||||
type: string
|
||||
description: 'Form field: Alternative JSON representation for native histograms. Set to ''native'' to emit raw bucket indices instead of bucket boundaries (optional).'
|
||||
example: native
|
||||
required:
|
||||
- query
|
||||
additionalProperties: false
|
||||
|
|
@ -3547,6 +3571,10 @@ components:
|
|||
type: string
|
||||
description: 'Form field: When provided, include query statistics in the response (the special value ''all'' enables more comprehensive statistics).'
|
||||
example: all
|
||||
histogram_format:
|
||||
type: string
|
||||
description: 'Form field: Alternative JSON representation for native histograms. Set to ''native'' to emit raw bucket indices instead of bucket boundaries (optional).'
|
||||
example: native
|
||||
required:
|
||||
- query
|
||||
- start
|
||||
|
|
@ -3921,12 +3949,35 @@ components:
|
|||
oneOf:
|
||||
- type: number
|
||||
- type: string
|
||||
description: Histogram buckets as [boundary_rule, lower, upper, count].
|
||||
description: Histogram buckets. The default representation uses [boundary_rule, lower, upper, count] entries; when histogram_format=native was requested each entry is [index, count] for the positive buckets only.
|
||||
schema:
|
||||
type: integer
|
||||
description: Bucket schema number. Only present when histogram_format=native was requested.
|
||||
zero_threshold:
|
||||
type: string
|
||||
description: Zero-bucket threshold for exponential-schema histograms. Always present when histogram_format=native was requested for an exponential-schema histogram, because it is part of the schema definition.
|
||||
zero_count:
|
||||
type: string
|
||||
description: Observation count in the zero bucket for exponential-schema histograms. Only present when histogram_format=native was requested and the zero-bucket count is non-zero.
|
||||
negative_buckets:
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
oneOf:
|
||||
- type: number
|
||||
- type: string
|
||||
description: Negative buckets for exponential-schema histograms as [index, count] pairs. Only present when histogram_format=native was requested.
|
||||
boundaries:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Bucket upper bounds for custom-bucket histograms (schema -53). Only present when histogram_format=native was requested.
|
||||
required:
|
||||
- count
|
||||
- sum
|
||||
additionalProperties: false
|
||||
description: Native histogram value representation.
|
||||
description: Native histogram value representation. The default response uses [boundary_rule, lower, upper, count] bucket entries. With histogram_format=native exponential histograms expose zero_threshold, optional zero_count, and optional negative_buckets/buckets ([index, count] pairs); custom-bucket histograms (schema -53) expose boundaries plus buckets.
|
||||
LabelsOutputBody:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
Loading…
Reference in a new issue