mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
325 lines
8.6 KiB
Go
325 lines
8.6 KiB
Go
package models
|
|
|
|
import (
|
|
"cmp"
|
|
"slices"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/cespare/xxhash/v2"
|
|
"github.com/fvbommel/sortorder"
|
|
"github.com/go-json-experiment/json/jsontext"
|
|
"github.com/prometheus/prometheus/model/labels"
|
|
|
|
"github.com/prymitive/karma/internal/config"
|
|
)
|
|
|
|
// AlertState encodes the state of an alert as a compact uint8.
|
|
type AlertState uint8
|
|
|
|
const (
|
|
// AlertStateUnprocessed means that Alertmanager notify didn't yet process it
|
|
// and AM doesn't know if alert is active or suppressed
|
|
AlertStateUnprocessed AlertState = iota
|
|
// AlertStateActive is the state in which we know that the alert should fire
|
|
AlertStateActive
|
|
// AlertStateSuppressed means that we know that alert is silenced or inhibited
|
|
AlertStateSuppressed
|
|
)
|
|
|
|
// AlertStateList exports all alert states so other packages can get this list
|
|
var AlertStateList = []AlertState{
|
|
AlertStateUnprocessed,
|
|
AlertStateActive,
|
|
AlertStateSuppressed,
|
|
}
|
|
|
|
var alertStateToString = [3]string{"unprocessed", "active", "suppressed"}
|
|
|
|
var alertStateFromString = map[string]AlertState{
|
|
"unprocessed": AlertStateUnprocessed,
|
|
"active": AlertStateActive,
|
|
"suppressed": AlertStateSuppressed,
|
|
}
|
|
|
|
func (s AlertState) String() string {
|
|
if int(s) < len(alertStateToString) {
|
|
return alertStateToString[s]
|
|
}
|
|
return "unprocessed"
|
|
}
|
|
|
|
func (s AlertState) MarshalJSON() ([]byte, error) {
|
|
return jsontext.AppendQuote(nil, s.String())
|
|
}
|
|
|
|
func (s *AlertState) UnmarshalJSON(data []byte) error {
|
|
unquoted, err := jsontext.AppendUnquote(nil, data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*s = ParseAlertState(string(unquoted))
|
|
return nil
|
|
}
|
|
|
|
// MarshalText implements encoding.TextMarshaler so AlertState can be used as
|
|
// a JSON map key.
|
|
func (s AlertState) MarshalText() ([]byte, error) {
|
|
return []byte(s.String()), nil
|
|
}
|
|
|
|
// UnmarshalText implements encoding.TextUnmarshaler so AlertState can be used
|
|
// as a JSON map key.
|
|
func (s *AlertState) UnmarshalText(data []byte) error {
|
|
*s = ParseAlertState(string(data))
|
|
return nil
|
|
}
|
|
|
|
// AlertStateFromString looks up an AlertState by its string representation.
|
|
// Returns the state and true if found, or (AlertStateUnprocessed, false) otherwise.
|
|
func AlertStateFromString(s string) (AlertState, bool) {
|
|
v, ok := alertStateFromString[s]
|
|
return v, ok
|
|
}
|
|
|
|
// ParseAlertState converts a string to an AlertState.
|
|
func ParseAlertState(s string) AlertState {
|
|
if v, ok := alertStateFromString[s]; ok {
|
|
return v
|
|
}
|
|
return AlertStateUnprocessed
|
|
}
|
|
|
|
// OrderedLabel mirrors labels.Label for JSON serialization in the
|
|
// [{"name":"...","value":"..."}] format expected by the frontend.
|
|
type OrderedLabel struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
// OrderedLabels is a slice of OrderedLabel used for JSON serialization.
|
|
// It preserves the display order configured via config.Config.Labels.Order.
|
|
type OrderedLabels []OrderedLabel
|
|
|
|
func (ol OrderedLabels) Get(name string) string {
|
|
for _, l := range ol {
|
|
if l.Name == name {
|
|
return l.Value
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (ol OrderedLabels) MarshalJSONTo(enc *jsontext.Encoder) error {
|
|
w := jsonWriter{enc: enc}
|
|
w.beginArray()
|
|
for _, l := range ol {
|
|
w.beginObject()
|
|
w.key("name")
|
|
w.str(l.Name)
|
|
w.key("value")
|
|
w.str(l.Value)
|
|
w.endObject()
|
|
}
|
|
w.endArray()
|
|
return w.err
|
|
}
|
|
|
|
// LabelsToOrderedLabels converts prometheus Labels to OrderedLabels sorted
|
|
// by the configured display order.
|
|
func LabelsToOrderedLabels(ls labels.Labels) OrderedLabels {
|
|
dl := make(OrderedLabels, 0, ls.Len())
|
|
ls.Range(func(l labels.Label) {
|
|
dl = append(dl, OrderedLabel{Name: l.Name, Value: l.Value})
|
|
})
|
|
slices.SortFunc(dl, CompareOrderedLabels)
|
|
return dl
|
|
}
|
|
|
|
// CompareOrderedLabels sorts display labels by the configured label order,
|
|
// then by name, then by value using natural sort.
|
|
func CompareOrderedLabels(a, b OrderedLabel) int {
|
|
ai, bi := -1, -1
|
|
for index, name := range config.Config.Labels.Order {
|
|
if a.Name == name {
|
|
ai = index
|
|
} else if b.Name == name {
|
|
bi = index
|
|
}
|
|
if ai >= 0 && bi >= 0 {
|
|
return cmp.Compare(ai, bi)
|
|
}
|
|
}
|
|
if ai != bi {
|
|
return cmp.Compare(bi, ai)
|
|
}
|
|
if a.Name == b.Name {
|
|
if sortorder.NaturalLess(a.Value, b.Value) {
|
|
return -1
|
|
}
|
|
if sortorder.NaturalLess(b.Value, a.Value) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
if sortorder.NaturalLess(a.Name, b.Name) {
|
|
return -1
|
|
}
|
|
if sortorder.NaturalLess(b.Name, a.Name) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// LabelsFromMap creates a Labels from a map, adding only keys not already present.
|
|
func LabelsFromMap(m map[string]string) labels.Labels {
|
|
return labels.FromMap(m)
|
|
}
|
|
|
|
// LabelsSetIfMissing returns a new Labels with the given name/value added only if
|
|
// the name is not already present.
|
|
func LabelsSetIfMissing(ls labels.Labels, name, value string) labels.Labels {
|
|
if ls.Has(name) {
|
|
return ls
|
|
}
|
|
b := labels.NewBuilder(ls)
|
|
b.Set(name, value)
|
|
return b.Labels()
|
|
}
|
|
|
|
// Alert is vanilla alert + some additional attributes
|
|
// karma extends an alert object with:
|
|
// - Links map, it's generated from annotations if annotation value is an url
|
|
// it's pulled out of annotation map and returned under links field,
|
|
// karma UI used this to show links differently than other annotations
|
|
type Alert struct {
|
|
StartsAt time.Time `json:"startsAt"`
|
|
Fingerprint string `json:"-"`
|
|
GeneratorURL string `json:"-"`
|
|
Receiver string `json:"receiver"`
|
|
LabelsFP string `json:"id"`
|
|
contentFP string
|
|
Labels labels.Labels `json:"-"`
|
|
Annotations Annotations `json:"annotations"`
|
|
SilencedBy []string `json:"-"`
|
|
InhibitedBy []string `json:"-"`
|
|
Alertmanager []AlertmanagerInstance `json:"alertmanager"`
|
|
State AlertState `json:"state"`
|
|
}
|
|
|
|
// APIAlert is the JSON-serializable representation of Alert.
|
|
// Labels are converted to OrderedLabels for the frontend.
|
|
type APIAlert struct {
|
|
StartsAt time.Time `json:"startsAt"`
|
|
State string `json:"state"`
|
|
Receiver string `json:"receiver"`
|
|
LabelsFP string `json:"id"`
|
|
Annotations Annotations `json:"annotations"`
|
|
Labels OrderedLabels `json:"labels"`
|
|
Alertmanager []AlertmanagerInstance `json:"alertmanager"`
|
|
}
|
|
|
|
func (a APIAlert) MarshalJSONTo(enc *jsontext.Encoder) error {
|
|
w := jsonWriter{enc: enc}
|
|
a.marshalTo(&w)
|
|
return w.err
|
|
}
|
|
|
|
func (a *APIAlert) marshalTo(w *jsonWriter) {
|
|
w.beginObject()
|
|
w.key("startsAt")
|
|
w.time(a.StartsAt)
|
|
w.key("state")
|
|
w.str(a.State)
|
|
w.key("receiver")
|
|
w.str(a.Receiver)
|
|
w.key("id")
|
|
w.str(a.LabelsFP)
|
|
w.key("annotations")
|
|
w.beginArray()
|
|
for i := range a.Annotations {
|
|
a.Annotations[i].marshalTo(w)
|
|
}
|
|
w.endArray()
|
|
w.key("labels")
|
|
w.beginArray()
|
|
for _, l := range a.Labels {
|
|
w.beginObject()
|
|
w.key("name")
|
|
w.str(l.Name)
|
|
w.key("value")
|
|
w.str(l.Value)
|
|
w.endObject()
|
|
}
|
|
w.endArray()
|
|
w.key("alertmanager")
|
|
w.beginArray()
|
|
for i := range a.Alertmanager {
|
|
a.Alertmanager[i].marshalTo(w)
|
|
}
|
|
w.endArray()
|
|
w.endObject()
|
|
}
|
|
|
|
var seps = []byte{'\xff'}
|
|
|
|
// UpdateFingerprints will generate a new set of fingerprints for this alert
|
|
func (a *Alert) UpdateFingerprints() {
|
|
labelsHash := a.Labels.Hash()
|
|
a.LabelsFP = strconv.FormatUint(labelsHash, 16)
|
|
|
|
h := xxhash.New()
|
|
for _, a := range a.Annotations {
|
|
_, _ = h.WriteString(a.Name)
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(a.Value)
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(strconv.FormatBool(a.IsAction))
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(strconv.FormatBool(a.IsLink))
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(strconv.FormatBool(a.Visible))
|
|
_, _ = h.Write(seps)
|
|
|
|
}
|
|
_, _ = h.WriteString(strconv.FormatUint(labelsHash, 16))
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(a.StartsAt.Format(time.RFC3339))
|
|
_, _ = h.WriteString(a.State.String())
|
|
for _, am := range a.Alertmanager {
|
|
_, _ = h.WriteString(am.Fingerprint)
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(am.Name)
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(am.Cluster)
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(am.State.String())
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(am.StartsAt.Format(time.RFC3339))
|
|
_, _ = h.Write(seps)
|
|
_, _ = h.WriteString(am.Source)
|
|
_, _ = h.Write(seps)
|
|
for _, s := range am.SilencedBy {
|
|
_, _ = h.WriteString(s)
|
|
_, _ = h.Write(seps)
|
|
}
|
|
for _, s := range am.InhibitedBy {
|
|
_, _ = h.WriteString(s)
|
|
_, _ = h.Write(seps)
|
|
}
|
|
}
|
|
_, _ = h.WriteString(a.Receiver)
|
|
a.contentFP = strconv.FormatUint(h.Sum64(), 16)
|
|
}
|
|
|
|
// LabelsFingerprint is a checksum computed only from labels which should be
|
|
// unique for every alert
|
|
func (a *Alert) LabelsFingerprint() string {
|
|
return a.LabelsFP
|
|
}
|
|
|
|
// ContentFingerprint is a checksum computed from entire alert object
|
|
func (a *Alert) ContentFingerprint() string {
|
|
return a.contentFP
|
|
}
|