Add OpenAPI 3.2 specification generation for Prometheus HTTP API (#17825)

* Add OpenAPI 3.2 specification generation for Prometheus HTTP API

This commit introduces an OpenAPI specification for the Prometheus API.
After testing multiple code-generation servers with built-in APIs, this
implementation uses an independent spec file outside of the critical path.
This spec file is tested with a framework present in this pull request.

The specification helps clients know which parameters they can use and is
served at /api/v1/openapi.yaml. The spec file will evolve with the
Prometheus API and has the same version number.

Downstream projects can tune the APIs presented in the spec file with
configuration options using the IncludePaths setting for path filtering.

In the future, there is room to generate a server from this spec file
(e.g. with interfaces), but this is out of scope for this pull request.

Architecture:
- Core OpenAPI infrastructure (openapi.go): Dynamic spec building,
  caching, and thread-safe spec generation
- Schema definitions (openapi_schemas.go): Complete type definitions
  for all API request and response types
- Path specifications (openapi_paths.go): Endpoint definitions with
  parameters, request bodies, and response schemas
- Examples (openapi_examples.go): Realistic request/response examples
- Helper functions (openapi_helpers.go): Reusable builders for common
  OpenAPI structures

Testing:
- Comprehensive test suite with golden file validation
- Test helpers package for API testing infrastructure
- OpenAPI compliance validation utilities

The golden file captures the complete specification for snapshot testing.
Update with: go test -run TestOpenAPIGolden -update-openapi-spec

REVIEWERS: The most important thing to check would be the OpenAPI golden
file (web/api/v1/testdata/openapi_golden.yaml). Test scenarios are important
as they test the actual OpenAPI spec validity.

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>

* Add OpenAPI 3.1 support with version selection

Add support for both OpenAPI 3.1 and 3.2 specifications with version
selection via openapi_version query parameter. Defaults to 3.1 for
broader compatibility

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>

* Enhance OpenAPI examples and add helper functions

- Add timestampExamples helper for consistent time formatting
- Add exampleMap helper to simplify example creation
- Improve example summaries with query details
- Add matrix result example for range vector queries

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>

* web/api: Add AtST method to test helper iterators

Implement the AtST() method required by chunkenc.Iterator interface
for FakeSeriesIterator and FakeHistogramSeriesIterator test helpers.
The method returns 0 as these test helpers don't use start timestamps

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>

* OpenAPI: Add minimum coverage test

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>

* OpenAPI: Improve examples handling

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>

---------

Signed-off-by: Julien Pivotto <291750+roidelapluie@users.noreply.github.com>
This commit is contained in:
Julien 2026-01-29 13:36:13 +01:00 committed by GitHub
parent 36ea75d203
commit 75f94903b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 15337 additions and 8 deletions

1
.gitattributes vendored Normal file
View file

@ -0,0 +1 @@
web/api/v1/testdata/openapi_golden.yaml linguist-generated

View file

@ -124,6 +124,8 @@ linters:
# Disable this check for now since it introduces too many changes in our existing codebase. # Disable this check for now since it introduces too many changes in our existing codebase.
# See https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_omitzero for more details. # See https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_omitzero for more details.
- omitzero - omitzero
# Disable waitgroup check until we really move to Go 1.25.
- waitgroup
perfsprint: perfsprint:
# Optimizes even if it requires an int or uint type cast. # Optimizes even if it requires an int or uint type cast.
int-conversion: true int-conversion: true

View file

@ -2,6 +2,7 @@
extends: default extends: default
ignore: | ignore: |
**/node_modules **/node_modules
web/api/v1/testdata/openapi_*_golden.yaml
rules: rules:
braces: braces:

View file

@ -6,6 +6,22 @@ sort_rank: 7
The current stable HTTP API is reachable under `/api/v1` on a Prometheus The current stable HTTP API is reachable under `/api/v1` on a Prometheus
server. Any non-breaking additions will be added under that endpoint. server. Any non-breaking additions will be added under that endpoint.
## OpenAPI Specification
An OpenAPI specification for the HTTP API is available at `/api/v1/openapi.yaml`.
By default, it returns OpenAPI 3.1 for broader compatibility. Use `?openapi_version=3.2`
for OpenAPI 3.2, which includes advanced features and endpoints like `/api/v1/notifications/live`.
This machine-readable specification describes all available endpoints, request parameters,
response formats, and schemas.
The OpenAPI specification can be used to:
- Generate client libraries in various programming languages.
- Validate API requests and responses.
- Generate interactive API documentation.
- Test API endpoints.
## Format overview ## Format overview
The API response format is JSON. Every successful API request returns a `2xx` The API response format is JSON. Every successful API request returns a `2xx`

View file

@ -1,6 +1,6 @@
module github.com/prometheus/prometheus/documentation/examples/remote_storage module github.com/prometheus/prometheus/documentation/examples/remote_storage
go 1.24.0 go 1.25.0
require ( require (
github.com/alecthomas/kingpin/v2 v2.4.0 github.com/alecthomas/kingpin/v2 v2.4.0

15
go.mod
View file

@ -1,6 +1,6 @@
module github.com/prometheus/prometheus module github.com/prometheus/prometheus
go 1.24.0 go 1.25.0
require ( require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
@ -54,6 +54,8 @@ require (
github.com/oklog/ulid/v2 v2.1.1 github.com/oklog/ulid/v2 v2.1.1
github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.142.0 github.com/open-telemetry/opentelemetry-collector-contrib/processor/deltatocumulativeprocessor v0.142.0
github.com/ovh/go-ovh v1.9.0 github.com/ovh/go-ovh v1.9.0
github.com/pb33f/libopenapi v0.31.1
github.com/pb33f/libopenapi-validator v0.10.0
github.com/prometheus/alertmanager v0.30.0 github.com/prometheus/alertmanager v0.30.0
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_golang/exp v0.0.0-20260101091701-2cd067eb23c9 github.com/prometheus/client_golang/exp v0.0.0-20260101091701-2cd067eb23c9
@ -85,6 +87,7 @@ require (
go.uber.org/goleak v1.3.0 go.uber.org/goleak v1.3.0
go.yaml.in/yaml/v2 v2.4.3 go.yaml.in/yaml/v2 v2.4.3
go.yaml.in/yaml/v3 v3.0.4 go.yaml.in/yaml/v3 v3.0.4
go.yaml.in/yaml/v4 v4.0.0-rc.3
golang.org/x/oauth2 v0.34.0 golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0 golang.org/x/sync v0.19.0
golang.org/x/sys v0.39.0 golang.org/x/sys v0.39.0
@ -93,6 +96,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b
google.golang.org/grpc v1.78.0 google.golang.org/grpc v1.78.0
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.34.3 k8s.io/api v0.34.3
k8s.io/apimachinery v0.34.3 k8s.io/apimachinery v0.34.3
k8s.io/client-go v0.34.3 k8s.io/client-go v0.34.3
@ -102,6 +106,9 @@ require (
require ( require (
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect github.com/go-openapi/swag/conv v0.25.4 // indirect
github.com/go-openapi/swag/fileutils v0.25.4 // indirect github.com/go-openapi/swag/fileutils v0.25.4 // indirect
@ -113,8 +120,10 @@ require (
github.com/go-openapi/swag/stringutils v0.25.4 // indirect github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/pb33f/jsonpath v0.7.0 // indirect
github.com/pb33f/ordered-map/v2 v2.3.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
) )
@ -237,7 +246,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0
gotest.tools/v3 v3.0.3 // indirect gotest.tools/v3 v3.0.3 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect

48
go.sum
View file

@ -81,6 +81,10 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad h1:3swAvbzgfaI6nKuDDU7BiKfZRdF+h2ZwKgMHd8Ha4t8=
github.com/basgys/goxml2json v1.1.1-0.20231018121955-e66ee54ceaad/go.mod h1:9+nBLYNWkvPcq9ep0owWUsPTLgL9ZXTsZWcCSVGGLJ0=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps=
github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@ -88,6 +92,10 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow=
github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@ -116,6 +124,8 @@ github.com/digitalocean/godo v1.171.0 h1:QwpkwWKr3v7yxc8D4NQG973NoR9APCEWjYnLOQe
github.com/digitalocean/godo v1.171.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU= github.com/digitalocean/godo v1.171.0/go.mod h1:xQsWpVCCbkDrWisHA72hPzPlnC+4W5w/McZY5ij9uvU=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
@ -437,6 +447,14 @@ github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pb33f/jsonpath v0.7.0 h1:3oG6yu1RqNoMZpqnRjBMqi8fSIXWoDAKDrsB0QGTcoU=
github.com/pb33f/jsonpath v0.7.0/go.mod h1:/+JlSIjWA2ijMVYGJ3IQPF4Q1nLMYbUTYNdk0exCDPQ=
github.com/pb33f/libopenapi v0.31.1 h1:smGr45U2Y+hHWYKiEV13oS2tP9IUnscqNb5qsvT9+YI=
github.com/pb33f/libopenapi v0.31.1/go.mod h1:oaebeA5l58AFbZ7qRKTtMnu15JEiPlaBas1vLDcw9vs=
github.com/pb33f/libopenapi-validator v0.10.0 h1:9XhgxW2jTDd+1aDMuIjGUsWaeUaPi5ql2z1Y+WBltiE=
github.com/pb33f/libopenapi-validator v0.10.0/go.mod h1:hW3wIpg4YCxLrJxyTrfrzP9Mtt9FvbD/nm0yemUcjSs=
github.com/pb33f/ordered-map/v2 v2.3.0 h1:k2OhVEQkhTCQMhAicQ3Z6iInzoZNQ7L9MVomwKBZ5WQ=
github.com/pb33f/ordered-map/v2 v2.3.0/go.mod h1:oe5ue+6ZNhy7QN9cPZvPA23Hx0vMHnNVeMg4fGdCANw=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
@ -491,6 +509,8 @@ github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPK
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 h1:ObX9hZmK+VmijreZO/8x9pQ8/P/ToHD/bdSb4Eg4tUo=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36/go.mod h1:LEsDu4BubxK7/cWhtlQWfuxwL4rf/2UEpxXz1o1EMtM=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
@ -517,6 +537,7 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@ -533,6 +554,7 @@ github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
@ -620,12 +642,16 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90= golang.org/x/exp v0.0.0-20250808145144-a408d31f581a h1:Y+7uR/b1Mw2iSXZ3G//1haIiSElDQZ8KWh0h+sZPG90=
golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/exp v0.0.0-20250808145144-a408d31f581a/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -638,6 +664,10 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
@ -648,6 +678,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -667,23 +699,37 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
@ -694,6 +740,8 @@ golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk= golang.org/x/tools/godoc v0.1.0-deprecated h1:o+aZ1BOj6Hsx/GBdJO/s815sqftjSnrZZwyYTHODvtk=

View file

@ -1,4 +1,4 @@
go 1.24.0 go 1.25.0
use ( use (
. .

View file

@ -1,6 +1,6 @@
module github.com/prometheus/prometheus/internal/tools module github.com/prometheus/prometheus/internal/tools
go 1.24.0 go 1.25.0
require ( require (
github.com/bufbuild/buf v1.62.1 github.com/bufbuild/buf v1.62.1

244
web/api/testhelpers/api.go Normal file
View file

@ -0,0 +1,244 @@
// 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 testhelpers provides utilities for testing the Prometheus HTTP API.
// This file contains helper functions for creating test API instances and managing test lifecycles.
package testhelpers
import (
"context"
"log/slog"
"net/http"
"net/url"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/promslog"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/promqltest"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/util/notifications"
)
// RulesRetriever provides a list of active rules and alerts.
type RulesRetriever interface {
RuleGroups() []*rules.Group
AlertingRules() []*rules.AlertingRule
}
// TargetRetriever provides the list of active/dropped targets to scrape or not.
type TargetRetriever interface {
TargetsActive() map[string][]*scrape.Target
TargetsDropped() map[string][]*scrape.Target
TargetsDroppedCounts() map[string]int
ScrapePoolConfig(string) (*config.ScrapeConfig, error)
}
// ScrapePoolsRetriever provide the list of all scrape pools.
type ScrapePoolsRetriever interface {
ScrapePools() []string
}
// AlertmanagerRetriever provides a list of all/dropped AlertManager URLs.
type AlertmanagerRetriever interface {
Alertmanagers() []*url.URL
DroppedAlertmanagers() []*url.URL
}
// TSDBAdminStats provides TSDB admin statistics.
type TSDBAdminStats interface {
CleanTombstones() error
Delete(ctx context.Context, mint, maxt int64, ms ...*labels.Matcher) error
Snapshot(dir string, withHead bool) error
Stats(statsByLabelName string, limit int) (*tsdb.Stats, error)
WALReplayStatus() (tsdb.WALReplayStatus, error)
BlockMetas() ([]tsdb.BlockMeta, error)
}
// APIConfig holds configuration for creating a test API instance.
type APIConfig struct {
// Core dependencies.
QueryEngine *LazyLoader[promql.QueryEngine]
Queryable *LazyLoader[storage.SampleAndChunkQueryable]
ExemplarQueryable *LazyLoader[storage.ExemplarQueryable]
// Retrievers.
RulesRetriever *LazyLoader[RulesRetriever]
TargetRetriever *LazyLoader[TargetRetriever]
ScrapePoolsRetriever *LazyLoader[ScrapePoolsRetriever]
AlertmanagerRetriever *LazyLoader[AlertmanagerRetriever]
// Admin.
TSDBAdmin *LazyLoader[TSDBAdminStats]
DBDir string
// Optional overrides.
Config func() config.Config
FlagsMap map[string]string
Now func() time.Time
}
// APIWrapper wraps the API and provides a handler for testing.
type APIWrapper struct {
Handler http.Handler
}
// PrometheusVersion contains build information about Prometheus.
type PrometheusVersion struct {
Version string `json:"version"`
Revision string `json:"revision"`
Branch string `json:"branch"`
BuildUser string `json:"buildUser"`
BuildDate string `json:"buildDate"`
GoVersion string `json:"goVersion"`
}
// RuntimeInfo contains runtime information about Prometheus.
type RuntimeInfo struct {
StartTime time.Time `json:"startTime"`
CWD string `json:"CWD"`
Hostname string `json:"hostname"`
ServerTime time.Time `json:"serverTime"`
ReloadConfigSuccess bool `json:"reloadConfigSuccess"`
LastConfigTime time.Time `json:"lastConfigTime"`
CorruptionCount int64 `json:"corruptionCount"`
GoroutineCount int `json:"goroutineCount"`
GOMAXPROCS int `json:"GOMAXPROCS"`
GOMEMLIMIT int64 `json:"GOMEMLIMIT"`
GOGC string `json:"GOGC"`
GODEBUG string `json:"GODEBUG"`
StorageRetention string `json:"storageRetention"`
}
// NewAPIParams holds all the parameters needed to create a v1.API instance.
type NewAPIParams struct {
QueryEngine promql.QueryEngine
Queryable storage.SampleAndChunkQueryable
ExemplarQueryable storage.ExemplarQueryable
ScrapePoolsRetriever func(context.Context) ScrapePoolsRetriever
TargetRetriever func(context.Context) TargetRetriever
AlertmanagerRetriever func(context.Context) AlertmanagerRetriever
ConfigFunc func() config.Config
FlagsMap map[string]string
ReadyFunc func(http.HandlerFunc) http.HandlerFunc
TSDBAdmin TSDBAdminStats
DBDir string
Logger *slog.Logger
RulesRetriever func(context.Context) RulesRetriever
RuntimeInfoFunc func() (RuntimeInfo, error)
BuildInfo *PrometheusVersion
NotificationsGetter func() []notifications.Notification
NotificationsSub func() (<-chan notifications.Notification, func(), bool)
Gatherer prometheus.Gatherer
Registerer prometheus.Registerer
}
// PrepareAPI creates a NewAPIParams with sensible defaults for testing.
func PrepareAPI(t *testing.T, cfg APIConfig) NewAPIParams {
t.Helper()
// Create defaults for unset lazy loaders.
if cfg.QueryEngine == nil {
cfg.QueryEngine = NewLazyLoader(func() promql.QueryEngine {
return promqltest.NewTestEngineWithOpts(t, promql.EngineOpts{
Logger: nil,
Reg: nil,
MaxSamples: 10000,
Timeout: 100 * time.Second,
NoStepSubqueryIntervalFn: func(int64) int64 { return 60 * 1000 },
EnableAtModifier: true,
EnableNegativeOffset: true,
EnablePerStepStats: true,
})
})
}
if cfg.Queryable == nil {
cfg.Queryable = NewLazyLoader(NewEmptyQueryable)
}
if cfg.ExemplarQueryable == nil {
cfg.ExemplarQueryable = NewLazyLoader(NewEmptyExemplarQueryable)
}
if cfg.RulesRetriever == nil {
cfg.RulesRetriever = NewLazyLoader(func() RulesRetriever {
return NewEmptyRulesRetriever()
})
}
if cfg.TargetRetriever == nil {
cfg.TargetRetriever = NewLazyLoader(func() TargetRetriever {
return NewEmptyTargetRetriever()
})
}
if cfg.ScrapePoolsRetriever == nil {
cfg.ScrapePoolsRetriever = NewLazyLoader(func() ScrapePoolsRetriever {
return NewEmptyScrapePoolsRetriever()
})
}
if cfg.AlertmanagerRetriever == nil {
cfg.AlertmanagerRetriever = NewLazyLoader(func() AlertmanagerRetriever {
return NewEmptyAlertmanagerRetriever()
})
}
if cfg.TSDBAdmin == nil {
cfg.TSDBAdmin = NewLazyLoader(func() TSDBAdminStats {
return NewEmptyTSDBAdminStats()
})
}
if cfg.Config == nil {
cfg.Config = func() config.Config { return config.Config{} }
}
if cfg.FlagsMap == nil {
cfg.FlagsMap = map[string]string{}
}
if cfg.DBDir == "" {
cfg.DBDir = t.TempDir()
}
return NewAPIParams{
QueryEngine: cfg.QueryEngine.Get(),
Queryable: cfg.Queryable.Get(),
ExemplarQueryable: cfg.ExemplarQueryable.Get(),
ScrapePoolsRetriever: func(context.Context) ScrapePoolsRetriever { return cfg.ScrapePoolsRetriever.Get() },
TargetRetriever: func(context.Context) TargetRetriever { return cfg.TargetRetriever.Get() },
AlertmanagerRetriever: func(context.Context) AlertmanagerRetriever { return cfg.AlertmanagerRetriever.Get() },
ConfigFunc: cfg.Config,
FlagsMap: cfg.FlagsMap,
ReadyFunc: func(f http.HandlerFunc) http.HandlerFunc { return f },
TSDBAdmin: cfg.TSDBAdmin.Get(),
DBDir: cfg.DBDir,
Logger: promslog.NewNopLogger(),
RulesRetriever: func(context.Context) RulesRetriever { return cfg.RulesRetriever.Get() },
RuntimeInfoFunc: func() (RuntimeInfo, error) { return RuntimeInfo{}, nil },
BuildInfo: &PrometheusVersion{},
NotificationsGetter: func() []notifications.Notification { return nil },
NotificationsSub: func() (<-chan notifications.Notification, func(), bool) { return nil, func() {}, false },
Gatherer: prometheus.NewRegistry(),
Registerer: prometheus.NewRegistry(),
}
}

View file

@ -0,0 +1,252 @@
// 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.
// This file provides assertion helpers for validating API responses in tests.
package testhelpers
import (
"fmt"
"slices"
"strings"
"github.com/stretchr/testify/require"
)
// RequireSuccess asserts that the response has status "success" and returns the response for chaining.
func (r *Response) RequireSuccess() *Response {
r.t.Helper()
require.NotNil(r.t, r.JSON, "response body is not JSON")
require.Equal(r.t, "success", r.JSON["status"], "expected status to be 'success'")
return r
}
// RequireError asserts that the response has status "error" and returns the response for chaining.
func (r *Response) RequireError() *Response {
r.t.Helper()
require.NotNil(r.t, r.JSON, "response body is not JSON")
require.Equal(r.t, "error", r.JSON["status"], "expected status to be 'error'")
return r
}
// RequireStatusCode asserts that the response has the given HTTP status code and returns the response for chaining.
func (r *Response) RequireStatusCode(expectedCode int) *Response {
r.t.Helper()
require.Equal(r.t, expectedCode, r.StatusCode, "unexpected HTTP status code")
return r
}
// RequireJSONPathExists asserts that a JSON path exists and returns the response for chaining.
func (r *Response) RequireJSONPathExists(path string) *Response {
r.t.Helper()
require.NotNil(r.t, r.JSON, "response body is not JSON")
value := getJSONPath(r.JSON, path)
require.NotNil(r.t, value, "JSON path %q does not exist", 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()
require.NotNil(r.t, r.JSON, "response body is not JSON")
value := getJSONPath(r.JSON, path)
require.NotNil(r.t, value, "JSON path %q does not exist", path)
require.Equal(r.t, expected, value, "JSON path %q has unexpected value", path)
return r
}
// RequireJSONArray asserts that a JSON path contains an array and returns the response for chaining.
func (r *Response) RequireJSONArray(path string) *Response {
r.t.Helper()
require.NotNil(r.t, r.JSON, "response body is not JSON")
value := getJSONPath(r.JSON, path)
require.NotNil(r.t, value, "JSON path %q does not exist", path)
_, ok := value.([]any)
require.True(r.t, ok, "JSON path %q is not an array", path)
return r
}
// RequireLenAtLeast asserts that a JSON path contains an array with at least minLen elements and returns the response for chaining.
func (r *Response) RequireLenAtLeast(path string, minLen int) *Response {
r.t.Helper()
require.NotNil(r.t, r.JSON, "response body is not JSON")
value := getJSONPath(r.JSON, path)
require.NotNil(r.t, value, "JSON path %q does not exist", path)
arr, ok := value.([]any)
require.True(r.t, ok, "JSON path %q is not an array", path)
require.GreaterOrEqual(r.t, len(arr), minLen, "JSON path %q has fewer than %d elements", path, minLen)
return r
}
// RequireArrayContains asserts that a JSON path contains an array with the expected element and returns the response for chaining.
func (r *Response) RequireArrayContains(path string, expected any) *Response {
r.t.Helper()
require.NotNil(r.t, r.JSON, "response body is not JSON")
value := getJSONPath(r.JSON, path)
require.NotNil(r.t, value, "JSON path %q does not exist", path)
arr, ok := value.([]any)
require.True(r.t, ok, "JSON path %q is not an array", path)
found := slices.Contains(arr, expected)
require.True(r.t, found, "JSON path %q does not contain expected value %v", path, expected)
return r
}
// RequireSome asserts that at least one element in an array satisfies the predicate and returns the response for chaining.
func (r *Response) RequireSome(path string, predicate func(any) bool) *Response {
r.t.Helper()
require.NotNil(r.t, r.JSON, "response body is not JSON")
value := getJSONPath(r.JSON, path)
require.NotNil(r.t, value, "JSON path %q does not exist", path)
arr, ok := value.([]any)
require.True(r.t, ok, "JSON path %q is not an array", path)
found := slices.ContainsFunc(arr, predicate)
require.True(r.t, found, "no element in JSON path %q satisfies the predicate", path)
return r
}
// getJSONPath extracts a value from a JSON object using a simple path notation.
// Supports paths like "$.data", "$.data.groups", "$.data.groups[0]".
func getJSONPath(data map[string]any, path string) any {
// Remove leading "$." if present.
path = strings.TrimPrefix(path, "$.")
if path == "" {
return data
}
parts := strings.Split(path, ".")
current := any(data)
for _, part := range parts {
// Handle array indexing (e.g., "groups[0]").
if strings.Contains(part, "[") {
// Not implementing array indexing for simplicity.
// Tests should use direct field access or RequireSome.
return nil
}
// Navigate to the next level.
m, ok := current.(map[string]any)
if !ok {
return nil
}
current = m[part]
}
return current
}
// RequireVectorResult is a convenience helper for checking vector query results.
func (r *Response) RequireVectorResult() *Response {
r.t.Helper()
return r.RequireSuccess().RequireEquals("$.data.resultType", "vector")
}
// RequireMatrixResult is a convenience helper for checking matrix query results.
func (r *Response) RequireMatrixResult() *Response {
r.t.Helper()
return r.RequireSuccess().RequireEquals("$.data.resultType", "matrix")
}
// RequireScalarResult is a convenience helper for checking scalar query results.
func (r *Response) RequireScalarResult() *Response {
r.t.Helper()
return r.RequireSuccess().RequireEquals("$.data.resultType", "scalar")
}
// RequireRulesGroupNamed asserts that a rules response contains a group with the given name.
func (r *Response) RequireRulesGroupNamed(name string) *Response {
r.t.Helper()
return r.RequireSuccess().RequireSome("$.data.groups", func(group any) bool {
if g, ok := group.(map[string]any); ok {
return g["name"] == name
}
return false
})
}
// RequireTargetCount asserts that a targets response contains at least n targets.
func (r *Response) RequireTargetCount(minCount int) *Response {
r.t.Helper()
r.RequireSuccess()
// The targets endpoint returns activeTargets as an array of targets.
value := getJSONPath(r.JSON, "$.data.activeTargets")
require.NotNil(r.t, value, "JSON path $.data.activeTargets does not exist")
arr, ok := value.([]any)
require.True(r.t, ok, "$.data.activeTargets is not an array")
require.GreaterOrEqual(r.t, len(arr), minCount, "expected at least %d targets, got %d", minCount, len(arr))
return r
}
// DebugJSON is a helper for debugging JSON responses in tests.
func (r *Response) DebugJSON() *Response {
r.t.Helper()
r.t.Logf("Response status code: %d", r.StatusCode)
r.t.Logf("Response body: %s", r.Body)
if r.JSON != nil {
r.t.Logf("Response JSON: %+v", r.JSON)
}
return r
}
// RequireContainsSubstring asserts that the response body contains the given substring.
func (r *Response) RequireContainsSubstring(substring string) *Response {
r.t.Helper()
require.Contains(r.t, r.Body, substring, "response body does not contain expected substring")
return r
}
// RequireField asserts that a field exists at the given path and returns its value.
// Note: This method cannot be chained further since it returns the field value, not the Response.
func (r *Response) RequireField(path string) any {
r.t.Helper()
require.NotNil(r.t, r.JSON, "response body is not JSON")
value := getJSONPath(r.JSON, path)
require.NotNil(r.t, value, "JSON path %q does not exist", path)
return value
}
// RequireFieldType asserts that a field exists and has the expected type.
func (r *Response) RequireFieldType(path, expectedType string) *Response {
r.t.Helper()
value := r.RequireField(path)
var actualType string
switch value.(type) {
case string:
actualType = "string"
case float64:
actualType = "number"
case bool:
actualType = "bool"
case []any:
actualType = "array"
case map[string]any:
actualType = "object"
default:
actualType = fmt.Sprintf("%T", value)
}
require.Equal(r.t, expectedType, actualType, "JSON path %q has unexpected type", path)
return r
}

View file

@ -0,0 +1,178 @@
// 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.
// This file provides test fixture data for API tests.
package testhelpers
import (
"time"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/storage"
)
// FixtureSeries creates a simple series with the "up" metric.
func FixtureSeries() []storage.Series {
// Use timestamps relative to "now" so queries work.
now := time.Now().UnixMilli()
return []storage.Series{
&FakeSeries{
labels: labels.FromStrings("__name__", "up", "job", "prometheus", "instance", "localhost:9090"),
samples: []promql.FPoint{
{T: now - 120000, F: 1},
{T: now - 60000, F: 1},
{T: now, F: 1},
},
},
}
}
// FixtureMultipleSeries creates multiple series for testing.
func FixtureMultipleSeries() []storage.Series {
// Use timestamps relative to "now" so queries work.
now := time.Now().UnixMilli()
return []storage.Series{
&FakeSeries{
labels: labels.FromStrings("__name__", "up", "job", "prometheus", "instance", "localhost:9090"),
samples: []promql.FPoint{
{T: now - 60000, F: 1},
{T: now, F: 1},
},
},
&FakeSeries{
labels: labels.FromStrings("__name__", "up", "job", "node", "instance", "localhost:9100"),
samples: []promql.FPoint{
{T: now - 60000, F: 1},
{T: now, F: 0},
},
},
&FakeSeries{
labels: labels.FromStrings("__name__", "http_requests_total", "job", "api", "instance", "localhost:8080"),
samples: []promql.FPoint{
{T: now - 60000, F: 100},
{T: now, F: 150},
},
},
}
}
// FixtureRuleGroups creates a simple set of rule groups for testing.
func FixtureRuleGroups() []*rules.Group {
// Create a simple recording rule.
expr, _ := parser.ParseExpr("up == 1")
recordingRule := rules.NewRecordingRule(
"job:up:sum",
expr,
labels.EmptyLabels(),
)
// Create a simple alerting rule.
alertExpr, _ := parser.ParseExpr("up == 0")
alertingRule := rules.NewAlertingRule(
"InstanceDown",
alertExpr,
time.Minute,
0,
labels.FromStrings("severity", "critical"),
labels.EmptyLabels(),
labels.EmptyLabels(),
"Instance {{ $labels.instance }} is down",
true,
nil,
)
// Create a rule group.
group := rules.NewGroup(rules.GroupOptions{
Name: "example",
File: "example.rules",
Interval: time.Minute,
Rules: []rules.Rule{
recordingRule,
alertingRule,
},
})
return []*rules.Group{group}
}
// FixtureEmptyRuleGroups returns an empty set of rule groups.
func FixtureEmptyRuleGroups() []*rules.Group {
return []*rules.Group{}
}
// FixtureSingleSeries creates a single series for simple tests.
func FixtureSingleSeries(metricName string, value float64) []storage.Series {
return []storage.Series{
&FakeSeries{
labels: labels.FromStrings("__name__", metricName),
samples: []promql.FPoint{
{T: 0, F: value},
},
},
}
}
// FixtureHistogramSeries creates a series with native histogram data.
func FixtureHistogramSeries() []storage.Series {
// Use timestamps relative to "now" so queries work.
now := time.Now().UnixMilli()
return []storage.Series{
&FakeHistogramSeries{
labels: labels.FromStrings("__name__", "test_histogram", "job", "prometheus", "instance", "localhost:9090"),
histograms: []promql.HPoint{
{
T: now - 60000,
H: &histogram.FloatHistogram{
Schema: 2,
ZeroThreshold: 0.001,
ZeroCount: 5,
Count: 50,
Sum: 100,
PositiveSpans: []histogram.Span{
{Offset: 0, Length: 2},
{Offset: 1, Length: 2},
},
NegativeSpans: []histogram.Span{
{Offset: 0, Length: 1},
},
PositiveBuckets: []float64{5, 10, 8, 7},
NegativeBuckets: []float64{3},
},
},
{
T: now,
H: &histogram.FloatHistogram{
Schema: 2,
ZeroThreshold: 0.001,
ZeroCount: 8,
Count: 60,
Sum: 120,
PositiveSpans: []histogram.Span{
{Offset: 0, Length: 2},
{Offset: 1, Length: 2},
},
NegativeSpans: []histogram.Span{
{Offset: 0, Length: 1},
},
PositiveBuckets: []float64{6, 12, 10, 9},
NegativeBuckets: []float64{4},
},
},
},
},
}
}

View file

@ -0,0 +1,534 @@
// 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.
// This file contains mock implementations of API dependencies for testing.
package testhelpers
import (
"context"
"net/url"
"github.com/prometheus/prometheus/config"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/histogram"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/tsdb/chunks"
"github.com/prometheus/prometheus/util/annotations"
)
// LazyLoader allows lazy initialization of mocks per test.
type LazyLoader[T any] struct {
loader func() T
value *T
}
// NewLazyLoader creates a new LazyLoader with the given loader function.
func NewLazyLoader[T any](loader func() T) *LazyLoader[T] {
return &LazyLoader[T]{loader: loader}
}
// Get returns the loaded value, initializing it if necessary.
func (l *LazyLoader[T]) Get() T {
if l.value == nil {
v := l.loader()
l.value = &v
}
return *l.value
}
// FakeQueryable implements storage.SampleAndChunkQueryable with configurable behavior.
type FakeQueryable struct {
series []storage.Series
}
func (f *FakeQueryable) Querier(_, _ int64) (storage.Querier, error) {
return &FakeQuerier{series: f.series}, nil
}
func (f *FakeQueryable) ChunkQuerier(_, _ int64) (storage.ChunkQuerier, error) {
return &FakeChunkQuerier{series: f.series}, nil
}
// FakeQuerier implements storage.Querier.
type FakeQuerier struct {
series []storage.Series
}
func (f *FakeQuerier) Select(_ context.Context, _ bool, _ *storage.SelectHints, _ ...*labels.Matcher) storage.SeriesSet {
return &FakeSeriesSet{series: f.series, idx: -1}
}
func (f *FakeQuerier) LabelValues(_ context.Context, name string, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
valuesMap := make(map[string]struct{})
for _, s := range f.series {
lbls := s.Labels()
if val := lbls.Get(name); val != "" {
valuesMap[val] = struct{}{}
}
}
values := make([]string, 0, len(valuesMap))
for v := range valuesMap {
values = append(values, v)
}
return values, nil, nil
}
func (f *FakeQuerier) LabelNames(_ context.Context, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
namesMap := make(map[string]struct{})
for _, s := range f.series {
lbls := s.Labels()
lbls.Range(func(l labels.Label) {
namesMap[l.Name] = struct{}{}
})
}
names := make([]string, 0, len(namesMap))
for n := range namesMap {
names = append(names, n)
}
return names, nil, nil
}
func (*FakeQuerier) Close() error {
return nil
}
// FakeChunkQuerier implements storage.ChunkQuerier.
type FakeChunkQuerier struct {
series []storage.Series
}
func (f *FakeChunkQuerier) Select(_ context.Context, _ bool, _ *storage.SelectHints, _ ...*labels.Matcher) storage.ChunkSeriesSet {
return &FakeChunkSeriesSet{series: f.series, idx: -1}
}
func (f *FakeChunkQuerier) LabelValues(_ context.Context, name string, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
valuesMap := make(map[string]struct{})
for _, s := range f.series {
lbls := s.Labels()
if val := lbls.Get(name); val != "" {
valuesMap[val] = struct{}{}
}
}
values := make([]string, 0, len(valuesMap))
for v := range valuesMap {
values = append(values, v)
}
return values, nil, nil
}
func (f *FakeChunkQuerier) LabelNames(_ context.Context, _ *storage.LabelHints, _ ...*labels.Matcher) ([]string, annotations.Annotations, error) {
namesMap := make(map[string]struct{})
for _, s := range f.series {
lbls := s.Labels()
lbls.Range(func(l labels.Label) {
namesMap[l.Name] = struct{}{}
})
}
names := make([]string, 0, len(namesMap))
for n := range namesMap {
names = append(names, n)
}
return names, nil, nil
}
func (*FakeChunkQuerier) Close() error {
return nil
}
// FakeSeriesSet implements storage.SeriesSet.
type FakeSeriesSet struct {
series []storage.Series
idx int
}
func (f *FakeSeriesSet) Next() bool {
f.idx++
return f.idx < len(f.series)
}
func (f *FakeSeriesSet) At() storage.Series {
return f.series[f.idx]
}
func (*FakeSeriesSet) Err() error {
return nil
}
func (*FakeSeriesSet) Warnings() annotations.Annotations {
return nil
}
// FakeChunkSeriesSet implements storage.ChunkSeriesSet.
type FakeChunkSeriesSet struct {
series []storage.Series
idx int
}
func (f *FakeChunkSeriesSet) Next() bool {
f.idx++
return f.idx < len(f.series)
}
func (f *FakeChunkSeriesSet) At() storage.ChunkSeries {
return &FakeChunkSeries{series: f.series[f.idx]}
}
func (*FakeChunkSeriesSet) Err() error {
return nil
}
func (*FakeChunkSeriesSet) Warnings() annotations.Annotations {
return nil
}
// FakeChunkSeries implements storage.ChunkSeries.
type FakeChunkSeries struct {
series storage.Series
}
func (f *FakeChunkSeries) Labels() labels.Labels {
return f.series.Labels()
}
func (*FakeChunkSeries) Iterator(_ chunks.Iterator) chunks.Iterator {
return &FakeChunkSeriesIterator{}
}
// FakeChunkSeriesIterator implements chunks.Iterator.
type FakeChunkSeriesIterator struct{}
func (*FakeChunkSeriesIterator) Next() bool {
return false
}
func (*FakeChunkSeriesIterator) At() chunks.Meta {
return chunks.Meta{}
}
func (*FakeChunkSeriesIterator) Err() error {
return nil
}
// FakeSeries implements storage.Series.
type FakeSeries struct {
labels labels.Labels
samples []promql.FPoint
}
func (f *FakeSeries) Labels() labels.Labels {
return f.labels
}
func (f *FakeSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator {
return &FakeSeriesIterator{samples: f.samples, idx: -1}
}
// FakeSeriesIterator implements chunkenc.Iterator.
type FakeSeriesIterator struct {
samples []promql.FPoint
idx int
}
func (f *FakeSeriesIterator) Next() chunkenc.ValueType {
f.idx++
if f.idx < len(f.samples) {
return chunkenc.ValFloat
}
return chunkenc.ValNone
}
func (f *FakeSeriesIterator) Seek(t int64) chunkenc.ValueType {
for f.idx < len(f.samples)-1 {
f.idx++
if f.samples[f.idx].T >= t {
return chunkenc.ValFloat
}
}
return chunkenc.ValNone
}
func (f *FakeSeriesIterator) At() (int64, float64) {
s := f.samples[f.idx]
return s.T, s.F
}
func (*FakeSeriesIterator) AtHistogram(*histogram.Histogram) (int64, *histogram.Histogram) {
panic("not implemented")
}
func (*FakeSeriesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) {
panic("not implemented")
}
func (f *FakeSeriesIterator) AtT() int64 {
return f.samples[f.idx].T
}
func (*FakeSeriesIterator) AtST() int64 {
return 0
}
func (*FakeSeriesIterator) Err() error {
return nil
}
// FakeHistogramSeries implements storage.Series for histogram data.
type FakeHistogramSeries struct {
labels labels.Labels
histograms []promql.HPoint
}
func (f *FakeHistogramSeries) Labels() labels.Labels {
return f.labels
}
func (f *FakeHistogramSeries) Iterator(chunkenc.Iterator) chunkenc.Iterator {
return &FakeHistogramSeriesIterator{histograms: f.histograms, idx: -1}
}
// FakeHistogramSeriesIterator implements chunkenc.Iterator for histogram data.
type FakeHistogramSeriesIterator struct {
histograms []promql.HPoint
idx int
}
func (f *FakeHistogramSeriesIterator) Next() chunkenc.ValueType {
f.idx++
if f.idx < len(f.histograms) {
return chunkenc.ValFloatHistogram
}
return chunkenc.ValNone
}
func (f *FakeHistogramSeriesIterator) Seek(t int64) chunkenc.ValueType {
for f.idx < len(f.histograms)-1 {
f.idx++
if f.histograms[f.idx].T >= t {
return chunkenc.ValFloatHistogram
}
}
return chunkenc.ValNone
}
func (*FakeHistogramSeriesIterator) At() (int64, float64) {
panic("not a float value")
}
func (*FakeHistogramSeriesIterator) AtHistogram(*histogram.Histogram) (int64, *histogram.Histogram) {
panic("not implemented")
}
func (f *FakeHistogramSeriesIterator) AtFloatHistogram(*histogram.FloatHistogram) (int64, *histogram.FloatHistogram) {
h := f.histograms[f.idx]
return h.T, h.H
}
func (f *FakeHistogramSeriesIterator) AtT() int64 {
return f.histograms[f.idx].T
}
func (*FakeHistogramSeriesIterator) AtST() int64 {
return 0
}
func (*FakeHistogramSeriesIterator) Err() error {
return nil
}
// FakeExemplarQueryable implements storage.ExemplarQueryable.
type FakeExemplarQueryable struct{}
func (*FakeExemplarQueryable) ExemplarQuerier(_ context.Context) (storage.ExemplarQuerier, error) {
return &FakeExemplarQuerier{}, nil
}
// FakeExemplarQuerier implements storage.ExemplarQuerier.
type FakeExemplarQuerier struct{}
func (*FakeExemplarQuerier) Select(_, _ int64, _ ...[]*labels.Matcher) ([]exemplar.QueryResult, error) {
return nil, nil
}
// FakeRulesRetriever implements v1.RulesRetriever.
type FakeRulesRetriever struct {
groups []*rules.Group
}
func (f *FakeRulesRetriever) RuleGroups() []*rules.Group {
return f.groups
}
func (f *FakeRulesRetriever) AlertingRules() []*rules.AlertingRule {
var alertingRules []*rules.AlertingRule
for _, g := range f.groups {
for _, r := range g.Rules() {
if ar, ok := r.(*rules.AlertingRule); ok {
alertingRules = append(alertingRules, ar)
}
}
}
return alertingRules
}
// FakeTargetRetriever implements v1.TargetRetriever.
type FakeTargetRetriever struct {
active map[string][]*scrape.Target
dropped map[string][]*scrape.Target
droppedCounts map[string]int
scrapeConfig map[string]*config.ScrapeConfig
}
func (f *FakeTargetRetriever) TargetsActive() map[string][]*scrape.Target {
if f.active == nil {
return make(map[string][]*scrape.Target)
}
return f.active
}
func (f *FakeTargetRetriever) TargetsDropped() map[string][]*scrape.Target {
if f.dropped == nil {
return make(map[string][]*scrape.Target)
}
return f.dropped
}
func (f *FakeTargetRetriever) TargetsDroppedCounts() map[string]int {
if f.droppedCounts == nil {
return make(map[string]int)
}
return f.droppedCounts
}
func (f *FakeTargetRetriever) ScrapePoolConfig(name string) (*config.ScrapeConfig, error) {
if f.scrapeConfig == nil {
return nil, nil
}
return f.scrapeConfig[name], nil
}
// FakeScrapePoolsRetriever implements v1.ScrapePoolsRetriever.
type FakeScrapePoolsRetriever struct {
pools []string
}
func (f *FakeScrapePoolsRetriever) ScrapePools() []string {
if f.pools == nil {
return []string{}
}
return f.pools
}
// FakeAlertmanagerRetriever implements v1.AlertmanagerRetriever.
type FakeAlertmanagerRetriever struct{}
func (*FakeAlertmanagerRetriever) Alertmanagers() []*url.URL {
return nil
}
func (*FakeAlertmanagerRetriever) DroppedAlertmanagers() []*url.URL {
return nil
}
// FakeTSDBAdminStats implements v1.TSDBAdminStats.
type FakeTSDBAdminStats struct{}
func (*FakeTSDBAdminStats) CleanTombstones() error {
return nil
}
func (*FakeTSDBAdminStats) Delete(_ context.Context, _, _ int64, _ ...*labels.Matcher) error {
return nil
}
func (*FakeTSDBAdminStats) Snapshot(_ string, _ bool) error {
return nil
}
func (*FakeTSDBAdminStats) Stats(_ string, _ int) (*tsdb.Stats, error) {
return &tsdb.Stats{}, nil
}
func (*FakeTSDBAdminStats) WALReplayStatus() (tsdb.WALReplayStatus, error) {
return tsdb.WALReplayStatus{}, nil
}
func (*FakeTSDBAdminStats) BlockMetas() ([]tsdb.BlockMeta, error) {
return []tsdb.BlockMeta{}, nil
}
// NewEmptyQueryable returns a queryable with no series.
func NewEmptyQueryable() storage.SampleAndChunkQueryable {
return &FakeQueryable{series: []storage.Series{}}
}
// NewQueryableWithSeries returns a queryable with the given series.
func NewQueryableWithSeries(series []storage.Series) storage.SampleAndChunkQueryable {
return &FakeQueryable{series: series}
}
// TSDBNotReadyQueryable implements storage.SampleAndChunkQueryable that returns tsdb.ErrNotReady.
type TSDBNotReadyQueryable struct{}
func (*TSDBNotReadyQueryable) Querier(_, _ int64) (storage.Querier, error) {
return nil, tsdb.ErrNotReady
}
func (*TSDBNotReadyQueryable) ChunkQuerier(_, _ int64) (storage.ChunkQuerier, error) {
return nil, tsdb.ErrNotReady
}
// NewTSDBNotReadyQueryable returns a queryable that always returns tsdb.ErrNotReady.
func NewTSDBNotReadyQueryable() storage.SampleAndChunkQueryable {
return &TSDBNotReadyQueryable{}
}
// NewEmptyExemplarQueryable returns an exemplar queryable with no exemplars.
func NewEmptyExemplarQueryable() storage.ExemplarQueryable {
return &FakeExemplarQueryable{}
}
// NewEmptyRulesRetriever returns a rules retriever with no rules.
func NewEmptyRulesRetriever() *FakeRulesRetriever {
return &FakeRulesRetriever{groups: []*rules.Group{}}
}
// NewRulesRetrieverWithGroups returns a rules retriever with the given groups.
func NewRulesRetrieverWithGroups(groups []*rules.Group) *FakeRulesRetriever {
return &FakeRulesRetriever{groups: groups}
}
// NewEmptyTargetRetriever returns a target retriever with no targets.
func NewEmptyTargetRetriever() *FakeTargetRetriever {
return &FakeTargetRetriever{}
}
// NewEmptyScrapePoolsRetriever returns a scrape pools retriever with no pools.
func NewEmptyScrapePoolsRetriever() *FakeScrapePoolsRetriever {
return &FakeScrapePoolsRetriever{pools: []string{}}
}
// NewEmptyAlertmanagerRetriever returns an alertmanager retriever with no alertmanagers.
func NewEmptyAlertmanagerRetriever() *FakeAlertmanagerRetriever {
return &FakeAlertmanagerRetriever{}
}
// NewEmptyTSDBAdminStats returns a TSDB admin stats with no-op implementations.
func NewEmptyTSDBAdminStats() *FakeTSDBAdminStats {
return &FakeTSDBAdminStats{}
}

View file

@ -0,0 +1,204 @@
// 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.
// This file provides OpenAPI-specific test utilities for validating spec compliance.
package testhelpers
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"github.com/pb33f/libopenapi"
validator "github.com/pb33f/libopenapi-validator"
valerrors "github.com/pb33f/libopenapi-validator/errors"
"github.com/stretchr/testify/require"
)
var (
openAPIValidator31 validator.Validator
openAPIValidator32 validator.Validator
openAPIValidatorOnce sync.Once
openAPIValidatorErr error
)
// loadOpenAPIValidators loads and caches both OpenAPI 3.1 and 3.2 validators from golden files.
func loadOpenAPIValidators() (v31, v32 validator.Validator, err error) {
openAPIValidatorOnce.Do(func() {
// Load OpenAPI 3.1 validator.
goldenPath31 := filepath.Join("testdata", "openapi_3.1_golden.yaml")
specBytes31, err := os.ReadFile(goldenPath31)
if err != nil {
openAPIValidatorErr = fmt.Errorf("failed to read OpenAPI 3.1 spec from %s: %w", goldenPath31, err)
return
}
doc31, err := libopenapi.NewDocument(specBytes31)
if err != nil {
openAPIValidatorErr = fmt.Errorf("failed to parse OpenAPI 3.1 document: %w", err)
return
}
v31, errs := validator.NewValidator(doc31)
if len(errs) > 0 {
openAPIValidatorErr = fmt.Errorf("failed to create OpenAPI 3.1 validator: %v", errs)
return
}
openAPIValidator31 = v31
// Load OpenAPI 3.2 validator.
goldenPath32 := filepath.Join("testdata", "openapi_3.2_golden.yaml")
specBytes32, err := os.ReadFile(goldenPath32)
if err != nil {
openAPIValidatorErr = fmt.Errorf("failed to read OpenAPI 3.2 spec from %s: %w", goldenPath32, err)
return
}
doc32, err := libopenapi.NewDocument(specBytes32)
if err != nil {
openAPIValidatorErr = fmt.Errorf("failed to parse OpenAPI 3.2 document: %w", err)
return
}
v32, errs := validator.NewValidator(doc32)
if len(errs) > 0 {
openAPIValidatorErr = fmt.Errorf("failed to create OpenAPI 3.2 validator: %v", errs)
return
}
openAPIValidator32 = v32
})
if openAPIValidatorErr != nil {
return nil, nil, openAPIValidatorErr
}
return openAPIValidator31, openAPIValidator32, nil
}
// ValidateOpenAPI validates the request and response against both OpenAPI 3.1 and 3.2 specifications.
// This ensures API endpoints are compatible with both OpenAPI versions.
// Returns the response for chaining.
func (r *Response) ValidateOpenAPI() *Response {
r.t.Helper()
// Load both validators (cached after first call).
v31, v32, err := loadOpenAPIValidators()
require.NoError(r.t, err, "failed to load OpenAPI validators")
// Validate against OpenAPI 3.1 spec.
if r.request != nil {
r.validateRequestWithVersion(v31, "3.1")
}
r.validateResponseWithVersion(v31, "3.1")
// Validate against OpenAPI 3.2 spec.
if r.request != nil {
r.validateRequestWithVersion(v32, "3.2")
}
r.validateResponseWithVersion(v32, "3.2")
return r
}
// validateRequestWithVersion validates the HTTP request against a specific OpenAPI version's spec.
func (r *Response) validateRequestWithVersion(v validator.Validator, version string) {
r.t.Helper()
// Create a validation request from the original request.
validationReq := &http.Request{
Method: r.request.Method,
URL: r.request.URL,
Header: r.request.Header,
Body: io.NopCloser(bytes.NewReader(r.requestBody)),
}
// Validate the request.
valid, errors := v.ValidateHttpRequest(validationReq)
if !valid {
// Check if the error is because the path doesn't exist in this version.
// Some endpoints (like /notifications/live) only exist in 3.2, not 3.1.
if isPathNotFoundError(errors) && version == "3.1" && strings.Contains(r.request.URL.Path, "/notifications/live") {
// Expected: /notifications/live is only in OpenAPI 3.2.
return
}
var errorMessages []string
for _, e := range errors {
errorMessages = append(errorMessages, e.Error())
}
require.Fail(r.t, fmt.Sprintf("OpenAPI %s request validation failed", version),
"Request to %s %s failed OpenAPI %s validation:\n%v",
r.request.Method, r.request.URL.Path, version, errorMessages)
}
}
// validateResponseWithVersion validates the HTTP response against a specific OpenAPI version's spec.
func (r *Response) validateResponseWithVersion(v validator.Validator, version string) {
r.t.Helper()
// Create a validation request (needed for response validation context).
validationReq := &http.Request{
Method: r.request.Method,
URL: r.request.URL,
Header: r.request.Header,
}
// Create a response for validation.
validationResp := &http.Response{
StatusCode: r.StatusCode,
Header: r.responseHeader,
Body: io.NopCloser(bytes.NewReader([]byte(r.Body))),
Request: validationReq,
}
// Validate the response.
valid, errors := v.ValidateHttpResponse(validationReq, validationResp)
if !valid {
// Check if the error is because the path doesn't exist in this version.
// Some endpoints (like /notifications/live) only exist in 3.2, not 3.1.
if isPathNotFoundError(errors) && version == "3.1" && strings.Contains(r.request.URL.Path, "/notifications/live") {
// Expected: /notifications/live is only in OpenAPI 3.2.
return
}
var errorMessages []string
for _, e := range errors {
errorMessages = append(errorMessages, e.Error())
}
require.Fail(r.t, fmt.Sprintf("OpenAPI %s response validation failed", version),
"Response from %s %s (status %d) failed OpenAPI %s validation:\n%v",
r.request.Method, r.request.URL.Path, r.StatusCode, version, errorMessages)
}
}
// isPathNotFoundError checks if the validation errors indicate a path was not found in the spec.
func isPathNotFoundError(errors []*valerrors.ValidationError) bool {
for _, err := range errors {
errStr := err.Error()
// Check for common "path not found" error messages from libopenapi-validator.
if strings.Contains(errStr, "path") && (strings.Contains(errStr, "not found") || strings.Contains(errStr, "does not exist")) {
return true
}
if strings.Contains(errStr, "GET /notifications/live") || strings.Contains(errStr, "/notifications/live not found") {
return true
}
}
return false
}

View file

@ -0,0 +1,145 @@
// 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.
// This file provides HTTP request builders for testing API endpoints.
package testhelpers
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
)
// Response wraps an HTTP response with parsed JSON data.
// It supports method chaining for assertions.
//
// Example usage:
//
// testhelpers.GET(t, api, "/api/v1/query", "query", "up").
// ValidateOpenAPI().
// RequireSuccess().
// RequireEquals("$.data.resultType", "vector").
// RequireLenAtLeast("$.data.result", 1)
//
// testhelpers.POST(t, api, "/api/v1/query", "query", "up").
// ValidateOpenAPI().
// RequireSuccess().
// RequireArrayContains("$.data.result", expectedValue)
type Response struct {
StatusCode int
Body string
JSON map[string]any
t *testing.T
request *http.Request
requestBody []byte
responseHeader http.Header
}
// GET sends a GET request to the API and returns a Response with parsed JSON.
// queryParams should be pairs of key-value strings.
func GET(t *testing.T, api *APIWrapper, path string, queryParams ...string) *Response {
t.Helper()
if len(queryParams)%2 != 0 {
t.Fatal("queryParams must be key-value pairs")
}
// Build query string.
values := url.Values{}
for i := 0; i < len(queryParams); i += 2 {
values.Add(queryParams[i], queryParams[i+1])
}
fullPath := path
if len(values) > 0 {
fullPath = path + "?" + values.Encode()
}
req := httptest.NewRequest(http.MethodGet, fullPath, nil)
return executeRequest(t, api, req)
}
// POST sends a POST request to the API with the given body and returns a Response with parsed JSON.
// bodyParams should be pairs of key-value strings for form data.
func POST(t *testing.T, api *APIWrapper, path string, bodyParams ...string) *Response {
t.Helper()
if len(bodyParams)%2 != 0 {
t.Fatal("bodyParams must be key-value pairs")
}
// Build form data.
values := url.Values{}
for i := 0; i < len(bodyParams); i += 2 {
values.Add(bodyParams[i], bodyParams[i+1])
}
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(values.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return executeRequest(t, api, req)
}
// executeRequest executes an HTTP request and parses the response as JSON.
func executeRequest(t *testing.T, api *APIWrapper, req *http.Request) *Response {
t.Helper()
// Capture the request body for validation.
var requestBody []byte
if req.Body != nil {
var err error
requestBody, err = io.ReadAll(req.Body)
if err != nil {
t.Fatalf("failed to read request body: %v", err)
}
// Restore the body for the actual request.
req.Body = io.NopCloser(strings.NewReader(string(requestBody)))
}
recorder := httptest.NewRecorder()
api.Handler.ServeHTTP(recorder, req)
result := recorder.Result()
defer result.Body.Close()
bodyBytes, err := io.ReadAll(result.Body)
if err != nil {
t.Fatalf("failed to read response body: %v", err)
}
resp := &Response{
StatusCode: result.StatusCode,
Body: string(bodyBytes),
t: t,
request: req,
requestBody: requestBody,
responseHeader: result.Header,
}
// Try to parse as JSON.
if result.Header.Get("Content-Type") == "application/json" || strings.Contains(result.Header.Get("Content-Type"), "application/json") {
var jsonData map[string]any
if err := json.Unmarshal(bodyBytes, &jsonData); err != nil {
// If JSON parsing fails, leave JSON as nil.
// This allows tests to handle non-JSON responses.
resp.JSON = nil
} else {
resp.JSON = jsonData
}
}
return resp
}

View file

@ -258,6 +258,7 @@ type API struct {
codecs []Codec codecs []Codec
featureRegistry features.Collector featureRegistry features.Collector
openAPIBuilder *OpenAPIBuilder
} }
// NewAPI returns an initialized API type. // NewAPI returns an initialized API type.
@ -299,6 +300,7 @@ func NewAPI(
appendMetadata bool, appendMetadata bool,
overrideErrorCode OverrideErrorCode, overrideErrorCode OverrideErrorCode,
featureRegistry features.Collector, featureRegistry features.Collector,
openAPIOptions OpenAPIOptions,
) *API { ) *API {
a := &API{ a := &API{
QueryEngine: qe, QueryEngine: qe,
@ -329,6 +331,7 @@ func NewAPI(
notificationsSub: notificationsSub, notificationsSub: notificationsSub,
overrideErrorCode: overrideErrorCode, overrideErrorCode: overrideErrorCode,
featureRegistry: featureRegistry, featureRegistry: featureRegistry,
openAPIBuilder: NewOpenAPIBuilder(openAPIOptions, logger),
remoteReadHandler: remote.NewReadHandler(logger, registerer, q, configFunc, remoteReadSampleLimit, remoteReadConcurrencyLimit, remoteReadMaxBytesInFrame), remoteReadHandler: remote.NewReadHandler(logger, registerer, q, configFunc, remoteReadSampleLimit, remoteReadConcurrencyLimit, remoteReadMaxBytesInFrame),
} }
@ -400,7 +403,7 @@ func (api *API) Register(r *route.Router) {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
}) })
return api.ready(httputil.CompressionHandler{ return api.ready(httputil.CompressionHandler{
Handler: hf, Handler: api.openAPIBuilder.WrapHandler(hf),
}.ServeHTTP) }.ServeHTTP)
} }
@ -469,6 +472,9 @@ func (api *API) Register(r *route.Router) {
r.Put("/admin/tsdb/delete_series", wrapAgent(api.deleteSeries)) r.Put("/admin/tsdb/delete_series", wrapAgent(api.deleteSeries))
r.Put("/admin/tsdb/clean_tombstones", wrapAgent(api.cleanTombstones)) r.Put("/admin/tsdb/clean_tombstones", wrapAgent(api.cleanTombstones))
r.Put("/admin/tsdb/snapshot", wrapAgent(api.snapshot)) r.Put("/admin/tsdb/snapshot", wrapAgent(api.snapshot))
// OpenAPI endpoint.
r.Get("/openapi.yaml", api.ready(api.openAPIBuilder.ServeOpenAPI))
} }
type QueryData struct { type QueryData struct {

View file

@ -0,0 +1,419 @@
// 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)
})
}

View file

@ -169,6 +169,7 @@ func createPrometheusAPI(t *testing.T, q storage.SampleAndChunkQueryable, overri
false, false,
overrideErrorCode, overrideErrorCode,
nil, nil,
OpenAPIOptions{},
) )
promRouter := route.New().WithPrefix("/api/v1") promRouter := route.New().WithPrefix("/api/v1")

320
web/api/v1/openapi.go Normal file
View file

@ -0,0 +1,320 @@
// 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.
// This file implements OpenAPI 3.2 specification generation for the Prometheus HTTP API.
// It provides dynamic spec building with optional path filtering.
package v1
import (
"log/slog"
"net/http"
"net/url"
"path"
"strings"
"sync"
"github.com/pb33f/libopenapi/datamodel/high/base"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pb33f/libopenapi/orderedmap"
)
const (
// OpenAPI 3.1.0 is the default version with broader compatibility.
openAPIVersion31 = "3.1.0"
// OpenAPI 3.2.0 supports advanced features like itemSchema for SSE streams.
openAPIVersion32 = "3.2.0"
)
// OpenAPIOptions configures the OpenAPI spec builder.
type OpenAPIOptions struct {
// IncludePaths filters which paths to include in the spec.
// If empty, all paths are included.
// Paths are matched by prefix (e.g., "/query" matches "/query" and "/query_range").
IncludePaths []string
// ExternalURL is the external URL of the Prometheus server (e.g., "http://prometheus.example.com:9090").
ExternalURL string
// Version is the API version to include in the OpenAPI spec.
// If empty, defaults to "0.0.1-undefined".
Version string
}
// OpenAPIBuilder builds and caches OpenAPI specifications.
type OpenAPIBuilder struct {
mu sync.RWMutex
cachedYAML31 []byte // Cached OpenAPI 3.1 spec.
cachedYAML32 []byte // Cached OpenAPI 3.2 spec.
options OpenAPIOptions
logger *slog.Logger
}
// NewOpenAPIBuilder creates a new OpenAPI builder with the given options.
func NewOpenAPIBuilder(opts OpenAPIOptions, logger *slog.Logger) *OpenAPIBuilder {
b := &OpenAPIBuilder{
options: opts,
logger: logger,
}
b.rebuild()
return b
}
// rebuild constructs the OpenAPI specs for both 3.1 and 3.2 versions based on current options.
func (b *OpenAPIBuilder) rebuild() {
b.mu.Lock()
defer b.mu.Unlock()
// Build OpenAPI 3.1 spec.
doc31 := b.buildDocument(openAPIVersion31)
yamlBytes31, err := doc31.Render()
if err != nil {
b.logger.Error("failed to render OpenAPI 3.1 spec - this is a bug, please report it", "err", err)
return
}
b.cachedYAML31 = yamlBytes31
// Build OpenAPI 3.2 spec.
doc32 := b.buildDocument(openAPIVersion32)
yamlBytes32, err := doc32.Render()
if err != nil {
b.logger.Error("failed to render OpenAPI 3.2 spec - this is a bug, please report it", "err", err)
return
}
b.cachedYAML32 = yamlBytes32
}
// ServeOpenAPI returns the OpenAPI specification as YAML.
// By default, serves OpenAPI 3.1.0. Use ?openapi_version=3.2 for OpenAPI 3.2.0.
func (b *OpenAPIBuilder) ServeOpenAPI(w http.ResponseWriter, r *http.Request) {
// Parse query parameter to determine which version to serve.
requestedVersion := r.URL.Query().Get("openapi_version")
b.mu.RLock()
var yamlData []byte
switch requestedVersion {
case "3.2", "3.2.0":
yamlData = b.cachedYAML32
case "3.1", "3.1.0":
yamlData = b.cachedYAML31
default:
// Default to OpenAPI 3.1.0 for broader compatibility.
yamlData = b.cachedYAML31
}
b.mu.RUnlock()
w.Header().Set("Content-Type", "application/yaml; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.WriteHeader(http.StatusOK)
w.Write(yamlData)
}
// WrapHandler returns the handler unchanged (no validation).
func (*OpenAPIBuilder) WrapHandler(next http.HandlerFunc) http.HandlerFunc {
return next
}
// shouldIncludePath checks if a path should be included based on options.
func (b *OpenAPIBuilder) shouldIncludePath(path string) bool {
if len(b.options.IncludePaths) == 0 {
return true
}
for _, include := range b.options.IncludePaths {
if strings.HasPrefix(path, include) || path == include {
return true
}
}
return false
}
// shouldIncludePathForVersion checks if a path should be included for a specific OpenAPI version.
func (b *OpenAPIBuilder) shouldIncludePathForVersion(path, version string) bool {
// First check IncludePaths filter.
if !b.shouldIncludePath(path) {
return false
}
// OpenAPI 3.1 excludes paths that require 3.2 features.
// The /notifications/live endpoint uses itemSchema which is a 3.2-only feature.
if version == openAPIVersion31 && path == "/notifications/live" {
return false
}
return true
}
// buildDocument creates the OpenAPI document for the specified version using high-level structs.
func (b *OpenAPIBuilder) buildDocument(version string) *v3.Document {
return &v3.Document{
Version: version,
Info: b.buildInfo(),
Servers: b.buildServers(),
Tags: b.buildTags(version),
Paths: b.buildPaths(version),
Components: b.buildComponents(),
}
}
// buildInfo constructs the info section.
func (b *OpenAPIBuilder) buildInfo() *base.Info {
apiVersion := b.options.Version
if apiVersion == "" {
apiVersion = "0.0.1-undefined"
}
return &base.Info{
Title: "Prometheus API",
Description: "Prometheus is an Open-Source monitoring system with a dimensional data model, flexible query language, efficient time series database and modern alerting approach.",
Version: apiVersion,
Contact: &base.Contact{
Name: "Prometheus Community",
URL: "https://prometheus.io/community/",
},
}
}
// buildServers constructs the servers section.
func (b *OpenAPIBuilder) buildServers() []*v3.Server {
// ExternalURL is always set by computeExternalURL in main.go.
// It includes scheme, host, port, and optional path prefix (without trailing slash).
serverURL := "/api/v1"
if b.options.ExternalURL != "" {
baseURL, err := url.Parse(b.options.ExternalURL)
if err == nil {
// Use path.Join to properly append /api/v1 to the existing path.
// Then use ResolveReference to construct the full URL.
baseURL.Path = path.Join(baseURL.Path, "/api/v1")
serverURL = baseURL.String()
}
}
return []*v3.Server{
{URL: serverURL},
}
}
// buildTags constructs the global tags list.
// Tag summary is an OpenAPI 3.2 feature, excluded from 3.1.
// Tag description is supported in both 3.1 and 3.2.
func (*OpenAPIBuilder) buildTags(version string) []*base.Tag {
// Define tags with all metadata.
tagData := []struct {
name string
summary string
description string
}{
{"query", "Query", "Query and evaluate PromQL expressions."},
{"metadata", "Metadata", "Retrieve metric metadata such as type and unit."},
{"labels", "Labels", "Query label names and values."},
{"series", "Series", "Query and manage time series."},
{"targets", "Targets", "Retrieve target and scrape pool information."},
{"rules", "Rules", "Query recording and alerting rules."},
{"alerts", "Alerts", "Query active alerts and alertmanager discovery."},
{"status", "Status", "Retrieve server status and configuration."},
{"admin", "Admin", "Administrative operations for TSDB management."},
{"features", "Features", "Query enabled features."},
{"remote", "Remote Storage", "Remote read and write endpoints."},
{"otlp", "OTLP", "OpenTelemetry Protocol metrics ingestion."},
{"notifications", "Notifications", "Server notifications and events."},
}
tags := make([]*base.Tag, 0, len(tagData))
for _, td := range tagData {
tag := &base.Tag{
Name: td.name,
Description: td.description, // Description is supported in both 3.1 and 3.2.
}
// Summary is an OpenAPI 3.2 feature only.
if version == openAPIVersion32 {
tag.Summary = td.summary
}
tags = append(tags, tag)
}
return tags
}
// buildPaths constructs all API path definitions.
func (b *OpenAPIBuilder) buildPaths(version string) *v3.Paths {
pathItems := orderedmap.New[string, *v3.PathItem]()
allPaths := b.getAllPathDefinitions()
for pair := allPaths.First(); pair != nil; pair = pair.Next() {
if b.shouldIncludePathForVersion(pair.Key(), version) {
pathItems.Set(pair.Key(), pair.Value())
}
}
return &v3.Paths{PathItems: pathItems}
}
// getAllPathDefinitions returns all path definitions.
func (b *OpenAPIBuilder) getAllPathDefinitions() *orderedmap.Map[string, *v3.PathItem] {
paths := orderedmap.New[string, *v3.PathItem]()
// Query endpoints.
paths.Set("/query", b.queryPath())
paths.Set("/query_range", b.queryRangePath())
paths.Set("/query_exemplars", b.queryExemplarsPath())
paths.Set("/format_query", b.formatQueryPath())
paths.Set("/parse_query", b.parseQueryPath())
// Label endpoints.
paths.Set("/labels", b.labelsPath())
paths.Set("/label/{name}/values", b.labelValuesPath())
// Series endpoints.
paths.Set("/series", b.seriesPath())
// Metadata endpoints.
paths.Set("/metadata", b.metadataPath())
// Target endpoints.
paths.Set("/scrape_pools", b.scrapePoolsPath())
paths.Set("/targets", b.targetsPath())
paths.Set("/targets/metadata", b.targetsMetadataPath())
paths.Set("/targets/relabel_steps", b.targetsRelabelStepsPath())
// Rules and alerts endpoints.
paths.Set("/rules", b.rulesPath())
paths.Set("/alerts", b.alertsPath())
paths.Set("/alertmanagers", b.alertmanagersPath())
// Status endpoints.
paths.Set("/status/config", b.statusConfigPath())
paths.Set("/status/runtimeinfo", b.statusRuntimeInfoPath())
paths.Set("/status/buildinfo", b.statusBuildInfoPath())
paths.Set("/status/flags", b.statusFlagsPath())
paths.Set("/status/tsdb", b.statusTSDBPath())
paths.Set("/status/tsdb/blocks", b.statusTSDBBlocksPath())
paths.Set("/status/walreplay", b.statusWALReplayPath())
// Admin endpoints.
paths.Set("/admin/tsdb/delete_series", b.adminDeleteSeriesPath())
paths.Set("/admin/tsdb/clean_tombstones", b.adminCleanTombstonesPath())
paths.Set("/admin/tsdb/snapshot", b.adminSnapshotPath())
// Remote endpoints.
paths.Set("/read", b.remoteReadPath())
paths.Set("/write", b.remoteWritePath())
paths.Set("/otlp/v1/metrics", b.otlpWritePath())
// Notifications endpoints.
paths.Set("/notifications", b.notificationsPath())
paths.Set("/notifications/live", b.notificationsLivePath())
// Features endpoint.
paths.Set("/features", b.featuresPath())
return paths
}

View file

@ -0,0 +1,258 @@
// 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 (
_ "embed"
"go/ast"
"go/parser"
"go/token"
"strconv"
"strings"
"testing"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
)
//go:embed api.go
var apiGoSource string
// routeInfo represents a route extracted from the Register function.
type routeInfo struct {
method string
path string
}
// extractRoutesFromRegister parses the api.go source and extracts all routes
// registered in the (*API) Register function using AST.
func extractRoutesFromRegister(t *testing.T, source string) []routeInfo {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "api.go", source, parser.ParseComments)
require.NoError(t, err, "failed to parse api.go")
var registerFunc *ast.FuncDecl
// Find the Register method on *API.
ast.Inspect(f, func(n ast.Node) bool {
fn, ok := n.(*ast.FuncDecl)
if !ok || fn.Body == nil {
return true
}
if fn.Name.Name != "Register" {
return true
}
// Ensure it's a method on *API.
if fn.Recv == nil || len(fn.Recv.List) != 1 {
return true
}
star, ok := fn.Recv.List[0].Type.(*ast.StarExpr)
if !ok {
return true
}
ident, ok := star.X.(*ast.Ident)
if !ok || ident.Name != "API" {
return true
}
registerFunc = fn
return false // Stop walking once found.
})
require.NotNil(t, registerFunc, "Register method not found")
var routes []routeInfo
// Extract all r.Get, r.Post, r.Put, r.Delete, r.Options calls.
ast.Inspect(registerFunc.Body, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok {
return true
}
// Check if it's a router method call.
method := sel.Sel.Name
if method != "Get" && method != "Post" && method != "Put" && method != "Delete" && method != "Del" && method != "Options" {
return true
}
// Ensure the receiver is 'r'.
if x, ok := sel.X.(*ast.Ident); !ok || x.Name != "r" {
return true
}
if len(call.Args) == 0 {
return true
}
// Extract the path from the first argument.
lit, ok := call.Args[0].(*ast.BasicLit)
if !ok || lit.Kind != token.STRING {
return true
}
path, err := strconv.Unquote(lit.Value)
if err != nil {
return true
}
// Normalize Del to DELETE.
if method == "Del" {
method = "Delete"
}
routes = append(routes, routeInfo{
method: strings.ToUpper(method),
path: path,
})
return true
})
return routes
}
// normalizePathForOpenAPI converts route paths with colon parameters to OpenAPI format.
// e.g., "/label/:name/values" -> "/label/{name}/values".
func normalizePathForOpenAPI(path string) string {
// Replace :param with {param}.
parts := strings.Split(path, "/")
for i, part := range parts {
if trimmed, ok := strings.CutPrefix(part, ":"); ok {
parts[i] = "{" + trimmed + "}"
}
}
return strings.Join(parts, "/")
}
// TestOpenAPICoverage verifies that all routes registered in the Register function
// are documented in the OpenAPI specification.
func TestOpenAPICoverage(t *testing.T) {
// Extract routes from api.go using AST.
routes := extractRoutesFromRegister(t, apiGoSource)
require.NotEmpty(t, routes, "no routes found in Register function")
// Build OpenAPI spec.
builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
allPaths := builder.getAllPathDefinitions()
// Create a map of OpenAPI paths for quick lookup.
// Key is the normalized path, value is the PathItem.
openAPIPaths := make(map[string]bool)
for pair := allPaths.First(); pair != nil; pair = pair.Next() {
pathItem := pair.Value()
path := pair.Key()
// Track which methods are defined for this path.
if pathItem.Get != nil {
openAPIPaths[path+":GET"] = true
}
if pathItem.Post != nil {
openAPIPaths[path+":POST"] = true
}
if pathItem.Put != nil {
openAPIPaths[path+":PUT"] = true
}
if pathItem.Delete != nil {
openAPIPaths[path+":DELETE"] = true
}
if pathItem.Options != nil {
openAPIPaths[path+":OPTIONS"] = true
}
}
// Check coverage for each route.
var missingRoutes []string
ignoredRoutes := map[string]bool{
"/*path:OPTIONS": true, // Wildcard OPTIONS handler.
"/openapi.yaml:GET": true, // Self-referential endpoint.
"/notifications/live:GET": true, // SSE endpoint (version-specific).
}
for _, route := range routes {
normalizedPath := normalizePathForOpenAPI(route.path)
key := normalizedPath + ":" + route.method
// Skip ignored routes.
if ignoredRoutes[key] {
continue
}
if !openAPIPaths[key] {
missingRoutes = append(missingRoutes, key)
}
}
if len(missingRoutes) > 0 {
t.Errorf("The following routes are registered but not documented in OpenAPI spec:\n%s",
strings.Join(missingRoutes, "\n"))
}
}
// TestOpenAPIHasNoExtraRoutes verifies that the OpenAPI spec doesn't document
// routes that aren't actually registered.
func TestOpenAPIHasNoExtraRoutes(t *testing.T) {
// Extract routes from api.go using AST.
routes := extractRoutesFromRegister(t, apiGoSource)
require.NotEmpty(t, routes, "no routes found in Register function")
// Create a map of registered routes.
registeredRoutes := make(map[string]bool)
for _, route := range routes {
normalizedPath := normalizePathForOpenAPI(route.path)
key := normalizedPath + ":" + route.method
registeredRoutes[key] = true
}
// Build OpenAPI spec.
builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
allPaths := builder.getAllPathDefinitions()
// Check if any OpenAPI paths are not registered.
var extraRoutes []string
for pair := allPaths.First(); pair != nil; pair = pair.Next() {
pathItem := pair.Value()
path := pair.Key()
checkMethod := func(method string, op *v3.Operation) {
if op != nil {
key := path + ":" + method
if !registeredRoutes[key] {
extraRoutes = append(extraRoutes, key)
}
}
}
checkMethod("GET", pathItem.Get)
checkMethod("POST", pathItem.Post)
checkMethod("PUT", pathItem.Put)
checkMethod("DELETE", pathItem.Delete)
checkMethod("OPTIONS", pathItem.Options)
}
if len(extraRoutes) > 0 {
t.Errorf("The following routes are documented in OpenAPI but not registered:\n%s",
strings.Join(extraRoutes, "\n"))
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,176 @@
// 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 (
"flag"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/prometheus/prometheus/web/api/testhelpers"
)
var updateOpenAPISpec = flag.Bool("update-openapi-spec", false, "update openapi golden files with the current specs")
// TestOpenAPIGolden_3_1 verifies that the OpenAPI 3.1 spec matches the golden file.
func TestOpenAPIGolden_3_1(t *testing.T) {
// Create an API instance to serve the OpenAPI spec.
api := newTestAPI(t, testhelpers.APIConfig{})
// Fetch the OpenAPI 3.1 spec from the API (default, no query param).
resp := testhelpers.GET(t, api, "/api/v1/openapi.yaml")
require.Equal(t, 200, resp.StatusCode, "expected HTTP 200 for OpenAPI spec endpoint")
require.NotEmpty(t, resp.Body, "OpenAPI spec should not be empty")
goldenPath := filepath.Join("testdata", "openapi_3.1_golden.yaml")
if *updateOpenAPISpec {
// Update mode: write the current spec to the golden file.
t.Logf("Updating golden file: %s", goldenPath)
// Ensure the testdata directory exists.
err := os.MkdirAll(filepath.Dir(goldenPath), 0o755)
require.NoError(t, err, "failed to create testdata directory")
// Write the golden file.
err = os.WriteFile(goldenPath, []byte(resp.Body), 0o644)
require.NoError(t, err, "failed to write golden file")
t.Logf("Golden file updated successfully")
return
}
// Comparison mode: verify the spec matches the golden file.
goldenData, err := os.ReadFile(goldenPath)
require.NoError(t, err, "failed to read golden file (run with -update-openapi-spec to generate it)")
require.Equal(t, string(goldenData), resp.Body,
"OpenAPI 3.1 spec does not match golden file. Run 'go test -update-openapi-spec' to update.")
// Verify version field is 3.1.0.
var spec map[string]any
err = yaml.Unmarshal([]byte(resp.Body), &spec)
require.NoError(t, err)
require.Equal(t, "3.1.0", spec["openapi"], "OpenAPI version should be 3.1.0")
// Verify /notifications/live is NOT present in 3.1 spec.
paths := spec["paths"].(map[string]any)
_, found := paths["/notifications/live"]
require.False(t, found, "/notifications/live should not be in OpenAPI 3.1 spec")
}
// TestOpenAPIGolden_3_2 verifies that the OpenAPI 3.2 spec matches the golden file.
func TestOpenAPIGolden_3_2(t *testing.T) {
// Create an API instance to serve the OpenAPI spec.
api := newTestAPI(t, testhelpers.APIConfig{})
// Fetch the OpenAPI 3.2 spec from the API with query parameter.
resp := testhelpers.GET(t, api, "/api/v1/openapi.yaml?openapi_version=3.2")
require.Equal(t, 200, resp.StatusCode, "expected HTTP 200 for OpenAPI spec endpoint")
require.NotEmpty(t, resp.Body, "OpenAPI spec should not be empty")
goldenPath := filepath.Join("testdata", "openapi_3.2_golden.yaml")
if *updateOpenAPISpec {
// Update mode: write the current spec to the golden file.
t.Logf("Updating golden file: %s", goldenPath)
// Ensure the testdata directory exists.
err := os.MkdirAll(filepath.Dir(goldenPath), 0o755)
require.NoError(t, err, "failed to create testdata directory")
// Write the golden file.
err = os.WriteFile(goldenPath, []byte(resp.Body), 0o644)
require.NoError(t, err, "failed to write golden file")
t.Logf("Golden file updated successfully")
return
}
// Comparison mode: verify the spec matches the golden file.
goldenData, err := os.ReadFile(goldenPath)
require.NoError(t, err, "failed to read golden file (run with -update-openapi-spec to generate it)")
require.Equal(t, string(goldenData), resp.Body,
"OpenAPI 3.2 spec does not match golden file. Run 'go test -update-openapi-spec' to update.")
// Verify version field is 3.2.0.
var spec map[string]any
err = yaml.Unmarshal([]byte(resp.Body), &spec)
require.NoError(t, err)
require.Equal(t, "3.2.0", spec["openapi"], "OpenAPI version should be 3.2.0")
// Verify /notifications/live IS present in 3.2 spec.
paths := spec["paths"].(map[string]any)
_, found := paths["/notifications/live"]
require.True(t, found, "/notifications/live should be in OpenAPI 3.2 spec")
}
// TestOpenAPIVersionSelection verifies version query parameter handling.
func TestOpenAPIVersionSelection(t *testing.T) {
api := newTestAPI(t, testhelpers.APIConfig{})
tests := []struct {
name string
url string
expectedVersion string
expectLivePath bool
}{
{
name: "default to 3.1.0",
url: "/api/v1/openapi.yaml",
expectedVersion: "3.1.0",
expectLivePath: false,
},
{
name: "explicit 3.1",
url: "/api/v1/openapi.yaml?openapi_version=3.1",
expectedVersion: "3.1.0",
expectLivePath: false,
},
{
name: "explicit 3.2",
url: "/api/v1/openapi.yaml?openapi_version=3.2",
expectedVersion: "3.2.0",
expectLivePath: true,
},
{
name: "invalid version defaults to 3.1.0",
url: "/api/v1/openapi.yaml?openapi_version=4.0",
expectedVersion: "3.1.0",
expectLivePath: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
resp := testhelpers.GET(t, api, tc.url)
require.Equal(t, 200, resp.StatusCode)
var spec map[string]any
err := yaml.Unmarshal([]byte(resp.Body), &spec)
require.NoError(t, err)
require.Equal(t, tc.expectedVersion, spec["openapi"])
paths := spec["paths"].(map[string]any)
_, found := paths["/notifications/live"]
require.Equal(t, tc.expectLivePath, found)
})
}
}

View file

@ -0,0 +1,343 @@
// 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 (
"time"
jsoniter "github.com/json-iterator/go"
"github.com/pb33f/libopenapi/datamodel/high/base"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pb33f/libopenapi/orderedmap"
yaml "go.yaml.in/yaml/v4"
"github.com/prometheus/prometheus/promql"
)
// Helper functions for building common structures.
// exampleTime is a reference time used for timestamp examples.
var exampleTime = time.Date(2026, 1, 2, 13, 37, 0, 0, time.UTC)
func boolPtr(b bool) *bool {
return &b
}
func int64Ptr(i int64) *int64 {
return &i
}
type example struct {
name string
value any
}
// exampleMap creates an Examples map from the provided examples.
func exampleMap(exs []example) *orderedmap.Map[string, *base.Example] {
examples := orderedmap.New[string, *base.Example]()
for _, ex := range exs {
examples.Set(ex.name, &base.Example{
Value: createYAMLNode(ex.value),
})
}
return examples
}
func schemaRef(ref string) *base.SchemaProxy {
return base.CreateSchemaProxyRef(ref)
}
func schemaFromType(t string) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{Type: []string{t}})
}
func stringSchema() *base.SchemaProxy {
return schemaFromType("string")
}
func integerSchema() *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"integer"},
Format: "int64",
})
}
func stringSchemaWithDescription(description string) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"string"},
Description: description,
})
}
func stringSchemaWithDescriptionAndExample(description string, example any) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"string"},
Description: description,
Example: createYAMLNode(example),
})
}
func integerSchemaWithDescription(description string) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"integer"},
Format: "int64",
Description: description,
})
}
func integerSchemaWithDescriptionAndExample(description string, example any) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"integer"},
Format: "int64",
Description: description,
Example: createYAMLNode(example),
})
}
func stringArraySchemaWithDescription(description string) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
Description: description,
})
}
func stringArraySchemaWithDescriptionAndExample(description string, example any) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
Description: description,
Example: createYAMLNode(example),
})
}
func statusSchema() *base.SchemaProxy {
successNode := &yaml.Node{Kind: yaml.ScalarNode, Value: "success"}
errorNode := &yaml.Node{Kind: yaml.ScalarNode, Value: "error"}
exampleNode := &yaml.Node{Kind: yaml.ScalarNode, Value: "success"}
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"string"},
Enum: []*yaml.Node{successNode, errorNode},
Description: "Response status.",
Example: exampleNode,
})
}
func warningsSchema() *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
Description: "Only set if there were warnings while executing the request. There will still be data in the data field.",
})
}
func infosSchema() *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
Description: "Only set if there were info-level annotations while executing the request.",
})
}
func timestampSchema() *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
OneOf: []*base.SchemaProxy{
base.CreateSchemaProxy(&base.Schema{
Type: []string{"string"},
Format: "date-time",
Description: "RFC3339 timestamp.",
}),
base.CreateSchemaProxy(&base.Schema{
Type: []string{"number"},
Format: "unixtime",
Description: "Unix timestamp in seconds.",
}),
},
Description: "Timestamp in RFC3339 format or Unix timestamp in seconds.",
})
}
func stringSchemaWithConstValue(value string) *base.SchemaProxy {
node := &yaml.Node{Kind: yaml.ScalarNode, Value: value}
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"string"},
Enum: []*yaml.Node{node},
})
}
func dateTimeSchemaWithDescription(description string) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"string"},
Format: "date-time",
Description: description,
})
}
func numberSchemaWithDescription(description string) *base.SchemaProxy {
return base.CreateSchemaProxy(&base.Schema{
Type: []string{"number"},
Format: "double",
Description: description,
})
}
func errorResponse() *v3.Response {
content := orderedmap.New[string, *v3.MediaType]()
content.Set("application/json", &v3.MediaType{
Schema: schemaRef("#/components/schemas/Error"),
})
return &v3.Response{
Description: "Error",
Content: content,
}
}
func noContentResponse() *v3.Response {
return &v3.Response{Description: "No Content"}
}
func responsesNoContent() *v3.Responses {
codes := orderedmap.New[string, *v3.Response]()
codes.Set("204", noContentResponse())
codes.Set("default", errorResponse())
return &v3.Responses{Codes: codes}
}
func pathParam(name, description string, schema *base.SchemaProxy) *v3.Parameter {
return &v3.Parameter{
Name: name,
In: "path",
Description: description,
Required: boolPtr(true),
Schema: schema,
}
}
// createYAMLNode converts Go data to yaml.Node for use in examples.
func createYAMLNode(data any) *yaml.Node {
node := &yaml.Node{}
bytes, _ := yaml.Marshal(data)
_ = yaml.Unmarshal(bytes, node)
return node
}
// formRequestBodyWithExamples creates a form-encoded request body with examples.
func formRequestBodyWithExamples(schemaRef string, examples *orderedmap.Map[string, *base.Example], description string) *v3.RequestBody {
content := orderedmap.New[string, *v3.MediaType]()
mediaType := &v3.MediaType{
Schema: base.CreateSchemaProxyRef("#/components/schemas/" + schemaRef),
}
if examples != nil {
mediaType.Examples = examples
}
content.Set("application/x-www-form-urlencoded", mediaType)
return &v3.RequestBody{
Required: boolPtr(true),
Description: description,
Content: content,
}
}
// jsonResponseWithExamples creates a JSON response with examples.
func jsonResponseWithExamples(schemaRef string, examples *orderedmap.Map[string, *base.Example], description string) *v3.Response {
content := orderedmap.New[string, *v3.MediaType]()
mediaType := &v3.MediaType{
Schema: base.CreateSchemaProxyRef("#/components/schemas/" + schemaRef),
}
if examples != nil {
mediaType.Examples = examples
}
content.Set("application/json", mediaType)
return &v3.Response{
Description: description,
Content: content,
}
}
// responsesWithErrorExamples creates responses with both success and error examples.
func responsesWithErrorExamples(okSchemaRef string, successExamples, errorExamples *orderedmap.Map[string, *base.Example], successDescription, errorDescription string) *v3.Responses {
codes := orderedmap.New[string, *v3.Response]()
codes.Set("200", jsonResponseWithExamples(okSchemaRef, successExamples, successDescription))
codes.Set("default", jsonResponseWithExamples("Error", errorExamples, errorDescription))
return &v3.Responses{Codes: codes}
}
// timestampExamples returns examples for timestamp parameters (RFC3339 and epoch).
func timestampExamples(t time.Time) []example {
return []example{
{"RFC3339", t.Format(time.RFC3339Nano)},
{"epoch", t.Unix()},
}
}
// queryParamWithExample creates a query parameter with examples.
func queryParamWithExample(name, description string, required bool, schema *base.SchemaProxy, examples []example) *v3.Parameter {
param := &v3.Parameter{
Name: name,
In: "query",
Description: description,
Required: &required,
Explode: boolPtr(false),
Schema: schema,
}
if len(examples) > 0 {
param.Examples = exampleMap(examples)
}
return param
}
// marshalToYAMLNode marshals a value using jsoniter (production marshaling) and converts to yaml.Node.
// The result is an inline JSON representation that preserves integer types for timestamps.
func marshalToYAMLNode(v any) *yaml.Node {
jsonAPI := jsoniter.ConfigCompatibleWithStandardLibrary
jsonBytes, err := jsonAPI.Marshal(v)
if err != nil {
panic(err)
}
node := &yaml.Node{}
if err := yaml.Unmarshal(jsonBytes, node); err != nil {
panic(err)
}
return node
}
// vectorExample creates an example for a vector query response using production marshaling.
func vectorExample(v promql.Vector) *yaml.Node {
type response struct {
Status string `json:"status"`
Data struct {
ResultType string `json:"resultType"`
Result promql.Vector `json:"result"`
} `json:"data"`
}
resp := response{Status: "success"}
resp.Data.ResultType = "vector"
resp.Data.Result = v
return marshalToYAMLNode(resp)
}
// matrixExample creates an example for a matrix query response using production marshaling.
func matrixExample(m promql.Matrix) *yaml.Node {
type response struct {
Status string `json:"status"`
Data struct {
ResultType string `json:"resultType"`
Result promql.Matrix `json:"result"`
} `json:"data"`
}
resp := response{Status: "success"}
resp.Data.ResultType = "matrix"
resp.Data.Result = m
return marshalToYAMLNode(resp)
}

626
web/api/v1/openapi_paths.go Normal file
View file

@ -0,0 +1,626 @@
// 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.
// This file defines all API path specifications including parameters, request bodies,
// and response schemas. Each path definition corresponds to an endpoint registered in api.go.
package v1
import (
"time"
"github.com/pb33f/libopenapi/datamodel/high/base"
v3 "github.com/pb33f/libopenapi/datamodel/high/v3"
"github.com/pb33f/libopenapi/orderedmap"
)
// Path definition methods for API endpoints.
func (*OpenAPIBuilder) queryPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("limit", "The maximum number of metrics to return.", false, integerSchema(), []example{{"example", 100}}),
queryParamWithExample("time", "The evaluation timestamp (optional, defaults to current time).", false, timestampSchema(), timestampExamples(exampleTime)),
queryParamWithExample("query", "The PromQL query to execute.", true, stringSchema(), []example{{"example", "up"}}),
queryParamWithExample("timeout", "Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.", false, stringSchema(), []example{{"example", "30s"}}),
queryParamWithExample("lookback_delta", "Override the lookback period for this query. Optional.", false, stringSchema(), []example{{"example", "5m"}}),
queryParamWithExample("stats", "When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.", false, stringSchema(), []example{{"example", "all"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "query",
Summary: "Evaluate an instant query",
Tags: []string{"query"},
Parameters: params,
Responses: responsesWithErrorExamples("QueryOutputBody", queryResponseExamples(), errorResponseExamples(), "Query executed successfully.", "Error executing query."),
},
Post: &v3.Operation{
OperationId: "query-post",
Summary: "Evaluate an instant query",
Tags: []string{"query"},
RequestBody: formRequestBodyWithExamples("QueryPostInputBody", queryPostExamples(), "Submit an instant query. This endpoint accepts the same parameters as the GET version."),
Responses: responsesWithErrorExamples("QueryOutputBody", queryResponseExamples(), errorResponseExamples(), "Instant query executed successfully.", "Error executing instant query."),
},
}
}
func (*OpenAPIBuilder) queryRangePath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("limit", "The maximum number of metrics to return.", false, integerSchema(), []example{{"example", 100}}),
queryParamWithExample("start", "The start time of the query.", true, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
queryParamWithExample("end", "The end time of the query.", true, timestampSchema(), timestampExamples(exampleTime)),
queryParamWithExample("step", "The step size of the query.", true, stringSchema(), []example{{"example", "15s"}}),
queryParamWithExample("query", "The query to execute.", true, stringSchema(), []example{{"example", "rate(prometheus_http_requests_total{handler=\"/api/v1/query\"}[5m])"}}),
queryParamWithExample("timeout", "Evaluation timeout. Optional. Defaults to and is capped by the value of the -query.timeout flag.", false, stringSchema(), []example{{"example", "30s"}}),
queryParamWithExample("lookback_delta", "Override the lookback period for this query. Optional.", false, stringSchema(), []example{{"example", "5m"}}),
queryParamWithExample("stats", "When provided, include query statistics in the response. The special value 'all' enables more comprehensive statistics.", false, stringSchema(), []example{{"example", "all"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "query-range",
Summary: "Evaluate a range query",
Tags: []string{"query"},
Parameters: params,
Responses: responsesWithErrorExamples("QueryRangeOutputBody", queryRangeResponseExamples(), errorResponseExamples(), "Range query executed successfully.", "Error executing range query."),
},
Post: &v3.Operation{
OperationId: "query-range-post",
Summary: "Evaluate a range query",
Tags: []string{"query"},
RequestBody: formRequestBodyWithExamples("QueryRangePostInputBody", queryRangePostExamples(), "Submit a range query. This endpoint accepts the same parameters as the GET version."),
Responses: responsesWithErrorExamples("QueryRangeOutputBody", queryRangeResponseExamples(), errorResponseExamples(), "Range query executed successfully.", "Error executing range query."),
},
}
}
func (*OpenAPIBuilder) queryExemplarsPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("start", "Start timestamp for exemplars query.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
queryParamWithExample("end", "End timestamp for exemplars query.", false, timestampSchema(), timestampExamples(exampleTime)),
queryParamWithExample("query", "PromQL query to extract exemplars for.", true, stringSchema(), []example{{"example", "prometheus_http_requests_total"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "query-exemplars",
Summary: "Query exemplars",
Tags: []string{"query"},
Parameters: params,
Responses: responsesWithErrorExamples("QueryExemplarsOutputBody", queryExemplarsResponseExamples(), errorResponseExamples(), "Exemplars retrieved successfully.", "Error retrieving exemplars."),
},
Post: &v3.Operation{
OperationId: "query-exemplars-post",
Summary: "Query exemplars",
Tags: []string{"query"},
RequestBody: formRequestBodyWithExamples("QueryExemplarsPostInputBody", queryExemplarsPostExamples(), "Submit an exemplars query. This endpoint accepts the same parameters as the GET version."),
Responses: responsesWithErrorExamples("QueryExemplarsOutputBody", queryExemplarsResponseExamples(), errorResponseExamples(), "Exemplars query completed successfully.", "Error processing exemplars query."),
},
}
}
func (*OpenAPIBuilder) formatQueryPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("query", "PromQL expression to format.", true, stringSchema(), []example{{"example", "sum(rate(http_requests_total[5m])) by (job)"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "format-query",
Summary: "Format a PromQL query",
Tags: []string{"query"},
Parameters: params,
Responses: responsesWithErrorExamples("FormatQueryOutputBody", formatQueryResponseExamples(), errorResponseExamples(), "Query formatted successfully.", "Error formatting query."),
},
Post: &v3.Operation{
OperationId: "format-query-post",
Summary: "Format a PromQL query",
Tags: []string{"query"},
RequestBody: formRequestBodyWithExamples("FormatQueryPostInputBody", formatQueryPostExamples(), "Submit a PromQL query to format. This endpoint accepts the same parameters as the GET version."),
Responses: responsesWithErrorExamples("FormatQueryOutputBody", formatQueryResponseExamples(), errorResponseExamples(), "Query formatting completed successfully.", "Error formatting query."),
},
}
}
func (*OpenAPIBuilder) parseQueryPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("query", "PromQL expression to parse.", true, stringSchema(), []example{{"example", "up{job=\"prometheus\"}"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "parse-query",
Summary: "Parse a PromQL query",
Tags: []string{"query"},
Parameters: params,
Responses: responsesWithErrorExamples("ParseQueryOutputBody", parseQueryResponseExamples(), errorResponseExamples(), "Query parsed successfully.", "Error parsing query."),
},
Post: &v3.Operation{
OperationId: "parse-query-post",
Summary: "Parse a PromQL query",
Tags: []string{"query"},
RequestBody: formRequestBodyWithExamples("ParseQueryPostInputBody", parseQueryPostExamples(), "Submit a PromQL query to parse. This endpoint accepts the same parameters as the GET version."),
Responses: responsesWithErrorExamples("ParseQueryOutputBody", parseQueryResponseExamples(), errorResponseExamples(), "Query parsed successfully via POST.", "Error parsing query via POST."),
},
}
}
func (*OpenAPIBuilder) labelsPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("start", "Start timestamp for label names query.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
queryParamWithExample("end", "End timestamp for label names query.", false, timestampSchema(), timestampExamples(exampleTime)),
queryParamWithExample("match[]", "Series selector argument.", false, base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
}), []example{{"example", []string{"{job=\"prometheus\"}"}}}),
queryParamWithExample("limit", "Maximum number of label names to return.", false, integerSchema(), []example{{"example", 100}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "labels",
Summary: "Get label names",
Tags: []string{"labels"},
Parameters: params,
Responses: responsesWithErrorExamples("LabelsOutputBody", labelsResponseExamples(), errorResponseExamples(), "Label names retrieved successfully.", "Error retrieving label names."),
},
Post: &v3.Operation{
OperationId: "labels-post",
Summary: "Get label names",
Tags: []string{"labels"},
RequestBody: formRequestBodyWithExamples("LabelsPostInputBody", labelsPostExamples(), "Submit a label names query. This endpoint accepts the same parameters as the GET version."),
Responses: responsesWithErrorExamples("LabelsOutputBody", labelsResponseExamples(), errorResponseExamples(), "Label names retrieved successfully via POST.", "Error retrieving label names via POST."),
},
}
}
func (*OpenAPIBuilder) labelValuesPath() *v3.PathItem {
params := []*v3.Parameter{
pathParam("name", "Label name.", stringSchema()),
queryParamWithExample("start", "Start timestamp for label values query.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
queryParamWithExample("end", "End timestamp for label values query.", false, timestampSchema(), timestampExamples(exampleTime)),
queryParamWithExample("match[]", "Series selector argument.", false, base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
}), []example{{"example", []string{"{job=\"prometheus\"}"}}}),
queryParamWithExample("limit", "Maximum number of label values to return.", false, integerSchema(), []example{{"example", 1000}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "label-values",
Summary: "Get label values",
Tags: []string{"labels"},
Parameters: params,
Responses: responsesWithErrorExamples("LabelValuesOutputBody", labelValuesResponseExamples(), errorResponseExamples(), "Label values retrieved successfully.", "Error retrieving label values."),
},
}
}
func (*OpenAPIBuilder) seriesPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("start", "Start timestamp for series query.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
queryParamWithExample("end", "End timestamp for series query.", false, timestampSchema(), timestampExamples(exampleTime)),
queryParamWithExample("match[]", "Series selector argument.", true, base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
}), []example{{"example", []string{"{job=\"prometheus\"}"}}}),
queryParamWithExample("limit", "Maximum number of series to return.", false, integerSchema(), []example{{"example", 100}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "series",
Summary: "Find series by label matchers",
Tags: []string{"series"},
Parameters: params,
Responses: responsesWithErrorExamples("SeriesOutputBody", seriesResponseExamples(), errorResponseExamples(), "Series returned matching the provided label matchers.", "Error retrieving series."),
},
Post: &v3.Operation{
OperationId: "series-post",
Summary: "Find series by label matchers",
Tags: []string{"series"},
RequestBody: formRequestBodyWithExamples("SeriesPostInputBody", seriesPostExamples(), "Submit a series query. This endpoint accepts the same parameters as the GET version."),
Responses: responsesWithErrorExamples("SeriesOutputBody", seriesResponseExamples(), errorResponseExamples(), "Series returned matching the provided label matchers via POST.", "Error retrieving series via POST."),
},
Delete: &v3.Operation{
OperationId: "delete-series",
Summary: "Delete series",
Description: "Delete series matching selectors. Note: This is deprecated, use POST /admin/tsdb/delete_series instead.",
Tags: []string{"series"},
Responses: responsesWithErrorExamples("SeriesDeleteOutputBody", seriesDeleteResponseExamples(), errorResponseExamples(), "Series marked for deletion.", "Error deleting series."),
},
}
}
func (*OpenAPIBuilder) metadataPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("limit", "The maximum number of metrics to return.", false, integerSchema(), []example{{"example", 100}}),
queryParamWithExample("limit_per_metric", "The maximum number of metadata entries per metric.", false, integerSchema(), []example{{"example", 10}}),
queryParamWithExample("metric", "A metric name to filter metadata for.", false, stringSchema(), []example{{"example", "http_requests_total"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-metadata",
Summary: "Get metadata",
Tags: []string{"metadata"},
Parameters: params,
Responses: responsesWithErrorExamples("MetadataOutputBody", metadataResponseExamples(), errorResponseExamples(), "Metric metadata retrieved successfully.", "Error retrieving metadata."),
},
}
}
func (*OpenAPIBuilder) scrapePoolsPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-scrape-pools",
Summary: "Get scrape pools",
Tags: []string{"targets"},
Responses: responsesWithErrorExamples("ScrapePoolsOutputBody", scrapePoolsResponseExamples(), errorResponseExamples(), "Scrape pools retrieved successfully.", "Error retrieving scrape pools."),
},
}
}
func (*OpenAPIBuilder) targetsPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("scrapePool", "Filter targets by scrape pool name.", false, stringSchema(), []example{{"example", "prometheus"}}),
queryParamWithExample("state", "Filter by state: active, dropped, or any.", false, stringSchema(), []example{{"example", "active"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-targets",
Summary: "Get targets",
Tags: []string{"targets"},
Parameters: params,
Responses: responsesWithErrorExamples("TargetsOutputBody", targetsResponseExamples(), errorResponseExamples(), "Target discovery information retrieved successfully.", "Error retrieving targets."),
},
}
}
func (*OpenAPIBuilder) targetsMetadataPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("match_target", "Label selector to filter targets.", false, stringSchema(), []example{{"example", "{job=\"prometheus\"}"}}),
queryParamWithExample("metric", "Metric name to retrieve metadata for.", false, stringSchema(), []example{{"example", "http_requests_total"}}),
queryParamWithExample("limit", "Maximum number of targets to match.", false, integerSchema(), []example{{"example", 10}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-targets-metadata",
Summary: "Get targets metadata",
Tags: []string{"targets"},
Parameters: params,
Responses: responsesWithErrorExamples("TargetMetadataOutputBody", targetsMetadataResponseExamples(), errorResponseExamples(), "Target metadata retrieved successfully.", "Error retrieving target metadata."),
},
}
}
func (*OpenAPIBuilder) targetsRelabelStepsPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("scrapePool", "Name of the scrape pool.", true, stringSchema(), []example{{"example", "prometheus"}}),
queryParamWithExample("labels", "JSON-encoded labels to apply relabel rules to.", true, stringSchema(), []example{{"example", "{\"__address__\":\"localhost:9090\",\"job\":\"prometheus\"}"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-targets-relabel-steps",
Summary: "Get targets relabel steps",
Tags: []string{"targets"},
Parameters: params,
Responses: responsesWithErrorExamples("TargetRelabelStepsOutputBody", targetsRelabelStepsResponseExamples(), errorResponseExamples(), "Relabel steps retrieved successfully.", "Error retrieving relabel steps."),
},
}
}
func (*OpenAPIBuilder) rulesPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("type", "Filter by rule type: alert or record.", false, stringSchema(), []example{{"example", "alert"}}),
queryParamWithExample("rule_name[]", "Filter by rule name.", false, base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
}), []example{{"example", []string{"HighErrorRate"}}}),
queryParamWithExample("rule_group[]", "Filter by rule group name.", false, base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
}), []example{{"example", []string{"example_alerts"}}}),
queryParamWithExample("file[]", "Filter by file path.", false, base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
}), []example{{"example", []string{"/etc/prometheus/rules.yml"}}}),
queryParamWithExample("match[]", "Label matchers to filter rules.", false, base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
}), []example{{"example", []string{"{severity=\"critical\"}"}}}),
queryParamWithExample("exclude_alerts", "Exclude active alerts from response.", false, stringSchema(), []example{{"example", "false"}}),
queryParamWithExample("group_limit", "Maximum number of rule groups to return.", false, integerSchema(), []example{{"example", 100}}),
queryParamWithExample("group_next_token", "Pagination token for next page.", false, stringSchema(), []example{{"example", "abc123"}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "rules",
Summary: "Get alerting and recording rules",
Tags: []string{"rules"},
Parameters: params,
Responses: responsesWithErrorExamples("RulesOutputBody", rulesResponseExamples(), errorResponseExamples(), "Rules retrieved successfully.", "Error retrieving rules."),
},
}
}
func (*OpenAPIBuilder) alertsPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "alerts",
Summary: "Get active alerts",
Tags: []string{"alerts"},
Responses: responsesWithErrorExamples("AlertsOutputBody", alertsResponseExamples(), errorResponseExamples(), "Active alerts retrieved successfully.", "Error retrieving alerts."),
},
}
}
func (*OpenAPIBuilder) alertmanagersPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "alertmanagers",
Summary: "Get Alertmanager discovery",
Tags: []string{"alerts"},
Responses: responsesWithErrorExamples("AlertmanagersOutputBody", alertmanagersResponseExamples(), errorResponseExamples(), "Alertmanager targets retrieved successfully.", "Error retrieving Alertmanager targets."),
},
}
}
func (*OpenAPIBuilder) statusConfigPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-status-config",
Summary: "Get status config",
Tags: []string{"status"},
Responses: responsesWithErrorExamples("StatusConfigOutputBody", statusConfigResponseExamples(), errorResponseExamples(), "Configuration retrieved successfully.", "Error retrieving configuration."),
},
}
}
func (*OpenAPIBuilder) statusRuntimeInfoPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-status-runtimeinfo",
Summary: "Get status runtimeinfo",
Tags: []string{"status"},
Responses: responsesWithErrorExamples("StatusRuntimeInfoOutputBody", statusRuntimeInfoResponseExamples(), errorResponseExamples(), "Runtime information retrieved successfully.", "Error retrieving runtime information."),
},
}
}
func (*OpenAPIBuilder) statusBuildInfoPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-status-buildinfo",
Summary: "Get status buildinfo",
Tags: []string{"status"},
Responses: responsesWithErrorExamples("StatusBuildInfoOutputBody", statusBuildInfoResponseExamples(), errorResponseExamples(), "Build information retrieved successfully.", "Error retrieving build information."),
},
}
}
func (*OpenAPIBuilder) statusFlagsPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-status-flags",
Summary: "Get status flags",
Tags: []string{"status"},
Responses: responsesWithErrorExamples("StatusFlagsOutputBody", statusFlagsResponseExamples(), errorResponseExamples(), "Command-line flags retrieved successfully.", "Error retrieving flags."),
},
}
}
func (*OpenAPIBuilder) statusTSDBPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("limit", "The maximum number of items to return per category.", false, integerSchema(), []example{{"example", 10}}),
}
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "status-tsdb",
Summary: "Get TSDB status",
Tags: []string{"status"},
Parameters: params,
Responses: responsesWithErrorExamples("StatusTSDBOutputBody", statusTSDBResponseExamples(), errorResponseExamples(), "TSDB status retrieved successfully.", "Error retrieving TSDB status."),
},
}
}
func (*OpenAPIBuilder) statusTSDBBlocksPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "status-tsdb-blocks",
Summary: "Get TSDB blocks information",
Tags: []string{"status"},
Responses: responsesWithErrorExamples("StatusTSDBBlocksOutputBody", statusTSDBBlocksResponseExamples(), errorResponseExamples(), "TSDB blocks information retrieved successfully.", "Error retrieving TSDB blocks."),
},
}
}
func (*OpenAPIBuilder) statusWALReplayPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-status-walreplay",
Summary: "Get status walreplay",
Tags: []string{"status"},
Responses: responsesWithErrorExamples("StatusWALReplayOutputBody", statusWALReplayResponseExamples(), errorResponseExamples(), "WAL replay status retrieved successfully.", "Error retrieving WAL replay status."),
},
}
}
func (*OpenAPIBuilder) adminDeleteSeriesPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("match[]", "Series selectors to identify series to delete.", true, base.CreateSchemaProxy(&base.Schema{
Type: []string{"array"},
Items: &base.DynamicValue[*base.SchemaProxy, bool]{A: stringSchema()},
}), []example{{"example", []string{"{__name__=~\"test.*\"}"}}}),
queryParamWithExample("start", "Start timestamp for deletion.", false, timestampSchema(), timestampExamples(exampleTime.Add(-1*time.Hour))),
queryParamWithExample("end", "End timestamp for deletion.", false, timestampSchema(), timestampExamples(exampleTime)),
}
return &v3.PathItem{
Post: &v3.Operation{
OperationId: "deleteSeriesPost",
Summary: "Delete series matching selectors",
Description: "Deletes data for a selection of series in a time range.",
Tags: []string{"admin"},
Parameters: params,
Responses: responsesWithErrorExamples("DeleteSeriesOutputBody", deleteSeriesResponseExamples(), errorResponseExamples(), "Series deleted successfully.", "Error deleting series."),
},
Put: &v3.Operation{
OperationId: "deleteSeriesPut",
Summary: "Delete series matching selectors via PUT",
Description: "Deletes data for a selection of series in a time range using PUT method.",
Tags: []string{"admin"},
Parameters: params,
Responses: responsesWithErrorExamples("DeleteSeriesOutputBody", deleteSeriesResponseExamples(), errorResponseExamples(), "Series deleted successfully via PUT.", "Error deleting series via PUT."),
},
}
}
func (*OpenAPIBuilder) adminCleanTombstonesPath() *v3.PathItem {
return &v3.PathItem{
Post: &v3.Operation{
OperationId: "cleanTombstonesPost",
Summary: "Clean tombstones in the TSDB",
Description: "Removes deleted data from disk and cleans up existing tombstones.",
Tags: []string{"admin"},
Responses: responsesWithErrorExamples("CleanTombstonesOutputBody", cleanTombstonesResponseExamples(), errorResponseExamples(), "Tombstones cleaned successfully.", "Error cleaning tombstones."),
},
Put: &v3.Operation{
OperationId: "cleanTombstonesPut",
Summary: "Clean tombstones in the TSDB via PUT",
Description: "Removes deleted data from disk and cleans up existing tombstones using PUT method.",
Tags: []string{"admin"},
Responses: responsesWithErrorExamples("CleanTombstonesOutputBody", cleanTombstonesResponseExamples(), errorResponseExamples(), "Tombstones cleaned successfully via PUT.", "Error cleaning tombstones via PUT."),
},
}
}
func (*OpenAPIBuilder) adminSnapshotPath() *v3.PathItem {
params := []*v3.Parameter{
queryParamWithExample("skip_head", "If true, do not snapshot data in the head block.", false, stringSchema(), []example{{"example", "false"}}),
}
return &v3.PathItem{
Post: &v3.Operation{
OperationId: "snapshotPost",
Summary: "Create a snapshot of the TSDB",
Description: "Creates a snapshot of all current data.",
Tags: []string{"admin"},
Parameters: params,
Responses: responsesWithErrorExamples("SnapshotOutputBody", snapshotResponseExamples(), errorResponseExamples(), "Snapshot created successfully.", "Error creating snapshot."),
},
Put: &v3.Operation{
OperationId: "snapshotPut",
Summary: "Create a snapshot of the TSDB via PUT",
Description: "Creates a snapshot of all current data using PUT method.",
Tags: []string{"admin"},
Parameters: params,
Responses: responsesWithErrorExamples("SnapshotOutputBody", snapshotResponseExamples(), errorResponseExamples(), "Snapshot created successfully via PUT.", "Error creating snapshot via PUT."),
},
}
}
func (*OpenAPIBuilder) remoteReadPath() *v3.PathItem {
return &v3.PathItem{
Post: &v3.Operation{
OperationId: "remoteRead",
Summary: "Remote read endpoint",
Description: "Prometheus remote read endpoint for federated queries. Accepts and returns Protocol Buffer encoded data.",
Tags: []string{"remote"},
Responses: responsesNoContent(),
},
}
}
func (*OpenAPIBuilder) remoteWritePath() *v3.PathItem {
return &v3.PathItem{
Post: &v3.Operation{
OperationId: "remoteWrite",
Summary: "Remote write endpoint",
Description: "Prometheus remote write endpoint for sending metrics. Accepts Protocol Buffer encoded write requests.",
Tags: []string{"remote"},
Responses: responsesNoContent(),
},
}
}
func (*OpenAPIBuilder) otlpWritePath() *v3.PathItem {
return &v3.PathItem{
Post: &v3.Operation{
OperationId: "otlpWrite",
Summary: "OTLP metrics write endpoint",
Description: "OpenTelemetry Protocol metrics ingestion endpoint. Accepts OTLP/HTTP metrics in Protocol Buffer format.",
Tags: []string{"otlp"},
Responses: responsesNoContent(),
},
}
}
func (*OpenAPIBuilder) notificationsPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-notifications",
Summary: "Get notifications",
Tags: []string{"notifications"},
Responses: responsesWithErrorExamples("NotificationsOutputBody", notificationsResponseExamples(), errorResponseExamples(), "Notifications retrieved successfully.", "Error retrieving notifications."),
},
}
}
// notificationsLivePath defines the /notifications/live endpoint.
// This endpoint uses OpenAPI 3.2's itemSchema feature for documenting SSE streams.
// It is excluded from the OpenAPI 3.1 specification.
func (*OpenAPIBuilder) notificationsLivePath() *v3.PathItem {
codes := orderedmap.New[string, *v3.Response]()
content := orderedmap.New[string, *v3.MediaType]()
// Create a schema for the SSE message structure.
// Each SSE message has a 'data' field containing JSON.
sseItemProps := orderedmap.New[string, *base.SchemaProxy]()
sseItemProps.Set("data", base.CreateSchemaProxy(&base.Schema{
Type: []string{"string"},
Description: "SSE data field containing JSON-encoded notification.",
ContentMediaType: "application/json",
ContentSchema: schemaRef("#/components/schemas/Notification"),
}))
content.Set("text/event-stream", &v3.MediaType{
// Use ItemSchema (OpenAPI 3.2) instead of Schema to describe each SSE message.
ItemSchema: base.CreateSchemaProxy(&base.Schema{
Type: []string{"object"},
Title: "Server Sent Event Message",
Description: "A single SSE message. The data field contains a JSON-encoded Notification object.",
Properties: sseItemProps,
Required: []string{"data"},
AdditionalProperties: &base.DynamicValue[*base.SchemaProxy, bool]{N: 1, B: false},
}),
Examples: notificationLiveExamples(),
})
codes.Set("200", &v3.Response{
Description: "Server-sent events stream established.",
Content: content,
})
codes.Set("default", errorResponse())
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "notifications-live",
Summary: "Stream live notifications via Server-Sent Events",
Description: "Subscribe to real-time server notifications using SSE. Each event contains a JSON-encoded Notification object in the data field.",
Tags: []string{"notifications"},
Responses: &v3.Responses{Codes: codes},
},
}
}
func (*OpenAPIBuilder) featuresPath() *v3.PathItem {
return &v3.PathItem{
Get: &v3.Operation{
OperationId: "get-features",
Summary: "Get features",
Tags: []string{"features"},
Responses: responsesWithErrorExamples("FeaturesOutputBody", featuresResponseExamples(), errorResponseExamples(), "Feature flags retrieved successfully.", "Error retrieving features."),
},
}
}

File diff suppressed because it is too large Load diff

289
web/api/v1/openapi_test.go Normal file
View file

@ -0,0 +1,289 @@
// 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 (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)
// TestOpenAPIHTTPHandler verifies that the OpenAPI endpoint serves a valid specification
// with correct headers, structure conforming to OpenAPI 3.1 standards, and consistent responses.
func TestOpenAPIHTTPHandler(t *testing.T) {
builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
// First request.
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec1 := httptest.NewRecorder()
builder.ServeOpenAPI(rec1, req1)
// Verify status code and headers.
require.Equal(t, http.StatusOK, rec1.Code)
require.True(t, strings.HasPrefix(rec1.Header().Get("Content-Type"), "application/yaml"), "Content-Type should start with application/yaml")
require.Equal(t, "no-cache, no-store, must-revalidate", rec1.Header().Get("Cache-Control"))
// Verify it is valid YAML.
var spec map[string]any
err := yaml.Unmarshal(rec1.Body.Bytes(), &spec)
require.NoError(t, err)
// Verify structure.
require.Contains(t, spec, "openapi")
require.Contains(t, spec, "info")
require.Contains(t, spec, "paths")
require.Contains(t, spec, "components")
// Verify OpenAPI version (default is 3.1.0).
require.Equal(t, "3.1.0", spec["openapi"])
// Verify info section.
info, ok := spec["info"].(map[any]any)
require.True(t, ok, "info should be a map")
require.Equal(t, "Prometheus API", info["title"])
// Verify paths exist.
paths, ok := spec["paths"].(map[any]any)
require.True(t, ok, "paths should be a map")
require.NotEmpty(t, paths, "paths should not be empty")
// Second request to verify response consistency.
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec2 := httptest.NewRecorder()
builder.ServeOpenAPI(rec2, req2)
// Both responses should be identical.
require.Equal(t, rec1.Body.String(), rec2.Body.String())
}
// TestOpenAPIPathFiltering verifies that the IncludePaths option correctly filters
// which API paths are included in the generated specification.
func TestOpenAPIPathFiltering(t *testing.T) {
tests := []struct {
name string
includePaths []string
wantPaths []string
excludePaths []string
}{
{
name: "no filter includes all",
includePaths: nil,
wantPaths: []string{"/query", "/labels", "/alerts", "/targets"},
},
{
name: "filter query paths",
includePaths: []string{"/query"},
wantPaths: []string{"/query", "/query_range", "/query_exemplars"},
excludePaths: []string{"/labels", "/alerts", "/targets"},
},
{
name: "filter status paths",
includePaths: []string{"/status"},
wantPaths: []string{"/status/config", "/status/flags", "/status/runtimeinfo"},
excludePaths: []string{"/query", "/alerts", "/targets"},
},
{
name: "filter multiple prefixes",
includePaths: []string{"/label", "/series"},
wantPaths: []string{"/labels", "/label/{name}/values", "/series"},
excludePaths: []string{"/query", "/alerts", "/targets"},
},
{
name: "exact path match",
includePaths: []string{"/alerts"},
wantPaths: []string{"/alerts"},
excludePaths: []string{"/alertmanagers", "/query"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
builder := NewOpenAPIBuilder(OpenAPIOptions{
IncludePaths: tc.includePaths,
}, promslog.NewNopLogger())
req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec := httptest.NewRecorder()
builder.ServeOpenAPI(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var spec map[string]any
err := yaml.Unmarshal(rec.Body.Bytes(), &spec)
require.NoError(t, err)
paths, ok := spec["paths"].(map[any]any)
require.True(t, ok, "paths should be a map")
for _, want := range tc.wantPaths {
require.Contains(t, paths, want)
}
for _, exclude := range tc.excludePaths {
require.NotContains(t, paths, exclude)
}
})
}
}
// TestOpenAPISchemaCompleteness verifies that all referenced schemas in paths
// are defined in the components/schemas section of the specification.
func TestOpenAPISchemaCompleteness(t *testing.T) {
builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec := httptest.NewRecorder()
builder.ServeOpenAPI(rec, req)
var spec map[string]any
err := yaml.Unmarshal(rec.Body.Bytes(), &spec)
require.NoError(t, err)
components, ok := spec["components"].(map[any]any)
require.True(t, ok, "components should be a map")
schemas, ok := components["schemas"].(map[any]any)
require.True(t, ok, "schemas should be a map")
// Verify essential schemas are present.
essentialSchemas := []string{
"Error",
"Labels",
"QueryOutputBody",
"LabelsOutputBody",
"SeriesOutputBody",
"TargetsOutputBody",
"AlertsOutputBody",
"RulesOutputBody",
"StatusConfigOutputBody",
"StatusFlagsOutputBody",
"PrometheusVersion",
}
for _, schema := range essentialSchemas {
require.Contains(t, schemas, schema)
}
}
// TODO: Add test to verify all routes from api.go Register() are covered in OpenAPI spec.
// Consider wrapping Router to track registered paths and cross-check with OpenAPI paths.
// TestOpenAPIShouldIncludePath verifies the shouldIncludePath method correctly
// matches paths against the IncludePaths filter configuration.
func TestOpenAPIShouldIncludePath(t *testing.T) {
tests := []struct {
name string
includePaths []string
path string
expected bool
}{
{
name: "empty filter includes all",
includePaths: nil,
path: "/query",
expected: true,
},
{
name: "exact match",
includePaths: []string{"/query"},
path: "/query",
expected: true,
},
{
name: "prefix match",
includePaths: []string{"/query"},
path: "/query_range",
expected: true,
},
{
name: "no match",
includePaths: []string{"/query"},
path: "/labels",
expected: false,
},
{
name: "multiple filters with match",
includePaths: []string{"/labels", "/series"},
path: "/series",
expected: true,
},
{
name: "multiple filters without match",
includePaths: []string{"/labels", "/series"},
path: "/query",
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
builder := &OpenAPIBuilder{
options: OpenAPIOptions{
IncludePaths: tc.includePaths,
},
}
result := builder.shouldIncludePath(tc.path)
require.Equal(t, tc.expected, result)
})
}
}
// TestOpenAPIVersionConsistency verifies that both OpenAPI versions are properly generated
// and that 3.2 has exactly one more path than 3.1 (/notifications/live).
func TestOpenAPIVersionConsistency(t *testing.T) {
builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
// Fetch OpenAPI 3.1 spec (default).
req31 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec31 := httptest.NewRecorder()
builder.ServeOpenAPI(rec31, req31)
require.Equal(t, http.StatusOK, rec31.Code)
// Fetch OpenAPI 3.2 spec.
req32 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml?openapi_version=3.2", nil)
rec32 := httptest.NewRecorder()
builder.ServeOpenAPI(rec32, req32)
require.Equal(t, http.StatusOK, rec32.Code)
// Parse both specs.
var spec31, spec32 map[string]any
err := yaml.Unmarshal(rec31.Body.Bytes(), &spec31)
require.NoError(t, err)
err = yaml.Unmarshal(rec32.Body.Bytes(), &spec32)
require.NoError(t, err)
// Verify versions are different.
require.Equal(t, "3.1.0", spec31["openapi"])
require.Equal(t, "3.2.0", spec32["openapi"])
// Verify /notifications/live is only in 3.2.
paths31 := spec31["paths"].(map[any]any)
paths32 := spec32["paths"].(map[any]any)
require.NotContains(t, paths31, "/notifications/live")
require.Contains(t, paths32, "/notifications/live")
// Verify 3.2 has exactly one more path than 3.1.
require.Len(t, paths32, len(paths31)+1,
"OpenAPI 3.2 should have exactly one more path than 3.1")
}

157
web/api/v1/test_helpers.go Normal file
View file

@ -0,0 +1,157 @@
// 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 (
"context"
"testing"
"time"
"github.com/prometheus/common/route"
"github.com/prometheus/prometheus/web/api/testhelpers"
)
// newTestAPI creates a new API instance for testing using testhelpers.
func newTestAPI(t *testing.T, cfg testhelpers.APIConfig) *testhelpers.APIWrapper {
t.Helper()
params := testhelpers.PrepareAPI(t, cfg)
// Adapt the testhelpers interfaces to v1 interfaces.
api := NewAPI(
params.QueryEngine,
params.Queryable,
nil, // appendable
params.ExemplarQueryable,
func(ctx context.Context) ScrapePoolsRetriever {
return adaptScrapePoolsRetriever(params.ScrapePoolsRetriever(ctx))
},
func(ctx context.Context) TargetRetriever {
return adaptTargetRetriever(params.TargetRetriever(ctx))
},
func(ctx context.Context) AlertmanagerRetriever {
return adaptAlertmanagerRetriever(params.AlertmanagerRetriever(ctx))
},
params.ConfigFunc,
params.FlagsMap,
GlobalURLOptions{},
params.ReadyFunc,
adaptTSDBAdminStats(params.TSDBAdmin),
params.DBDir,
false, // enableAdmin
params.Logger,
func(ctx context.Context) RulesRetriever {
return adaptRulesRetriever(params.RulesRetriever(ctx))
},
0, // remoteReadSampleLimit
0, // remoteReadConcurrencyLimit
0, // remoteReadMaxBytesInFrame
false, // isAgent
nil, // corsOrigin
func() (RuntimeInfo, error) {
info, err := params.RuntimeInfoFunc()
return RuntimeInfo{
StartTime: info.StartTime,
CWD: info.CWD,
Hostname: info.Hostname,
ServerTime: info.ServerTime,
ReloadConfigSuccess: info.ReloadConfigSuccess,
LastConfigTime: info.LastConfigTime,
CorruptionCount: info.CorruptionCount,
GoroutineCount: info.GoroutineCount,
GOMAXPROCS: info.GOMAXPROCS,
GOMEMLIMIT: info.GOMEMLIMIT,
GOGC: info.GOGC,
GODEBUG: info.GODEBUG,
StorageRetention: info.StorageRetention,
}, err
},
&PrometheusVersion{
Version: params.BuildInfo.Version,
Revision: params.BuildInfo.Revision,
Branch: params.BuildInfo.Branch,
BuildUser: params.BuildInfo.BuildUser,
BuildDate: params.BuildInfo.BuildDate,
GoVersion: params.BuildInfo.GoVersion,
},
params.NotificationsGetter,
params.NotificationsSub,
params.Gatherer,
params.Registerer,
nil, // statsRenderer
false, // rwEnabled
nil, // acceptRemoteWriteProtoMsgs
false, // otlpEnabled
false, // otlpDeltaToCumulative
false, // otlpNativeDeltaIngestion
false, // stZeroIngestionEnabled
5*time.Minute, // lookbackDelta
false, // enableTypeAndUnitLabels
false, // appendMetadata
nil, // overrideErrorCode
nil, // featureRegistry
OpenAPIOptions{}, // openAPIOptions
)
// Register routes.
router := route.New()
api.Register(router.WithPrefix("/api/v1"))
return &testhelpers.APIWrapper{
Handler: router,
}
}
// Adapter functions to convert testhelpers interfaces to v1 interfaces.
type rulesRetrieverAdapter struct {
testhelpers.RulesRetriever
}
func adaptRulesRetriever(r testhelpers.RulesRetriever) RulesRetriever {
return &rulesRetrieverAdapter{r}
}
type targetRetrieverAdapter struct {
testhelpers.TargetRetriever
}
func adaptTargetRetriever(t testhelpers.TargetRetriever) TargetRetriever {
return &targetRetrieverAdapter{t}
}
type scrapePoolsRetrieverAdapter struct {
testhelpers.ScrapePoolsRetriever
}
func adaptScrapePoolsRetriever(s testhelpers.ScrapePoolsRetriever) ScrapePoolsRetriever {
return &scrapePoolsRetrieverAdapter{s}
}
type alertmanagerRetrieverAdapter struct {
testhelpers.AlertmanagerRetriever
}
func adaptAlertmanagerRetriever(a testhelpers.AlertmanagerRetriever) AlertmanagerRetriever {
return &alertmanagerRetrieverAdapter{a}
}
type tsdbAdminStatsAdapter struct {
testhelpers.TSDBAdminStats
}
func adaptTSDBAdminStats(t testhelpers.TSDBAdminStats) TSDBAdminStats {
return &tsdbAdminStatsAdapter{t}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
module github.com/prometheus/prometheus/web/ui/mantine-ui/src/promql/tools module github.com/prometheus/prometheus/web/ui/mantine-ui/src/promql/tools
go 1.24.0 go 1.25.0
require ( require (
github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853 github.com/grafana/regexp v0.0.0-20250905093917-f7b3be9d1853

View file

@ -361,6 +361,11 @@ func New(logger *slog.Logger, o *Options) *Handler {
app = h.storage app = h.storage
} }
version := ""
if o.Version != nil {
version = o.Version.Version
}
h.apiV1 = api_v1.NewAPI(h.queryEngine, h.storage, app, h.exemplarStorage, factorySPr, factoryTr, factoryAr, h.apiV1 = api_v1.NewAPI(h.queryEngine, h.storage, app, h.exemplarStorage, factorySPr, factoryTr, factoryAr,
func() config.Config { func() config.Config {
h.mtx.RLock() h.mtx.RLock()
@ -402,6 +407,10 @@ func New(logger *slog.Logger, o *Options) *Handler {
o.AppendMetadata, o.AppendMetadata,
nil, nil,
o.FeatureRegistry, o.FeatureRegistry,
api_v1.OpenAPIOptions{
ExternalURL: o.ExternalURL.String(),
Version: version,
},
) )
if r := o.FeatureRegistry; r != nil { if r := o.FeatureRegistry; r != nil {

View file

@ -328,6 +328,7 @@ func TestDebugHandler(t *testing.T) {
Host: "localhost.localdomain:9090", Host: "localhost.localdomain:9090",
Scheme: "http", Scheme: "http",
}, },
Version: &PrometheusVersion{},
} }
handler := New(nil, opts) handler := New(nil, opts)
handler.SetReady(Ready) handler.SetReady(Ready)
@ -353,6 +354,7 @@ func TestHTTPMetrics(t *testing.T) {
Host: "localhost.localdomain:9090", Host: "localhost.localdomain:9090",
Scheme: "http", Scheme: "http",
}, },
Version: &PrometheusVersion{},
}) })
getReady := func() int { getReady := func() int {
t.Helper() t.Helper()