diff --git a/Makefile b/Makefile index 43020998ef..30295c56e5 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/cmd/prometheus/features_test.go b/cmd/prometheus/features_test.go new file mode 100644 index 0000000000..5907c87247 --- /dev/null +++ b/cmd/prometheus/features_test.go @@ -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.") +} diff --git a/cmd/prometheus/main.go b/cmd/prometheus/main.go index f7757968b7..53379dc940 100644 --- a/cmd/prometheus/main.go +++ b/cmd/prometheus/main.go @@ -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, } } diff --git a/cmd/prometheus/testdata/features.json b/cmd/prometheus/testdata/features.json new file mode 100644 index 0000000000..fbffd941fd --- /dev/null +++ b/cmd/prometheus/testdata/features.json @@ -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 + } +} diff --git a/discovery/manager.go b/discovery/manager.go index 878bc5f6d4..431050aa0b 100644 --- a/discovery/manager.go +++ b/discovery/manager.go @@ -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. diff --git a/discovery/registry.go b/discovery/registry.go index 33938cef3e..b3b82cdeec 100644 --- a/discovery/registry.go +++ b/discovery/registry.go @@ -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 +} diff --git a/docs/querying/api.md b/docs/querying/api.md index 4804443343..4cd5e175fd 100644 --- a/docs/querying/api.md +++ b/docs/querying/api.md @@ -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* diff --git a/model/labels/labels_dedupelabels.go b/model/labels/labels_dedupelabels.go index 1e736c832e..4518482c96 100644 --- a/model/labels/labels_dedupelabels.go +++ b/model/labels/labels_dedupelabels.go @@ -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. diff --git a/model/labels/labels_slicelabels.go b/model/labels/labels_slicelabels.go index e999432bf4..71dbcd0044 100644 --- a/model/labels/labels_slicelabels.go +++ b/model/labels/labels_slicelabels.go @@ -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 diff --git a/model/labels/labels_stringlabels.go b/model/labels/labels_stringlabels.go index f087223802..1460e7db93 100644 --- a/model/labels/labels_stringlabels.go +++ b/model/labels/labels_stringlabels.go @@ -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. diff --git a/promql/engine.go b/promql/engine.go index d3b67e3d81..8f922abaab 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -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, diff --git a/promql/parser/features.go b/promql/parser/features.go new file mode 100644 index 0000000000..ec64678237 --- /dev/null +++ b/promql/parser/features.go @@ -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) +} diff --git a/rules/manager.go b/rules/manager.go index 7d07217336..d610c154be 100644 --- a/rules/manager.go +++ b/rules/manager.go @@ -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, diff --git a/scrape/manager.go b/scrape/manager.go index c63d7d0eae..9bb6988df9 100644 --- a/scrape/manager.go +++ b/scrape/manager.go @@ -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 } diff --git a/template/template.go b/template/template.go index ea7e93b18c..572e8450d3 100644 --- a/template/template.go +++ b/template/template.go @@ -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) + } +} diff --git a/tsdb/db.go b/tsdb/db.go index 73300d74f1..cd1a090686 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -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) } diff --git a/util/features/features.go b/util/features/features.go new file mode 100644 index 0000000000..d52384dbd8 --- /dev/null +++ b/util/features/features.go @@ -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 ®istry{ + 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() +} diff --git a/web/api/v1/api.go b/web/api/v1/api.go index fd3652f4e4..2a6036ba0b 100644 --- a/web/api/v1/api.go +++ b/web/api/v1/api.go @@ -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"` diff --git a/web/api/v1/errors_test.go b/web/api/v1/errors_test.go index c44444404b..5bd943ba98 100644 --- a/web/api/v1/errors_test.go +++ b/web/api/v1/errors_test.go @@ -168,6 +168,7 @@ func createPrometheusAPI(t *testing.T, q storage.SampleAndChunkQueryable, overri false, false, overrideErrorCode, + nil, ) promRouter := route.New().WithPrefix("/api/v1") diff --git a/web/web.go b/web/web.go index d7b647e3db..2d216502c1 100644 --- a/web/web.go +++ b/web/web.go @@ -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) {