Skip to content
Merged
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
32 changes: 29 additions & 3 deletions web_ui/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import (
"github.com/prometheus/prometheus/model/metadata"
"github.com/prometheus/prometheus/model/relabel"
"github.com/prometheus/prometheus/promql"
"github.com/prometheus/prometheus/rules"
"github.com/prometheus/prometheus/scrape"
"github.com/prometheus/prometheus/storage"

Expand Down Expand Up @@ -346,6 +347,31 @@ func (a LogrusAdapter) Log(keyvals ...interface{}) error {
return nil
}

// stubRulesRetriever is a no-op implementation of api_v1.RulesRetriever
// that returns empty results instead of nil to prevent nil pointer panics
// when external tools (like Grafana) query the /rules or /alerts endpoints.
type stubRulesRetriever struct{}

func (s stubRulesRetriever) RuleGroups() []*rules.Group {
return []*rules.Group{}
}

func (s stubRulesRetriever) AlertingRules() []*rules.AlertingRule {
return []*rules.AlertingRule{}
}

// stubAlertmanagerRetriever is a no-op implementation of api_v1.AlertmanagerRetriever
// that returns empty results instead of nil to prevent nil pointer panics.
type stubAlertmanagerRetriever struct{}

func (s stubAlertmanagerRetriever) Alertmanagers() []*url.URL {
return []*url.URL{}
}

func (s stubAlertmanagerRetriever) DroppedAlertmanagers() []*url.URL {
return []*url.URL{}
}

func ConfigureEmbeddedPrometheus(ctx context.Context, engine *gin.Engine, dialContextFunc func(context.Context, string, string) (net.Conn, error)) error {
// This is fine if each process has only one server enabled
// Since the "federation-in-the-box" feature won't include any web components
Expand Down Expand Up @@ -571,8 +597,8 @@ func ConfigureEmbeddedPrometheus(ctx context.Context, engine *gin.Engine, dialCo

factorySPr := func(_ context.Context) api_v1.ScrapePoolsRetriever { return scrapeManager }
factoryTr := func(_ context.Context) api_v1.TargetRetriever { return scrapeManager }
factoryAr := func(_ context.Context) api_v1.AlertmanagerRetriever { return nil }
FactoryRr := func(_ context.Context) api_v1.RulesRetriever { return nil }
factoryAr := func(_ context.Context) api_v1.AlertmanagerRetriever { return stubAlertmanagerRetriever{} }
factoryRr := func(_ context.Context) api_v1.RulesRetriever { return stubRulesRetriever{} }

readyHandler := ReadyHandler{}
readyHandler.SetReady(false)
Expand Down Expand Up @@ -602,7 +628,7 @@ func ConfigureEmbeddedPrometheus(ctx context.Context, engine *gin.Engine, dialCo
TSDBDir,
false,
logger,
FactoryRr,
factoryRr,
RemoteReadSampleLimit,
RemoteReadConcurrencyLimit,
RemoteReadBytesInFrame,
Expand Down
174 changes: 174 additions & 0 deletions web_ui/prometheus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package web_ui
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
Expand Down Expand Up @@ -290,3 +291,176 @@ func TestPrometheusProtectionOriginHeaderScope(t *testing.T) {
assert.Equal(t, 403, w.Result().StatusCode, "Expected status code of 403 due to bad token scope")
})
}

// TestPrometheusRulesEndpoint tests that the Prometheus /rules endpoint returns empty results
// without panicking when using the stub implementation.
func TestPrometheusRulesEndpoint(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
ctx, cancel, egrp := test_utils.TestContext(context.Background(), t)
defer func() { require.NoError(t, egrp.Wait()) }()
defer cancel()

server_utils.ResetTestState()

// Create a mock Prometheus API v1 router with stub implementations
av1 := route.New().WithPrefix("/api/v1.0/prometheus/api/v1")

// Register handlers that use the stub implementations
stubRules := stubRulesRetriever{}
av1.Get("/rules", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Simulate what Prometheus API would do with the stub
ruleGroups := stubRules.RuleGroups()
response := map[string]interface{}{
"status": "success",
"data": map[string]interface{}{
"groups": ruleGroups,
},
}
err := json.NewEncoder(w).Encode(response)
require.NoError(t, err)
})

// Create temp dir for the origin key file
tDir := t.TempDir()
kDir := filepath.Join(tDir, "testKeyDir")
require.NoError(t, param.Set(param.IssuerKeysDirectory.GetName(), kDir))
require.NoError(t, param.Set("ConfigDir", t.TempDir()))

test_utils.MockFederationRoot(t, nil, nil)
err := config.InitServer(ctx, server_structs.OriginType)
require.NoError(t, err)

w := httptest.NewRecorder()
c, r := gin.CreateTestContext(w)

require.NoError(t, param.Set(param.Monitoring_PromQLAuthorization.GetName(), false))
require.NoError(t, param.Set(param.Server_ExternalWebUrl.GetName(), "https://test-origin.org:8444"))

c.Request = &http.Request{
URL: &url.URL{},
}

// Set the request to run through the promQueryEngineAuthHandler function
r.GET("/api/v1.0/prometheus/api/v1/*any", promQueryEngineAuthHandler(av1))
c.Request, _ = http.NewRequest(http.MethodGet, "/api/v1.0/prometheus/api/v1/rules", nil)
r.ServeHTTP(w, c.Request)

assert.Equal(t, 200, w.Result().StatusCode, "Expected status code 200 for /rules endpoint")
resultBytes, err := io.ReadAll(w.Result().Body)
require.NoError(t, err, "Error reading the response body")

assert.NotEmpty(t, string(resultBytes), "Response should not be empty")
assert.Contains(t, string(resultBytes), "success", "Response should contain success status")
assert.Contains(t, string(resultBytes), "groups", "Response should contain groups field")
assert.Contains(t, string(resultBytes), "[]", "Response should contain empty array")
}

// TestPrometheusAlertsEndpoint tests that the Prometheus /alerts endpoint returns empty results
// without panicking when using the stub implementation.
func TestPrometheusAlertsEndpoint(t *testing.T) {
t.Cleanup(test_utils.SetupTestLogging(t))
ctx, cancel, egrp := test_utils.TestContext(context.Background(), t)
defer func() { require.NoError(t, egrp.Wait()) }()
defer cancel()

server_utils.ResetTestState()

// Create a mock Prometheus API v1 router with stub implementations
av1 := route.New().WithPrefix("/api/v1.0/prometheus/api/v1")

// Register handler that uses the stub implementation
stubAlertmgr := stubAlertmanagerRetriever{}
av1.Get("/alertmanagers", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Simulate what Prometheus API would do with the stub
activeAlertmanagers := stubAlertmgr.Alertmanagers()
droppedAlertmanagers := stubAlertmgr.DroppedAlertmanagers()
response := map[string]interface{}{
"status": "success",
"data": map[string]interface{}{
"activeAlertmanagers": activeAlertmanagers,
"droppedAlertmanagers": droppedAlertmanagers,
},
}
err := json.NewEncoder(w).Encode(response)
require.NoError(t, err)
})

// Test the alerts endpoint as well
stubRules := stubRulesRetriever{}
av1.Get("/alerts", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Simulate what Prometheus API would do with the stub
alertingRules := stubRules.AlertingRules()
response := map[string]interface{}{
"status": "success",
"data": map[string]interface{}{
"alerts": alertingRules,
},
}
err := json.NewEncoder(w).Encode(response)
require.NoError(t, err)
})

// Create temp dir for the origin key file
tDir := t.TempDir()
kDir := filepath.Join(tDir, "testKeyDir")
require.NoError(t, param.Set(param.IssuerKeysDirectory.GetName(), kDir))
require.NoError(t, param.Set("ConfigDir", t.TempDir()))

test_utils.MockFederationRoot(t, nil, nil)
err := config.InitServer(ctx, server_structs.OriginType)
require.NoError(t, err)

require.NoError(t, param.Set(param.Monitoring_PromQLAuthorization.GetName(), false))
require.NoError(t, param.Set(param.Server_ExternalWebUrl.GetName(), "https://test-origin.org:8444"))

t.Run("alertmanagers-endpoint", func(t *testing.T) {
w := httptest.NewRecorder()
c, r := gin.CreateTestContext(w)

c.Request = &http.Request{
URL: &url.URL{},
}

r.GET("/api/v1.0/prometheus/api/v1/*any", promQueryEngineAuthHandler(av1))
c.Request, _ = http.NewRequest(http.MethodGet, "/api/v1.0/prometheus/api/v1/alertmanagers", nil)
r.ServeHTTP(w, c.Request)

assert.Equal(t, 200, w.Result().StatusCode, "Expected status code 200 for /alertmanagers endpoint")
resultBytes, err := io.ReadAll(w.Result().Body)
require.NoError(t, err, "Error reading the response body")

assert.NotEmpty(t, string(resultBytes), "Response should not be empty")
assert.Contains(t, string(resultBytes), "success", "Response should contain success status")
assert.Contains(t, string(resultBytes), "activeAlertmanagers", "Response should contain activeAlertmanagers field")
assert.Contains(t, string(resultBytes), "droppedAlertmanagers", "Response should contain droppedAlertmanagers field")
assert.Contains(t, string(resultBytes), "[]", "Response should contain empty arrays")
})

t.Run("alerts-endpoint", func(t *testing.T) {
w := httptest.NewRecorder()
c, r := gin.CreateTestContext(w)

c.Request = &http.Request{
URL: &url.URL{},
}

r.GET("/api/v1.0/prometheus/api/v1/*any", promQueryEngineAuthHandler(av1))
c.Request, _ = http.NewRequest(http.MethodGet, "/api/v1.0/prometheus/api/v1/alerts", nil)
r.ServeHTTP(w, c.Request)

assert.Equal(t, 200, w.Result().StatusCode, "Expected status code 200 for /alerts endpoint")
resultBytes, err := io.ReadAll(w.Result().Body)
require.NoError(t, err, "Error reading the response body")

assert.NotEmpty(t, string(resultBytes), "Response should not be empty")
assert.Contains(t, string(resultBytes), "success", "Response should contain success status")
assert.Contains(t, string(resultBytes), "alerts", "Response should contain alerts field")
assert.Contains(t, string(resultBytes), "[]", "Response should contain empty array")
})
}
Loading