mirror of
https://github.com/prometheus/prometheus.git
synced 2026-02-12 19:31:02 +01:00
An oversight on the OpenAPI specification; which did not include stats. Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
511 lines
16 KiB
Go
511 lines
16 KiB
Go
// 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 (
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/prometheus/prometheus/storage"
|
|
"github.com/prometheus/prometheus/web/api/testhelpers"
|
|
)
|
|
|
|
// TODO: Generate automated tests from OpenAPI spec to validate API responses.
|
|
|
|
// TestAPIEmpty tests the API with no metrics and no rules.
|
|
func TestAPIEmpty(t *testing.T) {
|
|
// Create an API with empty defaults (no series, no rules).
|
|
api := newTestAPI(t, testhelpers.APIConfig{})
|
|
|
|
t.Run("GET /api/v1/labels returns success with empty array", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/labels").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONArray("$.data")
|
|
})
|
|
|
|
t.Run("GET /api/v1/query?query=up returns success (empty result ok)", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query", "query", "up").
|
|
ValidateOpenAPI().
|
|
RequireSuccess().
|
|
RequireEquals("$.data.resultType", "vector")
|
|
})
|
|
|
|
t.Run("GET /api/v1/query_range?query=up returns success", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query_range",
|
|
"query", "up",
|
|
"start", "0",
|
|
"end", "100",
|
|
"step", "10").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "matrix")
|
|
})
|
|
|
|
t.Run("GET /api/v1/series returns success with empty result", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/series",
|
|
"match[]", "up",
|
|
"start", "0",
|
|
"end", "100").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONArray("$.data")
|
|
})
|
|
|
|
t.Run("GET /api/v1/label/__name__/values returns success with empty array", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/label/__name__/values").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONArray("$.data")
|
|
})
|
|
|
|
t.Run("GET /api/v1/targets returns success", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/targets").
|
|
RequireSuccess().
|
|
RequireJSONPathExists("$.data.activeTargets")
|
|
})
|
|
|
|
t.Run("GET /api/v1/rules returns success with empty groups", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/rules").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data.groups")
|
|
})
|
|
|
|
t.Run("GET /api/v1/alerts returns success with empty alerts", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/alerts").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data.alerts")
|
|
})
|
|
|
|
t.Run("GET /api/v1/alertmanagers returns success", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/alertmanagers").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data.activeAlertmanagers")
|
|
})
|
|
|
|
t.Run("GET /api/v1/metadata returns success", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/metadata").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data")
|
|
})
|
|
|
|
t.Run("GET /api/v1/status/config returns success", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/status/config").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data.yaml")
|
|
})
|
|
|
|
t.Run("GET /api/v1/status/flags returns success", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/status/flags").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data")
|
|
})
|
|
|
|
t.Run("GET /api/v1/status/runtimeinfo returns success", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/status/runtimeinfo").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data")
|
|
})
|
|
|
|
t.Run("GET /api/v1/status/buildinfo returns success", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/status/buildinfo").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data")
|
|
})
|
|
|
|
t.Run("POST /api/v1/query with form data returns success", func(t *testing.T) {
|
|
testhelpers.POST(t, api, "/api/v1/query", "query", "up").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "vector")
|
|
})
|
|
}
|
|
|
|
// TestAPIWithSeries tests the API with metrics/series data.
|
|
func TestAPIWithSeries(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())
|
|
}),
|
|
})
|
|
|
|
t.Run("GET /api/v1/query returns vector with >= 1 sample", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query", "query", "up").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "vector").
|
|
RequireLenAtLeast("$.data.result", 1)
|
|
})
|
|
|
|
t.Run("GET /api/v1/query_range returns matrix result type", func(t *testing.T) {
|
|
// Use relative timestamps to match our fixtures.
|
|
now := time.Now().Unix()
|
|
testhelpers.GET(t, api, "/api/v1/query_range",
|
|
"query", "up",
|
|
"start", strconv.FormatInt(now-120, 10),
|
|
"end", strconv.FormatInt(now, 10),
|
|
"step", "60").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "matrix")
|
|
// Note: Result may be empty if timestamps don't align perfectly with samples.
|
|
})
|
|
|
|
t.Run("GET /api/v1/labels returns non-empty array", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/labels").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONArray("$.data").
|
|
RequireLenAtLeast("$.data", 1)
|
|
})
|
|
|
|
t.Run("GET /api/v1/label/__name__/values contains expected metric names", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/label/__name__/values").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireArrayContains("$.data", "up").
|
|
RequireArrayContains("$.data", "http_requests_total")
|
|
})
|
|
|
|
t.Run("GET /api/v1/label/job/values contains expected jobs", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/label/job/values").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONArray("$.data").
|
|
RequireArrayContains("$.data", "prometheus").
|
|
RequireArrayContains("$.data", "node").
|
|
RequireArrayContains("$.data", "api")
|
|
})
|
|
|
|
t.Run("GET /api/v1/series with match returns results", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/series",
|
|
"match[]", "up",
|
|
"start", "0",
|
|
"end", "120").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONArray("$.data").
|
|
RequireLenAtLeast("$.data", 1)
|
|
})
|
|
|
|
t.Run("GET /api/v1/query with specific job returns filtered results", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query", "query", `up{job="prometheus"}`).
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "vector").
|
|
RequireLenAtLeast("$.data.result", 1)
|
|
})
|
|
|
|
t.Run("GET /api/v1/query with aggregation returns result", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query", "query", "sum(up)").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "vector")
|
|
})
|
|
|
|
t.Run("POST /api/v1/query returns vector with data", func(t *testing.T) {
|
|
testhelpers.POST(t, api, "/api/v1/query", "query", "up").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "vector").
|
|
RequireLenAtLeast("$.data.result", 1)
|
|
})
|
|
}
|
|
|
|
// TestAPIWithRules tests the API with rules configured.
|
|
func TestAPIWithRules(t *testing.T) {
|
|
// Create an API with rule groups.
|
|
api := newTestAPI(t, testhelpers.APIConfig{
|
|
RulesRetriever: testhelpers.NewLazyLoader(func() testhelpers.RulesRetriever {
|
|
return testhelpers.NewRulesRetrieverWithGroups(testhelpers.FixtureRuleGroups())
|
|
}),
|
|
})
|
|
|
|
t.Run("GET /api/v1/rules returns groups with rules", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/rules").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data.groups").
|
|
RequireLenAtLeast("$.data.groups", 1).
|
|
RequireSome("$.data.groups", func(group any) bool {
|
|
if g, ok := group.(map[string]any); ok {
|
|
return g["name"] == "example"
|
|
}
|
|
return false
|
|
}).
|
|
RequireSome("$.data.groups", func(group any) bool {
|
|
if g, ok := group.(map[string]any); ok {
|
|
if g["name"] == "example" {
|
|
// Check that the group has rules.
|
|
if rules, ok := g["rules"].([]any); ok {
|
|
return len(rules) > 0
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
})
|
|
|
|
t.Run("GET /api/v1/alerts returns alerts array", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/alerts").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data.alerts").
|
|
RequireJSONArray("$.data.alerts")
|
|
})
|
|
|
|
t.Run("GET /api/v1/rules with rule_name filter", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/rules", "rule_name[]", "InstanceDown").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONPathExists("$.data.groups")
|
|
})
|
|
}
|
|
|
|
// TestAPITSDBNotReady tests the API when TSDB is not ready (e.g., during WAL replay).
|
|
// TSDB not ready errors are converted to errorUnavailable by setUnavailStatusOnTSDBNotReady,
|
|
// which returns HTTP 500 Internal Server Error (the default for errorUnavailable).
|
|
func TestAPITSDBNotReady(t *testing.T) {
|
|
// Create an API with a queryable that returns tsdb.ErrNotReady.
|
|
api := newTestAPI(t, testhelpers.APIConfig{
|
|
Queryable: testhelpers.NewLazyLoader(testhelpers.NewTSDBNotReadyQueryable),
|
|
})
|
|
|
|
t.Run("GET /api/v1/query returns 500 when TSDB not ready", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query", "query", "up").
|
|
RequireStatusCode(500).
|
|
ValidateOpenAPI().
|
|
RequireError()
|
|
})
|
|
|
|
t.Run("POST /api/v1/query returns 500 when TSDB not ready", func(t *testing.T) {
|
|
testhelpers.POST(t, api, "/api/v1/query", "query", "up").
|
|
RequireStatusCode(500).
|
|
ValidateOpenAPI().
|
|
RequireError()
|
|
})
|
|
|
|
t.Run("GET /api/v1/query_range returns 500 when TSDB not ready", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query_range",
|
|
"query", "up",
|
|
"start", "0",
|
|
"end", "100",
|
|
"step", "10").
|
|
RequireStatusCode(500).
|
|
ValidateOpenAPI().
|
|
RequireError()
|
|
})
|
|
|
|
t.Run("GET /api/v1/series returns 500 when TSDB not ready", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/series",
|
|
"match[]", "up",
|
|
"start", "0",
|
|
"end", "100").
|
|
RequireStatusCode(500).
|
|
ValidateOpenAPI().
|
|
RequireError()
|
|
})
|
|
|
|
t.Run("GET /api/v1/labels returns 500 when TSDB not ready", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/labels").
|
|
RequireStatusCode(500).
|
|
ValidateOpenAPI().
|
|
RequireError()
|
|
})
|
|
|
|
t.Run("GET /api/v1/label/{name}/values returns 500 when TSDB not ready", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/label/__name__/values").
|
|
RequireStatusCode(500).
|
|
ValidateOpenAPI().
|
|
RequireError()
|
|
})
|
|
}
|
|
|
|
// TestAPIWithNativeHistograms tests the API with native histogram data.
|
|
func TestAPIWithNativeHistograms(t *testing.T) {
|
|
// Create an API with histogram series data.
|
|
api := newTestAPI(t, testhelpers.APIConfig{
|
|
Queryable: testhelpers.NewLazyLoader(func() storage.SampleAndChunkQueryable {
|
|
return testhelpers.NewQueryableWithSeries(testhelpers.FixtureHistogramSeries())
|
|
}),
|
|
})
|
|
|
|
t.Run("GET /api/v1/query returns vector with native histogram", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query", "query", "test_histogram").
|
|
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
|
|
}
|
|
// Check that the sample has a histogram field (not a value field).
|
|
_, hasHistogram := sample["histogram"]
|
|
return hasHistogram
|
|
})
|
|
})
|
|
|
|
t.Run("POST /api/v1/query returns vector with native histogram", func(t *testing.T) {
|
|
testhelpers.POST(t, api, "/api/v1/query", "query", "test_histogram").
|
|
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
|
|
}
|
|
// Check that the sample has a histogram field (not a value field).
|
|
_, hasHistogram := sample["histogram"]
|
|
return hasHistogram
|
|
})
|
|
})
|
|
|
|
t.Run("GET /api/v1/query_range returns matrix with native histogram", func(t *testing.T) {
|
|
// Use relative timestamps to match our fixtures.
|
|
now := time.Now().Unix()
|
|
testhelpers.GET(t, api, "/api/v1/query_range",
|
|
"query", "test_histogram",
|
|
"start", strconv.FormatInt(now-120, 10),
|
|
"end", strconv.FormatInt(now, 10),
|
|
"step", "60").
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "matrix")
|
|
})
|
|
|
|
t.Run("GET /api/v1/query with histogram selector", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/query", "query", `test_histogram{job="prometheus"}`).
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireEquals("$.data.resultType", "vector").
|
|
RequireLenAtLeast("$.data.result", 1)
|
|
})
|
|
|
|
t.Run("GET /api/v1/series returns histogram metric series", func(t *testing.T) {
|
|
testhelpers.GET(t, api, "/api/v1/series",
|
|
"match[]", "test_histogram",
|
|
"start", "0",
|
|
"end", strconv.FormatInt(time.Now().Unix(), 10)).
|
|
RequireSuccess().
|
|
ValidateOpenAPI().
|
|
RequireJSONArray("$.data").
|
|
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")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|