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 }