From 528de314427bc958a04ea5a13124cf6aee7ead54 Mon Sep 17 00:00:00 2001 From: Ryan Chapin Date: Mon, 18 Mar 2024 18:22:05 +0000 Subject: [PATCH] Copy over required digest functions from notify --- .gitignore | 2 +- pkg/functions.go | 196 +++++++++++++++++++++++++++++++++++++++++++++ pkg/render_test.go | 2 +- pkg/types.go | 50 +++++++++++- 4 files changed, 246 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 6920eca..071f527 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ vendor/ # Output directory files -output/*.json +output/ diff --git a/pkg/functions.go b/pkg/functions.go index f0d138d..dada1aa 100644 --- a/pkg/functions.go +++ b/pkg/functions.go @@ -3,10 +3,14 @@ package main import ( "encoding/json" "fmt" + "math" "reflect" + "sort" "strings" "text/template" "time" + + "github.com/kentik/insights-api/pkg/greek" ) var TextTemplateFuncMap = template.FuncMap{ @@ -23,6 +27,11 @@ var TextTemplateFuncMap = template.FuncMap{ "timeRfc3339": timeRfc3339, "join": join, "joinWith": joinWith, + + "min": min, + "importanceToColor": importanceToColor, + "importanceToEmoji": importanceToEmoji, + "importanceLabel": importanceLabel, } func tryParseTime(input string) (time.Time, error) { @@ -112,3 +121,190 @@ func explodeJSONKeys(s string) string { } return "explodeJSONKeysErr" } + +func min(value, otherValue int) int { + if value < otherValue { + return value + } + return otherValue +} + +func importanceToColor(severity ViewModelImportance) string { + if color, ok := ImportanceToColors[severity]; ok { + return color + } + return "" +} + +func importanceToEmoji(severity ViewModelImportance) string { + if emoji, ok := ImportanceToEmojis[severity]; ok { + return emoji + } + return "" +} + +func importanceName(severity ViewModelImportance) string { + if label, ok := ImportanceNames[severity]; ok { + return label + } + return "" +} + +func importanceLabel(severity ViewModelImportance) string { + return strings.Title(importanceName(severity)) +} + +const ( + SYNTHETICS_BGP_ROUTE_PREFIX = "ktappprotocol__ktrac_bgp_updates__INET_01" + SYNTHETICS_BGP_PREFIX_LENGTH = "ktappprotocol__ktrac_bgp_updates__INT05" +) + +func (details EventViewModelDetails) FormatDimensions() EventViewModelDetails { + result := make(EventViewModelDetails, 0) + + // setup names we want to add to result + names := make(map[string]struct{}, len(details)) + for _, name := range details.Names() { + names[name] = struct{}{} + } + + // combine BGP route prefix & length + _, hasRoutePrefix := names[SYNTHETICS_BGP_ROUTE_PREFIX] + _, hasPrefixLength := names[SYNTHETICS_BGP_PREFIX_LENGTH] + if hasRoutePrefix && hasPrefixLength { + routePrefix := details.GetValue(SYNTHETICS_BGP_ROUTE_PREFIX) + prefixLength := details.GetValue(SYNTHETICS_BGP_PREFIX_LENGTH) + + combined := &EventViewModelDetail{ + Name: "combined_route_prefix_length", + Label: "Route Prefix/Length", + Tag: "dimension", + Value: fmt.Sprintf("%s/%s", routePrefix, prefixLength), + } + result = append(result, combined) + delete(names, SYNTHETICS_BGP_ROUTE_PREFIX) + delete(names, SYNTHETICS_BGP_PREFIX_LENGTH) + } + + // add the rest of details + for _, detail := range details { + if _, ok := names[detail.Name]; ok { + result = append(result, detail) + } + } + return result +} + +func (details EventViewModelDetails) FormatMetrics() EventViewModelDetails { + result := make(EventViewModelDetails, 0) + for _, detail := range details { + if detail.Tag != "metric" { + result = append(result, detail) + continue + } + + floatValue, err := toFloat(detail.Value) + if err != nil { + result = append(result, detail) + continue + } + + // prevent showing fractions when unnecessary + stringValue := fmt.Sprintf("%.2f", floatValue) + if _, fraction := math.Modf(floatValue); fraction < 0.05 { + stringValue = fmt.Sprintf("%.0f", floatValue) + } + + formatted := &EventViewModelDetail{ + Name: detail.Name, + Label: detail.Label, + Tag: detail.Tag, + Value: stringValue, + } + // format bits with "greek" bytes + if detail.Name == "bits" { + converted := strings.Split(greek.FormatBytesGreek(floatValue, "bits/s"), " ") + if len(converted) == 2 { + formatted.Label = converted[1] + formatted.Value = converted[0] + } + } + result = append(result, formatted) + } + return result +} + +func toFloat(value interface{}) (float64, error) { + switch v := value.(type) { + case float64: + return v, nil + case int: + return float64(v), nil + default: + return 0, fmt.Errorf("don't know how to convert %T to float64", v) + } +} + +func (vm *NotificationViewModel) EventsGroupedByImportance() []*EventGroupsByImportance { + importanceToEvent := make(map[ViewModelImportance][]*EventViewModel) + for _, event := range vm.RawEvents { + importanceToEvent[event.Importance] = append(importanceToEvent[event.Importance], event) + } + + byImportance := make([]*EventGroupsByImportance, 0, len(VieModelImportanceOrdered)) + for _, importance := range VieModelImportanceOrdered { + if len(importanceToEvent[importance]) <= 0 { + continue + } + byImportance = append(byImportance, vm.GetGroupsByImportance(importance, importanceToEvent[importance])) + } + + return byImportance +} + +func (vm *NotificationViewModel) GetGroupsByImportance(importance ViewModelImportance, events []*EventViewModel) *EventGroupsByImportance { + groupNameToEvents := make(map[string][]*EventViewModel) + + for _, event := range events { + groupNameToEvents[event.GroupName] = append(groupNameToEvents[event.GroupName], event) + } + + groups := make([]*EventGroup, 0, len(groupNameToEvents)) + for groupName, groupEvents := range groupNameToEvents { + // sort each group by abstract normalized date + sort.Slice(groupEvents, func(i, j int) bool { + return groupEvents[i].StartTime < groupEvents[j].StartTime // RFC3339 to the rescue + }) + groups = append(groups, &EventGroup{ + Name: groupName, + Url: vm.getGroupUrl(groupEvents), + Events: groupEvents, + }) + } + + return &EventGroupsByImportance{ + Importance: importance, + Groups: groups, + Count: len(events), + } +} + +func (vm *NotificationViewModel) getGroupUrl(events []*EventViewModel) string { + event := events[0] + if event.IsAlarm() { + if event.Details.Has("DashboardAlarmURL") { + return fmt.Sprintf("%v", event.Details.GetValue("DashboardAlarmURL")) + } + return fmt.Sprintf("%v", event.Details.GetValue("AttackLogURL")) + } + if event.IsSynthetic() { + return fmt.Sprintf("%v", event.Details.GetValue("SyntheticsTestURL")) + } + if event.IsMitigation() { + return fmt.Sprintf("%v", event.Details.GetValue("MitigationURL")) + } + if event.IsInsight() { + return fmt.Sprintf("%v", event.Details.GetValue("InsightsSeverityURL")) + } + return "" +} diff --git a/pkg/render_test.go b/pkg/render_test.go index 4d37a1f..60c976b 100644 --- a/pkg/render_test.go +++ b/pkg/render_test.go @@ -60,7 +60,7 @@ func Test_AllExamples_Render(t *testing.T) { result := buf.Bytes() outputPath := fmt.Sprintf("../output/%s-%s", modelName, strings.TrimSuffix(entry.Name, ".tmpl")) - ioutil.WriteFile(outputPath, result, 0644) + ioutil.WriteFile(outputPath, result, 0o644) if entry.IsJson { var jsonValue interface{} diff --git a/pkg/types.go b/pkg/types.go index 015176c..24a6220 100644 --- a/pkg/types.go +++ b/pkg/types.go @@ -41,6 +41,51 @@ var VieModelImportanceOrdered = [...]ViewModelImportance{ ViewModelImportance_None, } +var ImportanceNames = map[ViewModelImportance]string{ + ViewModelImportance_None: "n/a", + ViewModelImportance_Healthy: "healthy", + ViewModelImportance_Notice: "notice", + ViewModelImportance_Minor: "minor", + ViewModelImportance_Warning: "warning", + ViewModelImportance_Major: "major", + ViewModelImportance_Severe: "severe", + ViewModelImportance_Critical: "critical", +} + +var ImportanceToColors = map[ViewModelImportance]string{ + ViewModelImportance_None: "#999999", + ViewModelImportance_Healthy: "#1E9E1E", + ViewModelImportance_Notice: "#157FF3", + ViewModelImportance_Minor: "#F29D49", + ViewModelImportance_Warning: "#EE7E0F", + ViewModelImportance_Major: "#DB3737", + ViewModelImportance_Severe: "#C23030", + ViewModelImportance_Critical: "#A82A2A", +} + +var ImportanceToEmojis = map[ViewModelImportance]string{ + ViewModelImportance_None: "", + ViewModelImportance_Healthy: ":warning:", + ViewModelImportance_Notice: ":warning:", + ViewModelImportance_Minor: ":warning:", + ViewModelImportance_Warning: ":warning:", + ViewModelImportance_Major: ":warning:", + ViewModelImportance_Severe: ":warning:", + ViewModelImportance_Critical: ":warning:", +} + +type EventGroupsByImportance struct { + Importance ViewModelImportance + Groups []*EventGroup + Count int +} + +type EventGroup struct { + Name string + Url string + Events []*EventViewModel +} + // use "export" key instead of standard json key when marshalling/unmarshalling using jsoniter (https://github.com/json-iterator/go), // so fields are not removed per standard json tag type EventViewModel struct { @@ -202,8 +247,9 @@ type NotificationViewModel struct { Config *NotificationViewConfig `json:"-" export:"Config"` } type NotificationViewConfig struct { - BaseDomain string - EmailTo []string + BaseDomain string + EmailTo []string + IsChannelDigest bool } func (vm *NotificationViewModel) BasePortalURL() string {