// 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 }