From db46de3dcbe5b83fdea72a4580fdf4e16005a63a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gy=C3=B6rgy=20Krajcsovits?= Date: Fri, 22 May 2026 17:43:45 +0200 Subject: [PATCH] feat(api): add histogram_format=native to query endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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": "", "sum": "", "schema": , "zero_threshold": "", "zero_count": "", // when non-zero "negative_buckets": [ [ , "" ], ... ], // when non-empty "buckets": [ [ , "" ], ... ] // 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": "", "sum": "", "schema": -53, "boundaries": [ "", ... ], "buckets": [ [ , "" ], ... ] } 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 --- docs/querying/api.md | 67 +++++++++ util/jsonutil/marshal.go | 110 ++++++++++++++ web/api/v1/api.go | 4 +- web/api/v1/api_scenarios_test.go | 86 +++++++++++ web/api/v1/json_codec_native_histogram.go | 155 ++++++++++++++++++++ web/api/v1/json_codec_test.go | 47 ++++++ web/api/v1/openapi_paths.go | 2 + web/api/v1/openapi_schemas.go | 42 ++++-- web/api/v1/testdata/openapi_3.1_golden.yaml | 55 ++++++- web/api/v1/testdata/openapi_3.2_golden.yaml | 55 ++++++- 10 files changed, 606 insertions(+), 17 deletions(-) create mode 100644 web/api/v1/json_codec_native_histogram.go diff --git a/docs/querying/api.md b/docs/querying/api.md index 238762d799..440801d648 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -103,6 +103,11 @@ URL query parameters: - `limit=`: 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=`: Override the [lookback period](#staleness) just for this query in `duration` format or float number of seconds. Optional. - `stats=`: Include query statistics in the response. If set to `all`, includes detailed statistics. Optional. +- `histogram_format=`: 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=`: Maximum number of returned series. Optional. 0 means disabled. - `lookback_delta=`: Override the [lookback period](#staleness) just for this query in `duration` format or float number of seconds. Optional. - `stats=`: Include query statistics in the response. If set to `all`, includes detailed statistics. Optional. +- `histogram_format=`: 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 `` 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": "", + "sum": "", + "schema": , + "zero_threshold": "", + "zero_count": "", + "negative_buckets": [ [ , "" ], ... ], + "buckets": [ [ , "" ], ... ] +} +``` + +- `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 `[ , ]` where `` is the + schema-defined bucket index. + +For custom-bucket histograms (schema `-53`): + +```json +{ + "count": "", + "sum": "", + "schema": -53, + "boundaries": [ "", ... ], + "buckets": [ [ , "" ], ... ] +} +``` + +- `boundaries` lists the bucket upper bounds. +- Each entry in `buckets` is `[ , ]` where `` 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 `[ , ]` entry. + +Empty buckets are omitted in both representations. + ## Scrape pools The following endpoint returns a list of all configured scrape pools: diff --git a/util/jsonutil/marshal.go b/util/jsonutil/marshal.go index 61ce4234eb..5f19310877 100644 --- a/util/jsonutil/marshal.go +++ b/util/jsonutil/marshal.go @@ -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 ": [ [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() + } +} diff --git a/web/api/v1/api.go b/web/api/v1/api.go index d906e105d8..c41e321994 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -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} } diff --git a/web/api/v1/api_scenarios_test.go b/web/api/v1/api_scenarios_test.go index 5bdccf08d5..3d832ef9f1 100644 --- a/web/api/v1/api_scenarios_test.go +++ b/web/api/v1/api_scenarios_test.go @@ -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. diff --git a/web/api/v1/json_codec_native_histogram.go b/web/api/v1/json_codec_native_histogram.go new file mode 100644 index 0000000000..3acb654206 --- /dev/null +++ b/web/api/v1/json_codec_native_histogram.go @@ -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() +} diff --git a/web/api/v1/json_codec_test.go b/web/api/v1/json_codec_test.go index 8d17a1759f..e6df448204 100644 --- a/web/api/v1/json_codec_test.go +++ b/web/api/v1/json_codec_test.go @@ -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"]}`, diff --git a/web/api/v1/openapi_paths.go b/web/api/v1/openapi_paths.go index 756a8cc0be..d18f09cd9a 100644 --- a/web/api/v1/openapi_paths.go +++ b/web/api/v1/openapi_paths.go @@ -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{ diff --git a/web/api/v1/openapi_schemas.go b/web/api/v1/openapi_schemas.go index 56d5ff12f0..f536299c3f 100644 --- a/web/api/v1/openapi_schemas.go +++ b/web/api/v1/openapi_schemas.go @@ -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"}, diff --git a/web/api/v1/testdata/openapi_3.1_golden.yaml b/web/api/v1/testdata/openapi_3.1_golden.yaml index aae0fd7434..6b790f0054 100644 --- a/web/api/v1/testdata/openapi_3.1_golden.yaml +++ b/web/api/v1/testdata/openapi_3.1_golden.yaml @@ -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: diff --git a/web/api/v1/testdata/openapi_3.2_golden.yaml b/web/api/v1/testdata/openapi_3.2_golden.yaml index ece14302d1..c5f1bcd570 100644 --- a/web/api/v1/testdata/openapi_3.2_golden.yaml +++ b/web/api/v1/testdata/openapi_3.2_golden.yaml @@ -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: