mirror of
https://github.com/prometheus/prometheus.git
synced 2026-05-28 04:02:21 -04:00
OpenAPI: Add support for stats
An oversight on the OpenAPI specification; which did not include stats. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
This commit is contained in:
parent
88f6ee4c8e
commit
e2d028a46e
5 changed files with 278 additions and 0 deletions
|
|
@ -55,6 +55,16 @@ func (r *Response) RequireJSONPathExists(path string) *Response {
|
|||
return r
|
||||
}
|
||||
|
||||
// RequireJSONPathNotExists asserts that a JSON path does not exist and returns the response for chaining.
|
||||
func (r *Response) RequireJSONPathNotExists(path string) *Response {
|
||||
r.t.Helper()
|
||||
require.NotNil(r.t, r.JSON, "response body is not JSON")
|
||||
|
||||
value := getJSONPath(r.JSON, path)
|
||||
require.Nil(r.t, value, "JSON path %q should not exist but was found", path)
|
||||
return r
|
||||
}
|
||||
|
||||
// RequireEquals asserts that a JSON path equals the expected value and returns the response for chaining.
|
||||
func (r *Response) RequireEquals(path string, expected any) *Response {
|
||||
r.t.Helper()
|
||||
|
|
|
|||
|
|
@ -417,3 +417,94 @@ func TestAPIWithNativeHistograms(t *testing.T) {
|
|||
RequireLenAtLeast("$.data", 1)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAPIWithStats tests the API with the stats query parameter.
|
||||
func TestAPIWithStats(t *testing.T) {
|
||||
// Create an API with sample series data.
|
||||
api := newTestAPI(t, testhelpers.APIConfig{
|
||||
Queryable: testhelpers.NewLazyLoader(func() storage.SampleAndChunkQueryable {
|
||||
return testhelpers.NewQueryableWithSeries(testhelpers.FixtureMultipleSeries())
|
||||
}),
|
||||
})
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
// Test combinations of methods, endpoints, and stats values.
|
||||
methods := []string{"GET", "POST"}
|
||||
statsValues := []struct {
|
||||
value string
|
||||
expectStats bool
|
||||
}{
|
||||
{"true", true},
|
||||
{"all", true},
|
||||
{"1", true},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, method := range methods {
|
||||
for _, stats := range statsValues {
|
||||
t.Run(method+" /api/v1/query with stats="+stats.value, func(t *testing.T) {
|
||||
var params []string
|
||||
if stats.value != "" {
|
||||
params = []string{"query", "up", "stats", stats.value}
|
||||
} else {
|
||||
params = []string{"query", "up"}
|
||||
}
|
||||
|
||||
var resp *testhelpers.Response
|
||||
if method == "GET" {
|
||||
resp = testhelpers.GET(t, api, "/api/v1/query", params...)
|
||||
} else {
|
||||
resp = testhelpers.POST(t, api, "/api/v1/query", params...)
|
||||
}
|
||||
|
||||
resp.RequireSuccess().ValidateOpenAPI()
|
||||
|
||||
if stats.expectStats {
|
||||
resp.RequireJSONPathExists("$.data.stats").
|
||||
RequireJSONPathExists("$.data.stats.timings").
|
||||
RequireJSONPathExists("$.data.stats.samples")
|
||||
} else {
|
||||
resp.RequireJSONPathNotExists("$.data.stats")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run(method+" /api/v1/query_range with stats="+stats.value, func(t *testing.T) {
|
||||
var params []string
|
||||
if stats.value != "" {
|
||||
params = []string{
|
||||
"query", "up",
|
||||
"start", strconv.FormatInt(now-120, 10),
|
||||
"end", strconv.FormatInt(now, 10),
|
||||
"step", "60",
|
||||
"stats", stats.value,
|
||||
}
|
||||
} else {
|
||||
params = []string{
|
||||
"query", "up",
|
||||
"start", strconv.FormatInt(now-120, 10),
|
||||
"end", strconv.FormatInt(now, 10),
|
||||
"step", "60",
|
||||
}
|
||||
}
|
||||
|
||||
var resp *testhelpers.Response
|
||||
if method == "GET" {
|
||||
resp = testhelpers.GET(t, api, "/api/v1/query_range", params...)
|
||||
} else {
|
||||
resp = testhelpers.POST(t, api, "/api/v1/query_range", params...)
|
||||
}
|
||||
|
||||
resp.RequireSuccess().ValidateOpenAPI()
|
||||
|
||||
if stats.expectStats {
|
||||
resp.RequireJSONPathExists("$.data.stats").
|
||||
RequireJSONPathExists("$.data.stats.timings").
|
||||
RequireJSONPathExists("$.data.stats.samples")
|
||||
} else {
|
||||
resp.RequireJSONPathNotExists("$.data.stats")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ func (b *OpenAPIBuilder) buildComponents() *v3.Components {
|
|||
schemas.Set("ParseQueryOutputBody", b.simpleResponseBodySchema())
|
||||
schemas.Set("ParseQueryPostInputBody", b.parseQueryPostInputBodySchema())
|
||||
schemas.Set("QueryData", b.queryDataSchema())
|
||||
schemas.Set("QueryStats", b.queryStatsSchema())
|
||||
schemas.Set("FloatSample", b.floatSampleSchema())
|
||||
schemas.Set("HistogramSample", b.histogramSampleSchema())
|
||||
schemas.Set("FloatSeries", b.floatSeriesSchema())
|
||||
|
|
@ -450,6 +451,7 @@ func (*OpenAPIBuilder) queryDataSchema() *base.SchemaProxy {
|
|||
},
|
||||
})},
|
||||
}))
|
||||
vectorProps.Set("stats", schemaRef("#/components/schemas/QueryStats"))
|
||||
|
||||
// Matrix query result.
|
||||
matrixProps := orderedmap.New[string, *base.SchemaProxy]()
|
||||
|
|
@ -464,6 +466,7 @@ func (*OpenAPIBuilder) queryDataSchema() *base.SchemaProxy {
|
|||
},
|
||||
})},
|
||||
}))
|
||||
matrixProps.Set("stats", schemaRef("#/components/schemas/QueryStats"))
|
||||
|
||||
// Scalar query result.
|
||||
scalarProps := orderedmap.New[string, *base.SchemaProxy]()
|
||||
|
|
@ -480,6 +483,7 @@ func (*OpenAPIBuilder) queryDataSchema() *base.SchemaProxy {
|
|||
MinItems: int64Ptr(2),
|
||||
MaxItems: int64Ptr(2),
|
||||
}))
|
||||
scalarProps.Set("stats", schemaRef("#/components/schemas/QueryStats"))
|
||||
|
||||
// String query result.
|
||||
stringResultProps := orderedmap.New[string, *base.SchemaProxy]()
|
||||
|
|
@ -491,6 +495,7 @@ func (*OpenAPIBuilder) queryDataSchema() *base.SchemaProxy {
|
|||
MinItems: int64Ptr(2),
|
||||
MaxItems: int64Ptr(2),
|
||||
}))
|
||||
stringResultProps.Set("stats", schemaRef("#/components/schemas/QueryStats"))
|
||||
|
||||
return base.CreateSchemaProxy(&base.Schema{
|
||||
Description: "Query result data. The structure of 'result' depends on 'resultType'.",
|
||||
|
|
@ -536,6 +541,74 @@ func (*OpenAPIBuilder) queryDataSchema() *base.SchemaProxy {
|
|||
})
|
||||
}
|
||||
|
||||
func (*OpenAPIBuilder) queryStatsSchema() *base.SchemaProxy {
|
||||
// Timings object.
|
||||
timingsProps := orderedmap.New[string, *base.SchemaProxy]()
|
||||
timingsProps.Set("evalTotalTime", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"number"},
|
||||
Description: "Total evaluation time in seconds.",
|
||||
}))
|
||||
timingsProps.Set("resultSortTime", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"number"},
|
||||
Description: "Time spent sorting results in seconds.",
|
||||
}))
|
||||
timingsProps.Set("queryPreparationTime", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"number"},
|
||||
Description: "Query preparation time in seconds.",
|
||||
}))
|
||||
timingsProps.Set("innerEvalTime", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"number"},
|
||||
Description: "Inner evaluation time in seconds.",
|
||||
}))
|
||||
timingsProps.Set("execQueueTime", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"number"},
|
||||
Description: "Execution queue wait time in seconds.",
|
||||
}))
|
||||
timingsProps.Set("execTotalTime", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"number"},
|
||||
Description: "Total execution time in seconds.",
|
||||
}))
|
||||
|
||||
// Samples object.
|
||||
samplesProps := orderedmap.New[string, *base.SchemaProxy]()
|
||||
samplesProps.Set("totalQueryableSamples", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"integer"},
|
||||
Description: "Total number of samples that were queryable.",
|
||||
}))
|
||||
samplesProps.Set("peakSamples", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"integer"},
|
||||
Description: "Peak number of samples in memory.",
|
||||
}))
|
||||
samplesProps.Set("totalQueryableSamplesPerStep", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"array"},
|
||||
Description: "Total queryable samples per step (only included with stats=all).",
|
||||
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"array"},
|
||||
Description: "Timestamp and sample count as [timestamp, count].",
|
||||
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: base.CreateSchemaProxy(&base.Schema{Type: []string{"number"}})},
|
||||
MinItems: int64Ptr(2),
|
||||
MaxItems: int64Ptr(2),
|
||||
})},
|
||||
}))
|
||||
|
||||
// Main stats object.
|
||||
statsProps := orderedmap.New[string, *base.SchemaProxy]()
|
||||
statsProps.Set("timings", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"object"},
|
||||
Properties: timingsProps,
|
||||
}))
|
||||
statsProps.Set("samples", base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"object"},
|
||||
Properties: samplesProps,
|
||||
}))
|
||||
|
||||
return base.CreateSchemaProxy(&base.Schema{
|
||||
Type: []string{"object"},
|
||||
Description: "Query execution statistics (included when the stats query parameter is provided).",
|
||||
Properties: statsProps,
|
||||
})
|
||||
}
|
||||
|
||||
func (*OpenAPIBuilder) queryPostInputBodySchema() *base.SchemaProxy {
|
||||
props := orderedmap.New[string, *base.SchemaProxy]()
|
||||
props.Set("query", stringSchemaWithDescriptionAndExample("Form field: The PromQL query to execute.", "up"))
|
||||
|
|
|
|||
52
web/api/v1/testdata/openapi_3.1_golden.yaml
vendored
52
web/api/v1/testdata/openapi_3.1_golden.yaml
vendored
|
|
@ -2843,6 +2843,8 @@ components:
|
|||
- $ref: '#/components/schemas/FloatSample'
|
||||
- $ref: '#/components/schemas/HistogramSample'
|
||||
description: Array of samples (either float or histogram).
|
||||
stats:
|
||||
$ref: '#/components/schemas/QueryStats'
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
|
|
@ -2860,6 +2862,8 @@ components:
|
|||
- $ref: '#/components/schemas/FloatSeries'
|
||||
- $ref: '#/components/schemas/HistogramSeries'
|
||||
description: Array of time series (either float or histogram).
|
||||
stats:
|
||||
$ref: '#/components/schemas/QueryStats'
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
|
|
@ -2879,6 +2883,8 @@ components:
|
|||
maxItems: 2
|
||||
minItems: 2
|
||||
description: Scalar value as [timestamp, stringValue].
|
||||
stats:
|
||||
$ref: '#/components/schemas/QueryStats'
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
|
|
@ -2896,6 +2902,8 @@ components:
|
|||
maxItems: 2
|
||||
minItems: 2
|
||||
description: String value as [timestamp, stringValue].
|
||||
stats:
|
||||
$ref: '#/components/schemas/QueryStats'
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
|
|
@ -2910,6 +2918,50 @@ components:
|
|||
- 1627845600
|
||||
- "1"
|
||||
resultType: vector
|
||||
QueryStats:
|
||||
type: object
|
||||
properties:
|
||||
timings:
|
||||
type: object
|
||||
properties:
|
||||
evalTotalTime:
|
||||
type: number
|
||||
description: Total evaluation time in seconds.
|
||||
resultSortTime:
|
||||
type: number
|
||||
description: Time spent sorting results in seconds.
|
||||
queryPreparationTime:
|
||||
type: number
|
||||
description: Query preparation time in seconds.
|
||||
innerEvalTime:
|
||||
type: number
|
||||
description: Inner evaluation time in seconds.
|
||||
execQueueTime:
|
||||
type: number
|
||||
description: Execution queue wait time in seconds.
|
||||
execTotalTime:
|
||||
type: number
|
||||
description: Total execution time in seconds.
|
||||
samples:
|
||||
type: object
|
||||
properties:
|
||||
totalQueryableSamples:
|
||||
type: integer
|
||||
description: Total number of samples that were queryable.
|
||||
peakSamples:
|
||||
type: integer
|
||||
description: Peak number of samples in memory.
|
||||
totalQueryableSamplesPerStep:
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
maxItems: 2
|
||||
minItems: 2
|
||||
description: Timestamp and sample count as [timestamp, count].
|
||||
description: Total queryable samples per step (only included with stats=all).
|
||||
description: Query execution statistics (included when the stats query parameter is provided).
|
||||
FloatSample:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
52
web/api/v1/testdata/openapi_3.2_golden.yaml
vendored
52
web/api/v1/testdata/openapi_3.2_golden.yaml
vendored
|
|
@ -2881,6 +2881,8 @@ components:
|
|||
- $ref: '#/components/schemas/FloatSample'
|
||||
- $ref: '#/components/schemas/HistogramSample'
|
||||
description: Array of samples (either float or histogram).
|
||||
stats:
|
||||
$ref: '#/components/schemas/QueryStats'
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
|
|
@ -2898,6 +2900,8 @@ components:
|
|||
- $ref: '#/components/schemas/FloatSeries'
|
||||
- $ref: '#/components/schemas/HistogramSeries'
|
||||
description: Array of time series (either float or histogram).
|
||||
stats:
|
||||
$ref: '#/components/schemas/QueryStats'
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
|
|
@ -2917,6 +2921,8 @@ components:
|
|||
maxItems: 2
|
||||
minItems: 2
|
||||
description: Scalar value as [timestamp, stringValue].
|
||||
stats:
|
||||
$ref: '#/components/schemas/QueryStats'
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
|
|
@ -2934,6 +2940,8 @@ components:
|
|||
maxItems: 2
|
||||
minItems: 2
|
||||
description: String value as [timestamp, stringValue].
|
||||
stats:
|
||||
$ref: '#/components/schemas/QueryStats'
|
||||
required:
|
||||
- resultType
|
||||
- result
|
||||
|
|
@ -2948,6 +2956,50 @@ components:
|
|||
- 1627845600
|
||||
- "1"
|
||||
resultType: vector
|
||||
QueryStats:
|
||||
type: object
|
||||
properties:
|
||||
timings:
|
||||
type: object
|
||||
properties:
|
||||
evalTotalTime:
|
||||
type: number
|
||||
description: Total evaluation time in seconds.
|
||||
resultSortTime:
|
||||
type: number
|
||||
description: Time spent sorting results in seconds.
|
||||
queryPreparationTime:
|
||||
type: number
|
||||
description: Query preparation time in seconds.
|
||||
innerEvalTime:
|
||||
type: number
|
||||
description: Inner evaluation time in seconds.
|
||||
execQueueTime:
|
||||
type: number
|
||||
description: Execution queue wait time in seconds.
|
||||
execTotalTime:
|
||||
type: number
|
||||
description: Total execution time in seconds.
|
||||
samples:
|
||||
type: object
|
||||
properties:
|
||||
totalQueryableSamples:
|
||||
type: integer
|
||||
description: Total number of samples that were queryable.
|
||||
peakSamples:
|
||||
type: integer
|
||||
description: Peak number of samples in memory.
|
||||
totalQueryableSamplesPerStep:
|
||||
type: array
|
||||
items:
|
||||
type: array
|
||||
items:
|
||||
type: number
|
||||
maxItems: 2
|
||||
minItems: 2
|
||||
description: Timestamp and sample count as [timestamp, count].
|
||||
description: Total queryable samples per step (only included with stats=all).
|
||||
description: Query execution statistics (included when the stats query parameter is provided).
|
||||
FloatSample:
|
||||
type: object
|
||||
properties:
|
||||
|
|
|
|||
Loading…
Reference in a new issue