Merge pull request #17427 from roidelapluie/roidelapluie/ffapi

API: Add a /api/v1/features endpoint
This commit is contained in:
Julien 2025-12-10 10:14:03 +01:00 committed by GitHub
commit f73aba34cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 830 additions and 6 deletions

View file

@ -184,6 +184,11 @@ check-go-mod-version:
@echo ">> checking go.mod version matching"
@./scripts/check-go-mod-version.sh
.PHONY: update-features-testdata
update-features-testdata:
@echo ">> updating features testdata"
@$(GO) test ./cmd/prometheus -run TestFeaturesAPI -update-features
.PHONY: update-all-go-deps
update-all-go-deps:
@$(MAKE) update-go-deps

View file

@ -0,0 +1,125 @@
// 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 main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/util/testutil"
)
var updateFeatures = flag.Bool("update-features", false, "update features.json golden file")
func TestFeaturesAPI(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
t.Parallel()
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "prometheus.yml")
require.NoError(t, os.WriteFile(configFile, []byte{}, 0o644))
port := testutil.RandomUnprivilegedPort(t)
prom := prometheusCommandWithLogging(
t,
configFile,
port,
fmt.Sprintf("--storage.tsdb.path=%s", tmpDir),
)
require.NoError(t, prom.Start())
baseURL := fmt.Sprintf("http://127.0.0.1:%d", port)
// Wait for Prometheus to be ready.
require.Eventually(t, func() bool {
resp, err := http.Get(baseURL + "/-/ready")
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode == http.StatusOK
}, 10*time.Second, 100*time.Millisecond, "Prometheus didn't become ready in time")
// Fetch features from the API.
resp, err := http.Get(baseURL + "/api/v1/features")
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode)
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
// Parse API response.
var apiResponse struct {
Status string `json:"status"`
Data map[string]map[string]bool `json:"data"`
}
require.NoError(t, json.Unmarshal(body, &apiResponse))
require.Equal(t, "success", apiResponse.Status)
goldenPath := filepath.Join("testdata", "features.json")
// If update flag is set, write the current features to the golden file.
if *updateFeatures {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
require.NoError(t, encoder.Encode(apiResponse.Data))
// Ensure testdata directory exists.
require.NoError(t, os.MkdirAll(filepath.Dir(goldenPath), 0o755))
require.NoError(t, os.WriteFile(goldenPath, buf.Bytes(), 0o644))
t.Logf("Updated golden file: %s", goldenPath)
return
}
// Load golden file.
goldenData, err := os.ReadFile(goldenPath)
require.NoError(t, err, "Failed to read golden file %s. Run 'make update-features-testdata' to generate it.", goldenPath)
var expectedFeatures map[string]map[string]bool
require.NoError(t, json.Unmarshal(goldenData, &expectedFeatures))
// The labels implementation depends on build tags (stringlabels, slicelabels, or dedupelabels).
// We need to update the expected features to match the current build.
if prometheusFeatures, ok := expectedFeatures["prometheus"]; ok {
// Remove all label implementation features from expected.
delete(prometheusFeatures, "stringlabels")
delete(prometheusFeatures, "slicelabels")
delete(prometheusFeatures, "dedupelabels")
// Add the current implementation.
if actualPrometheus, ok := apiResponse.Data["prometheus"]; ok {
for _, impl := range []string{"stringlabels", "slicelabels", "dedupelabels"} {
if actualPrometheus[impl] {
prometheusFeatures[impl] = true
}
}
}
}
// Compare the features data with the golden file.
require.Equal(t, expectedFeatures, apiResponse.Data, "Features mismatch. Run 'make update-features-testdata' to update the golden file.")
}

View file

@ -73,11 +73,13 @@ import (
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/storage/remote"
"github.com/prometheus/prometheus/template"
"github.com/prometheus/prometheus/tracing"
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/agent"
"github.com/prometheus/prometheus/util/compression"
"github.com/prometheus/prometheus/util/documentcli"
"github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/logging"
"github.com/prometheus/prometheus/util/notifications"
prom_runtime "github.com/prometheus/prometheus/util/runtime"
@ -236,6 +238,7 @@ func (c *flagConfig) setFeatureListOptions(logger *slog.Logger) error {
case "metadata-wal-records":
c.scrape.AppendMetadata = true
c.web.AppendMetadata = true
features.Enable(features.TSDB, "metadata_wal_records")
logger.Info("Experimental metadata records in WAL enabled")
case "promql-per-step-stats":
c.enablePerStepStats = true
@ -342,10 +345,14 @@ func main() {
Registerer: prometheus.DefaultRegisterer,
},
web: web.Options{
Registerer: prometheus.DefaultRegisterer,
Gatherer: prometheus.DefaultGatherer,
Registerer: prometheus.DefaultRegisterer,
Gatherer: prometheus.DefaultGatherer,
FeatureRegistry: features.DefaultRegistry,
},
promslogConfig: promslog.Config{},
scrape: scrape.Options{
FeatureRegistry: features.DefaultRegistry,
},
}
a := kingpin.New(filepath.Base(os.Args[0]), "The Prometheus monitoring server").UsageWriter(os.Stdout)
@ -797,6 +804,12 @@ func main() {
"vm_limits", prom_runtime.VMLimits(),
)
features.Set(features.Prometheus, "agent_mode", agentMode)
features.Set(features.Prometheus, "server_mode", !agentMode)
features.Set(features.Prometheus, "auto_reload_config", cfg.enableAutoReload)
features.Enable(features.Prometheus, labels.ImplementationName)
template.RegisterFeatures(features.DefaultRegistry)
var (
localStorage = &readyStorage{stats: tsdb.NewDBStats()}
scraper = &readyScrapeManager{}
@ -833,13 +846,13 @@ func main() {
os.Exit(1)
}
discoveryManagerScrape = discovery.NewManager(ctxScrape, logger.With("component", "discovery manager scrape"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("scrape"))
discoveryManagerScrape = discovery.NewManager(ctxScrape, logger.With("component", "discovery manager scrape"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("scrape"), discovery.FeatureRegistry(features.DefaultRegistry))
if discoveryManagerScrape == nil {
logger.Error("failed to create a discovery manager scrape")
os.Exit(1)
}
discoveryManagerNotify = discovery.NewManager(ctxNotify, logger.With("component", "discovery manager notify"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("notify"))
discoveryManagerNotify = discovery.NewManager(ctxNotify, logger.With("component", "discovery manager notify"), prometheus.DefaultRegisterer, sdMetrics, discovery.Name("notify"), discovery.FeatureRegistry(features.DefaultRegistry))
if discoveryManagerNotify == nil {
logger.Error("failed to create a discovery manager notify")
os.Exit(1)
@ -880,6 +893,7 @@ func main() {
EnablePerStepStats: cfg.enablePerStepStats,
EnableDelayedNameRemoval: cfg.promqlEnableDelayedNameRemoval,
EnableTypeAndUnitLabels: cfg.scrape.EnableTypeAndUnitLabels,
FeatureRegistry: features.DefaultRegistry,
}
queryEngine = promql.NewEngine(opts)
@ -902,6 +916,7 @@ func main() {
DefaultRuleQueryOffset: func() time.Duration {
return time.Duration(cfgFile.GlobalConfig.RuleQueryOffset)
},
FeatureRegistry: features.DefaultRegistry,
})
}
@ -1919,6 +1934,7 @@ func (opts tsdbOptions) ToTSDBOptions() tsdb.Options {
EnableOverlappingCompaction: opts.EnableOverlappingCompaction,
UseUncachedIO: opts.UseUncachedIO,
BlockCompactionExcludeFunc: opts.BlockCompactionExcludeFunc,
FeatureRegistry: features.DefaultRegistry,
}
}

249
cmd/prometheus/testdata/features.json vendored Normal file
View file

@ -0,0 +1,249 @@
{
"api": {
"admin": false,
"exclude_alerts": true,
"label_values_match": true,
"lifecycle": false,
"otlp_write_receiver": false,
"query_stats": true,
"query_warnings": true,
"remote_write_receiver": false,
"time_range_labels": true,
"time_range_series": true
},
"otlp_receiver": {
"delta_conversion": false,
"native_delta_ingestion": false
},
"prometheus": {
"agent_mode": false,
"auto_reload_config": false,
"server_mode": true,
"stringlabels": true
},
"promql": {
"anchored": false,
"at_modifier": true,
"bool": true,
"by": true,
"delayed_name_removal": false,
"duration_expr": false,
"group_left": true,
"group_right": true,
"ignoring": true,
"negative_offset": true,
"offset": true,
"on": true,
"per_query_lookback_delta": true,
"per_step_stats": false,
"smoothed": false,
"subqueries": true,
"type_and_unit_labels": false,
"without": true
},
"promql_functions": {
"abs": true,
"absent": true,
"absent_over_time": true,
"acos": true,
"acosh": true,
"asin": true,
"asinh": true,
"atan": true,
"atanh": true,
"avg_over_time": true,
"ceil": true,
"changes": true,
"clamp": true,
"clamp_max": true,
"clamp_min": true,
"cos": true,
"cosh": true,
"count_over_time": true,
"day_of_month": true,
"day_of_week": true,
"day_of_year": true,
"days_in_month": true,
"deg": true,
"delta": true,
"deriv": true,
"double_exponential_smoothing": false,
"exp": true,
"first_over_time": false,
"floor": true,
"histogram_avg": true,
"histogram_count": true,
"histogram_fraction": true,
"histogram_quantile": true,
"histogram_stddev": true,
"histogram_stdvar": true,
"histogram_sum": true,
"hour": true,
"idelta": true,
"increase": true,
"info": false,
"irate": true,
"label_join": true,
"label_replace": true,
"last_over_time": true,
"ln": true,
"log10": true,
"log2": true,
"mad_over_time": false,
"max_over_time": true,
"min_over_time": true,
"minute": true,
"month": true,
"pi": true,
"predict_linear": true,
"present_over_time": true,
"quantile_over_time": true,
"rad": true,
"rate": true,
"resets": true,
"round": true,
"scalar": true,
"sgn": true,
"sin": true,
"sinh": true,
"sort": true,
"sort_by_label": false,
"sort_by_label_desc": false,
"sort_desc": true,
"sqrt": true,
"stddev_over_time": true,
"stdvar_over_time": true,
"sum_over_time": true,
"tan": true,
"tanh": true,
"time": true,
"timestamp": true,
"ts_of_first_over_time": false,
"ts_of_last_over_time": false,
"ts_of_max_over_time": false,
"ts_of_min_over_time": false,
"vector": true,
"year": true
},
"promql_operators": {
"!=": true,
"!~": true,
"%": true,
"*": true,
"+": true,
"-": true,
"/": true,
"<": true,
"<=": true,
"==": true,
"=~": true,
">": true,
">=": true,
"@": true,
"^": true,
"and": true,
"atan2": true,
"avg": true,
"bottomk": true,
"count": true,
"count_values": true,
"group": true,
"limit_ratio": false,
"limitk": false,
"max": true,
"min": true,
"or": true,
"quantile": true,
"stddev": true,
"stdvar": true,
"sum": true,
"topk": true,
"unless": true
},
"rules": {
"concurrent_rule_eval": false,
"keep_firing_for": true,
"query_offset": true
},
"scrape": {
"extra_scrape_metrics": false,
"start_timestamp_zero_ingestion": false,
"type_and_unit_labels": false
},
"service_discovery_providers": {
"aws": true,
"azure": true,
"consul": true,
"digitalocean": true,
"dns": true,
"docker": true,
"dockerswarm": true,
"ec2": true,
"ecs": true,
"eureka": true,
"file": true,
"gce": true,
"hetzner": true,
"http": true,
"ionos": true,
"kubernetes": true,
"kuma": true,
"lightsail": true,
"linode": true,
"marathon": true,
"nerve": true,
"nomad": true,
"openstack": true,
"ovhcloud": true,
"puppetdb": true,
"scaleway": true,
"serverset": true,
"stackit": true,
"static": true,
"triton": true,
"uyuni": true,
"vultr": true
},
"templating_functions": {
"args": true,
"externalURL": true,
"first": true,
"graphLink": true,
"humanize": true,
"humanize1024": true,
"humanizeDuration": true,
"humanizePercentage": true,
"humanizeTimestamp": true,
"label": true,
"match": true,
"now": true,
"parseDuration": true,
"pathPrefix": true,
"query": true,
"reReplaceAll": true,
"safeHtml": true,
"sortByLabel": true,
"stripDomain": true,
"stripPort": true,
"strvalue": true,
"tableLink": true,
"title": true,
"toDuration": true,
"toLower": true,
"toTime": true,
"toUpper": true,
"urlQueryEscape": true,
"value": true
},
"tsdb": {
"delayed_compaction": false,
"exemplar_storage": false,
"isolation": true,
"native_histograms": true,
"use_uncached_io": false
},
"ui": {
"ui_v2": false,
"ui_v3": true
}
}

View file

@ -27,6 +27,7 @@ import (
"github.com/prometheus/common/promslog"
"github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/util/features"
)
type poolKey struct {
@ -111,6 +112,13 @@ func NewManager(ctx context.Context, logger *slog.Logger, registerer prometheus.
}
mgr.metrics = metrics
// Register all available service discovery providers with the feature registry.
if mgr.featureRegistry != nil {
for _, sdName := range RegisteredConfigNames() {
mgr.featureRegistry.Enable(features.ServiceDiscoveryProviders, sdName)
}
}
return mgr
}
@ -141,6 +149,15 @@ func HTTPClientOptions(opts ...config.HTTPClientOption) func(*Manager) {
}
}
// FeatureRegistry sets the feature registry for the manager.
func FeatureRegistry(fr features.Collector) func(*Manager) {
return func(m *Manager) {
m.mtx.Lock()
defer m.mtx.Unlock()
m.featureRegistry = fr
}
}
// Manager maintains a set of discovery providers and sends each update to a map channel.
// Targets are grouped by the target set name.
type Manager struct {
@ -175,6 +192,9 @@ type Manager struct {
metrics *Metrics
sdMetrics map[string]DiscovererMetrics
// featureRegistry is used to track which service discovery providers are configured.
featureRegistry features.Collector
}
// Providers returns the currently configured SD providers.

View file

@ -280,3 +280,13 @@ func RegisterSDMetrics(registerer prometheus.Registerer, rmm RefreshMetricsManag
}
return metrics, nil
}
// RegisteredConfigNames returns the names of all registered service discovery providers.
func RegisteredConfigNames() []string {
names := make([]string, 0, len(configNames))
for name := range configNames {
names = append(names, name)
}
sort.Strings(names)
return names
}

View file

@ -1700,3 +1700,80 @@ GET /api/v1/notifications/live
```
*New in v3.0*
### Features
The following endpoint returns a list of enabled features in the Prometheus server:
```
GET /api/v1/features
```
This endpoint provides information about which features are currently enabled or disabled in the Prometheus instance. Features are organized into categories such as `api`, `promql`, `promql_functions`, etc.
The `data` section contains a map where each key is a feature category, and each value is a map of feature names to their enabled status (boolean).
```bash
curl http://localhost:9090/api/v1/features
```
```json
{
"status": "success",
"data": {
"api": {
"admin": false,
"exclude_alerts": true
},
"otlp_receiver": {
"delta_conversion": false,
"native_delta_ingestion": false
},
"prometheus": {
"agent_mode": false,
"auto_reload_config": false
},
"promql": {
"anchored": false,
"at_modifier": true
},
"promql_functions": {
"abs": true,
"absent": true
},
"promql_operators": {
"!=": true,
"!~": true
},
"rules": {
"concurrent_rule_eval": false,
"keep_firing_for": true
},
"scrape": {
"start_timestamp_zero_ingestion": false,
"extra_metrics": false
},
"service_discovery": {
"azure": true,
"consul": true
},
"templating": {
"args": true,
"externalURL": true
},
"tsdb": {
"delayed_compaction": false,
"exemplar_storage": false
}
}
}
```
**Notes:**
- All feature names use `snake_case` naming convention
- Features set to `false` may be omitted from the response
- Clients should treat absent features as equivalent to `false`
- Clients must ignore unknown feature names and categories for forward compatibility
*New in v3.8*

View file

@ -24,6 +24,9 @@ import (
"github.com/cespare/xxhash/v2"
)
// ImplementationName is the name of the labels implementation.
const ImplementationName = "dedupelabels"
// Labels is implemented by a SymbolTable and string holding name/value
// pairs encoded as indexes into the table in varint encoding.
// Names are in alphabetical order.

View file

@ -25,6 +25,9 @@ import (
"github.com/cespare/xxhash/v2"
)
// ImplementationName is the name of the labels implementation.
const ImplementationName = "slicelabels"
// Labels is a sorted set of labels. Order has to be guaranteed upon
// instantiation.
type Labels []Label

View file

@ -23,6 +23,9 @@ import (
"github.com/cespare/xxhash/v2"
)
// ImplementationName is the name of the labels implementation.
const ImplementationName = "stringlabels"
// Labels is implemented by a single flat string holding name/value pairs.
// Each name and value is preceded by its length, encoded as a single byte
// for size 0-254, or the following 3 bytes little-endian, if the first byte is 255.

View file

@ -49,6 +49,7 @@ import (
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/tsdb/chunkenc"
"github.com/prometheus/prometheus/util/annotations"
"github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/logging"
"github.com/prometheus/prometheus/util/stats"
"github.com/prometheus/prometheus/util/zeropool"
@ -330,6 +331,9 @@ type EngineOpts struct {
EnableDelayedNameRemoval bool
// EnableTypeAndUnitLabels will allow PromQL Engine to make decisions based on the type and unit labels.
EnableTypeAndUnitLabels bool
// FeatureRegistry is the registry for tracking enabled/disabled features.
FeatureRegistry features.Collector
}
// Engine handles the lifetime of queries from beginning to end.
@ -446,6 +450,18 @@ func NewEngine(opts EngineOpts) *Engine {
)
}
if r := opts.FeatureRegistry; r != nil {
r.Set(features.PromQL, "at_modifier", opts.EnableAtModifier)
r.Set(features.PromQL, "negative_offset", opts.EnableNegativeOffset)
r.Set(features.PromQL, "per_step_stats", opts.EnablePerStepStats)
r.Set(features.PromQL, "delayed_name_removal", opts.EnableDelayedNameRemoval)
r.Set(features.PromQL, "type_and_unit_labels", opts.EnableTypeAndUnitLabels)
r.Enable(features.PromQL, "per_query_lookback_delta")
r.Enable(features.PromQL, "subqueries")
parser.RegisterFeatures(r)
}
return &Engine{
timeout: opts.Timeout,
logger: opts.Logger,

57
promql/parser/features.go Normal file
View file

@ -0,0 +1,57 @@
// 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 parser
import "github.com/prometheus/prometheus/util/features"
// RegisterFeatures registers all PromQL features with the feature registry.
// This includes operators (arithmetic and comparison/set), aggregators (standard
// and experimental), and functions.
func RegisterFeatures(r features.Collector) {
// Register core PromQL language keywords.
for keyword, itemType := range key {
if itemType.IsKeyword() {
// Handle experimental keywords separately.
switch keyword {
case "anchored", "smoothed":
r.Set(features.PromQL, keyword, EnableExtendedRangeSelectors)
default:
r.Enable(features.PromQL, keyword)
}
}
}
// Register operators.
for o := ItemType(operatorsStart + 1); o < operatorsEnd; o++ {
if o.IsOperator() {
r.Set(features.PromQLOperators, o.String(), true)
}
}
// Register aggregators.
for a := ItemType(aggregatorsStart + 1); a < aggregatorsEnd; a++ {
if a.IsAggregator() {
experimental := a.IsExperimentalAggregator() && !EnableExperimentalFunctions
r.Set(features.PromQLOperators, a.String(), !experimental)
}
}
// Register functions.
for f, fc := range Functions {
r.Set(features.PromQLFunctions, f, !fc.Experimental || EnableExperimentalFunctions)
}
// Register experimental parser features.
r.Set(features.PromQL, "duration_expr", ExperimentalDurationExpr)
}

View file

@ -37,6 +37,7 @@ import (
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/strutil"
)
@ -134,6 +135,9 @@ type ManagerOptions struct {
RestoreNewRuleGroups bool
Metrics *Metrics
// FeatureRegistry is used to register rule manager features.
FeatureRegistry features.Collector
}
// NewManager returns an implementation of Manager, ready to be started
@ -174,6 +178,13 @@ func NewManager(o *ManagerOptions) *Manager {
o.Logger = promslog.NewNopLogger()
}
// Register rule manager features if a registry is provided.
if o.FeatureRegistry != nil {
o.FeatureRegistry.Set(features.Rules, "concurrent_rule_eval", o.ConcurrentEvalsEnabled)
o.FeatureRegistry.Enable(features.Rules, "query_offset")
o.FeatureRegistry.Enable(features.Rules, "keep_firing_for")
}
m := &Manager{
groups: map[string]*Group{},
opts: o,

View file

@ -33,6 +33,7 @@ import (
"github.com/prometheus/prometheus/discovery/targetgroup"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/logging"
"github.com/prometheus/prometheus/util/osutil"
"github.com/prometheus/prometheus/util/pool"
@ -67,6 +68,13 @@ func NewManager(o *Options, logger *slog.Logger, newScrapeFailureLogger func(str
m.metrics.setTargetMetadataCacheGatherer(m)
// Register scrape features.
if r := o.FeatureRegistry; r != nil {
r.Set(features.Scrape, "extra_scrape_metrics", o.ExtraMetrics)
r.Set(features.Scrape, "start_timestamp_zero_ingestion", o.EnableStartTimestampZeroIngestion)
r.Set(features.Scrape, "type_and_unit_labels", o.EnableTypeAndUnitLabels)
}
return m, nil
}
@ -93,6 +101,9 @@ type Options struct {
// Optional HTTP client options to use when scraping.
HTTPClientOptions []config_util.HTTPClientOption
// FeatureRegistry is the registry for tracking enabled/disabled features.
FeatureRegistry features.Collector
// private option for testability.
skipOffsetting bool
}

View file

@ -36,6 +36,7 @@ import (
"golang.org/x/text/language"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/strutil"
)
@ -413,3 +414,29 @@ func floatToTime(v float64) (*time.Time, error) {
t := model.TimeFromUnixNano(int64(timestamp)).Time().UTC()
return &t, nil
}
// templateFunctions returns a representative funcMap with all available template functions.
// This is used to discover which functions are available for feature registration.
func templateFunctions() text_template.FuncMap {
// Create a dummy expander to get the function map.
expander := NewTemplateExpander(
context.Background(),
"",
"",
nil,
0,
nil,
&url.URL{},
nil,
)
return expander.funcMap
}
// RegisterFeatures registers all template functions with the feature registry.
func RegisterFeatures(r features.Collector) {
// Get all function names from the template function map.
funcMap := templateFunctions()
for name := range funcMap {
r.Enable(features.TemplatingFunctions, name)
}
}

View file

@ -47,6 +47,7 @@ import (
"github.com/prometheus/prometheus/tsdb/tsdbutil"
"github.com/prometheus/prometheus/tsdb/wlog"
"github.com/prometheus/prometheus/util/compression"
"github.com/prometheus/prometheus/util/features"
)
const (
@ -237,6 +238,9 @@ type Options struct {
// BlockCompactionExcludeFunc is a function which returns true for blocks that should NOT be compacted.
// It's passed down to the TSDB compactor.
BlockCompactionExcludeFunc BlockExcludeFilterFunc
// FeatureRegistry is used to register TSDB features.
FeatureRegistry features.Collector
}
type NewCompactorFunc func(ctx context.Context, r prometheus.Registerer, l *slog.Logger, ranges []int64, pool chunkenc.Pool, opts *Options) (Compactor, error)
@ -797,6 +801,15 @@ func Open(dir string, l *slog.Logger, r prometheus.Registerer, opts *Options, st
var rngs []int64
opts, rngs = validateOpts(opts, nil)
// Register TSDB features if a registry is provided.
if opts.FeatureRegistry != nil {
opts.FeatureRegistry.Set(features.TSDB, "exemplar_storage", opts.EnableExemplarStorage)
opts.FeatureRegistry.Set(features.TSDB, "delayed_compaction", opts.EnableDelayedCompaction)
opts.FeatureRegistry.Set(features.TSDB, "isolation", !opts.IsolationDisabled)
opts.FeatureRegistry.Set(features.TSDB, "use_uncached_io", opts.UseUncachedIO)
opts.FeatureRegistry.Enable(features.TSDB, "native_histograms")
}
return open(dir, l, r, opts, rngs, stats)
}

127
util/features/features.go Normal file
View file

@ -0,0 +1,127 @@
// 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 features
import (
"maps"
"sync"
)
// Category constants define the standard feature flag categories used in Prometheus.
const (
API = "api"
OTLPReceiver = "otlp_receiver"
Prometheus = "prometheus"
PromQL = "promql"
PromQLFunctions = "promql_functions"
PromQLOperators = "promql_operators"
Rules = "rules"
Scrape = "scrape"
ServiceDiscoveryProviders = "service_discovery_providers"
TemplatingFunctions = "templating_functions"
TSDB = "tsdb"
UI = "ui"
)
// Collector defines the interface for collecting and managing feature flags.
// It provides methods to enable, disable, and retrieve feature states.
type Collector interface {
// Enable marks a feature as enabled in the registry.
// The category and name should use snake_case naming convention.
Enable(category, name string)
// Disable marks a feature as disabled in the registry.
// The category and name should use snake_case naming convention.
Disable(category, name string)
// Set sets a feature to the specified enabled state.
// The category and name should use snake_case naming convention.
Set(category, name string, enabled bool)
// Get returns a copy of all registered features organized by category.
// Returns a map where the keys are category names and values are maps
// of feature names to their enabled status.
Get() map[string]map[string]bool
}
// registry is the private implementation of the Collector interface.
// It stores feature information organized by category.
type registry struct {
mu sync.RWMutex
features map[string]map[string]bool
}
// DefaultRegistry is the package-level registry used by Prometheus.
var DefaultRegistry = NewRegistry()
// NewRegistry creates a new feature registry.
func NewRegistry() Collector {
return &registry{
features: make(map[string]map[string]bool),
}
}
// Enable marks a feature as enabled in the registry.
func (r *registry) Enable(category, name string) {
r.Set(category, name, true)
}
// Disable marks a feature as disabled in the registry.
func (r *registry) Disable(category, name string) {
r.Set(category, name, false)
}
// Set sets a feature to the specified enabled state.
func (r *registry) Set(category, name string, enabled bool) {
r.mu.Lock()
defer r.mu.Unlock()
if r.features[category] == nil {
r.features[category] = make(map[string]bool)
}
r.features[category][name] = enabled
}
// Get returns a copy of all registered features organized by category.
func (r *registry) Get() map[string]map[string]bool {
r.mu.RLock()
defer r.mu.RUnlock()
result := make(map[string]map[string]bool, len(r.features))
for category, features := range r.features {
result[category] = make(map[string]bool, len(features))
maps.Copy(result[category], features)
}
return result
}
// Enable marks a feature as enabled in the default registry.
func Enable(category, name string) {
DefaultRegistry.Enable(category, name)
}
// Disable marks a feature as disabled in the default registry.
func Disable(category, name string) {
DefaultRegistry.Disable(category, name)
}
// Set sets a feature to the specified enabled state in the default registry.
func Set(category, name string, enabled bool) {
DefaultRegistry.Set(category, name, enabled)
}
// Get returns all features from the default registry.
func Get() map[string]map[string]bool {
return DefaultRegistry.Get()
}

View file

@ -56,6 +56,7 @@ import (
"github.com/prometheus/prometheus/tsdb"
"github.com/prometheus/prometheus/tsdb/index"
"github.com/prometheus/prometheus/util/annotations"
"github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/httputil"
"github.com/prometheus/prometheus/util/notifications"
"github.com/prometheus/prometheus/util/stats"
@ -255,6 +256,8 @@ type API struct {
otlpWriteHandler http.Handler
codecs []Codec
featureRegistry features.Collector
}
// NewAPI returns an initialized API type.
@ -295,6 +298,7 @@ func NewAPI(
enableTypeAndUnitLabels bool,
appendMetadata bool,
overrideErrorCode OverrideErrorCode,
featureRegistry features.Collector,
) *API {
a := &API{
QueryEngine: qe,
@ -324,6 +328,7 @@ func NewAPI(
notificationsGetter: notificationsGetter,
notificationsSub: notificationsSub,
overrideErrorCode: overrideErrorCode,
featureRegistry: featureRegistry,
remoteReadHandler: remote.NewReadHandler(logger, registerer, q, configFunc, remoteReadSampleLimit, remoteReadConcurrencyLimit, remoteReadMaxBytesInFrame),
}
@ -445,6 +450,7 @@ func (api *API) Register(r *route.Router) {
r.Get("/status/flags", wrap(api.serveFlags))
r.Get("/status/tsdb", wrapAgent(api.serveTSDBStatus))
r.Get("/status/tsdb/blocks", wrapAgent(api.serveTSDBBlocks))
r.Get("/features", wrap(api.features))
r.Get("/status/walreplay", api.serveWALReplayStatus)
r.Get("/notifications", api.notifications)
r.Get("/notifications/live", api.notificationsSSE)
@ -1789,6 +1795,29 @@ func (api *API) serveFlags(*http.Request) apiFuncResult {
return apiFuncResult{api.flagsMap, nil, nil, nil}
}
// featuresData wraps feature flags data to provide custom JSON marshaling without HTML escaping.
// featuresData does not contain user-provided input, and it is more convenient to have unescaped
// representation of PromQL operators like >=.
type featuresData struct {
data map[string]map[string]bool
}
func (f featuresData) MarshalJSON() ([]byte, error) {
json := jsoniter.Config{
EscapeHTML: false,
SortMapKeys: true,
ValidateJsonRawMessage: true,
}.Froze()
return json.Marshal(f.data)
}
func (api *API) features(*http.Request) apiFuncResult {
if api.featureRegistry == nil {
return apiFuncResult{nil, &apiError{errorInternal, errors.New("feature registry not configured")}, nil, nil}
}
return apiFuncResult{featuresData{data: api.featureRegistry.Get()}, nil, nil, nil}
}
// TSDBStat holds the information about individual cardinality.
type TSDBStat struct {
Name string `json:"name"`

View file

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

View file

@ -57,6 +57,7 @@ import (
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/storage"
"github.com/prometheus/prometheus/template"
"github.com/prometheus/prometheus/util/features"
"github.com/prometheus/prometheus/util/httputil"
"github.com/prometheus/prometheus/util/netconnlimit"
"github.com/prometheus/prometheus/util/notifications"
@ -300,8 +301,9 @@ type Options struct {
AcceptRemoteWriteProtoMsgs remoteapi.MessageTypes
Gatherer prometheus.Gatherer
Registerer prometheus.Registerer
Gatherer prometheus.Gatherer
Registerer prometheus.Registerer
FeatureRegistry features.Collector
}
// New initializes a new web Handler.
@ -399,8 +401,27 @@ func New(logger *slog.Logger, o *Options) *Handler {
o.EnableTypeAndUnitLabels,
o.AppendMetadata,
nil,
o.FeatureRegistry,
)
if r := o.FeatureRegistry; r != nil {
// Set dynamic API features (based on configuration).
r.Set(features.API, "lifecycle", o.EnableLifecycle)
r.Set(features.API, "admin", o.EnableAdminAPI)
r.Set(features.API, "remote_write_receiver", o.EnableRemoteWriteReceiver)
r.Set(features.API, "otlp_write_receiver", o.EnableOTLPWriteReceiver)
r.Set(features.OTLPReceiver, "delta_conversion", o.ConvertOTLPDelta)
r.Set(features.OTLPReceiver, "native_delta_ingestion", o.NativeOTLPDeltaIngestion)
r.Enable(features.API, "label_values_match") // match[] parameter for label values endpoint.
r.Enable(features.API, "query_warnings") // warnings in query responses.
r.Enable(features.API, "query_stats") // stats parameter for query endpoints.
r.Enable(features.API, "time_range_series") // start/end parameters for /series endpoint.
r.Enable(features.API, "time_range_labels") // start/end parameters for /labels endpoints.
r.Enable(features.API, "exclude_alerts") // exclude_alerts parameter for /rules endpoint.
r.Set(features.UI, "ui_v3", !o.UseOldUI)
r.Set(features.UI, "ui_v2", o.UseOldUI)
}
if o.RoutePrefix != "/" {
// If the prefix is missing for the root path, prepend it.
router.Get("/", func(w http.ResponseWriter, r *http.Request) {