Skip to content

Commit

Permalink
feat: add /api/v1/alerts endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
yama6a authored and bwplotka committed Oct 7, 2024
1 parent aae0d4a commit cb718ba
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 126 deletions.
38 changes: 38 additions & 0 deletions cmd/rule-evaluator/internal/alerts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2024 Google LLC
//
// 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 internal

import (
"net/http"

"github.com/prometheus/prometheus/rules"
apiv1 "github.com/prometheus/prometheus/web/api/v1"
)

type alertsEndpointResponse struct {
Alerts []*apiv1.Alert `json:"alerts"`
}

func (api *API) HandleAlertsEndpoint(w http.ResponseWriter, _ *http.Request) {
activeAlerts := []*rules.Alert{}
for _, rule := range api.rulesManager.AlertingRules() {
activeAlerts = append(activeAlerts, rule.ActiveAlerts()...)
}

alertsResponse := alertsEndpointResponse{
Alerts: alertsToAPIAlerts(activeAlerts),
}
api.writeSuccessResponse(w, http.StatusOK, "/api/v1/alerts", alertsResponse)
}
62 changes: 62 additions & 0 deletions cmd/rule-evaluator/internal/alerts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2024 Google LLC
//
// 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 internal

import (
"io"
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/go-kit/log"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/promql/parser"
"github.com/prometheus/prometheus/rules"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAPI_HandleAlertsEndpoint(t *testing.T) {
t.Parallel()

api := &API{
rulesManager: RuleGroupsRetrieverMock{
AlertingRulesFunc: func() []*rules.AlertingRule {
return []*rules.AlertingRule{
rules.NewAlertingRule("test-alert-1", &parser.NumberLiteral{Val: 33}, time.Hour, time.Hour*4, []labels.Label{{Name: "instance", Value: "localhost:9090"}}, []labels.Label{{Name: "summary", Value: "Test alert 1"}, {Name: "description", Value: "This is a test alert"}}, nil, "", false, log.NewNopLogger()),
rules.NewAlertingRule("test-alert-2", &parser.NumberLiteral{Val: 33}, time.Hour, time.Hour*4, []labels.Label{{Name: "instance", Value: "localhost:9090"}}, []labels.Label{{Name: "summary", Value: "Test alert 2"}, {Name: "description", Value: "This is another test alert"}}, nil, "", false, log.NewNopLogger()),
}
},
},
logger: log.NewNopLogger(),
}
w := httptest.NewRecorder()

req := httptest.NewRequest(http.MethodGet, "/api/v1/alerts", nil)

api.HandleAlertsEndpoint(w, req)

result := w.Result()
defer result.Body.Close()

data, err := io.ReadAll(result.Body)
if err != nil {
t.Errorf("Error: %v", err)
}

assert.Equal(t, http.StatusOK, result.StatusCode)
require.JSONEq(t, `{"status":"success","data":{"alerts":[]}}`, string(data))
}
34 changes: 30 additions & 4 deletions cmd/rule-evaluator/internal/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ package internal
import (
"encoding/json"
"net/http"
"strconv"
"time"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/prometheus/prometheus/rules"
apiv1 "github.com/prometheus/prometheus/web/api/v1"
)

type errorType string
Expand Down Expand Up @@ -57,19 +60,20 @@ type response struct {
Infos []string `json:"infos,omitempty"`
}

// RuleGroupsRetriever RulesRetriever provides a list of active rules.
type RuleGroupsRetriever interface {
// RuleRetriever provides a list of active rules.
type RuleRetriever interface {
RuleGroups() []*rules.Group
AlertingRules() []*rules.AlertingRule
}

// API provides an HTTP API singleton for handling http endpoints in the rule evaluator.
type API struct {
rulesManager RuleGroupsRetriever
rulesManager RuleRetriever
logger log.Logger
}

// NewAPI creates a new API instance.
func NewAPI(logger log.Logger, rulesManager RuleGroupsRetriever) *API {
func NewAPI(logger log.Logger, rulesManager RuleRetriever) *API {
return &API{
rulesManager: rulesManager,
logger: logger,
Expand Down Expand Up @@ -110,3 +114,25 @@ func (api *API) writeError(w http.ResponseWriter, errType errorType, errMsg stri
Error: errMsg,
})
}

// alertsToAPIAlerts converts a slice of rules.Alert to a slice of apiv1.Alert.
func alertsToAPIAlerts(alerts []*rules.Alert) []*apiv1.Alert {
apiAlerts := make([]*apiv1.Alert, 0, len(alerts))
for _, ruleAlert := range alerts {
var keepFiringSince *time.Time
if !ruleAlert.KeepFiringSince.IsZero() {
keepFiringSince = &ruleAlert.KeepFiringSince
}

apiAlerts = append(apiAlerts, &apiv1.Alert{
Labels: ruleAlert.Labels,
Annotations: ruleAlert.Annotations,
State: ruleAlert.State.String(),
ActiveAt: &ruleAlert.ActiveAt,
KeepFiringSince: keepFiringSince,
Value: strconv.FormatFloat(ruleAlert.Value, 'e', -1, 64),
})
}

return apiAlerts
}
122 changes: 122 additions & 0 deletions cmd/rule-evaluator/internal/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2024 Google LLC
//
// 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 internal

import (
"testing"
"time"

"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/rules"
apiv1 "github.com/prometheus/prometheus/web/api/v1"
"github.com/stretchr/testify/assert"
)

func Test_rulesAlertsToAPIAlerts(t *testing.T) {
t.Parallel()
location, _ := time.LoadLocation("Europe/Stockholm")
activeAt := time.Date(1998, time.February, 1, 2, 3, 4, 567, location)
keepFiringSince := activeAt.Add(-time.Hour)

tests := []struct {
name string
rulesAlerts []*rules.Alert
want []*apiv1.Alert
}{
{
name: "empty rules alerts",
rulesAlerts: []*rules.Alert{},
want: []*apiv1.Alert{},
},
{
name: "happy path with two alerts",
rulesAlerts: []*rules.Alert{
{
Labels: []labels.Label{{Name: "alertname", Value: "test-alert-1"}, {Name: "instance", Value: "localhost:9090"}},
Annotations: []labels.Label{{Name: "summary", Value: "Test alert 1"}, {Name: "description", Value: "This is a test alert"}},
State: rules.StateFiring,
ActiveAt: activeAt,
KeepFiringSince: keepFiringSince,
Value: 1.23,
},
{
Labels: []labels.Label{{Name: "alertname", Value: "test-alert-1"}, {Name: "instance", Value: "localhost:9090"}},
Annotations: []labels.Label{{Name: "summary", Value: "Test alert 1"}, {Name: "description", Value: "This is a test alert"}},
State: rules.StatePending,
ActiveAt: activeAt,
KeepFiringSince: keepFiringSince,
Value: 1234234.24,
},
},
want: []*apiv1.Alert{
{
Labels: []labels.Label{{Name: "alertname", Value: "test-alert-1"}, {Name: "instance", Value: "localhost:9090"}},
Annotations: []labels.Label{{Name: "summary", Value: "Test alert 1"}, {Name: "description", Value: "This is a test alert"}},
State: "firing",
ActiveAt: &activeAt,
KeepFiringSince: &keepFiringSince,
Value: "1.23e+00",
},
{
Labels: []labels.Label{{Name: "alertname", Value: "test-alert-1"}, {Name: "instance", Value: "localhost:9090"}},
Annotations: []labels.Label{{Name: "summary", Value: "Test alert 1"}, {Name: "description", Value: "This is a test alert"}},
State: rules.StatePending.String(),
ActiveAt: &activeAt,
KeepFiringSince: &keepFiringSince,
Value: "1.23423424e+06",
},
},
},
{
name: "handlesZeroTime",
rulesAlerts: []*rules.Alert{
{
Labels: []labels.Label{{Name: "alertname", Value: "test-alert-1"}, {Name: "instance", Value: "localhost:9090"}},
Annotations: []labels.Label{{Name: "summary", Value: "Test alert 1"}, {Name: "description", Value: "This is a test alert"}},
State: rules.StateFiring,
ActiveAt: activeAt,
KeepFiringSince: time.Time{},
Value: 1.23,
},
},
want: []*apiv1.Alert{
{
Labels: []labels.Label{{Name: "alertname", Value: "test-alert-1"}, {Name: "instance", Value: "localhost:9090"}},
Annotations: []labels.Label{{Name: "summary", Value: "Test alert 1"}, {Name: "description", Value: "This is a test alert"}},
State: "firing",
ActiveAt: &activeAt,
KeepFiringSince: nil,
Value: "1.23e+00",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

result := alertsToAPIAlerts(tt.rulesAlerts)
assert.Len(t, result, len(tt.want))
for i := range result {
assert.Equal(t, tt.want[i].Labels, result[i].Labels)
assert.Equal(t, tt.want[i].Annotations, result[i].Annotations)
assert.Equal(t, tt.want[i].State, result[i].State)
assert.Equal(t, tt.want[i].ActiveAt, result[i].ActiveAt)
assert.Equal(t, tt.want[i].KeepFiringSince, result[i].KeepFiringSince)
assert.Equal(t, tt.want[i].Value, result[i].Value)
}
})
}
}
24 changes: 0 additions & 24 deletions cmd/rule-evaluator/internal/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ import (
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"

"github.com/go-kit/log/level"
"github.com/prometheus/prometheus/rules"
Expand Down Expand Up @@ -211,25 +209,3 @@ func alertingRuleToAPIRule(rule *rules.AlertingRule, shouldExcludeAlertsFromAler
Type: ruleKindAlerting,
}
}

// alertsToAPIAlerts converts a slice of rules.Alert to a slice of apiv1.Alert.
func alertsToAPIAlerts(alerts []*rules.Alert) []*apiv1.Alert {
apiAlerts := make([]*apiv1.Alert, len(alerts))
for i, ruleAlert := range alerts {
var keepFiringSince *time.Time
if !ruleAlert.KeepFiringSince.IsZero() {
keepFiringSince = &ruleAlert.KeepFiringSince
}

apiAlerts[i] = &apiv1.Alert{
Labels: ruleAlert.Labels,
Annotations: ruleAlert.Annotations,
State: ruleAlert.State.String(),
ActiveAt: &ruleAlert.ActiveAt,
KeepFiringSince: keepFiringSince,
Value: strconv.FormatFloat(ruleAlert.Value, 'e', -1, 64),
}
}

return apiAlerts
}
Loading

0 comments on commit cb718ba

Please sign in to comment.