cmd/rule-evaluator/internal/api.go (108 lines of code) (raw):
// 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 (
"encoding/json"
"net/http"
"sort"
"strconv"
"strings"
"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
// Copied from github.com/prometheus/prometheus/web/api/v1.
const (
errorNone errorType = ""
errorTimeout errorType = "timeout"
errorCanceled errorType = "canceled"
errorExec errorType = "execution"
errorBadData errorType = "bad_data"
errorInternal errorType = "internal"
errorUnavailable errorType = "unavailable"
errorNotFound errorType = "not_found"
)
// https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview
// status is the prometheus-compatible status type.
type status string
const (
statusSuccess status = "success"
statusError status = "error"
)
// https://prometheus.io/docs/prometheus/latest/querying/api/#format-overview
// response is the prometheus-compatible response format.
type response struct {
Status status `json:"status"`
Data interface{} `json:"data,omitempty"`
ErrorType errorType `json:"errorType,omitempty"`
Error string `json:"error,omitempty"`
Warnings []string `json:"warnings,omitempty"`
Infos []string `json:"infos,omitempty"`
}
// 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 RuleRetriever
logger log.Logger
}
// NewAPI creates a new API instance.
func NewAPI(logger log.Logger, rulesManager RuleRetriever) *API {
return &API{
rulesManager: rulesManager,
logger: logger,
}
}
func (api *API) writeResponse(w http.ResponseWriter, httpResponseCode int, endpointURI string, resp response) {
logger := log.With(api.logger, "endpointURI", endpointURI, "intendedStatusCode", httpResponseCode)
w.Header().Set("Content-Type", "application/json")
jsonResponse, err := json.Marshal(resp)
if err != nil {
_ = level.Error(logger).Log("msg", "failed to marshal response", "err", err)
w.WriteHeader(http.StatusInternalServerError)
if _, err = w.Write([]byte(`{"status":"error","errorType":"internal","error":"failed to marshal response"}`)); err != nil {
_ = level.Error(logger).Log("msg", "failed to write error response to responseWriter", "err", err)
}
}
w.WriteHeader(httpResponseCode)
if _, err = w.Write(jsonResponse); err != nil {
_ = level.Error(logger).Log("msg", "failed to write response to responseWriter", "err", err)
}
}
func (api *API) writeSuccessResponse(w http.ResponseWriter, httpResponseCode int, endpointURI string, data interface{}) {
api.writeResponse(w, httpResponseCode, endpointURI, response{
Status: statusSuccess,
Data: data,
})
}
// writeError writes an error response to the client if it can, otherwise it logs the error and writes a generic error.
func (api *API) writeError(w http.ResponseWriter, errType errorType, errMsg string, httpResponseCode int, endpointURI string) {
api.writeResponse(w, httpResponseCode, endpointURI, response{
Status: statusError,
ErrorType: errType,
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),
})
}
// Sort for testability.
sort.Slice(apiAlerts, func(i, j int) bool {
a, b := apiAlerts[i].Labels.Hash(), apiAlerts[j].Labels.Hash()
if a == b {
a, b = apiAlerts[i].Annotations.Hash(), apiAlerts[j].Annotations.Hash()
}
if a == b {
return strings.Compare(apiAlerts[i].State, apiAlerts[j].State) < 0 // firing before pending.
}
return a >= b
})
return apiAlerts
}