Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ type Config struct {
buildInfoEnabled bool `yaml:"build_info_enabled"`

QuerierDefaultCodec string `yaml:"querier_default_codec"`

// Features is a list of enabled feature names to be exposed via the /api/v1/features endpoint.
// This is injected by the upstream caller.
Features []string `yaml:"-"`
}

var (
Expand Down Expand Up @@ -116,6 +120,11 @@ func (cfg *Config) Validate() error {
return nil
}

// BuildInfoEnabled returns true if the build info API is enabled.
func (cfg *Config) BuildInfoEnabled() bool {
return cfg.buildInfoEnabled
}

// Push either wraps the distributor push function as configured or returns the distributor push directly.
func (cfg *Config) wrapDistributorPush(d *distributor.Distributor) push.Func {
if cfg.DistributorPushWrapper != nil {
Expand Down Expand Up @@ -484,6 +493,11 @@ func (a *API) RegisterQueryAPI(handler http.Handler) {
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/status/buildinfo"), infoHandler, true, "GET")
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/status/buildinfo"), infoHandler, true, "GET")
}

// Register /api/v1/features endpoint on both Prometheus and legacy HTTP prefixes.
fHandler := &featuresHandler{features: a.cfg.Features, logger: a.logger}
a.RegisterRoute(path.Join(a.cfg.PrometheusHTTPPrefix, "/api/v1/features"), fHandler, true, "GET")
a.RegisterRoute(path.Join(a.cfg.LegacyHTTPPrefix, "/api/v1/features"), fHandler, true, "GET")
}

// RegisterQueryFrontendHandler registers the Prometheus routes supported by the
Expand Down
33 changes: 33 additions & 0 deletions pkg/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ func NewQuerierHandler(
router.Path(path.Join(legacyPrefix, "/api/v1/status/buildinfo")).Methods("GET").Handler(legacyPromRouter)
}

// Register /api/v1/features endpoint on the internal querier router.
fHandler := &featuresHandler{features: cfg.Features, logger: logger}
router.Path(path.Join(prefix, "/api/v1/features")).Methods("GET").Handler(fHandler)
router.Path(path.Join(legacyPrefix, "/api/v1/features")).Methods("GET").Handler(fHandler)

// Track execution time.
return stats.NewWallTimeMiddleware().Wrap(router)
}
Expand Down Expand Up @@ -389,3 +394,31 @@ func (h *buildInfoHandler) ServeHTTP(writer http.ResponseWriter, _ *http.Request
level.Error(h.logger).Log("msg", "write build info response", "error", err)
}
}

type featuresHandler struct {
features []string
logger log.Logger
}

type featuresResponse struct {
Status string `json:"status"`
Data []string `json:"data"`
}

func (h *featuresHandler) ServeHTTP(writer http.ResponseWriter, _ *http.Request) {
resp := featuresResponse{
Status: "success",
Data: h.features,
}
output, err := json.Marshal(resp)
if err != nil {
level.Error(h.logger).Log("msg", "marshal features response", "error", err)
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
if _, err := writer.Write(output); err != nil {
level.Error(h.logger).Log("msg", "write features response", "error", err)
}
}
48 changes: 48 additions & 0 deletions pkg/api/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,51 @@ func TestBuildInfoAPI(t *testing.T) {
})
}
}

func TestFeaturesAPI(t *testing.T) {
for _, tc := range []struct {
name string
features []string
expectedStatus int
expectedFeatures []string
}{
{
name: "no features enabled",
features: nil,
expectedStatus: 200,
expectedFeatures: nil,
},
{
name: "single feature enabled",
features: []string{"remote_write_v2"},
expectedStatus: 200,
expectedFeatures: []string{"remote_write_v2"},
},
{
name: "multiple features enabled",
features: []string{"remote_write_v2", "streaming_ingestion", "parquet_queryable", "tenant_federation"},
expectedStatus: 200,
expectedFeatures: []string{"remote_write_v2", "streaming_ingestion", "parquet_queryable", "tenant_federation"},
},
} {
t.Run(tc.name, func(t *testing.T) {
handler := &featuresHandler{
features: tc.features,
logger: &FakeLogger{},
}

writer := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/api/v1/features", nil)
handler.ServeHTTP(writer, req)

assert.Equal(t, tc.expectedStatus, writer.Code)
assert.Equal(t, "application/json", writer.Header().Get("Content-Type"))

var resp featuresResponse
err := json.Unmarshal(writer.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, "success", resp.Status)
assert.Equal(t, tc.expectedFeatures, resp.Data)
})
}
}
32 changes: 32 additions & 0 deletions pkg/cortex/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ func (t *Cortex) initAPI() (services.Service, error) {
t.Cfg.API.ServerPrefix = t.Cfg.Server.PathPrefix
t.Cfg.API.LegacyHTTPPrefix = t.Cfg.HTTPPrefix

// Compute the list of enabled features from the root config.
t.Cfg.API.Features = cortexFeatures(t.Cfg)

a, err := api.New(t.Cfg.API, t.Cfg.Server, t.Server, util_log.Logger)
if err != nil {
return nil, err
Expand All @@ -124,6 +127,35 @@ func (t *Cortex) initAPI() (services.Service, error) {
return nil, nil
}

// cortexFeatures returns a list of feature names that are enabled in the given config.
func cortexFeatures(cfg Config) []string {
var features []string

if cfg.Distributor.RemoteWriteV2Enabled {
features = append(features, "remote_write_v2")
}
if cfg.Distributor.UseStreamPush {
features = append(features, "streaming_ingestion")
}
if cfg.Querier.EnableParquetQueryable {
features = append(features, "parquet_queryable")
}
if cfg.TenantFederation.Enabled {
features = append(features, "tenant_federation")
}
if cfg.Querier.DistributedExecEnabled {
features = append(features, "distributed_execution")
}
if cfg.Querier.EnablePromQLExperimentalFunctions {
features = append(features, "promql_experimental_functions")
}
if cfg.API.BuildInfoEnabled() {
features = append(features, "build_info")
}

return features
}

func (t *Cortex) initServer() (services.Service, error) {
// Cortex handles signals on its own.
DisableSignalHandling(&t.Cfg.Server)
Expand Down
75 changes: 75 additions & 0 deletions pkg/cortex/modules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,3 +353,78 @@ func setAllSecrets(v reflect.Value, sentinel string) {
}
}
}

func TestCortexFeatures(t *testing.T) {
tests := []struct {
name string
configFn func(*Config)
expectedFeatures []string
}{
{
name: "no features enabled",
configFn: func(cfg *Config) {},
expectedFeatures: nil,
},
{
name: "remote_write_v2 enabled",
configFn: func(cfg *Config) {
cfg.Distributor.RemoteWriteV2Enabled = true
},
expectedFeatures: []string{"remote_write_v2"},
},
{
name: "streaming_ingestion enabled",
configFn: func(cfg *Config) {
cfg.Distributor.UseStreamPush = true
},
expectedFeatures: []string{"streaming_ingestion"},
},
{
name: "parquet_queryable enabled",
configFn: func(cfg *Config) {
cfg.Querier.EnableParquetQueryable = true
},
expectedFeatures: []string{"parquet_queryable"},
},
{
name: "tenant_federation enabled",
configFn: func(cfg *Config) {
cfg.TenantFederation.Enabled = true
},
expectedFeatures: []string{"tenant_federation"},
},
{
name: "distributed_execution enabled",
configFn: func(cfg *Config) {
cfg.Querier.DistributedExecEnabled = true
},
expectedFeatures: []string{"distributed_execution"},
},
{
name: "promql_experimental_functions enabled",
configFn: func(cfg *Config) {
cfg.Querier.EnablePromQLExperimentalFunctions = true
},
expectedFeatures: []string{"promql_experimental_functions"},
},
{
name: "multiple features enabled",
configFn: func(cfg *Config) {
cfg.Distributor.RemoteWriteV2Enabled = true
cfg.Distributor.UseStreamPush = true
cfg.TenantFederation.Enabled = true
cfg.Querier.EnableParquetQueryable = true
},
expectedFeatures: []string{"remote_write_v2", "streaming_ingestion", "parquet_queryable", "tenant_federation"},
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cfg := Config{}
tc.configFn(&cfg)
features := cortexFeatures(cfg)
assert.Equal(t, tc.expectedFeatures, features)
})
}
}
Loading