diff --git a/api/controller/contact.go b/api/controller/contact.go index a07ebf09a..61f08cad8 100644 --- a/api/controller/contact.go +++ b/api/controller/contact.go @@ -125,11 +125,10 @@ func RemoveContact(database moira.Database, contactID string, userLogin string) // SendTestContactNotification push test notification to verify the correct contact settings func SendTestContactNotification(dataBase moira.Database, contactID string) *api.ErrorResponse { - var value float64 = 1 eventData := &moira.NotificationEvent{ ContactID: contactID, Metric: "Test.metric.value", - Value: &value, + Values: map[string]float64{"t1": 1}, OldState: moira.StateTEST, State: moira.StateTEST, Timestamp: date.DateParamToEpoch("now", "", time.Now().Add(-24*time.Hour).Unix(), time.UTC), diff --git a/api/controller/subscription.go b/api/controller/subscription.go index aaa8578cc..b006605a4 100644 --- a/api/controller/subscription.go +++ b/api/controller/subscription.go @@ -81,11 +81,10 @@ func RemoveSubscription(database moira.Database, subscriptionID string) *api.Err // SendTestNotification push test notification to verify the correct notification settings func SendTestNotification(database moira.Database, subscriptionID string) *api.ErrorResponse { - var value float64 = 1 eventData := &moira.NotificationEvent{ SubscriptionID: &subscriptionID, Metric: "Test.metric.value", - Value: &value, + Values: map[string]float64{"t1": 1}, OldState: moira.StateTEST, State: moira.StateTEST, Timestamp: date.DateParamToEpoch("now", "", time.Now().Add(-24*time.Hour).Unix(), time.UTC), diff --git a/api/controller/trigger_metrics.go b/api/controller/trigger_metrics.go index f8c1fc7bd..a2a7c11b5 100644 --- a/api/controller/trigger_metrics.go +++ b/api/controller/trigger_metrics.go @@ -12,27 +12,25 @@ import ( // GetTriggerEvaluationResult evaluates every target in trigger and returns // result, separated on main and additional targets metrics -func GetTriggerEvaluationResult(dataBase moira.Database, metricSourceProvider *metricSource.SourceProvider, from, to int64, triggerID string, fetchRealtimeData bool) (*metricSource.TriggerMetricsData, *moira.Trigger, error) { +func GetTriggerEvaluationResult(dataBase moira.Database, metricSourceProvider *metricSource.SourceProvider, from, to int64, triggerID string, fetchRealtimeData bool) (metricSource.FetchedMetrics, *moira.Trigger, error) { trigger, err := dataBase.GetTrigger(triggerID) if err != nil { return nil, nil, err } - triggerMetrics := metricSource.NewTriggerMetricsData() + triggerMetrics := metricSource.NewFetchedMetricsWithCapacity(0) metricsSource, err := metricSourceProvider.GetTriggerMetricSource(&trigger) if err != nil { return nil, &trigger, err } - for i, tar := range trigger.Targets { - fetchResult, err := metricsSource.Fetch(tar, from, to, fetchRealtimeData) + for i, target := range trigger.Targets { + i++ // Increase counter to have trigger names start from t1 + fetchResult, err := metricsSource.Fetch(target, from, to, fetchRealtimeData) if err != nil { return nil, &trigger, err } metricData := fetchResult.GetMetricsData() - if i == 0 { - triggerMetrics.Main = metricData - } else { - triggerMetrics.Additional = append(triggerMetrics.Additional, metricData...) - } + + triggerMetrics.AddMetrics(i, metricData) } return triggerMetrics, &trigger, nil } @@ -57,31 +55,22 @@ func GetTriggerMetrics(dataBase moira.Database, metricSourceProvider *metricSour } return nil, api.ErrorInternalServer(err) } - triggerMetrics := dto.TriggerMetrics{ - Main: make(map[string][]*moira.MetricValue), - Additional: make(map[string][]*moira.MetricValue), - } - for _, timeSeries := range tts.Main { - values := make([]*moira.MetricValue, 0) - for i := 0; i < len(timeSeries.Values); i++ { - timestamp := timeSeries.StartTime + int64(i)*timeSeries.StepTime - value := timeSeries.GetTimestampValue(timestamp) - if moira.IsValidFloat64(value) { - values = append(values, &moira.MetricValue{Value: value, Timestamp: timestamp}) - } - } - triggerMetrics.Main[timeSeries.Name] = values - } - for _, timeSeries := range tts.Additional { - values := make([]*moira.MetricValue, 0) - for i := 0; i < len(timeSeries.Values); i++ { - timestamp := timeSeries.StartTime + int64(i)*timeSeries.StepTime - value := timeSeries.GetTimestampValue(timestamp) - if moira.IsValidFloat64(value) { - values = append(values, &moira.MetricValue{Value: value, Timestamp: timestamp}) + triggerMetrics := make(dto.TriggerMetrics) + + for targetName, target := range tts { + targetMetrics := make(map[string][]moira.MetricValue) + for _, timeSeries := range target { + values := make([]moira.MetricValue, 0) + for i, l := 0, len(timeSeries.Values); i < l; i++ { + timestamp := timeSeries.StartTime + int64(i)*timeSeries.StepTime + value := timeSeries.GetTimestampValue(timestamp) + if moira.IsValidFloat64(value) { + values = append(values, moira.MetricValue{Value: value, Timestamp: timestamp}) + } } + targetMetrics[timeSeries.Name] = values } - triggerMetrics.Additional[timeSeries.Name] = values + triggerMetrics[targetName] = targetMetrics } return &triggerMetrics, nil } diff --git a/api/controller/trigger_metrics_test.go b/api/controller/trigger_metrics_test.go index 537b0a464..89b512847 100644 --- a/api/controller/trigger_metrics_test.go +++ b/api/controller/trigger_metrics_test.go @@ -297,10 +297,10 @@ func TestGetTriggerMetrics(t *testing.T) { dataBase.EXPECT().GetTrigger(triggerID).Return(moira.Trigger{ID: triggerID, Targets: []string{pattern}}, nil) localSource.EXPECT().IsConfigured().Return(true, nil) localSource.EXPECT().Fetch(pattern, from, until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)}) + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{*metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)}) triggerMetrics, err := GetTriggerMetrics(dataBase, sourceProvider, from, until, triggerID) So(err, ShouldBeNil) - So(*triggerMetrics, ShouldResemble, dto.TriggerMetrics{Main: map[string][]*moira.MetricValue{metric: {{Value: 0, Timestamp: 17}, {Value: 1, Timestamp: 27}, {Value: 2, Timestamp: 37}, {Value: 3, Timestamp: 47}, {Value: 4, Timestamp: 57}}}, Additional: make(map[string][]*moira.MetricValue)}) + So(*triggerMetrics, ShouldResemble, dto.TriggerMetrics{"t1": map[string][]moira.MetricValue{metric: {{Value: 0, Timestamp: 17}, {Value: 1, Timestamp: 27}, {Value: 2, Timestamp: 37}, {Value: 3, Timestamp: 47}, {Value: 4, Timestamp: 57}}}}) }) Convey("GetTrigger error", t, func() { diff --git a/api/dto/triggers.go b/api/dto/triggers.go index 6f3ea2019..b3707df0f 100644 --- a/api/dto/triggers.go +++ b/api/dto/triggers.go @@ -4,6 +4,8 @@ package dto import ( "fmt" "net/http" + "regexp" + "strconv" "time" "github.com/moira-alert/moira" @@ -13,6 +15,8 @@ import ( metricSource "github.com/moira-alert/moira/metric_source" ) +var targetNameRegex = regexp.MustCompile("t(\\d+)") + type TriggersList struct { Page *int64 `json:"page,omitempty"` Size *int64 `json:"size,omitempty"` @@ -61,10 +65,16 @@ type TriggerModel struct { IsRemote bool `json:"is_remote"` // If true, first event NODATA → OK will be omitted MuteNewMetrics bool `json:"mute_new_metrics"` + // A list of targets that have only alone metrics + AloneMetrics []string `json:"alone_metrics"` } // ToMoiraTrigger transforms TriggerModel to moira.Trigger func (model *TriggerModel) ToMoiraTrigger() *moira.Trigger { + aloneMetrics := make(map[string]bool, len(model.AloneMetrics)) + for _, targetName := range model.AloneMetrics{ + aloneMetrics[targetName] = true + } return &moira.Trigger{ ID: model.ID, Name: model.Name, @@ -81,6 +91,7 @@ func (model *TriggerModel) ToMoiraTrigger() *moira.Trigger { Patterns: model.Patterns, IsRemote: model.IsRemote, MuteNewMetrics: model.MuteNewMetrics, + AloneMetrics: aloneMetrics, } } @@ -119,6 +130,19 @@ func (trigger *Trigger) Bind(request *http.Request) error { if err := checkWarnErrorExpression(trigger); err != nil { return api.ErrInvalidRequestContent{ValidationError: err} } + for _, targetName := range trigger.AloneMetrics { + if !targetNameRegex.MatchString(targetName) { + return api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("alone metrics target name should be in pattern: t\\d+")} + } + targetIndexStr := targetNameRegex.FindStringSubmatch(targetName)[0] + targetIndex, err := strconv.Atoi(targetIndexStr) + if err != nil { + return api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("alone metrics target index should be valid number: %w", err)} + } + if targetIndex < 0 || targetIndex > len(trigger.Targets) { + return api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("alone metrics target index should be in range from 1 to length of targets")} + } + } triggerExpression := expression.TriggerExpression{ AdditionalTargetsValues: make(map[string]float64), @@ -305,10 +329,7 @@ func (*SaveTriggerResponse) Render(w http.ResponseWriter, r *http.Request) error return nil } -type TriggerMetrics struct { - Main map[string][]*moira.MetricValue `json:"main"` - Additional map[string][]*moira.MetricValue `json:"additional,omitempty"` -} +type TriggerMetrics map[string]map[string][]moira.MetricValue func (*TriggerMetrics) Render(w http.ResponseWriter, r *http.Request) error { return nil diff --git a/api/dto/triggers_test.go b/api/dto/triggers_test.go index e69c6bd65..1217ff8ad 100644 --- a/api/dto/triggers_test.go +++ b/api/dto/triggers_test.go @@ -30,7 +30,7 @@ func TestExpressionModeMultipleTargetsWarnValue(t *testing.T) { localSource.EXPECT().IsConfigured().Return(true, nil).AnyTimes() localSource.EXPECT().Fetch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(fetchResult, nil).AnyTimes() fetchResult.EXPECT().GetPatterns().Return(make([]string, 0), nil).AnyTimes() - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData("", []float64{}, 0, 0)}).AnyTimes() + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{*metricSource.MakeMetricData("", []float64{}, 0, 0)}).AnyTimes() request, _ := http.NewRequest("PUT", "/api/trigger", nil) request.Header.Set("Content-Type", "application/json") diff --git a/api/handler/trigger.go b/api/handler/trigger.go index 5ded55727..5dae7fc24 100644 --- a/api/handler/trigger.go +++ b/api/handler/trigger.go @@ -29,7 +29,7 @@ func trigger(router chi.Router) { }) router.Route("/metrics", triggerMetrics) router.Put("/setMaintenance", setTriggerMaintenance) - router.With(middleware.DateRange("-1hour", "now")).Get("/render", renderTrigger) + router.With(middleware.DateRange("-1hour", "now")).With(middleware.TargetName("t1")).Get("/render", renderTrigger) } func updateTrigger(writer http.ResponseWriter, request *http.Request) { diff --git a/api/handler/trigger_render.go b/api/handler/trigger_render.go index 4d115277b..8604f453a 100644 --- a/api/handler/trigger_render.go +++ b/api/handler/trigger_render.go @@ -18,17 +18,23 @@ import ( ) func renderTrigger(writer http.ResponseWriter, request *http.Request) { - sourceProvider, from, to, triggerID, fetchRealtimeData, err := getEvaluationParameters(request) + sourceProvider, targetName, from, to, triggerID, fetchRealtimeData, err := getEvaluationParameters(request) if err != nil { render.Render(writer, request, api.ErrorInvalidRequest(err)) return } - metricsData, trigger, err := evaluateTriggerMetrics(sourceProvider, from, to, triggerID, fetchRealtimeData) + metricsData, trigger, err := evaluateTargetMetrics(sourceProvider, from, to, triggerID, fetchRealtimeData) if err != nil { render.Render(writer, request, api.ErrorInternalServer(err)) return } - renderable, err := buildRenderable(request, trigger, metricsData) + + targetMetrics, ok := metricsData[targetName] + if !ok { + render.Render(writer, request, api.ErrorNotFound(fmt.Sprintf("Cannot find target %s", targetName))) + } + + renderable, err := buildRenderable(request, trigger, targetMetrics, targetName) if err != nil { render.Render(writer, request, api.ErrorInternalServer(err)) return @@ -40,19 +46,21 @@ func renderTrigger(writer http.ResponseWriter, request *http.Request) { } } -func getEvaluationParameters(request *http.Request) (sourceProvider *metricSource.SourceProvider, from int64, to int64, triggerID string, fetchRealtimeData bool, err error) { +func getEvaluationParameters(request *http.Request) (sourceProvider *metricSource.SourceProvider, targetName string, from int64, to int64, triggerID string, fetchRealtimeData bool, err error) { sourceProvider = middleware.GetTriggerTargetsSourceProvider(request) + targetName = middleware.GetTargetName(request) triggerID = middleware.GetTriggerID(request) fromStr := middleware.GetFromStr(request) toStr := middleware.GetToStr(request) from = date.DateParamToEpoch(fromStr, "UTC", 0, time.UTC) + if from == 0 { - return sourceProvider, 0, 0, "", false, fmt.Errorf("can not parse from: %s", fromStr) + return sourceProvider, "", 0, 0, "", false, fmt.Errorf("can not parse from: %s", fromStr) } from -= from % 60 to = date.DateParamToEpoch(toStr, "UTC", 0, time.UTC) if to == 0 { - return sourceProvider, 0, 0, "", false, fmt.Errorf("can not parse to: %s", fromStr) + return sourceProvider, "", 0, 0, "", false, fmt.Errorf("can not parse to: %s", fromStr) } realtime := request.URL.Query().Get("realtime") if realtime == "" { @@ -60,23 +68,21 @@ func getEvaluationParameters(request *http.Request) (sourceProvider *metricSourc } fetchRealtimeData, err = strconv.ParseBool(realtime) if err != nil { - return sourceProvider, 0, 0, "", false, fmt.Errorf("invalid realtime param: %s", err.Error()) + return sourceProvider, "", 0, 0, "", false, fmt.Errorf("invalid realtime param: %s", err.Error()) } return } -func evaluateTriggerMetrics(metricSourceProvider *metricSource.SourceProvider, from, to int64, triggerID string, fetchRealtimeData bool) ([]*metricSource.MetricData, *moira.Trigger, error) { +func evaluateTargetMetrics(metricSourceProvider *metricSource.SourceProvider, from, to int64, triggerID string, fetchRealtimeData bool) (metricSource.FetchedMetrics, *moira.Trigger, error) { tts, trigger, err := controller.GetTriggerEvaluationResult(database, metricSourceProvider, from, to, triggerID, fetchRealtimeData) if err != nil { return nil, trigger, err } - var metricsData = make([]*metricSource.MetricData, 0, len(tts.Main)+len(tts.Additional)) - metricsData = append(metricsData, tts.Main...) - metricsData = append(metricsData, tts.Additional...) - return metricsData, trigger, err + + return tts, trigger, err } -func buildRenderable(request *http.Request, trigger *moira.Trigger, metricsData []*metricSource.MetricData) (*chart.Chart, error) { +func buildRenderable(request *http.Request, trigger *moira.Trigger, metricsData []metricSource.MetricData, targetName string) (*chart.Chart, error) { timezone := request.URL.Query().Get("timezone") location, err := time.LoadLocation(timezone) if err != nil { @@ -87,7 +93,7 @@ func buildRenderable(request *http.Request, trigger *moira.Trigger, metricsData if err != nil { return nil, fmt.Errorf("can not initialize plot theme %s", err.Error()) } - renderable, err := plotTemplate.GetRenderable(trigger, metricsData) + renderable, err := plotTemplate.GetRenderable(targetName, trigger, metricsData) if err != nil { return nil, err } diff --git a/api/middleware/context.go b/api/middleware/context.go index 92855d7c4..65726534a 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -143,3 +143,17 @@ func DateRange(defaultFrom, defaultTo string) func(next http.Handler) http.Handl }) } } + +// TargetName is a function that gets target name value from query string and places it in context. If query does not have value sets given value. +func TargetName(defaultTargetName string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + targetName := request.URL.Query().Get("target") + if targetName == "" { + targetName = defaultTargetName + } + ctx := context.WithValue(request.Context(), targetNameKey, targetName) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index 1682e94c3..1a28d63ca 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -29,6 +29,7 @@ var ( loginKey ContextKey = "login" timeSeriesNamesKey ContextKey = "timeSeriesNames" metricSourceProvider ContextKey = "metricSourceProvider" + targetNameKey ContextKey = "target" ) // GetDatabase gets moira.Database realization from request context @@ -96,3 +97,8 @@ func GetTimeSeriesNames(request *http.Request) map[string]bool { func GetTriggerTargetsSourceProvider(request *http.Request) *metricSource.SourceProvider { return request.Context().Value(metricSourceProvider).(*metricSource.SourceProvider) } + +// GetTargetName gets target name +func GetTargetName(request *http.Request) string { + return request.Context().Value(targetNameKey).(string) +} diff --git a/checker/check.go b/checker/check.go index f24f26c1c..a069f0f10 100644 --- a/checker/check.go +++ b/checker/check.go @@ -10,54 +10,149 @@ import ( "github.com/moira-alert/moira/metric_source/remote" ) -var ( +const ( + secondsInHour int64 = 3600 checkPointGap int64 = 120 ) // Check handle trigger and last check and write new state of trigger, if state were change then write new NotificationEvent func (triggerChecker *TriggerChecker) Check() error { + passError := false triggerChecker.logger.Debugf("Checking trigger %s", triggerChecker.triggerID) - checkData, err := triggerChecker.checkTrigger() + checkData := newCheckData(triggerChecker.lastCheck, triggerChecker.until) + triggerMetricsData, err := triggerChecker.fetchTriggerMetrics() + if err != nil { + return triggerChecker.handleFetchError(checkData, err) + } - checkData, err = triggerChecker.handleCheckResult(checkData, err) + preparedMetrics, aloneMetrics, err := triggerChecker.prepareMetrics(triggerMetricsData) if err != nil { - return err + passError, checkData, err = triggerChecker.handlePrepareError(checkData, err) + if !passError { + return err + } + } + + checkData.MetricsToTargetRelation = aloneMetrics.GetRelations() + checkData, err = triggerChecker.check(preparedMetrics, aloneMetrics, checkData) + if err != nil { + return triggerChecker.handleUndefinedError(checkData, err) } + if !passError { + checkData.State = moira.StateOK + } + checkData.LastSuccessfulCheckTimestamp = checkData.Timestamp + if checkData.LastSuccessfulCheckTimestamp != 0 { + checkData, err = triggerChecker.compareTriggerStates(checkData) + if err != nil { + return err + } + } checkData.UpdateScore() return triggerChecker.database.SetTriggerLastCheck(triggerChecker.triggerID, &checkData, triggerChecker.trigger.IsRemote) } -func (triggerChecker *TriggerChecker) checkTrigger() (moira.CheckData, error) { - checkData := newCheckData(triggerChecker.lastCheck, triggerChecker.until) - triggerMetricsData, err := triggerChecker.fetchTriggerMetrics() +// handlePrepareError is a function that checks error returned from prepareMetrics function. If error +// is not serious and check process can be continued first return value became true and Filled CheckData returned. +// in the other case first return value became true and error passed to this function is handled. +func (triggerChecker *TriggerChecker) handlePrepareError(checkData moira.CheckData, err error) (bool, moira.CheckData, error) { + switch err.(type) { + case ErrTriggerHasSameMetricNames: + checkData.State = moira.StateERROR + checkData.Message = err.Error() + return true, checkData, nil + case ErrUnexpectedAloneMetric: + checkData.State = moira.StateEXCEPTION + checkData.Message = err.Error() + default: + return false, checkData, triggerChecker.handleUndefinedError(checkData, err) + } + + checkData, err = triggerChecker.compareTriggerStates(checkData) if err != nil { - return checkData, err + return false, checkData, err } - return triggerChecker.checkTriggerMetrics(triggerMetricsData, checkData) + checkData.UpdateScore() + return true, checkData, triggerChecker.database.SetTriggerLastCheck(triggerChecker.triggerID, &checkData, triggerChecker.trigger.IsRemote) +} + +// handleFetchError is a function that checks error returned from fetchTriggerMetrics function. +func (triggerChecker *TriggerChecker) handleFetchError(checkData moira.CheckData, err error) error { + switch err.(type) { + case ErrTargetHasNoMetrics, ErrTriggerHasOnlyWildcards: + triggerChecker.logger.Debugf("Trigger %s: %s", triggerChecker.triggerID, err.Error()) + triggerState := triggerChecker.ttlState.ToTriggerState() + checkData.State = triggerState + checkData.Message = err.Error() + if triggerChecker.ttl == 0 { + // Do not alert when user don't wanna receive + // NODATA state alerts, but change trigger status + checkData.UpdateScore() + return triggerChecker.database.SetTriggerLastCheck(triggerChecker.triggerID, &checkData, triggerChecker.trigger.IsRemote) + } + case remote.ErrRemoteTriggerResponse: + timeSinceLastSuccessfulCheck := checkData.Timestamp - checkData.LastSuccessfulCheckTimestamp + if timeSinceLastSuccessfulCheck >= triggerChecker.ttl { + checkData.State = moira.StateEXCEPTION + checkData.Message = fmt.Sprintf("Remote server unavailable. Trigger is not checked for %d seconds", timeSinceLastSuccessfulCheck) + checkData, err = triggerChecker.compareTriggerStates(checkData) + } + triggerChecker.logger.Errorf("Trigger %s: %s", triggerChecker.triggerID, err.Error()) + case local.ErrUnknownFunction, local.ErrEvalExpr: + checkData.State = moira.StateEXCEPTION + checkData.Message = err.Error() + triggerChecker.logger.Warningf("Trigger %s: %s", triggerChecker.triggerID, err.Error()) + default: + return triggerChecker.handleUndefinedError(checkData, err) + } + checkData, err = triggerChecker.compareTriggerStates(checkData) + if err != nil { + return err + } + checkData.UpdateScore() + return triggerChecker.database.SetTriggerLastCheck(triggerChecker.triggerID, &checkData, triggerChecker.trigger.IsRemote) +} + +// handleUndefinedError is a function that check error with undefined type. +func (triggerChecker *TriggerChecker) handleUndefinedError(checkData moira.CheckData, err error) error { + triggerChecker.metrics.CheckError.Mark(1) + triggerChecker.logger.Errorf("Trigger %s check failed: %s", triggerChecker.triggerID, err.Error()) + checkData, err = triggerChecker.compareTriggerStates(checkData) + if err != nil { + return err + } + checkData.UpdateScore() + return triggerChecker.database.SetTriggerLastCheck(triggerChecker.triggerID, &checkData, triggerChecker.trigger.IsRemote) } // Set new last check timestamp that equal to "until" targets fetch interval -// Do not copy message, if will be set if needed +// Do not copy message, it will be set if needed func newCheckData(lastCheck *moira.CheckData, checkTimeStamp int64) moira.CheckData { + lastMetrics := make(map[string]moira.MetricState, len(lastCheck.Metrics)) for k, v := range lastCheck.Metrics { lastMetrics[k] = v } + metricsToTargetRelation := make(map[string]string, len(lastCheck.MetricsToTargetRelation)) + for k, v := range lastCheck.MetricsToTargetRelation { + metricsToTargetRelation[k] = v + } newCheckData := *lastCheck newCheckData.Metrics = lastMetrics newCheckData.Timestamp = checkTimeStamp + newCheckData.MetricsToTargetRelation = metricsToTargetRelation newCheckData.Message = "" return newCheckData } -func newMetricState(oldMetricState moira.MetricState, newState moira.State, newTimestamp int64, newValue *float64) *moira.MetricState { +func newMetricState(oldMetricState moira.MetricState, newState moira.State, newTimestamp int64, newValues map[string]float64) *moira.MetricState { newMetricState := oldMetricState // This field always changed in every metric check operation newMetricState.State = newState newMetricState.Timestamp = newTimestamp - newMetricState.Value = newValue + newMetricState.Values = newValues // Always set. This fields only changed by user actions newMetricState.Maintenance = oldMetricState.Maintenance @@ -73,132 +168,107 @@ func newMetricState(oldMetricState moira.MetricState, newState moira.State, newT return &newMetricState } -func (triggerChecker *TriggerChecker) checkTriggerMetrics(triggerMetricsData *metricSource.TriggerMetricsData, checkData moira.CheckData) (moira.CheckData, error) { - metricsDataToCheck, duplicateError := triggerChecker.getMetricsToCheck(triggerMetricsData.Main) - - for _, metricData := range metricsDataToCheck { - triggerChecker.logger.Debugf("[TriggerID:%s] Checking metricData %s: %v", triggerChecker.triggerID, metricData.Name, metricData.Values) - triggerChecker.logger.Debugf("[TriggerID:%s][MetricName:%s] Checking interval: %v - %v (%vs), step: %v", triggerChecker.triggerID, metricData.Name, metricData.StartTime, metricData.StopTime, metricData.StepTime, metricData.StopTime-metricData.StartTime) - - metricState, needToDeleteMetric, err := triggerChecker.checkMetricData(metricData, triggerMetricsData) - if needToDeleteMetric { - triggerChecker.logger.Infof("[TriggerID:%s] Remove metric: '%s'", triggerChecker.triggerID, metricData.Name) - delete(checkData.Metrics, metricData.Name) - err = triggerChecker.database.RemovePatternsMetrics(triggerChecker.trigger.Patterns) - } else { - checkData.Metrics[metricData.Name] = metricState - } - if err != nil { - return checkData, err +// prepareMetrics is a function that takes fetched metrics and prepare it to check. +// The sequence of check is following: +// Call preparePatternMetrics that converts fetched metrics to TriggerPatternMetrics -> +// Populate metrics -> +// Filter alone metrics -> +// Check that targets with alone metrics declared in trigger -> +// Convert to TriggerMetricsToCheck +func (triggerChecker *TriggerChecker) prepareMetrics(fetchedMetrics metricSource.FetchedMetrics) (metricSource.TriggerMetricsToCheck, metricSource.MetricsToCheck, error) { + preparedPatternMetrics := metricSource.NewTriggerMetricsWithCapacity(len(fetchedMetrics)) + duplicates := make(map[string][]string) + + for targetName, patternMetrics := range fetchedMetrics { + prepared, patternDuplicates := triggerChecker.preparePatternMetrics(patternMetrics) + preparedPatternMetrics[targetName] = prepared + if len(patternDuplicates) > 0 { + duplicates[targetName] = patternDuplicates } } - return checkData, duplicateError -} -func (triggerChecker *TriggerChecker) getMetricsToCheck(fetchedMetrics []*metricSource.MetricData) ([]*metricSource.MetricData, error) { - metricNamesHash := make(map[string]struct{}, len(fetchedMetrics)) - duplicateNames := make([]string, 0) - metricsToCheck := make([]*metricSource.MetricData, 0, len(fetchedMetrics)) - lastCheckMetricNamesHash := make(map[string]struct{}, len(triggerChecker.lastCheck.Metrics)) - for metricName := range triggerChecker.lastCheck.Metrics { - lastCheckMetricNamesHash[metricName] = struct{}{} + populated := preparedPatternMetrics.Populate(*triggerChecker.lastCheck, triggerChecker.from, triggerChecker.until) + + multiMetricTargets, aloneMetrics := populated.FilterAloneMetrics() + + if len(aloneMetrics) != len(triggerChecker.trigger.AloneMetrics) { + return nil, nil, NewErrUnexpectedAloneMetric(triggerChecker.trigger.AloneMetrics, aloneMetrics.GetRelations()) } - for _, metricData := range fetchedMetrics { - if metricData.Wildcard { - continue + for targetName := range aloneMetrics.GetRelations() { + if !triggerChecker.trigger.AloneMetrics[targetName] { + return nil, nil, NewErrUnexpectedAloneMetric(triggerChecker.trigger.AloneMetrics, aloneMetrics.GetRelations()) } - if _, ok := metricNamesHash[metricData.Name]; ok { - triggerChecker.logger.Debugf("[TriggerID:%s][MetricName:%s] Trigger has same metric names", triggerChecker.triggerID, metricData.Name) - duplicateNames = append(duplicateNames, metricData.Name) - continue - } - metricNamesHash[metricData.Name] = struct{}{} - metricsToCheck = append(metricsToCheck, metricData) - delete(lastCheckMetricNamesHash, metricData.Name) } - for metricName := range lastCheckMetricNamesHash { - step := int64(60) - if len(fetchedMetrics) > 0 && fetchedMetrics[0].StepTime != 0 { - step = fetchedMetrics[0].StepTime - } - metricData := metricSource.MakeEmptyMetricData(metricName, step, triggerChecker.from, triggerChecker.until) - metricsToCheck = append(metricsToCheck, metricData) + converted := multiMetricTargets.ConvertForCheck() + if len(duplicates) > 0 { + return converted, aloneMetrics, NewErrTriggerHasSameMetricNames(duplicates) } + return converted, aloneMetrics, nil +} + +// preparePatternMetrics is a function that takes PatternMetrics and applies following operations on it: +// PatternMetrics -> +// Remove wildcards -> +// Remove duplicated metrics and collect the names of duplicated metrics -> +// Convert to TriggerPatternMetrics +func (triggerChecker *TriggerChecker) preparePatternMetrics(fetchedMetrics metricSource.FetchedPatternMetrics) (metricSource.TriggerPatternMetrics, []string) { + withoutWildcards := fetchedMetrics.CleanWildcards() + deduplicated, duplicates := withoutWildcards.Deduplicate() - if len(duplicateNames) > 0 { - return metricsToCheck, ErrTriggerHasSameMetricNames{names: duplicateNames} + result := metricSource.NewTriggerPatternMetrics(deduplicated) + + return result, duplicates +} + +// check is the function that handles check on prepared metrics. +func (triggerChecker *TriggerChecker) check(metrics metricSource.TriggerMetricsToCheck, aloneMetrics metricSource.MetricsToCheck, checkData moira.CheckData) (moira.CheckData, error) { + if len(metrics) == 0 { // Case when trigger have only alone metrics + metricName := aloneMetrics.MetricName() + metrics[metricName] = make(metricSource.MetricsToCheck) + } + for metricName, targets := range metrics { + triggerChecker.logger.Debugf("[TriggerID:%s] Checking metrics %s", triggerChecker.triggerID, metricName) // TODO(litleleprikon): Add structured logging instead of [Field:Value] + targets = targets.Merge(aloneMetrics) + metricState, needToDeleteMetric, err := triggerChecker.checkTargets(metricName, targets) + if needToDeleteMetric { + triggerChecker.logger.Infof("[TriggerID:%s] Remove metric: '%s'", triggerChecker.triggerID, metricName) + delete(checkData.Metrics, metricName) // TODO(litleleprikon): change to RemoveMetric method of CheckData + err = triggerChecker.database.RemovePatternsMetrics(triggerChecker.trigger.Patterns) + } else { + checkData.Metrics[metricName] = metricState + } + if err != nil { + return checkData, err + } } - return metricsToCheck, nil + return checkData, nil } -func (triggerChecker *TriggerChecker) checkMetricData(metricData *metricSource.MetricData, triggerMetricsData *metricSource.TriggerMetricsData) (lastState moira.MetricState, needToDeleteMetric bool, err error) { - lastState = triggerChecker.lastCheck.GetOrCreateMetricState(metricData.Name, metricData.StartTime-3600, triggerChecker.trigger.MuteNewMetrics) - metricStates, err := triggerChecker.getMetricStepsStates(triggerMetricsData, metricData, lastState) +// checkTargets is a Function that takes a +func (triggerChecker *TriggerChecker) checkTargets(metricName string, metrics metricSource.MetricsToCheck) (lastState moira.MetricState, needToDeleteMetric bool, err error) { + lastState, metricStates, err := triggerChecker.getMetricStepsStates(metricName, metrics) if err != nil { - return + return lastState, needToDeleteMetric, err } for _, currentState := range metricStates { - lastState, err = triggerChecker.compareMetricStates(metricData.Name, currentState, lastState) + lastState, err = triggerChecker.compareMetricStates(metricName, currentState, lastState) if err != nil { - return + return lastState, needToDeleteMetric, err } } - needToDeleteMetric, noDataState := triggerChecker.checkForNoData(metricData, lastState) + needToDeleteMetric, noDataState := triggerChecker.checkForNoData(metricName, lastState) if needToDeleteMetric { - return + return lastState, needToDeleteMetric, err } if noDataState != nil { - lastState, err = triggerChecker.compareMetricStates(metricData.Name, *noDataState, lastState) + lastState, err = triggerChecker.compareMetricStates(metricName, *noDataState, lastState) } - return + return lastState, needToDeleteMetric, err } -func (triggerChecker *TriggerChecker) handleCheckResult(checkData moira.CheckData, checkingError error) (moira.CheckData, error) { - if checkingError == nil { - checkData.State = moira.StateOK - if checkData.LastSuccessfulCheckTimestamp == 0 { - checkData.LastSuccessfulCheckTimestamp = checkData.Timestamp - return checkData, nil - } - checkData.LastSuccessfulCheckTimestamp = checkData.Timestamp - return triggerChecker.compareTriggerStates(checkData) - } - - switch checkingError.(type) { - case ErrTriggerHasNoMetrics, ErrTriggerHasOnlyWildcards: - triggerChecker.logger.Debugf("Trigger %s: %s", triggerChecker.triggerID, checkingError.Error()) - triggerState := triggerChecker.ttlState.ToTriggerState() - checkData.State = triggerState - checkData.Message = checkingError.Error() - if triggerChecker.ttl == 0 { - // Do not alert when user don't wanna receive - // NODATA state alerts, but change trigger status - return checkData, nil - } - case ErrWrongTriggerTargets, ErrTriggerHasSameMetricNames: - checkData.State = moira.StateERROR - checkData.Message = checkingError.Error() - case remote.ErrRemoteTriggerResponse: - timeSinceLastSuccessfulCheck := checkData.Timestamp - checkData.LastSuccessfulCheckTimestamp - if timeSinceLastSuccessfulCheck >= triggerChecker.ttl { - checkData.State = moira.StateEXCEPTION - checkData.Message = fmt.Sprintf("Remote server unavailable. Trigger is not checked for %d seconds", timeSinceLastSuccessfulCheck) - } - triggerChecker.logger.Errorf("Trigger %s: %s", triggerChecker.triggerID, checkingError.Error()) - case local.ErrUnknownFunction, local.ErrEvalExpr: - checkData.State = moira.StateEXCEPTION - checkData.Message = checkingError.Error() - triggerChecker.logger.Warningf("Trigger %s: %s", triggerChecker.triggerID, checkingError.Error()) - default: - triggerChecker.metrics.CheckError.Mark(1) - triggerChecker.logger.Errorf("Trigger %s check failed: %s", triggerChecker.triggerID, checkingError.Error()) - } - return triggerChecker.compareTriggerStates(checkData) -} - -func (triggerChecker *TriggerChecker) checkForNoData(metricData *metricSource.MetricData, metricLastState moira.MetricState) (bool, *moira.MetricState) { +func (triggerChecker *TriggerChecker) checkForNoData(metricName string, metricLastState moira.MetricState) (bool, *moira.MetricState) { if triggerChecker.ttl == 0 { return false, nil } @@ -207,7 +277,7 @@ func (triggerChecker *TriggerChecker) checkForNoData(metricData *metricSource.Me if metricLastState.Timestamp+triggerChecker.ttl >= lastCheckTimeStamp { return false, nil } - triggerChecker.logger.Debugf("[TriggerID:%s][MetricName:%s] Metric TTL expired for state %v", triggerChecker.triggerID, metricData.Name, metricLastState) + triggerChecker.logger.Debugf("[TriggerID:%s][MetricName:%s] Metric TTL expired for state %v", triggerChecker.triggerID, metricName, metricLastState) if triggerChecker.ttlState == moira.TTLStateDEL && metricLastState.EventTimestamp != 0 { return true, nil } @@ -215,42 +285,50 @@ func (triggerChecker *TriggerChecker) checkForNoData(metricData *metricSource.Me metricLastState, triggerChecker.ttlState.ToMetricState(), lastCheckTimeStamp, - nil, + map[string]float64{}, ) } -func (triggerChecker *TriggerChecker) getMetricStepsStates(triggerMetricsData *metricSource.TriggerMetricsData, metricData *metricSource.MetricData, metricLastState moira.MetricState) ([]moira.MetricState, error) { - startTime := metricData.StartTime - stepTime := metricData.StepTime +func (triggerChecker *TriggerChecker) getMetricStepsStates(metricName string, metrics metricSource.MetricsToCheck) (last moira.MetricState, current []moira.MetricState, err error) { + var startTime int64 + var stepTime int64 + + for _, metric := range metrics { // Taking values from any metric + last = triggerChecker.lastCheck.GetOrCreateMetricState(metricName, metric.StartTime-secondsInHour, triggerChecker.trigger.MuteNewMetrics) + startTime = metric.StartTime + stepTime = metric.StepTime + break + } - checkPoint := metricLastState.GetCheckPoint(checkPointGap) - triggerChecker.logger.Debugf("[TriggerID:%s][MetricName:%s] Checkpoint: %v", triggerChecker.triggerID, metricData.Name, checkPoint) + checkPoint := last.GetCheckPoint(checkPointGap) + triggerChecker.logger.Debugf("[TriggerID:%s][MetricName:%s] Checkpoint: %v", triggerChecker.triggerID, metricName, checkPoint) - metricStates := make([]moira.MetricState, 0) + current = make([]moira.MetricState, 0) + previousState := last for valueTimestamp := startTime; valueTimestamp < triggerChecker.until+stepTime; valueTimestamp += stepTime { - metricNewState, err := triggerChecker.getMetricDataState(triggerMetricsData, metricData, metricLastState, valueTimestamp, checkPoint) + metricNewState, err := triggerChecker.getMetricDataState(metricName, metrics, previousState, valueTimestamp, checkPoint) if err != nil { - return nil, err + return last, current, err } if metricNewState == nil { continue } - metricLastState = *metricNewState - metricStates = append(metricStates, *metricNewState) + previousState = *metricNewState + current = append(current, *metricNewState) } - return metricStates, nil + return last, current, nil } -func (triggerChecker *TriggerChecker) getMetricDataState(triggerMetricsData *metricSource.TriggerMetricsData, metricData *metricSource.MetricData, lastState moira.MetricState, valueTimestamp, checkPoint int64) (*moira.MetricState, error) { +func (triggerChecker *TriggerChecker) getMetricDataState(metricName string, metrics metricSource.MetricsToCheck, lastState moira.MetricState, valueTimestamp, checkPoint int64) (*moira.MetricState, error) { if valueTimestamp <= checkPoint { return nil, nil } - triggerExpression, noEmptyValues := getExpressionValues(triggerMetricsData, metricData, valueTimestamp) + triggerExpression, values, noEmptyValues := getExpressionValues(metrics, valueTimestamp) if !noEmptyValues { return nil, nil } - triggerChecker.logger.Debugf("[TriggerID:%s][MetricName:%s] Values for ts %v: MainTargetValue: %v, additionalTargetValues: %v", triggerChecker.triggerID, metricData.Name, valueTimestamp, triggerExpression.MainTargetValue, triggerExpression.AdditionalTargetsValues) + triggerChecker.logger.Debugf("[TriggerID:%s][MetricName:%s] Values for ts %v: MainTargetValue: %v, additionalTargetValues: %v", triggerChecker.triggerID, metricName, valueTimestamp, triggerExpression.MainTargetValue, triggerExpression.AdditionalTargetsValues) triggerExpression.WarnValue = triggerChecker.trigger.WarnValue triggerExpression.ErrorValue = triggerChecker.trigger.ErrorValue @@ -267,30 +345,29 @@ func (triggerChecker *TriggerChecker) getMetricDataState(triggerMetricsData *met lastState, expressionState, valueTimestamp, - &triggerExpression.MainTargetValue, + values, ), nil } -func getExpressionValues(triggerMetricsData *metricSource.TriggerMetricsData, firstTargetMetricData *metricSource.MetricData, valueTimestamp int64) (*expression.TriggerExpression, bool) { - expressionValues := &expression.TriggerExpression{ - AdditionalTargetsValues: make(map[string]float64, len(triggerMetricsData.Additional)), +func getExpressionValues(metrics metricSource.MetricsToCheck, valueTimestamp int64) (*expression.TriggerExpression, map[string]float64, bool) { + expression := &expression.TriggerExpression{ + AdditionalTargetsValues: make(map[string]float64, len(metrics)-1), } - firstTargetValue := firstTargetMetricData.GetTimestampValue(valueTimestamp) - if !moira.IsValidFloat64(firstTargetValue) { - return expressionValues, false - } - expressionValues.MainTargetValue = firstTargetValue - - for targetNumber := 0; targetNumber < len(triggerMetricsData.Additional); targetNumber++ { - additionalMetricData := triggerMetricsData.Additional[targetNumber] - if additionalMetricData == nil { - return expressionValues, false + values := make(map[string]float64, len(metrics)) + + for i := 0; i < len(metrics); i++ { + targetName := fmt.Sprintf("t%d", i+1) + metric := metrics[targetName] + value := metric.GetTimestampValue(valueTimestamp) + values[targetName] = value + if !moira.IsValidFloat64(value) { + return expression, values, false } - tnValue := additionalMetricData.GetTimestampValue(valueTimestamp) - if !moira.IsValidFloat64(tnValue) { - return expressionValues, false + if i == 0 { + expression.MainTargetValue = value + continue } - expressionValues.AdditionalTargetsValues[triggerMetricsData.GetAdditionalTargetName(targetNumber)] = tnValue + expression.AdditionalTargetsValues[targetName] = value } - return expressionValues, true + return expression, values, true } diff --git a/checker/check_test.go b/checker/check_test.go index dac39437d..ff56ec14d 100644 --- a/checker/check_test.go +++ b/checker/check_test.go @@ -8,9 +8,9 @@ import ( "github.com/golang/mock/gomock" "github.com/moira-alert/moira" + "github.com/moira-alert/moira/expression" metricSource "github.com/moira-alert/moira/metric_source" "github.com/moira-alert/moira/metric_source/local" - "github.com/moira-alert/moira/metric_source/remote" mock_metric_source "github.com/moira-alert/moira/mock/metric_source" mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" "github.com/op/go-logging" @@ -35,44 +35,44 @@ func TestGetMetricDataState(t *testing.T) { TriggerType: moira.RisingTrigger, }, } - metricData := metricSource.MetricData{ + metricName := "main.metric" + metricT1 := metricSource.MetricData{ Name: "main.metric", StartTime: triggerChecker.from, StopTime: triggerChecker.until, StepTime: 10, Values: []float64{1, math.NaN(), 3, 4, math.NaN()}, } - addMetricData := metricSource.MetricData{ - Name: "additional.metric", + metricT2 := metricSource.MetricData{ + Name: "main.metric", StartTime: triggerChecker.from, StopTime: triggerChecker.until, StepTime: 10, Values: []float64{math.NaN(), 4, 3, 2, 1}, } - addMetricData.Name = "additional.metric" - tts := metricSource.MakeTriggerMetricsData( - []*metricSource.MetricData{&metricData}, - []*metricSource.MetricData{&addMetricData}, - ) + metrics := metricSource.MetricsToCheck{ + "t1": metricT1, + "t2": metricT2, + } metricLastState := moira.MetricState{ Maintenance: 11111, Suppressed: true, } Convey("Checkpoint more than valueTimestamp", t, func() { - metricState, err := triggerChecker.getMetricDataState(tts, tts.Main[0], metricLastState, 37, 47) + metricState, err := triggerChecker.getMetricDataState(metricName, metrics, metricLastState, 37, 47) So(err, ShouldBeNil) So(metricState, ShouldBeNil) }) Convey("Checkpoint lover than valueTimestamp", t, func() { Convey("Has all value by eventTimestamp step", func() { - metricState, err := triggerChecker.getMetricDataState(tts, tts.Main[0], metricLastState, 42, 27) + metricState, err := triggerChecker.getMetricDataState(metricName, metrics, metricLastState, 42, 27) So(err, ShouldBeNil) So(metricState, ShouldResemble, &moira.MetricState{ State: moira.StateOK, Timestamp: 42, - Value: &metricData.Values[2], + Values: map[string]float64{"t1": 3, "t2": 3}, Maintenance: metricLastState.Maintenance, Suppressed: metricLastState.Suppressed, EventTimestamp: 0, @@ -80,19 +80,19 @@ func TestGetMetricDataState(t *testing.T) { }) Convey("No value in main metric data by eventTimestamp step", func() { - metricState, err := triggerChecker.getMetricDataState(tts, tts.Main[0], metricLastState, 66, 11) + metricState, err := triggerChecker.getMetricDataState(metricName, metrics, metricLastState, 66, 11) So(err, ShouldBeNil) So(metricState, ShouldBeNil) }) Convey("IsAbsent in main metric data by eventTimestamp step", func() { - metricState, err := triggerChecker.getMetricDataState(tts, tts.Main[0], metricLastState, 29, 11) + metricState, err := triggerChecker.getMetricDataState(metricName, metrics, metricLastState, 29, 11) So(err, ShouldBeNil) So(metricState, ShouldBeNil) }) Convey("No value in additional metric data by eventTimestamp step", func() { - metricState, err := triggerChecker.getMetricDataState(tts, tts.Main[0], metricLastState, 26, 11) + metricState, err := triggerChecker.getMetricDataState(metricName, metrics, metricLastState, 26, 11) So(err, ShouldBeNil) So(metricState, ShouldBeNil) }) @@ -101,116 +101,208 @@ func TestGetMetricDataState(t *testing.T) { Convey("No warn and error value with default expression", t, func() { triggerChecker.trigger.WarnValue = nil triggerChecker.trigger.ErrorValue = nil - metricState, err := triggerChecker.getMetricDataState(tts, tts.Main[0], metricLastState, 42, 27) + metricState, err := triggerChecker.getMetricDataState(metricName, metrics, metricLastState, 42, 27) So(err.Error(), ShouldResemble, "error value and warning value can not be empty") So(metricState, ShouldBeNil) }) } -func TestGetMetricsDataToCheck(t *testing.T) { +func TestTriggerChecker_PrepareMetrics(t *testing.T) { logger, _ := logging.GetLogger("Test") - Convey("Get metrics data to check:", t, func() { + Convey("Prepare metrics for check:", t, func() { triggerChecker := TriggerChecker{ triggerID: "ID", logger: logger, from: 0, until: 60, lastCheck: &moira.CheckData{}, + trigger: &moira.Trigger{ + AloneMetrics: map[string]bool{}, + }, } Convey("last check has no metrics", func() { Convey("fetched metrics is empty", func() { - actual, err := triggerChecker.getMetricsToCheck([]*metricSource.MetricData{}) - So(actual, ShouldHaveLength, 0) + prepared, alone, err := triggerChecker.prepareMetrics(metricSource.FetchedMetrics{}) + So(prepared, ShouldHaveLength, 0) + So(alone, ShouldHaveLength, 0) So(err, ShouldBeNil) }) Convey("fetched metrics has metrics", func() { - actual, err := triggerChecker.getMetricsToCheck([]*metricSource.MetricData{metricSource.MakeMetricData("123", []float64{1, 2, 3}, 10, 0)}) - So(actual, ShouldHaveLength, 1) + triggerChecker.trigger.AloneMetrics = map[string]bool{"t1": true} + fetched := metricSource.FetchedMetrics{ + "t1": metricSource.FetchedPatternMetrics{ + *metricSource.MakeMetricData("123", []float64{1, 2, 3}, 10, 0), + }, + } + prepared, alone, err := triggerChecker.prepareMetrics(fetched) + So(prepared, ShouldHaveLength, 0) + So(alone, ShouldHaveLength, 1) So(err, ShouldBeNil) }) Convey("fetched metrics has duplicate metrics", func() { - actual, err := triggerChecker.getMetricsToCheck( - []*metricSource.MetricData{ - metricSource.MakeMetricData("123", []float64{1, 2, 3}, 10, 0), - metricSource.MakeMetricData("123", []float64{4, 5, 6}, 10, 0), - }) - So(actual, ShouldResemble, []*metricSource.MetricData{metricSource.MakeMetricData("123", []float64{1, 2, 3}, 10, 0)}) - So(err, ShouldResemble, ErrTriggerHasSameMetricNames{names: []string{"123"}}) + triggerChecker.trigger.AloneMetrics = map[string]bool{"t1": true} + fetched := metricSource.FetchedMetrics{ + "t1": metricSource.FetchedPatternMetrics{ + *metricSource.MakeMetricData("123", []float64{1, 2, 3}, 10, 0), + *metricSource.MakeMetricData("123", []float64{4, 5, 6}, 10, 0), + }, + } + prepared, alone, err := triggerChecker.prepareMetrics(fetched) + So(prepared, ShouldHaveLength, 0) + So(alone, ShouldResemble, metricSource.MetricsToCheck{"t1": *metricSource.MakeMetricData("123", []float64{1, 2, 3}, 10, 0)}) + So(err, ShouldResemble, ErrTriggerHasSameMetricNames{duplicates: map[string][]string{"t1": []string{"123"}}}) + }) + + Convey("Targets have different metrics", func() { + fetched := metricSource.FetchedMetrics{ + "t1": metricSource.FetchedPatternMetrics{ + *metricSource.MakeMetricData("first.metric", []float64{1, 2, 3}, 10, 0), + *metricSource.MakeMetricData("second.metric", []float64{4, 5, 6}, 10, 0), + *metricSource.MakeMetricData("third.metric", []float64{4, 5, 6}, 10, 0), + }, + "t2": metricSource.FetchedPatternMetrics{ + *metricSource.MakeMetricData("second.metric", []float64{4, 5, 6}, 10, 0), + *metricSource.MakeMetricData("third.metric", []float64{4, 5, 6}, 10, 0), + }, + } + prepared, alone, err := triggerChecker.prepareMetrics(fetched) + So(prepared, ShouldHaveLength, 3) + So(prepared["first.metric"], ShouldNotBeNil) + So(prepared["first.metric"], ShouldHaveLength, 2) + So(prepared["second.metric"], ShouldNotBeNil) + So(prepared["second.metric"], ShouldHaveLength, 2) + So(prepared["third.metric"], ShouldNotBeNil) + So(prepared["third.metric"], ShouldHaveLength, 2) + So(alone, ShouldBeEmpty) + So(err, ShouldBeNil) }) }) Convey("last check has metrics", func() { triggerChecker.lastCheck = &moira.CheckData{ Metrics: map[string]moira.MetricState{ - "first": {}, - "second": {}, - "third": {}, + "first": {Values: map[string]float64{"t1": 0}}, + "second": {Values: map[string]float64{"t1": 0}}, + "third": {Values: map[string]float64{"t1": 0}}, }} + Convey("last check has aloneMetrics", func() { + triggerChecker.trigger.AloneMetrics = map[string]bool{"t2": true} + triggerChecker.lastCheck = &moira.CheckData{ + MetricsToTargetRelation: map[string]string{"t2": "alone"}, + Metrics: map[string]moira.MetricState{ + "first": {Values: map[string]float64{"t1": 0, "t2": 0}}, + "second": {Values: map[string]float64{"t1": 0, "t2": 0}}, + "third": {Values: map[string]float64{"t1": 0, "t2": 0}}, + }} + Convey("fetched metrics is empty", func() { + triggerChecker.trigger.AloneMetrics = map[string]bool{"t2": true} + prepared, alone, err := triggerChecker.prepareMetrics(metricSource.FetchedMetrics{}) + So(prepared, ShouldHaveLength, 3) + for _, actualMetricData := range prepared["t1"] { + So(actualMetricData.Values, ShouldHaveLength, 1) + So(actualMetricData.StepTime, ShouldResemble, int64(60)) + So(actualMetricData.StartTime, ShouldResemble, int64(0)) + So(actualMetricData.StopTime, ShouldResemble, int64(60)) + } + So(alone, ShouldHaveLength, 1) + aloneMetric := alone["t2"] + So(aloneMetric.Name, ShouldEqual, "alone") + So(aloneMetric.Values, ShouldHaveLength, 1) + So(aloneMetric.StepTime, ShouldResemble, int64(60)) + So(aloneMetric.StartTime, ShouldResemble, int64(0)) + So(aloneMetric.StopTime, ShouldResemble, int64(60)) + + So(err, ShouldBeNil) + }) + }) Convey("fetched metrics is empty", func() { - actual, err := triggerChecker.getMetricsToCheck([]*metricSource.MetricData{}) - So(actual, ShouldHaveLength, 3) - for _, actualMetricData := range actual { + prepared, alone, err := triggerChecker.prepareMetrics(metricSource.FetchedMetrics{}) + So(prepared, ShouldHaveLength, 3) + for _, actualMetricData := range prepared["t1"] { So(actualMetricData.Values, ShouldHaveLength, 1) So(actualMetricData.StepTime, ShouldResemble, int64(60)) So(actualMetricData.StartTime, ShouldResemble, int64(0)) So(actualMetricData.StopTime, ShouldResemble, int64(60)) } + So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) - Convey("fetched metrics has only wildcards, step is 0", func() { - actual, err := triggerChecker.getMetricsToCheck([]*metricSource.MetricData{{Name: "wildcard", Wildcard: true}}) - So(actual, ShouldHaveLength, 3) - for _, actualMetricData := range actual { + prepared, alone, err := triggerChecker.prepareMetrics( + metricSource.FetchedMetrics{ + "t1": metricSource.FetchedPatternMetrics{ + metricSource.MetricData{Name: "wildcard", + Wildcard: true, + }, + }, + }) + So(prepared, ShouldHaveLength, 3) + for _, actualMetricData := range prepared["t1"] { So(actualMetricData.Values, ShouldHaveLength, 1) So(actualMetricData.StepTime, ShouldResemble, int64(60)) So(actualMetricData.StartTime, ShouldResemble, int64(0)) So(actualMetricData.StopTime, ShouldResemble, int64(60)) } + So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) - Convey("fetched metrics has only wildcards, step is 10", func() { - actual, err := triggerChecker.getMetricsToCheck([]*metricSource.MetricData{{Name: "wildcard", Wildcard: true, StepTime: 10}}) - So(actual, ShouldHaveLength, 3) - for _, actualMetricData := range actual { + prepared, alone, err := triggerChecker.prepareMetrics( + metricSource.FetchedMetrics{ + "t1": metricSource.FetchedPatternMetrics{ + metricSource.MetricData{ + Name: "wildcard", + Wildcard: true, + StepTime: 10, + }, + }, + }) + So(prepared, ShouldHaveLength, 3) + for _, actualMetricData := range prepared["t1"] { So(actualMetricData.Values, ShouldHaveLength, 6) So(actualMetricData.StepTime, ShouldResemble, int64(10)) So(actualMetricData.StartTime, ShouldResemble, int64(0)) So(actualMetricData.StopTime, ShouldResemble, int64(60)) } + So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) - Convey("fetched metrics has one of last check metrics", func() { - actual, err := triggerChecker.getMetricsToCheck([]*metricSource.MetricData{ - metricSource.MakeMetricData("first", []float64{1, 2, 3, 4, 5, 6}, 10, 0), - }) - So(actual, ShouldHaveLength, 3) - for _, actualMetricData := range actual { + prepared, alone, err := triggerChecker.prepareMetrics( + metricSource.FetchedMetrics{ + "t1": metricSource.FetchedPatternMetrics{ + *metricSource.MakeMetricData("first", []float64{1, 2, 3, 4, 5, 6}, 10, 0), + }, + }) + So(prepared, ShouldHaveLength, 3) + for _, actualMetricData := range prepared["t1"] { So(actualMetricData.Values, ShouldHaveLength, 6) So(actualMetricData.StepTime, ShouldResemble, int64(10)) So(actualMetricData.StartTime, ShouldResemble, int64(0)) So(actualMetricData.StopTime, ShouldResemble, int64(60)) } + So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) - Convey("fetched metrics has one of last check metrics and one new", func() { - actual, err := triggerChecker.getMetricsToCheck([]*metricSource.MetricData{ - metricSource.MakeMetricData("first", []float64{1, 2, 3, 4, 5, 6}, 10, 0), - metricSource.MakeMetricData("fourth", []float64{7, 8, 9, 1, 2, 3}, 10, 0), - }) - So(actual, ShouldHaveLength, 4) - for _, actualMetricData := range actual { + prepared, alone, err := triggerChecker.prepareMetrics( + metricSource.FetchedMetrics{ + "t1": metricSource.FetchedPatternMetrics{ + *metricSource.MakeMetricData("first", []float64{1, 2, 3, 4, 5, 6}, 10, 0), + *metricSource.MakeMetricData("fourth", []float64{7, 8, 9, 1, 2, 3}, 10, 0), + }, + }) + So(prepared, ShouldHaveLength, 4) + for _, actualMetricData := range prepared["t1"] { So(actualMetricData.Values, ShouldHaveLength, 6) So(actualMetricData.StepTime, ShouldResemble, int64(10)) So(actualMetricData.StartTime, ShouldResemble, int64(0)) So(actualMetricData.StopTime, ShouldResemble, int64(60)) } + So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) }) @@ -222,6 +314,7 @@ func TestGetMetricStepsStates(t *testing.T) { logging.SetLevel(logging.INFO, "Test") var warnValue float64 = 10 var errValue float64 = 20 + triggerChecker := TriggerChecker{ logger: logger, until: 67, @@ -231,101 +324,106 @@ func TestGetMetricStepsStates(t *testing.T) { ErrorValue: &errValue, TriggerType: moira.RisingTrigger, }, + lastCheck: &moira.CheckData{}, } - metricData1 := &metricSource.MetricData{ + + maintenance := int64(11111) + suppressed := true + metricData1 := metricSource.MetricData{ Name: "main.metric", StartTime: triggerChecker.from, StopTime: triggerChecker.until, StepTime: 10, Values: []float64{1, math.NaN(), 3, 4, math.NaN()}, } - metricData2 := &metricSource.MetricData{ + metricData2 := metricSource.MetricData{ Name: "main.metric", StartTime: triggerChecker.from, StopTime: triggerChecker.until, StepTime: 10, Values: []float64{1, 2, 3, 4, 5}, } - addMetricData := &metricSource.MetricData{ + addMetricData := metricSource.MetricData{ Name: "additional.metric", StartTime: triggerChecker.from, StopTime: triggerChecker.until, StepTime: 10, Values: []float64{5, 4, 3, 2, 1}, } - addMetricData.Name = "additional.metric" - tts := &metricSource.TriggerMetricsData{ - Main: []*metricSource.MetricData{metricData1, metricData2}, - Additional: []*metricSource.MetricData{addMetricData}, - } - metricLastState := moira.MetricState{ - Maintenance: 11111, - Suppressed: true, - EventTimestamp: 11, - } metricsState1 := moira.MetricState{ State: moira.StateOK, Timestamp: 17, - Value: &metricData2.Values[0], - Maintenance: metricLastState.Maintenance, - Suppressed: metricLastState.Suppressed, + Values: map[string]float64{"t1": 1, "t2": 5}, + Value: nil, + Maintenance: maintenance, + Suppressed: suppressed, EventTimestamp: 0, } metricsState2 := moira.MetricState{ State: moira.StateOK, Timestamp: 27, - Value: &metricData2.Values[1], - Maintenance: metricLastState.Maintenance, - Suppressed: metricLastState.Suppressed, + Values: map[string]float64{"t1": 2, "t2": 4}, + Value: nil, + Maintenance: maintenance, + Suppressed: suppressed, EventTimestamp: 0, } metricsState3 := moira.MetricState{ State: moira.StateOK, Timestamp: 37, - Value: &metricData2.Values[2], - Maintenance: metricLastState.Maintenance, - Suppressed: metricLastState.Suppressed, + Values: map[string]float64{"t1": 3, "t2": 3}, + Value: nil, + Maintenance: maintenance, + Suppressed: suppressed, EventTimestamp: 0, } metricsState4 := moira.MetricState{ State: moira.StateOK, Timestamp: 47, - Value: &metricData2.Values[3], - Maintenance: metricLastState.Maintenance, - Suppressed: metricLastState.Suppressed, + Values: map[string]float64{"t1": 4, "t2": 2}, + Value: nil, + Maintenance: maintenance, + Suppressed: suppressed, EventTimestamp: 0, } metricsState5 := moira.MetricState{ State: moira.StateOK, Timestamp: 57, - Value: &metricData2.Values[4], - Maintenance: metricLastState.Maintenance, - Suppressed: metricLastState.Suppressed, + Values: map[string]float64{"t1": 5, "t2": 1}, + Value: nil, + Maintenance: maintenance, + Suppressed: suppressed, EventTimestamp: 0, } Convey("ValueTimestamp covers all metric range", t, func() { - metricLastState.EventTimestamp = 11 + triggerChecker.lastCheck.Metrics = map[string]moira.MetricState{ + "main.metric": moira.MetricState{ + Maintenance: 11111, + Suppressed: true, + EventTimestamp: 11, + }, + } Convey("Metric has all valid values", func() { - metricStates, err := triggerChecker.getMetricStepsStates(tts, tts.Main[1], metricLastState) + _, metricStates, err := triggerChecker.getMetricStepsStates("main.metric", metricSource.MetricsToCheck{"t1": metricData2, "t2": addMetricData}) So(err, ShouldBeNil) So(metricStates, ShouldResemble, []moira.MetricState{metricsState1, metricsState2, metricsState3, metricsState4, metricsState5}) }) Convey("Metric has invalid values", func() { - metricStates, err := triggerChecker.getMetricStepsStates(tts, tts.Main[0], metricLastState) + _, metricStates, err := triggerChecker.getMetricStepsStates("main.metric", metricSource.MetricsToCheck{"t1": metricData1, "t2": addMetricData}) So(err, ShouldBeNil) So(metricStates, ShouldResemble, []moira.MetricState{metricsState1, metricsState3, metricsState4}) }) Convey("Until + stepTime covers last value", func() { triggerChecker.until = 56 - metricStates, err := triggerChecker.getMetricStepsStates(tts, tts.Main[1], metricLastState) + _, metricStates, err := triggerChecker.getMetricStepsStates("main.metric", metricSource.MetricsToCheck{"t1": metricData2, "t2": addMetricData}) So(err, ShouldBeNil) So(metricStates, ShouldResemble, []moira.MetricState{metricsState1, metricsState2, metricsState3, metricsState4, metricsState5}) }) @@ -335,36 +433,60 @@ func TestGetMetricStepsStates(t *testing.T) { Convey("ValueTimestamp don't covers begin of metric data", t, func() { Convey("Exclude 1 first element", func() { - metricLastState.EventTimestamp = 22 - metricStates, err := triggerChecker.getMetricStepsStates(tts, tts.Main[1], metricLastState) + triggerChecker.lastCheck.Metrics = map[string]moira.MetricState{ + "main.metric": moira.MetricState{ + Maintenance: 11111, + Suppressed: true, + EventTimestamp: 22, + }, + } + _, metricStates, err := triggerChecker.getMetricStepsStates("main.metric", metricSource.MetricsToCheck{"t1": metricData2, "t2": addMetricData}) So(err, ShouldBeNil) So(metricStates, ShouldResemble, []moira.MetricState{metricsState2, metricsState3, metricsState4, metricsState5}) }) Convey("Exclude 2 first elements", func() { - metricLastState.EventTimestamp = 27 - metricStates, err := triggerChecker.getMetricStepsStates(tts, tts.Main[1], metricLastState) + triggerChecker.lastCheck.Metrics = map[string]moira.MetricState{ + "main.metric": moira.MetricState{ + Maintenance: 11111, + Suppressed: true, + EventTimestamp: 27, + }, + } + _, metricStates, err := triggerChecker.getMetricStepsStates("main.metric", metricSource.MetricsToCheck{"t1": metricData2, "t2": addMetricData}) So(err, ShouldBeNil) So(metricStates, ShouldResemble, []moira.MetricState{metricsState3, metricsState4, metricsState5}) }) Convey("Exclude last element", func() { - metricLastState.EventTimestamp = 11 + triggerChecker.lastCheck.Metrics = map[string]moira.MetricState{ + "main.metric": moira.MetricState{ + Maintenance: 11111, + Suppressed: true, + EventTimestamp: 11, + }, + } triggerChecker.until = 47 - metricStates, err := triggerChecker.getMetricStepsStates(tts, tts.Main[1], metricLastState) + _, metricStates, err := triggerChecker.getMetricStepsStates("main.metric", metricSource.MetricsToCheck{"t1": metricData2, "t2": addMetricData}) So(err, ShouldBeNil) So(metricStates, ShouldResemble, []moira.MetricState{metricsState1, metricsState2, metricsState3, metricsState4}) }) }) Convey("No warn and error value with default expression", t, func() { - metricLastState.EventTimestamp = 11 + triggerChecker.lastCheck.Metrics = map[string]moira.MetricState{ + "main.metric": moira.MetricState{ + Maintenance: 11111, + Suppressed: true, + EventTimestamp: 11, + }, + } triggerChecker.until = 47 triggerChecker.trigger.WarnValue = nil triggerChecker.trigger.ErrorValue = nil - metricState, err := triggerChecker.getMetricStepsStates(tts, tts.Main[1], metricLastState) + _, metricStates, err := triggerChecker.getMetricStepsStates("main.metric", metricSource.MetricsToCheck{"t1": metricData2, "t2": addMetricData}) So(err.Error(), ShouldResemble, "error value and warning value can not be empty") - So(metricState, ShouldBeNil) + So(metricStates, ShouldBeEmpty) }) } @@ -376,12 +498,10 @@ func TestCheckForNODATA(t *testing.T) { Maintenance: 11111, Suppressed: true, } - metricData1 := &metricSource.MetricData{ - Name: "main.metric", - } + metricName := "main.metric" Convey("No TTL", t, func() { triggerChecker := TriggerChecker{} - needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricData1, metricLastState) + needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricName, metricLastState) So(needToDeleteMetric, ShouldBeFalse) So(currentState, ShouldBeNil) }) @@ -401,13 +521,13 @@ func TestCheckForNODATA(t *testing.T) { Convey("Last check is resent", t, func() { Convey("1", func() { metricLastState.Timestamp = 1100 - needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricData1, metricLastState) + needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricName, metricLastState) So(needToDeleteMetric, ShouldBeFalse) So(currentState, ShouldBeNil) }) Convey("2", func() { metricLastState.Timestamp = 401 - needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricData1, metricLastState) + needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricName, metricLastState) So(needToDeleteMetric, ShouldBeFalse) So(currentState, ShouldBeNil) }) @@ -417,7 +537,7 @@ func TestCheckForNODATA(t *testing.T) { triggerChecker.ttlState = moira.TTLStateDEL Convey("TTLState is DEL and has EventTimeStamp", t, func() { - needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricData1, metricLastState) + needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricName, metricLastState) So(needToDeleteMetric, ShouldBeTrue) So(currentState, ShouldBeNil) }) @@ -425,12 +545,12 @@ func TestCheckForNODATA(t *testing.T) { Convey("Has new metricState", t, func() { Convey("TTLState is DEL, but no EventTimestamp", func() { metricLastState.EventTimestamp = 0 - needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricData1, metricLastState) + needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricName, metricLastState) So(needToDeleteMetric, ShouldBeFalse) So(currentState, ShouldResemble, &moira.MetricState{ State: moira.StateNODATA, Timestamp: triggerChecker.lastCheck.Timestamp, - Value: nil, + Values: map[string]float64{}, Maintenance: metricLastState.Maintenance, Suppressed: metricLastState.Suppressed, }) @@ -439,12 +559,12 @@ func TestCheckForNODATA(t *testing.T) { Convey("TTLState is OK and no EventTimestamp", func() { metricLastState.EventTimestamp = 0 triggerChecker.ttlState = moira.TTLStateOK - needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricData1, metricLastState) + needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricName, metricLastState) So(needToDeleteMetric, ShouldBeFalse) So(currentState, ShouldResemble, &moira.MetricState{ State: triggerChecker.ttlState.ToMetricState(), Timestamp: triggerChecker.lastCheck.Timestamp, - Value: nil, + Values: map[string]float64{}, Maintenance: metricLastState.Maintenance, Suppressed: metricLastState.Suppressed, }) @@ -452,12 +572,12 @@ func TestCheckForNODATA(t *testing.T) { Convey("TTLState is OK and has EventTimestamp", func() { metricLastState.EventTimestamp = 111 - needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricData1, metricLastState) + needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricName, metricLastState) So(needToDeleteMetric, ShouldBeFalse) So(currentState, ShouldResemble, &moira.MetricState{ State: triggerChecker.ttlState.ToMetricState(), Timestamp: triggerChecker.lastCheck.Timestamp, - Value: nil, + Values: map[string]float64{}, Maintenance: metricLastState.Maintenance, Suppressed: metricLastState.Suppressed, }) @@ -501,12 +621,13 @@ func TestCheck(t *testing.T) { ttl: ttl, ttlState: moira.TTLStateNODATA, trigger: &moira.Trigger{ - Name: "Super trigger", - ErrorValue: &errValue, - WarnValue: &warnValue, - TriggerType: moira.RisingTrigger, - Targets: []string{pattern}, - Patterns: []string{pattern}, + Name: "Super trigger", + ErrorValue: &errValue, + WarnValue: &warnValue, + TriggerType: moira.RisingTrigger, + Targets: []string{pattern}, + Patterns: []string{pattern}, + AloneMetrics: map[string]bool{"t1": true}, }, lastCheck: &moira.CheckData{ State: moira.StateOK, @@ -522,12 +643,13 @@ func TestCheck(t *testing.T) { Convey("Fetch error", func() { lastCheck := moira.CheckData{ - Metrics: triggerChecker.lastCheck.Metrics, - State: moira.StateOK, - Timestamp: triggerChecker.until, - EventTimestamp: triggerChecker.until, - Score: 0, - Message: "", + Metrics: triggerChecker.lastCheck.Metrics, + State: moira.StateOK, + Timestamp: triggerChecker.until, + EventTimestamp: triggerChecker.until, + Score: 0, + Message: "", + MetricsToTargetRelation: map[string]string{}, } gomock.InOrder( @@ -557,6 +679,7 @@ func TestCheck(t *testing.T) { Score: 100000, Message: messageException, LastSuccessfulCheckTimestamp: 0, + MetricsToTargetRelation: map[string]string{}, } gomock.InOrder( @@ -572,15 +695,13 @@ func TestCheck(t *testing.T) { triggerChecker.lastCheck.State = moira.StateEXCEPTION triggerChecker.lastCheck.EventTimestamp = 67 triggerChecker.lastCheck.LastSuccessfulCheckTimestamp = triggerChecker.until - lastValue := float64(4) - eventMetrics := map[string]moira.MetricState{ metric: { EventTimestamp: 17, State: moira.StateOK, Suppressed: false, Timestamp: 57, - Value: &lastValue, + Values: map[string]float64{"t1": 4}, }, } @@ -600,11 +721,11 @@ func TestCheck(t *testing.T) { EventTimestamp: triggerChecker.until, Score: 0, LastSuccessfulCheckTimestamp: triggerChecker.until, + MetricsToTargetRelation: map[string]string{"t1": "super.puper.metric"}, } - gomock.InOrder( source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil), - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)}), + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{*metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)}), fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil), dataBase.EXPECT().RemoveMetricsValues([]string{metric}, int64(57)).Return(nil), dataBase.EXPECT().PushNotificationEvent(&event, true).Return(nil), @@ -616,7 +737,6 @@ func TestCheck(t *testing.T) { }) Convey("Trigger switch to Error", func() { - value := float64(25) lastCheck := moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { @@ -624,14 +744,15 @@ func TestCheck(t *testing.T) { State: moira.StateERROR, Timestamp: 57, MaintenanceInfo: moira.MaintenanceInfo{}, - Value: &value, + Values: map[string]float64{"t1": 25}, }, }, Score: 100, State: moira.StateOK, Timestamp: triggerChecker.until, - EventTimestamp: 0, + EventTimestamp: triggerChecker.until, LastSuccessfulCheckTimestamp: triggerChecker.until, + MetricsToTargetRelation: map[string]string{"t1": "super.puper.metric"}, } event := moira.NotificationEvent{ IsTriggerEvent: false, @@ -640,13 +761,13 @@ func TestCheck(t *testing.T) { OldState: moira.StateOK, Timestamp: 57, Metric: metric, - Value: &value, + Values: map[string]float64{"t1": 25}, } dataBase.EXPECT().RemoveMetricsValues([]string{metric}, int64(57)).Return(nil) source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{ - metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 25}, retention, triggerChecker.from), + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{ + *metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 25}, retention, triggerChecker.from), }) fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) dataBase.EXPECT().PushNotificationEvent(&event, true).Return(nil) @@ -656,7 +777,6 @@ func TestCheck(t *testing.T) { So(err, ShouldBeNil) }) Convey("Duplicate error", func() { - value := float64(4) lastCheck := moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { @@ -664,14 +784,16 @@ func TestCheck(t *testing.T) { State: moira.StateOK, Timestamp: 57, MaintenanceInfo: moira.MaintenanceInfo{}, - Value: &value, + Values: map[string]float64{"t1": 4}, }, }, - Score: 100, - State: moira.StateERROR, - Timestamp: triggerChecker.until, - EventTimestamp: triggerChecker.until, - Message: "Several metrics have an identical name: super.puper.metric", + MetricsToTargetRelation: map[string]string{"t1": "super.puper.metric"}, + Score: 100, + State: moira.StateERROR, + Timestamp: triggerChecker.until, + EventTimestamp: triggerChecker.until, + LastSuccessfulCheckTimestamp: triggerChecker.until, + Message: "Targets have metrics with identical name: t1:super.puper.metric; ", } event := moira.NotificationEvent{ IsTriggerEvent: true, @@ -684,9 +806,9 @@ func TestCheck(t *testing.T) { dataBase.EXPECT().RemoveMetricsValues([]string{metric}, int64(57)).Return(nil) source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{ - metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from), - metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from), + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{ + *metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from), + *metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from), }) fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) dataBase.EXPECT().PushNotificationEvent(&event, true).Return(nil) @@ -699,10 +821,7 @@ func TestCheck(t *testing.T) { func TestIgnoreNodataToOk(t *testing.T) { mockCtrl := gomock.NewController(t) - dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) logger, _ := logging.GetLogger("Test") - source := mock_metric_source.NewMockMetricSource(mockCtrl) - fetchResult := mock_metric_source.NewMockFetchResult(mockCtrl) logging.SetLevel(logging.INFO, "Test") defer mockCtrl.Finish() @@ -719,8 +838,6 @@ func TestIgnoreNodataToOk(t *testing.T) { } triggerChecker := TriggerChecker{ triggerID: "SuperId", - database: dataBase, - source: source, logger: logger, config: &Config{ MetricsTTLSeconds: 3600, @@ -739,26 +856,29 @@ func TestIgnoreNodataToOk(t *testing.T) { lastCheck: &lastCheck, } + aloneMetrics := metricSource.MetricsToCheck{"t1": *metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)} + triggerChecker.lastCheck.MetricsToTargetRelation = aloneMetrics.GetRelations() + metricsToCheck := metricSource.TriggerMetricsToCheck{} + checkData := newCheckData(&lastCheck, triggerChecker.until) + Convey("First Event, NODATA - OK is ignored", t, func() { triggerChecker.trigger.MuteNewMetrics = true - source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)}) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) - dataBase.EXPECT().RemoveMetricsValues([]string{metric}, triggerChecker.until-triggerChecker.config.MetricsTTLSeconds) - checkData, err := triggerChecker.checkTrigger() + newCheckData, err := triggerChecker.check(metricsToCheck, aloneMetrics, checkData) So(err, ShouldBeNil) - So(checkData, ShouldResemble, moira.CheckData{ + So(newCheckData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { Timestamp: time.Now().Unix(), EventTimestamp: time.Now().Unix(), State: moira.StateOK, Value: nil, + Values: nil, }, }, - Timestamp: triggerChecker.until, - State: moira.StateNODATA, - Score: 0, + MetricsToTargetRelation: map[string]string{"t1": metric}, + Timestamp: triggerChecker.until, + State: moira.StateNODATA, + Score: 0, }) }) } @@ -766,8 +886,6 @@ func TestIgnoreNodataToOk(t *testing.T) { func TestHandleTrigger(t *testing.T) { mockCtrl := gomock.NewController(t) dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) - source := mock_metric_source.NewMockMetricSource(mockCtrl) - fetchResult := mock_metric_source.NewMockFetchResult(mockCtrl) logger, _ := logging.GetLogger("Test") logging.SetLevel(logging.INFO, "Test") defer mockCtrl.Finish() @@ -786,7 +904,6 @@ func TestHandleTrigger(t *testing.T) { triggerChecker := TriggerChecker{ triggerID: "SuperId", database: dataBase, - source: source, logger: logger, config: &Config{ MetricsTTLSeconds: 3600, @@ -806,21 +923,19 @@ func TestHandleTrigger(t *testing.T) { } Convey("First Event", t, func() { - source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)}) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) - var val float64 - var val1 float64 = 4 - dataBase.EXPECT().RemoveMetricsValues([]string{metric}, triggerChecker.until-triggerChecker.config.MetricsTTLSeconds) + aloneMetrics := metricSource.MetricsToCheck{"t1": *metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)} + lastCheck.MetricsToTargetRelation = aloneMetrics.GetRelations() + checkData := newCheckData(&lastCheck, triggerChecker.until) + metricsToCheck := metricSource.TriggerMetricsToCheck{} dataBase.EXPECT().PushNotificationEvent(&moira.NotificationEvent{ TriggerID: triggerChecker.triggerID, Timestamp: 3617, State: moira.StateOK, OldState: moira.StateNODATA, Metric: metric, - Value: &val, + Values: map[string]float64{"t1": 0}, Message: nil}, true).Return(nil) - checkData, err := triggerChecker.checkTrigger() + checkData, err := triggerChecker.check(metricsToCheck, aloneMetrics, checkData) So(err, ShouldBeNil) So(checkData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ @@ -828,23 +943,24 @@ func TestHandleTrigger(t *testing.T) { Timestamp: 3657, EventTimestamp: 3617, State: moira.StateOK, - Value: &val1, + Value: nil, + Values: map[string]float64{"t1": 4}, }, }, - Timestamp: triggerChecker.until, - State: moira.StateNODATA, - Score: 0, + MetricsToTargetRelation: map[string]string{"t1": metric}, + Timestamp: triggerChecker.until, + State: moira.StateNODATA, + Score: 0, }) }) - var val float64 = 3 lastCheck = moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { Timestamp: 3647, EventTimestamp: 3607, State: moira.StateOK, - Value: &val, + Values: map[string]float64{"t1": 3}, }, }, State: moira.StateOK, @@ -852,25 +968,27 @@ func TestHandleTrigger(t *testing.T) { } Convey("Last check is not empty", t, func() { - source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)}) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) - dataBase.EXPECT().RemoveMetricsValues([]string{metric}, triggerChecker.until-triggerChecker.config.MetricsTTLSeconds) - checkData, err := triggerChecker.checkTrigger() + aloneMetrics := metricSource.MetricsToCheck{"t1": *metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)} + lastCheck.MetricsToTargetRelation = aloneMetrics.GetRelations() + checkData := newCheckData(&lastCheck, triggerChecker.until) + metricsToCheck := metricSource.TriggerMetricsToCheck{} + + checkData, err := triggerChecker.check(metricsToCheck, aloneMetrics, checkData) So(err, ShouldBeNil) - var val1 float64 = 4 So(checkData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { Timestamp: 3657, EventTimestamp: 3607, State: moira.StateOK, - Value: &val1, + Value: nil, + Values: map[string]float64{"t1": 4}, }, }, - Timestamp: triggerChecker.until, - State: moira.StateOK, - Score: 0, + MetricsToTargetRelation: map[string]string{"t1": metric}, + Timestamp: triggerChecker.until, + State: moira.StateOK, + Score: 0, }) }) @@ -878,19 +996,21 @@ func TestHandleTrigger(t *testing.T) { triggerChecker.from = 4217 triggerChecker.until = 4267 lastCheck.Timestamp = 4267 - source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{}, retention, triggerChecker.from)}) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) - dataBase.EXPECT().RemoveMetricsValues([]string{metric}, triggerChecker.until-triggerChecker.config.MetricsTTLSeconds) dataBase.EXPECT().PushNotificationEvent(&moira.NotificationEvent{ TriggerID: triggerChecker.triggerID, Timestamp: lastCheck.Timestamp, State: moira.StateNODATA, OldState: moira.StateOK, Metric: metric, - Value: nil, + Values: map[string]float64{}, Message: nil}, true).Return(nil) - checkData, err := triggerChecker.checkTrigger() + aloneMetrics := metricSource.MetricsToCheck{"t1": *metricSource.MakeMetricData(metric, []float64{}, retention, triggerChecker.from)} + lastCheck.MetricsToTargetRelation = aloneMetrics.GetRelations() + checkData := newCheckData(&lastCheck, triggerChecker.until) + metricsToCheck := metricSource.TriggerMetricsToCheck{} + + checkData, err := triggerChecker.check(metricsToCheck, aloneMetrics, checkData) + So(err, ShouldBeNil) So(checkData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ @@ -898,74 +1018,13 @@ func TestHandleTrigger(t *testing.T) { Timestamp: lastCheck.Timestamp, EventTimestamp: lastCheck.Timestamp, State: moira.StateNODATA, - Value: nil, + Values: map[string]float64{}, }, }, - Timestamp: triggerChecker.until, - State: moira.StateOK, - Score: 0, - }) - }) - - Convey("Has duplicated metric names, should return trigger has same timeseries names error", t, func() { - metric1 := "super.puper.metric" - metric2 := "super.drupper.metric" - pattern1 := "super.*.metric" - f := 3.0 - - triggerChecker1 := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - source: source, - logger: logger, - config: &Config{ - MetricsTTLSeconds: 3600, - }, - from: 3617, - until: 3667, - ttl: ttl, - ttlState: moira.TTLStateNODATA, - trigger: &moira.Trigger{ - ErrorValue: &errValue, - WarnValue: &warnValue, - TriggerType: moira.RisingTrigger, - Targets: []string{"aliasByNode(super.*.metric, 0)"}, - Patterns: []string{pattern1}, - }, - lastCheck: &moira.CheckData{ - Metrics: make(map[string]moira.MetricState), - State: moira.StateNODATA, - Timestamp: 3647, - }, - } - - source.EXPECT().Fetch(triggerChecker1.trigger.Targets[0], triggerChecker1.from, triggerChecker1.until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{ - metricSource.MakeMetricData("super", []float64{0, 1, 2, 3}, retention, triggerChecker1.from), - metricSource.MakeMetricData("super", []float64{0, 1, 2, 3}, retention, triggerChecker1.from), - }) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric1, metric2}, nil) - dataBase.EXPECT().RemoveMetricsValues([]string{metric1, metric2}, gomock.Any()) - dataBase.EXPECT().PushNotificationEvent(gomock.Any(), true).Return(nil) - checkData, err := triggerChecker1.checkTrigger() - So(err, ShouldResemble, ErrTriggerHasSameMetricNames{names: []string{"super"}}) - So(checkData, ShouldResemble, moira.CheckData{ - Metrics: map[string]moira.MetricState{ - "super": { - EventTimestamp: 3617, - State: moira.StateOK, - Suppressed: false, - Timestamp: 3647, - Value: &f, - Maintenance: 0, - }, - }, - Score: 0, - State: moira.StateNODATA, - Timestamp: 3667, - EventTimestamp: 0, - Suppressed: false, - Message: "", + MetricsToTargetRelation: map[string]string{"t1": "super.puper.metric"}, + Timestamp: triggerChecker.until, + State: moira.StateOK, + Score: 0, }) }) @@ -975,13 +1034,15 @@ func TestHandleTrigger(t *testing.T) { triggerChecker.ttlState = moira.TTLStateDEL lastCheck.Timestamp = 4267 - source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{}, retention, triggerChecker.from)}) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) - dataBase.EXPECT().RemoveMetricsValues([]string{metric}, triggerChecker.until-triggerChecker.config.MetricsTTLSeconds) dataBase.EXPECT().RemovePatternsMetrics(triggerChecker.trigger.Patterns).Return(nil) - checkData, err := triggerChecker.checkTrigger() + aloneMetrics := metricSource.MetricsToCheck{"t1": *metricSource.MakeMetricData(metric, []float64{}, retention, triggerChecker.from)} + lastCheck.MetricsToTargetRelation = aloneMetrics.GetRelations() + checkData := newCheckData(&lastCheck, triggerChecker.until) + metricsToCheck := metricSource.TriggerMetricsToCheck{} + + checkData, err := triggerChecker.check(metricsToCheck, aloneMetrics, checkData) + So(err, ShouldBeNil) So(checkData, ShouldResemble, moira.CheckData{ Metrics: make(map[string]moira.MetricState), @@ -989,402 +1050,90 @@ func TestHandleTrigger(t *testing.T) { State: moira.StateOK, Score: 0, LastSuccessfulCheckTimestamp: 0, + MetricsToTargetRelation: map[string]string{"t1": metric}, }) }) } -func TestHandleTriggerCheck(t *testing.T) { +func TestTriggerChecker_Check(t *testing.T) { mockCtrl := gomock.NewController(t) - defer mockCtrl.Finish() - logger, _ := logging.GetLogger("Test") dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) - ttlState := moira.TTLStateNODATA - - Convey("Handle trigger was not successful checked and no error", t, func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 0, - ttlState: ttlState, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger, TTLState: &ttlState}, - lastCheck: &moira.CheckData{ - Timestamp: 0, - State: moira.StateNODATA, - }, - } - checkData := moira.CheckData{ - State: moira.StateOK, - Timestamp: time.Now().Unix(), - } - actual, err := triggerChecker.handleCheckResult(checkData, nil) - So(err, ShouldBeNil) - So(actual, ShouldResemble, moira.CheckData{ - State: moira.StateOK, - Timestamp: time.Now().Unix(), - LastSuccessfulCheckTimestamp: time.Now().Unix(), - }) - }) + source := mock_metric_source.NewMockMetricSource(mockCtrl) + fetchResult := mock_metric_source.NewMockFetchResult(mockCtrl) + logger, _ := logging.GetLogger("Test") + defer mockCtrl.Finish() - Convey("Handle error no metrics", t, func() { - Convey("TTL is 0", func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 0, - ttlState: ttlState, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger, TTLState: &ttlState}, - lastCheck: &moira.CheckData{ - Timestamp: 0, - State: moira.StateNODATA, - }, - } - checkData := moira.CheckData{ - State: moira.StateNODATA, - Timestamp: time.Now().Unix(), - Message: "Trigger has no metrics, check your target", - LastSuccessfulCheckTimestamp: time.Now().Unix(), - } - actual, err := triggerChecker.handleCheckResult(checkData, ErrTriggerHasNoMetrics{}) - So(err, ShouldBeNil) - So(actual, ShouldResemble, checkData) - }) + var retention int64 = 10 + var warnValue float64 = 10 + var errValue float64 = 20 + pattern := "super.puper.pattern" + metric := "super.puper.metric" - Convey("TTL is not 0", func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 60, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger}, - ttlState: moira.TTLStateNODATA, - lastCheck: &moira.CheckData{ - Timestamp: 0, - State: moira.StateNODATA, - LastSuccessfulCheckTimestamp: 0, - }, - } - var interval int64 = 24 - checkData := moira.CheckData{ - State: moira.StateOK, - Timestamp: time.Now().Unix(), - } - event := &moira.NotificationEvent{ - IsTriggerEvent: true, - Timestamp: checkData.Timestamp, - TriggerID: triggerChecker.triggerID, - OldState: moira.StateNODATA, - State: moira.StateNODATA, - MessageEventInfo: &moira.EventInfo{Interval: &interval}, - } + var ttl int64 = 30 - dataBase.EXPECT().PushNotificationEvent(event, true).Return(nil) - actual, err := triggerChecker.handleCheckResult(checkData, ErrTriggerHasNoMetrics{}) - expected := moira.CheckData{ - State: moira.StateNODATA, - Timestamp: checkData.Timestamp, - EventTimestamp: checkData.Timestamp, - Message: "Trigger has no metrics, check your target", - LastSuccessfulCheckTimestamp: 0, - } - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - }) - Convey("Handle trigger has only wildcards without metrics in last state", t, func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 60, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger}, - ttlState: moira.TTLStateERROR, - lastCheck: &moira.CheckData{ - Timestamp: time.Now().Unix(), - State: moira.StateOK, - LastSuccessfulCheckTimestamp: time.Now().Unix(), - }, - } - checkData := moira.CheckData{ + triggerChecker := TriggerChecker{ + triggerID: "SuperId", + database: dataBase, + source: source, + logger: logger, + config: &Config{ + MetricsTTLSeconds: 10, + }, + metrics: metrics.ConfigureCheckerMetrics(metrics.NewDummyRegistry(), false).LocalMetrics, + from: 17, + until: 67, + ttl: ttl, + ttlState: moira.TTLStateNODATA, + trigger: &moira.Trigger{ + Name: "Super trigger", + ErrorValue: &errValue, + WarnValue: &warnValue, + TriggerType: moira.RisingTrigger, + Targets: []string{pattern}, + Patterns: []string{pattern}, + AloneMetrics: map[string]bool{"t1": true}, + }, + lastCheck: &moira.CheckData{ State: moira.StateOK, - Timestamp: time.Now().Unix(), - } - - dataBase.EXPECT().PushNotificationEvent(gomock.Any(), true).Return(nil) - actual, err := triggerChecker.handleCheckResult(checkData, ErrTriggerHasOnlyWildcards{}) - expected := moira.CheckData{ - State: moira.StateERROR, - Timestamp: checkData.Timestamp, - EventTimestamp: checkData.Timestamp, - Message: "Trigger never received metrics", - LastSuccessfulCheckTimestamp: 0, - } - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - - Convey("Handle trigger has only wildcards with metrics in last state", t, func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 60, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger}, - ttlState: moira.TTLStateNODATA, - lastCheck: &moira.CheckData{ - Timestamp: time.Now().Unix(), - State: moira.StateOK, - LastSuccessfulCheckTimestamp: time.Now().Unix(), - }, - } - checkData := moira.CheckData{ + Timestamp: 57, Metrics: map[string]moira.MetricState{ - "123": {}, - }, - State: moira.StateOK, - Timestamp: time.Now().Unix(), - LastSuccessfulCheckTimestamp: 0, - } - - dataBase.EXPECT().PushNotificationEvent(gomock.Any(), true).Return(nil) - actual, err := triggerChecker.handleCheckResult(checkData, ErrTriggerHasOnlyWildcards{}) - expected := moira.CheckData{ - Metrics: checkData.Metrics, - State: moira.StateNODATA, - Timestamp: checkData.Timestamp, - EventTimestamp: checkData.Timestamp, - Message: "Trigger never received metrics", - LastSuccessfulCheckTimestamp: 0, - } - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - - Convey("Handle trigger has only wildcards and ttlState is OK", t, func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 60, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger}, - ttlState: moira.TTLStateOK, - lastCheck: &moira.CheckData{ - Timestamp: time.Now().Unix(), - State: moira.StateOK, - LastSuccessfulCheckTimestamp: 0, - }, - } - checkData := moira.CheckData{ - Metrics: map[string]moira.MetricState{}, - State: moira.StateOK, - Timestamp: time.Now().Unix(), - LastSuccessfulCheckTimestamp: 0, - } - - actual, err := triggerChecker.handleCheckResult(checkData, ErrTriggerHasOnlyWildcards{}) - expected := moira.CheckData{ - Metrics: checkData.Metrics, - State: moira.StateOK, - Timestamp: checkData.Timestamp, - EventTimestamp: checkData.Timestamp, - Message: "Trigger never received metrics", - LastSuccessfulCheckTimestamp: 0, - } - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - - Convey("Handle trigger has only wildcards and ttlState is DEL", t, func() { - now := time.Now().Unix() - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 60, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger}, - ttlState: moira.TTLStateDEL, - lastCheck: &moira.CheckData{ - Timestamp: now, - EventTimestamp: now - 3600, - State: moira.StateOK, + metric: { + State: moira.StateOK, + Timestamp: 26, + }, }, - } - checkData := moira.CheckData{ - Metrics: map[string]moira.MetricState{}, - State: moira.StateOK, - Timestamp: now, - } - - actual, err := triggerChecker.handleCheckResult(checkData, ErrTriggerHasOnlyWildcards{}) - expected := moira.CheckData{ - Metrics: checkData.Metrics, + MetricsToTargetRelation: map[string]string{"t1": metric}, + }, + } + eventMetrics := map[string]moira.MetricState{ + metric: { + EventTimestamp: 17, State: moira.StateOK, - Timestamp: now, - EventTimestamp: now - 3600, - Message: "Trigger never received metrics", - } - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - - Convey("Handle unknown function in evalExpr", t, func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 60, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger}, - ttlState: moira.TTLStateNODATA, - lastCheck: &moira.CheckData{ - Timestamp: time.Now().Unix(), - State: moira.StateOK, - LastSuccessfulCheckTimestamp: 0, - }, - } - checkData := moira.CheckData{ - State: moira.StateOK, - Timestamp: time.Now().Unix(), - } - - dataBase.EXPECT().PushNotificationEvent(gomock.Any(), true).Return(nil) - - actual, err := triggerChecker.handleCheckResult(checkData, local.ErrUnknownFunction{FuncName: "123"}) - expected := moira.CheckData{ - State: moira.StateEXCEPTION, - Timestamp: checkData.Timestamp, - EventTimestamp: checkData.Timestamp, - Message: "Unknown graphite function: \"123\"", - LastSuccessfulCheckTimestamp: 0, - } - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - - Convey("Handle trigger has same metric names", t, func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 60, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger}, - ttlState: moira.TTLStateNODATA, - lastCheck: &moira.CheckData{ - Timestamp: time.Now().Unix(), - State: moira.StateOK, - }, - } - checkData := moira.CheckData{ - State: moira.StateOK, - Timestamp: time.Now().Unix(), - } - - dataBase.EXPECT().PushNotificationEvent(gomock.Any(), true).Return(nil) - - actual, err := triggerChecker.handleCheckResult(checkData, ErrTriggerHasSameMetricNames{names: []string{"first", "second"}}) - expected := moira.CheckData{ - State: moira.StateERROR, - Timestamp: checkData.Timestamp, - EventTimestamp: checkData.Timestamp, - Message: "Several metrics have an identical name: first, second", - LastSuccessfulCheckTimestamp: 0, - } - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - - Convey("Handle trigger error remote trigger response", t, func() { - now := time.Now() - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 300, - trigger: &moira.Trigger{TriggerType: moira.RisingTrigger}, - ttlState: moira.TTLStateNODATA, - lastCheck: &moira.CheckData{ - Timestamp: time.Now().Unix(), - EventTimestamp: time.Now().Add(-1 * time.Hour).Unix(), - State: moira.StateOK, - }, - } - Convey("but time since last successful check less than ttl", func() { - checkData := moira.CheckData{ - State: moira.StateOK, - Timestamp: now.Unix(), - LastSuccessfulCheckTimestamp: now.Add(-1 * time.Minute).Unix(), - } - expected := moira.CheckData{ - State: moira.StateOK, - Timestamp: now.Unix(), - EventTimestamp: time.Now().Add(-1 * time.Hour).Unix(), - LastSuccessfulCheckTimestamp: now.Add(-1 * time.Minute).Unix(), - } - actual, err := triggerChecker.handleCheckResult(checkData, remote.ErrRemoteTriggerResponse{InternalError: fmt.Errorf("pain")}) - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - - Convey("and time since last successful check more than ttl", func() { - checkData := moira.CheckData{ - State: moira.StateOK, - Timestamp: now.Unix(), - LastSuccessfulCheckTimestamp: now.Add(-10 * time.Minute).Unix(), - } - expected := moira.CheckData{ - State: moira.StateEXCEPTION, - Message: fmt.Sprintf("Remote server unavailable. Trigger is not checked for %d seconds", checkData.Timestamp-checkData.LastSuccessfulCheckTimestamp), - Timestamp: now.Unix(), - EventTimestamp: now.Unix(), - LastSuccessfulCheckTimestamp: now.Add(-10 * time.Minute).Unix(), - } - dataBase.EXPECT().PushNotificationEvent(gomock.Any(), true).Return(nil) - actual, err := triggerChecker.handleCheckResult(checkData, remote.ErrRemoteTriggerResponse{InternalError: fmt.Errorf("pain")}) - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) - }) - - Convey("Handle additional trigger target has more than one metric data", t, func() { - triggerChecker := TriggerChecker{ - triggerID: "SuperId", - database: dataBase, - logger: logger, - ttl: 60, - trigger: &moira.Trigger{ - Targets: []string{"aliasByNode(some.data.*,2)", "aliasByNode(some.more.data.*,2)"}, - TriggerType: moira.RisingTrigger, - }, - ttlState: moira.TTLStateNODATA, - lastCheck: &moira.CheckData{ - Timestamp: time.Now().Unix(), - State: moira.StateNODATA, - }, - } - checkData := moira.CheckData{ - State: moira.StateNODATA, - Timestamp: time.Now().Unix(), - } + Suppressed: false, + Timestamp: 57, + Values: map[string]float64{"t1": 4}, + }, + } - dataBase.EXPECT().PushNotificationEvent(gomock.Any(), true).Return(nil) + lastCheck := moira.CheckData{ + Metrics: eventMetrics, + State: moira.StateOK, + Timestamp: triggerChecker.until, + EventTimestamp: triggerChecker.until, + Score: 0, + LastSuccessfulCheckTimestamp: triggerChecker.until, + MetricsToTargetRelation: map[string]string{"t1": metric}, + } - actual, err := triggerChecker.handleCheckResult(checkData, ErrWrongTriggerTargets([]int{2})) - expected := moira.CheckData{ - State: moira.StateERROR, - Timestamp: checkData.Timestamp, - EventTimestamp: checkData.Timestamp, - Message: "Target t2 has more than one metric", - LastSuccessfulCheckTimestamp: 0, - } - So(err, ShouldBeNil) - So(actual, ShouldResemble, expected) - }) + dataBase.EXPECT().RemoveMetricsValues([]string{metric}, int64(57)).Return(nil) + source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{*metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)}) + fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) + dataBase.EXPECT().SetTriggerLastCheck(triggerChecker.triggerID, &lastCheck, triggerChecker.trigger.IsRemote).Return(nil) + _ = triggerChecker.Check() } func BenchmarkTriggerChecker_Check(b *testing.B) { - if testing.Short() { - b.Skip() - } b.ReportAllocs() mockCtrl := gomock.NewController(b) dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) @@ -1409,18 +1158,19 @@ func BenchmarkTriggerChecker_Check(b *testing.B) { config: &Config{ MetricsTTLSeconds: 10, }, - metrics: metrics.ConfigureCheckerMetrics(metrics.NewDummyRegistry(), "checker", false).LocalMetrics, + metrics: metrics.ConfigureCheckerMetrics(metrics.NewDummyRegistry(), false).LocalMetrics, from: 17, until: 67, ttl: ttl, ttlState: moira.TTLStateNODATA, trigger: &moira.Trigger{ - Name: "Super trigger", - ErrorValue: &errValue, - WarnValue: &warnValue, - TriggerType: moira.RisingTrigger, - Targets: []string{pattern}, - Patterns: []string{pattern}, + Name: "Super trigger", + ErrorValue: &errValue, + WarnValue: &warnValue, + TriggerType: moira.RisingTrigger, + Targets: []string{pattern}, + Patterns: []string{pattern}, + AloneMetrics: map[string]bool{"t1": true}, }, lastCheck: &moira.CheckData{ State: moira.StateOK, @@ -1431,16 +1181,16 @@ func BenchmarkTriggerChecker_Check(b *testing.B) { Timestamp: 26, }, }, + MetricsToTargetRelation: map[string]string{"t1": metric}, }, } - lastValue := float64(4) eventMetrics := map[string]moira.MetricState{ metric: { EventTimestamp: 17, State: moira.StateOK, Suppressed: false, Timestamp: 57, - Value: &lastValue, + Values: map[string]float64{"t1": 4}, }, } @@ -1448,14 +1198,15 @@ func BenchmarkTriggerChecker_Check(b *testing.B) { Metrics: eventMetrics, State: moira.StateOK, Timestamp: triggerChecker.until, - EventTimestamp: 0, + EventTimestamp: triggerChecker.until, Score: 0, LastSuccessfulCheckTimestamp: triggerChecker.until, + MetricsToTargetRelation: map[string]string{"t1": metric}, } dataBase.EXPECT().RemoveMetricsValues([]string{metric}, int64(57)).Return(nil).AnyTimes() source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil).AnyTimes() - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)}).AnyTimes() + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{*metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, triggerChecker.from)}).AnyTimes() fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil).AnyTimes() dataBase.EXPECT().SetTriggerLastCheck(triggerChecker.triggerID, &lastCheck, triggerChecker.trigger.IsRemote).Return(nil).AnyTimes() for n := 0; n < b.N; n++ { @@ -1465,3 +1216,96 @@ func BenchmarkTriggerChecker_Check(b *testing.B) { } } } + +func TestGetExpressionValues(t *testing.T) { + Convey("Has only main metric data", t, func() { + metricData := metricSource.MetricData{ + Name: "m", + StartTime: 17, + StopTime: 67, + StepTime: 10, + Values: []float64{0.0, math.NaN(), math.NaN(), 3.0, math.NaN()}, + } + metrics := metricSource.MetricsToCheck{ + "t1": metricData, + } + + Convey("first value is valid", func() { + expectedExpression := &expression.TriggerExpression{ + AdditionalTargetsValues: make(map[string]float64), + } + expectedValues := map[string]float64{"t1": 0} + + expression, values, noEmptyValues := getExpressionValues(metrics, 17) + So(noEmptyValues, ShouldBeTrue) + So(expression, ShouldResemble, expectedExpression) + So(values, ShouldResemble, expectedValues) + }) + Convey("last value is empty", func() { + _, _, noEmptyValues := getExpressionValues(metrics, 67) + So(noEmptyValues, ShouldBeFalse) + }) + Convey("value before first value", func() { + _, _, noEmptyValues := getExpressionValues(metrics, 11) + So(noEmptyValues, ShouldBeFalse) + }) + + Convey("value in the middle is empty ", func() { + _, _, noEmptyValues := getExpressionValues(metrics, 44) + So(noEmptyValues, ShouldBeFalse) + }) + + Convey("value in the middle is valid", func() { + expectedExpression := &expression.TriggerExpression{ + MainTargetValue: 3, + AdditionalTargetsValues: make(map[string]float64), + } + expectedValues := map[string]float64{"t1": 3} + + expression, values, noEmptyValues := getExpressionValues(metrics, 53) + So(noEmptyValues, ShouldBeTrue) + So(expression, ShouldResemble, expectedExpression) + So(values, ShouldResemble, expectedValues) + }) + }) + + Convey("Has additional series", t, func() { + metricData := metricSource.MetricData{ + Name: "main", + StartTime: 17, + StopTime: 67, + StepTime: 10, + Values: []float64{0.0, math.NaN(), math.NaN(), 3.0, math.NaN()}, + } + metricDataAdd := metricSource.MetricData{ + Name: "main", + StartTime: 17, + StopTime: 67, + StepTime: 10, + Values: []float64{4.0, 3.0, math.NaN(), math.NaN(), 0.0}, + } + metrics := metricSource.MetricsToCheck{ + "t1": metricData, + "t2": metricDataAdd, + } + + Convey("t1 value in the middle is empty ", func() { + _, _, noEmptyValues := getExpressionValues(metrics, 29) + So(noEmptyValues, ShouldBeFalse) + }) + + Convey("t1 and t2 values in the middle is empty ", func() { + _, _, noEmptyValues := getExpressionValues(metrics, 42) + So(noEmptyValues, ShouldBeFalse) + }) + + Convey("both first values is valid ", func() { + expectedValues := map[string]float64{"t1": 0, "t2": 4} + + expression, values, noEmptyValues := getExpressionValues(metrics, 17) + So(noEmptyValues, ShouldBeTrue) + So(expression.MainTargetValue, ShouldBeIn, []float64{0, 4}) + So(values, ShouldResemble, expectedValues) + }) + }) +} diff --git a/checker/errors.go b/checker/errors.go index 81fc9aeee..5b8d493eb 100644 --- a/checker/errors.go +++ b/checker/errors.go @@ -1,23 +1,13 @@ package checker import ( - "bytes" "fmt" - "strconv" "strings" ) // ErrTriggerNotExists used if trigger to check does not exists var ErrTriggerNotExists = fmt.Errorf("trigger does not exists") -// ErrTriggerHasNoMetrics used if trigger has no metrics -type ErrTriggerHasNoMetrics struct{} - -// ErrTriggerHasNoMetrics implementation with constant error message -func (err ErrTriggerHasNoMetrics) Error() string { - return fmt.Sprintf("Trigger has no metrics, check your target") -} - // ErrTriggerHasOnlyWildcards used if trigger has only wildcard metrics type ErrTriggerHasOnlyWildcards struct{} @@ -28,12 +18,27 @@ func (err ErrTriggerHasOnlyWildcards) Error() string { // ErrTriggerHasSameMetricNames used if trigger has two metric data with same name type ErrTriggerHasSameMetricNames struct { - names []string + duplicates map[string][]string +} + +// NewErrTriggerHasSameMetricNames is a constructor function for ErrTriggerHasSameMetricNames. +func NewErrTriggerHasSameMetricNames(duplicates map[string][]string) ErrTriggerHasSameMetricNames { + return ErrTriggerHasSameMetricNames{ + duplicates: duplicates, + } } // ErrTriggerHasSameMetricNames implementation with constant error message func (err ErrTriggerHasSameMetricNames) Error() string { - return fmt.Sprintf("Several metrics have an identical name: %s", strings.Join(err.names, ", ")) + var builder strings.Builder + builder.WriteString("Targets have metrics with identical name: ") + for target, duplicates := range err.duplicates { + builder.WriteString(target) + builder.WriteRune(':') + builder.WriteString(strings.Join(duplicates, ", ")) + builder.WriteString("; ") + } + return builder.String() } // ErrTargetHasNoMetrics used if additional trigger target has not metrics data after fetch from source @@ -46,25 +51,22 @@ func (err ErrTargetHasNoMetrics) Error() string { return fmt.Sprintf("target t%v has no metrics", err.targetIndex+1) } -// ErrWrongTriggerTargets represents targets with inconsistent number of metrics -type ErrWrongTriggerTargets []int +// ErrUnexpectedAloneMetric is an error that fired by checker if alone metrics do not +// match alone metrics specified in trigger. +type ErrUnexpectedAloneMetric struct { + expected map[string]bool + actual map[string]string +} -// ErrWrongTriggerTarget implementation for list of invalid targets found -func (err ErrWrongTriggerTargets) Error() string { - var countType []byte - if len(err) > 1 { - countType = []byte("Targets ") - } else { - countType = []byte("Target ") - } - wrongTargets := bytes.NewBuffer(countType) - for tarInd, tar := range err { - wrongTargets.WriteString("t") - wrongTargets.WriteString(strconv.Itoa(tar)) - if tarInd != len(err)-1 { - wrongTargets.WriteString(", ") - } +// NewErrUnexpectedAloneMetric is a constructor function that creates ErrUnexpectedAloneMetric. +func NewErrUnexpectedAloneMetric(expected map[string]bool, actual map[string]string) ErrUnexpectedAloneMetric { + return ErrUnexpectedAloneMetric{ + expected: expected, + actual: actual, } - wrongTargets.WriteString(" has more than one metric") - return wrongTargets.String() +} + +// Error is a function that implements error interface. +func (err ErrUnexpectedAloneMetric) Error() string { + return fmt.Sprintf("Unexpected alone metrics. Expected alone metrics: %v. Got: %v", err.expected, err.actual) } diff --git a/checker/event.go b/checker/event.go index ae575a184..42310de8a 100644 --- a/checker/event.go +++ b/checker/event.go @@ -112,7 +112,7 @@ func (triggerChecker *TriggerChecker) compareMetricStates(metric string, current Timestamp: currentState.Timestamp, Metric: metric, MessageEventInfo: eventInfo, - Value: currentState.Value, + Values: currentState.Values, }, true) return currentState, err } diff --git a/checker/event_test.go b/checker/event_test.go index 2b00e1c25..dd962bc35 100644 --- a/checker/event_test.go +++ b/checker/event_test.go @@ -90,6 +90,7 @@ func TestCompareMetricStates(t *testing.T) { currentState.State = moira.StateNODATA currentState.Timestamp = 1502809200 + currentState.Values = map[string]float64{"t1": 0} var interval int64 = 24 dataBase.EXPECT().PushNotificationEvent(&moira.NotificationEvent{ TriggerID: triggerChecker.triggerID, @@ -97,7 +98,7 @@ func TestCompareMetricStates(t *testing.T) { State: moira.StateNODATA, OldState: moira.StateNODATA, Metric: "m1", - Value: currentState.Value, + Values: map[string]float64{"t1": 0}, Message: nil, MessageEventInfo: &moira.EventInfo{Interval: &interval}, }, true).Return(nil) @@ -118,6 +119,7 @@ func TestCompareMetricStates(t *testing.T) { lastState.State = moira.StateERROR currentState.State = moira.StateERROR currentState.Timestamp = 1502809200 + currentState.Values = map[string]float64{"t1": 0} var interval int64 = 24 dataBase.EXPECT().PushNotificationEvent(&moira.NotificationEvent{ @@ -126,7 +128,7 @@ func TestCompareMetricStates(t *testing.T) { State: moira.StateERROR, OldState: moira.StateERROR, Metric: "m1", - Value: currentState.Value, + Values: map[string]float64{"t1": 0}, Message: nil, MessageEventInfo: &moira.EventInfo{Interval: &interval}, }, true).Return(nil) @@ -412,6 +414,7 @@ func TestCheckMetricStateSuppressedState(t *testing.T) { Timestamp: 1800, Maintenance: 1500, State: moira.StateERROR, + Values: map[string]float64{"t1": 0}, } dataBase.EXPECT().PushNotificationEvent(&moira.NotificationEvent{ @@ -420,7 +423,7 @@ func TestCheckMetricStateSuppressedState(t *testing.T) { State: thirdState.State, OldState: secondState.State, Metric: "super.awesome.metric", - Value: thirdState.Value, + Values: map[string]float64{"t1": 0}, }, true).Return(nil) actual, err = triggerChecker.compareMetricStates("super.awesome.metric", thirdState, secondState) So(err, ShouldBeNil) @@ -450,6 +453,7 @@ func TestCheckMetricStateSuppressedState(t *testing.T) { Maintenance: 1500, State: moira.StateERROR, Suppressed: true, + Values: map[string]float64{"t1": 0}, } Convey("No maintenance info", func() { @@ -459,7 +463,7 @@ func TestCheckMetricStateSuppressedState(t *testing.T) { State: currentState.State, OldState: lastState.SuppressedState, Metric: "super.awesome.metric", - Value: currentState.Value, + Values: map[string]float64{"t1": 0}, MessageEventInfo: &moira.EventInfo{Maintenance: &moira.MaintenanceInfo{}}, }, true).Return(nil) actual, err := triggerChecker.compareMetricStates("super.awesome.metric", currentState, lastState) @@ -485,7 +489,7 @@ func TestCheckMetricStateSuppressedState(t *testing.T) { State: currentState.State, OldState: lastState.SuppressedState, Metric: "super.awesome.metric", - Value: currentState.Value, + Values: map[string]float64{"t1": 0}, MessageEventInfo: &moira.EventInfo{Maintenance: &lastState.MaintenanceInfo}, }, true).Return(nil) actual, err := triggerChecker.compareMetricStates("super.awesome.metric", currentState, lastState) @@ -516,7 +520,7 @@ func TestCheckMetricStateSuppressedState(t *testing.T) { State: currentState.State, OldState: lastState.SuppressedState, Metric: "super.awesome.metric", - Value: currentState.Value, + Values: map[string]float64{"t1": 0}, MessageEventInfo: &moira.EventInfo{Maintenance: &triggerChecker.lastCheck.MaintenanceInfo}, }, true).Return(nil) actual, err := triggerChecker.compareMetricStates("super.awesome.metric", currentState, lastState) @@ -558,6 +562,7 @@ func TestTriggerMaintenance(t *testing.T) { currentMetricState := moira.MetricState{ Timestamp: 1000, State: moira.StateWARN, + Values: map[string]float64{"t1": 0}, } lastTriggerState := moira.CheckData{ @@ -593,7 +598,7 @@ func TestTriggerMaintenance(t *testing.T) { State: moira.StateWARN, OldState: moira.StateOK, Metric: "m1", - Value: currentMetricState.Value, + Values: map[string]float64{"t1": 0}, }, true).Return(nil) actual, err := triggerChecker.compareMetricStates("m1", currentMetricState, lastMetricState) diff --git a/checker/fetch.go b/checker/fetch.go index 8cbaf3403..6f8bec511 100644 --- a/checker/fetch.go +++ b/checker/fetch.go @@ -4,7 +4,7 @@ import ( metricSource "github.com/moira-alert/moira/metric_source" ) -func (triggerChecker *TriggerChecker) fetchTriggerMetrics() (*metricSource.TriggerMetricsData, error) { +func (triggerChecker *TriggerChecker) fetchTriggerMetrics() (metricSource.FetchedMetrics, error) { triggerMetricsData, metrics, err := triggerChecker.fetch() if err != nil { return triggerMetricsData, err @@ -12,10 +12,6 @@ func (triggerChecker *TriggerChecker) fetchTriggerMetrics() (*metricSource.Trigg triggerChecker.cleanupMetricsValues(metrics, triggerChecker.until) if len(triggerChecker.lastCheck.Metrics) == 0 { - if len(triggerMetricsData.Main) == 0 { - return triggerMetricsData, ErrTriggerHasNoMetrics{} - } - if triggerMetricsData.HasOnlyWildcards() { return triggerMetricsData, ErrTriggerHasOnlyWildcards{} } @@ -24,45 +20,29 @@ func (triggerChecker *TriggerChecker) fetchTriggerMetrics() (*metricSource.Trigg return triggerMetricsData, nil } -func (triggerChecker *TriggerChecker) fetch() (*metricSource.TriggerMetricsData, []string, error) { - wrongTriggerTargets := make([]int, 0) - triggerMetricsData := metricSource.NewTriggerMetricsData() +func (triggerChecker *TriggerChecker) fetch() (metricSource.FetchedMetrics, []string, error) { + triggerMetricsData := metricSource.NewFetchedMetricsWithCapacity(0) metricsArr := make([]string, 0) isSimpleTrigger := triggerChecker.trigger.IsSimple() for targetIndex, target := range triggerChecker.trigger.Targets { + targetIndex++ // increasing target index to have target names started from 1 instead of 0 fetchResult, err := triggerChecker.source.Fetch(target, triggerChecker.from, triggerChecker.until, isSimpleTrigger) if err != nil { return nil, nil, err } metricsData := fetchResult.GetMetricsData() + metricsFetchResult, metricsErr := fetchResult.GetPatternMetrics() - if targetIndex == 0 { - triggerMetricsData.Main = metricsData - } else { - metricsDataCount := len(metricsData) - switch { - case metricsDataCount == 0: - if metricsErr != nil { - return nil, nil, ErrTargetHasNoMetrics{targetIndex: targetIndex + 1} - } - if len(metricsFetchResult) == 0 { - triggerMetricsData.Additional = append(triggerMetricsData.Additional, nil) - } else { - return nil, nil, ErrTargetHasNoMetrics{targetIndex: targetIndex + 1} - } - case metricsDataCount > 1: - wrongTriggerTargets = append(wrongTriggerTargets, targetIndex+1) - default: - triggerMetricsData.Additional = append(triggerMetricsData.Additional, metricsData[0]) - } + + if len(metricsFetchResult) == 0 { + return nil, nil, ErrTargetHasNoMetrics{targetIndex: targetIndex} } if metricsErr == nil { metricsArr = append(metricsArr, metricsFetchResult...) } - } - if len(wrongTriggerTargets) > 0 { - return nil, nil, ErrWrongTriggerTargets(wrongTriggerTargets) + + triggerMetricsData.AddMetrics(targetIndex, metricsData) } return triggerMetricsData, metricsArr, nil } diff --git a/checker/fetch_test.go b/checker/fetch_test.go index ac646fc2e..2b0096f28 100644 --- a/checker/fetch_test.go +++ b/checker/fetch_test.go @@ -2,12 +2,10 @@ package checker import ( "fmt" - "math" "testing" "github.com/golang/mock/gomock" "github.com/moira-alert/moira" - "github.com/moira-alert/moira/expression" metricSource "github.com/moira-alert/moira/metric_source" mock_metric_source "github.com/moira-alert/moira/mock/metric_source" mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" @@ -20,6 +18,7 @@ func TestFetchTriggerMetrics(t *testing.T) { mockCtrl := gomock.NewController(t) source := mock_metric_source.NewMockMetricSource(mockCtrl) fetchResult := mock_metric_source.NewMockFetchResult(mockCtrl) + database := mock_moira_alert.NewMockDatabase(mockCtrl) defer mockCtrl.Finish() var from int64 = 17 @@ -27,9 +26,13 @@ func TestFetchTriggerMetrics(t *testing.T) { pattern := "super.puper.pattern" triggerChecker := &TriggerChecker{ - source: source, - from: from, - until: until, + source: source, + from: from, + until: until, + database: database, + config: &Config{ + MetricsTTLSeconds: 3600, + }, trigger: &moira.Trigger{ Targets: []string{pattern}, Patterns: []string{pattern}, @@ -42,22 +45,23 @@ func TestFetchTriggerMetrics(t *testing.T) { Convey("no metrics in last check", func() { Convey("fetch returns wildcard", func() { source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{{Name: pattern, Wildcard: true}}) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{}, nil) + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{{Name: pattern, Wildcard: true}}) + fetchResult.EXPECT().GetPatternMetrics().Return([]string{pattern}, nil) + database.EXPECT().RemoveMetricsValues([]string{pattern}, until-triggerChecker.config.MetricsTTLSeconds).Return(nil) actual, err := triggerChecker.fetchTriggerMetrics() So(err, ShouldResemble, ErrTriggerHasOnlyWildcards{}) - So(actual, ShouldResemble, metricSource.MakeTriggerMetricsData([]*metricSource.MetricData{{Name: pattern, Wildcard: true}}, []*metricSource.MetricData{})) + So(actual, ShouldResemble, metricSource.FetchedMetrics{"t1": metricSource.FetchedPatternMetrics{{Name: pattern, Wildcard: true}}}) }) Convey("fetch returns no metrics", func() { source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{}) + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{}) fetchResult.EXPECT().GetPatternMetrics().Return([]string{}, nil) actual, err := triggerChecker.fetchTriggerMetrics() - So(err, ShouldResemble, ErrTriggerHasNoMetrics{}) - So(actual, ShouldResemble, metricSource.MakeTriggerMetricsData([]*metricSource.MetricData{}, []*metricSource.MetricData{})) + So(err, ShouldResemble, ErrTargetHasNoMetrics{targetIndex: 1}) + So(actual, ShouldBeNil) }) }) @@ -65,22 +69,23 @@ func TestFetchTriggerMetrics(t *testing.T) { triggerChecker.lastCheck.Metrics["metric"] = moira.MetricState{} Convey("fetch returns wildcard", func() { source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{{Name: pattern, Wildcard: true}}) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{}, nil) + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{{Name: pattern, Wildcard: true}}) + fetchResult.EXPECT().GetPatternMetrics().Return([]string{pattern}, nil) + database.EXPECT().RemoveMetricsValues([]string{pattern}, until-triggerChecker.config.MetricsTTLSeconds).Return(nil) actual, err := triggerChecker.fetchTriggerMetrics() So(err, ShouldBeEmpty) - So(actual, ShouldResemble, metricSource.MakeTriggerMetricsData([]*metricSource.MetricData{{Name: pattern, Wildcard: true}}, []*metricSource.MetricData{})) + So(actual, ShouldResemble, metricSource.FetchedMetrics{"t1": metricSource.FetchedPatternMetrics{{Name: pattern, Wildcard: true}}}) }) Convey("fetch returns no metrics", func() { source.EXPECT().Fetch(pattern, triggerChecker.from, triggerChecker.until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{}) + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{}) fetchResult.EXPECT().GetPatternMetrics().Return([]string{}, nil) actual, err := triggerChecker.fetchTriggerMetrics() - So(err, ShouldBeEmpty) - So(actual, ShouldResemble, metricSource.MakeTriggerMetricsData([]*metricSource.MetricData{}, []*metricSource.MetricData{})) + So(err, ShouldResemble, ErrTargetHasNoMetrics{targetIndex: 1}) + So(actual, ShouldBeNil) }) }) }) @@ -96,17 +101,10 @@ func TestFetch(t *testing.T) { pattern := "super.puper.pattern" metric := "super.puper.metric" - pattern2 := "super.duper.pattern" - metric2 := "super.duper.metric" - addPattern := "additional.pattern" addMetric := "additional.metric" addMetric2 := "additional.metric2" - oneMorePattern := "one.more.pattern" - oneMoreMetric1 := "one.more.metric.one" - oneMoreMetric2 := "one.more.metric.two" - var from int64 = 17 var until int64 = 67 var retention int64 = 10 @@ -132,92 +130,39 @@ func TestFetch(t *testing.T) { So(err, ShouldResemble, metricErr) }) - Convey("Test no metrics", t, func() { - Convey("In main target", func() { - metricData := &metricSource.MetricData{ - Name: pattern, - StartTime: from, - StopTime: until, - StepTime: 60, - Values: []float64{}, - Wildcard: true, - } - - source.EXPECT().Fetch(pattern, from, until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricData}) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{}, nil) - actual, metrics, err := triggerChecker.fetch() - So(actual, ShouldResemble, metricSource.MakeTriggerMetricsData([]*metricSource.MetricData{metricData}, make([]*metricSource.MetricData, 0))) - So(metrics, ShouldBeEmpty) - So(err, ShouldBeNil) - }) - - Convey("In additional target", func() { - metricError := fmt.Errorf("metric error") - triggerChecker1 := &TriggerChecker{ - database: dataBase, - source: source, - from: from, - until: until, - trigger: &moira.Trigger{ - Targets: []string{pattern, addPattern}, - Patterns: []string{pattern, addPattern}, - }, - } - - metricData := []*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)} - addMetricData := make([]*metricSource.MetricData, 0) - - source.EXPECT().Fetch(pattern, from, until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return(metricData) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) - - source.EXPECT().Fetch(addPattern, from, until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return(addMetricData) - - Convey("get pattern metrics error", func() { - fetchResult.EXPECT().GetPatternMetrics().Return([]string{}, metricError) - actual, metrics, err := triggerChecker1.fetch() - So(actual, ShouldBeNil) - So(metrics, ShouldBeNil) - So(err, ShouldBeError) - So(err, ShouldResemble, ErrTargetHasNoMetrics{targetIndex: 2}) - }) - - Convey("get pattern metrics has metrics", func() { - fetchResult.EXPECT().GetPatternMetrics().Return([]string{addMetric}, nil) - actual, metrics, err := triggerChecker1.fetch() - So(actual, ShouldBeNil) - So(metrics, ShouldBeNil) - So(err, ShouldBeError) - So(err, ShouldResemble, ErrTargetHasNoMetrics{targetIndex: 2}) - So(err.Error(), ShouldResemble, "target t3 has no metrics") - }) + Convey("Test no metrics in target", t, func() { + metricData := metricSource.MetricData{ + Name: pattern, + StartTime: from, + StopTime: until, + StepTime: 60, + Values: []float64{}, + Wildcard: true, + } - Convey("get pattern metrics has no metrics", func() { - fetchResult.EXPECT().GetPatternMetrics().Return([]string{}, nil) - actual, metrics, err := triggerChecker1.fetch() - So(actual, ShouldResemble, metricSource.MakeTriggerMetricsData(metricData, []*metricSource.MetricData{nil})) - So(metrics, ShouldResemble, []string{metric}) - So(err, ShouldBeNil) - }) - }) + source.EXPECT().Fetch(pattern, from, until, true).Return(fetchResult, nil) + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{metricData}) + fetchResult.EXPECT().GetPatternMetrics().Return([]string{}, nil) + actual, metrics, err := triggerChecker.fetch() + So(actual, ShouldBeNil) + So(metrics, ShouldBeEmpty) + So(err, ShouldResemble, ErrTargetHasNoMetrics{targetIndex: 1}) }) Convey("Test has metrics", t, func() { Convey("Only one target", func() { source.EXPECT().Fetch(pattern, from, until, true).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return([]*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)}) + fetchResult.EXPECT().GetMetricsData().Return([]metricSource.MetricData{*metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)}) fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) actual, metrics, err := triggerChecker.fetch() - metricData := &metricSource.MetricData{ + metricData := metricSource.MetricData{ Name: metric, StartTime: from, StopTime: until, StepTime: retention, Values: []float64{0, 1, 2, 3, 4}, } - expected := metricSource.MakeTriggerMetricsData([]*metricSource.MetricData{metricData}, make([]*metricSource.MetricData, 0)) + expected := metricSource.FetchedMetrics{"t1": []metricSource.MetricData{metricData}} So(err, ShouldBeNil) So(actual, ShouldResemble, expected) So(metrics, ShouldResemble, []string{metric}) @@ -227,8 +172,8 @@ func TestFetch(t *testing.T) { triggerChecker.trigger.Targets = []string{pattern, addPattern} triggerChecker.trigger.Patterns = []string{pattern, addPattern} - metricData := []*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)} - addMetricData := []*metricSource.MetricData{metricSource.MakeMetricData(addMetric, []float64{0, 1, 2, 3, 4}, retention, from)} + metricData := metricSource.FetchedPatternMetrics{*metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)} + addMetricData := metricSource.FetchedPatternMetrics{*metricSource.MakeMetricData(addMetric, []float64{0, 1, 2, 3, 4}, retention, from)} source.EXPECT().Fetch(pattern, from, until, false).Return(fetchResult, nil) fetchResult.EXPECT().GetMetricsData().Return(metricData) @@ -239,7 +184,7 @@ func TestFetch(t *testing.T) { fetchResult.EXPECT().GetPatternMetrics().Return([]string{addMetric}, nil) actual, metrics, err := triggerChecker.fetch() - expected := metricSource.MakeTriggerMetricsData(metricData, addMetricData) + expected := metricSource.FetchedMetrics{"t1": metricData, "t2": addMetricData} So(err, ShouldBeNil) So(actual, ShouldResemble, expected) @@ -247,41 +192,11 @@ func TestFetch(t *testing.T) { }) Convey("Two targets with many metrics in additional target", func() { - metricData := []*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)} - addMetricData := []*metricSource.MetricData{ - metricSource.MakeMetricData(addMetric, []float64{0, 1, 2, 3, 4}, retention, from), - metricSource.MakeMetricData(addMetric2, []float64{0, 1, 2, 3, 4}, retention, from), - } + metricData := []metricSource.MetricData{*metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)} - source.EXPECT().Fetch(pattern, from, until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return(metricData) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) - - source.EXPECT().Fetch(addPattern, from, until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return(addMetricData) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{addMetric, addMetric2}, nil) - - actual, metrics, err := triggerChecker.fetch() - So(err, ShouldBeError) - So(err, ShouldResemble, ErrWrongTriggerTargets([]int{2})) - So(err.Error(), ShouldResemble, "Target t2 has more than one metric") - So(actual, ShouldBeNil) - So(metrics, ShouldBeNil) - }) - - Convey("Four targets with many metrics in additional targets", func() { - triggerChecker.trigger.Targets = []string{pattern, addPattern, pattern2, oneMorePattern} - triggerChecker.trigger.Patterns = []string{pattern, addPattern, pattern2, oneMorePattern} - - metricData := []*metricSource.MetricData{metricSource.MakeMetricData(metric, []float64{0, 1, 2, 3, 4}, retention, from)} - add1MetricData := []*metricSource.MetricData{ - metricSource.MakeMetricData(addMetric, []float64{0, 1, 2, 3, 4}, retention, from), - metricSource.MakeMetricData(addMetric2, []float64{0, 1, 2, 3, 4}, retention, from), - } - add2MetricData := []*metricSource.MetricData{metricSource.MakeMetricData(metric2, []float64{0, 1, 2, 3, 4}, retention, from)} - oneMoreMetricData := []*metricSource.MetricData{ - metricSource.MakeMetricData(oneMoreMetric1, []float64{0, 1, 2, 3, 4}, retention, from), - metricSource.MakeMetricData(oneMoreMetric2, []float64{0, 1, 2, 3, 4}, retention, from), + addMetricData := []metricSource.MetricData{ + *metricSource.MakeMetricData(addMetric, []float64{0, 1, 2, 3, 4}, retention, from), + *metricSource.MakeMetricData(addMetric2, []float64{0, 1, 2, 3, 4}, retention, from), } source.EXPECT().Fetch(pattern, from, until, false).Return(fetchResult, nil) @@ -289,110 +204,13 @@ func TestFetch(t *testing.T) { fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric}, nil) source.EXPECT().Fetch(addPattern, from, until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return(add1MetricData) + fetchResult.EXPECT().GetMetricsData().Return(addMetricData) fetchResult.EXPECT().GetPatternMetrics().Return([]string{addMetric, addMetric2}, nil) - source.EXPECT().Fetch(pattern2, from, until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return(add2MetricData) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{metric2}, nil) - - source.EXPECT().Fetch(oneMorePattern, from, until, false).Return(fetchResult, nil) - fetchResult.EXPECT().GetMetricsData().Return(oneMoreMetricData) - fetchResult.EXPECT().GetPatternMetrics().Return([]string{oneMoreMetric1, oneMoreMetric2}, nil) - actual, metrics, err := triggerChecker.fetch() - So(err, ShouldBeError) - So(err, ShouldResemble, ErrWrongTriggerTargets([]int{2, 4})) - So(err.Error(), ShouldResemble, "Targets t2, t4 has more than one metric") - So(actual, ShouldBeNil) - So(metrics, ShouldBeNil) + So(err, ShouldBeNil) + So(actual, ShouldResemble, metricSource.FetchedMetrics{"t1": metricData, "t2": addMetricData}) + So(metrics, ShouldResemble, []string{metric, addMetric, addMetric2}) }) }) } - -func TestGetExpressionValues(t *testing.T) { - Convey("Has only main metric data", t, func() { - metricData := &metricSource.MetricData{ - Name: "m", - StartTime: 17, - StopTime: 67, - StepTime: 10, - Values: []float64{0.0, math.NaN(), math.NaN(), 3.0, math.NaN()}, - } - tts := &metricSource.TriggerMetricsData{ - Main: []*metricSource.MetricData{metricData}, - } - expectedExpressionValues := &expression.TriggerExpression{ - AdditionalTargetsValues: make(map[string]float64), - } - - values, noEmptyValues := getExpressionValues(tts, metricData, 17) - So(noEmptyValues, ShouldBeTrue) - So(values, ShouldResemble, expectedExpressionValues) - - values, noEmptyValues = getExpressionValues(tts, metricData, 67) - So(noEmptyValues, ShouldBeFalse) - So(values, ShouldResemble, expectedExpressionValues) - - values, noEmptyValues = getExpressionValues(tts, metricData, 11) - So(noEmptyValues, ShouldBeFalse) - So(values, ShouldResemble, expectedExpressionValues) - - values, noEmptyValues = getExpressionValues(tts, metricData, 44) - So(noEmptyValues, ShouldBeFalse) - So(values, ShouldResemble, expectedExpressionValues) - - expectedExpressionValues.MainTargetValue = 3 - values, noEmptyValues = getExpressionValues(tts, metricData, 53) - So(noEmptyValues, ShouldBeTrue) - So(values, ShouldResemble, expectedExpressionValues) - }) - - Convey("Has additional series", t, func() { - metricData := &metricSource.MetricData{ - Name: "main", - StartTime: 17, - StopTime: 67, - StepTime: 10, - Values: []float64{0.0, math.NaN(), math.NaN(), 3.0, math.NaN()}, - } - metricDataAdd := &metricSource.MetricData{ - Name: "main", - StartTime: 17, - StopTime: 67, - StepTime: 10, - Values: []float64{4.0, 3.0, math.NaN(), math.NaN(), 0.0}, - } - tts := &metricSource.TriggerMetricsData{ - Main: []*metricSource.MetricData{metricData}, - Additional: []*metricSource.MetricData{metricDataAdd}, - } - - expectedExpressionValues := &expression.TriggerExpression{ - AdditionalTargetsValues: make(map[string]float64), - } - - values, noEmptyValues := getExpressionValues(tts, metricData, 29) - So(noEmptyValues, ShouldBeFalse) - So(values, ShouldResemble, expectedExpressionValues) - - values, noEmptyValues = getExpressionValues(tts, metricData, 42) - So(noEmptyValues, ShouldBeFalse) - So(values, ShouldResemble, expectedExpressionValues) - - values, noEmptyValues = getExpressionValues(tts, metricData, 65) - So(noEmptyValues, ShouldBeFalse) - So(values, ShouldResemble, expectedExpressionValues) - - expectedExpressionValues.MainTargetValue = 3 - values, noEmptyValues = getExpressionValues(tts, metricData, 50) - So(noEmptyValues, ShouldBeFalse) - So(values, ShouldResemble, expectedExpressionValues) - - expectedExpressionValues.MainTargetValue = 0 - expectedExpressionValues.AdditionalTargetsValues["t2"] = 4 - values, noEmptyValues = getExpressionValues(tts, metricData, 17) - So(noEmptyValues, ShouldBeTrue) - So(values, ShouldResemble, expectedExpressionValues) - }) -} diff --git a/checker/trigger_checker_test.go b/checker/trigger_checker_test.go index ee03429eb..fd278c803 100644 --- a/checker/trigger_checker_test.go +++ b/checker/trigger_checker_test.go @@ -77,21 +77,21 @@ func TestInitTriggerChecker(t *testing.T) { Timestamp: 1502694427, State: moira.StateOK, Suppressed: false, - Value: &value, + Values: map[string]float64{"t1": value}, EventTimestamp: 1501680428, }, "2": { Timestamp: 1502694427, State: moira.StateOK, Suppressed: false, - Value: &value, + Values: map[string]float64{"t1": value}, EventTimestamp: 1501679827, }, "3": { Timestamp: 1502694427, State: moira.StateOK, Suppressed: false, - Value: &value, + Values: map[string]float64{"t1": value}, EventTimestamp: 1501679887, }, }, diff --git a/database/redis/compatibility26.go b/database/redis/compatibility26.go new file mode 100644 index 000000000..da47ea69a --- /dev/null +++ b/database/redis/compatibility26.go @@ -0,0 +1,76 @@ +package redis + +import ( + "fmt" + "github.com/moira-alert/moira" +) + +// Compatibility with moira < v2.6.0 +// TODO(litleleprikon): Remove this file in moira v2.8.0 + +const firstTarget = "t1" + +// checkDataDidUnmarshal is a function that adds to CheckData metrics +// a Values map. +func checkDataDidUnmarshal(checkData *moira.CheckData) { + for metricName, metricState := range checkData.Metrics { + if metricState.Values == nil { + metricState.Values = make(map[string]float64) + } + if metricState.Value != nil { + metricState.Values[firstTarget] = *metricState.Value + metricState.Value = nil + } + checkData.Metrics[metricName] = metricState + } + if checkData.MetricsToTargetRelation == nil { + checkData.MetricsToTargetRelation = make(map[string]string) + } +} + +// checkDataWillMarshal is a function that fill Value field +// from Values map. +func checkDataWillMarshal(checkData *moira.CheckData) { + for metricName, metricState := range checkData.Metrics { + if metricState.Value == nil { + if value, ok := metricState.Values[firstTarget]; ok { + metricState.Value = &value + checkData.Metrics[metricName] = metricState + } + } + } +} + +// notificationEventDidUnmarshal is a function that adds to NotificationEvent +// a Values map. +func notificationEventDidUnmarshal(event *moira.NotificationEvent) { + if event.Values == nil { + event.Values = make(map[string]float64) + } + if event.Value != nil { + event.Values[firstTarget] = *event.Value + event.Value = nil + } +} + +// notificationEventWillMarshal is a function that fill Value field +// from Values map. +func notificationEventWillMarshal(event *moira.NotificationEvent) { + if event.Value == nil { + if value, ok := event.Values[firstTarget]; ok { + event.Value = &value + } + } +} + +// triggerDidUnmarshal is a function that fills AloneMetrics map for trigger. +func triggerDidUnmarshal(trigger *moira.Trigger) { + if trigger.AloneMetrics == nil { + aloneMetricsLen := len(trigger.Targets) + trigger.AloneMetrics = make(map[string]bool, aloneMetricsLen) + for i := 2; i <= aloneMetricsLen; i++ { + targetName := fmt.Sprintf("t%d", i) + trigger.AloneMetrics[targetName] = true + } + } +} diff --git a/database/redis/compatibility26_test.go b/database/redis/compatibility26_test.go new file mode 100644 index 000000000..bffc3d860 --- /dev/null +++ b/database/redis/compatibility26_test.go @@ -0,0 +1,155 @@ +package redis + +import ( + . "github.com/glycerine/goconvey/convey" + "github.com/moira-alert/moira" + "testing" +) + +func Test_checkDataDidUnmarshal(t *testing.T) { + Convey("checkDataDidUnmarshal", t, func() { + checkData := &moira.CheckData{} + Convey("metrics are empty and metrics to target relation is empty", func() { + checkDataDidUnmarshal(checkData) + So(checkData, ShouldResemble, &moira.CheckData{ + MetricsToTargetRelation: map[string]string{}, + }) + }) + Convey("metrics are not empty and metrics to target relation is empty", func() { + checkData.Metrics = map[string]moira.MetricState{ + "metric.test.1": {}, + } + checkDataDidUnmarshal(checkData) + So(checkData, ShouldResemble, &moira.CheckData{ + Metrics: map[string]moira.MetricState{ + "metric.test.1": { + Values: map[string]float64{}, + }, + }, + MetricsToTargetRelation: map[string]string{}, + }) + }) + Convey("metrics are not empty with filled value and metrics to target relation is empty", func() { + value := float64(10) + checkData.Metrics = map[string]moira.MetricState{ + "metric.test.1": {Value: &value}, + } + checkDataDidUnmarshal(checkData) + So(checkData, ShouldResemble, &moira.CheckData{ + Metrics: map[string]moira.MetricState{ + "metric.test.1": { + Values: map[string]float64{"t1": 10}, + }, + }, + MetricsToTargetRelation: map[string]string{}, + }) + }) + }) +} + +func Test_checkDataWillMarshal(t *testing.T) { + Convey("checkDataWillMarshal", t, func() { + checkData := &moira.CheckData{} + Convey("metrics are empty", func() { + checkDataWillMarshal(checkData) + So(checkData, ShouldResemble, &moira.CheckData{}) + }) + Convey("metrics are not empty and values is empty", func() { + checkData.Metrics = map[string]moira.MetricState{ + "metric.test.1": {}, + } + checkDataWillMarshal(checkData) + So(checkData, ShouldResemble, &moira.CheckData{ + Metrics: map[string]moira.MetricState{ + "metric.test.1": {}, + }, + }) + }) + Convey("metrics are not empty and values is not empty", func() { + value := float64(10) + checkData.Metrics = map[string]moira.MetricState{ + "metric.test.1": { + Values: map[string]float64{"t1": 10}, + }, + } + checkDataWillMarshal(checkData) + So(checkData, ShouldResemble, &moira.CheckData{ + Metrics: map[string]moira.MetricState{ + "metric.test.1": { + Value: &value, + Values: map[string]float64{"t1": 10}, + }, + }, + }) + }) + }) +} + +func Test_notificationEventDidUnmarshal(t *testing.T) { + Convey("notificationEventDidUnmarshal", t, func() { + event := &moira.NotificationEvent{} + Convey("value is empty", func() { + notificationEventDidUnmarshal(event) + So(event, ShouldResemble, &moira.NotificationEvent{ + Values: map[string]float64{}, + }) + }) + Convey("value is not empty", func() { + value := float64(10) + event.Value = &value + notificationEventDidUnmarshal(event) + So(event, ShouldResemble, &moira.NotificationEvent{ + Values: map[string]float64{ + "t1": 10, + }, + }) + }) + }) +} + +func Test_notificationEventWillMarshal(t *testing.T) { + Convey("notificationEventWillMarshal", t, func() { + event := &moira.NotificationEvent{Values: map[string]float64{},} + Convey("values is empty", func() { + notificationEventWillMarshal(event) + So(event, ShouldResemble, &moira.NotificationEvent{ + Values: map[string]float64{}, + }) + }) + Convey("values is not empty", func() { + value := float64(10) + event.Values = map[string]float64{ + "t1": 10, + } + notificationEventWillMarshal(event) + So(event, ShouldResemble, &moira.NotificationEvent{ + Values: map[string]float64{ + "t1": 10, + }, + Value: &value, + }) + }) + }) +} + +func Test_triggerDidUnmarshal(t *testing.T) { + Convey("triggerDidUnmarshal", t, func() { + trigger := &moira.Trigger{} + Convey("have one target", func() { + trigger.Targets = []string{"target.test.1"} + triggerDidUnmarshal(trigger) + So(trigger, ShouldResemble, &moira.Trigger{ + Targets: []string{"target.test.1"}, + AloneMetrics: map[string]bool{}, + }) + }) + Convey("have more than one targets", func() { + trigger.Targets = []string{"target.test.1", "target.test.2"} + triggerDidUnmarshal(trigger) + So(trigger, ShouldResemble, &moira.Trigger{ + Targets: []string{"target.test.1", "target.test.2"}, + AloneMetrics: map[string]bool{"t2": true}, + }) + }) + }) +} diff --git a/database/redis/last_check.go b/database/redis/last_check.go index f407b3493..2c908ba91 100644 --- a/database/redis/last_check.go +++ b/database/redis/last_check.go @@ -15,12 +15,18 @@ import ( func (connector *DbConnector) GetTriggerLastCheck(triggerID string) (moira.CheckData, error) { c := connector.pool.Get() defer c.Close() - return reply.Check(c.Do("GET", metricLastCheckKey(triggerID))) + lastCheck, err := reply.Check(c.Do("GET", metricLastCheckKey(triggerID))) + if err != nil { + return lastCheck, err + } + checkDataDidUnmarshal(&lastCheck) //TODO(litleleprikon): remove in moira v2.8.0. Compatibility with moira < v2.6.0 + return lastCheck, nil } // SetTriggerLastCheck sets trigger last check data func (connector *DbConnector) SetTriggerLastCheck(triggerID string, checkData *moira.CheckData, isRemote bool) error { selfStateCheckCountKey := connector.getSelfStateCheckCountKey(isRemote) + checkDataWillMarshal(checkData) //TODO(litleleprikon): remove in moira v2.8.0. Compatibility with moira < v2.6.0 bytes, err := json.Marshal(checkData) if err != nil { return err diff --git a/database/redis/last_check_test.go b/database/redis/last_check_test.go index 25de08286..8b343226c 100644 --- a/database/redis/last_check_test.go +++ b/database/redis/last_check_test.go @@ -264,6 +264,46 @@ func TestLastCheck(t *testing.T) { So(err, ShouldBeNil) So(actual, ShouldResemble, []string{triggerID}) }) + + Convey("Test populate metric values", func() { + value := float64(1) + triggerID := uuid.Must(uuid.NewV4()).String() + err := dataBase.SetTriggerLastCheck(triggerID, &moira.CheckData{ + Score: 6000, + State: moira.StateOK, + Timestamp: 1504509981, + Maintenance: 1552723340, + Metrics: map[string]moira.MetricState{ + "metric1": { + EventTimestamp: 1504463770, + State: "Ok", + Suppressed: false, + Timestamp: 1504509380, + Value: &value, + }, + }, + }, false) + So(err, ShouldBeNil) + + actual, err := dataBase.GetTriggerLastCheck(triggerID) + So(err, ShouldBeNil) + So(actual, ShouldResemble, moira.CheckData{ + Score: 6000, + State: moira.StateOK, + Timestamp: 1504509981, + Maintenance: 1552723340, + Metrics: map[string]moira.MetricState{ + "metric1": { + EventTimestamp: 1504463770, + State: "Ok", + Suppressed: false, + Timestamp: 1504509380, + Values: map[string]float64{"t1": 1}, + }, + }, + MetricsToTargetRelation: map[string]string{}, + }) + }) }) } @@ -477,38 +517,52 @@ var lastCheckTest = moira.CheckData{ State: moira.StateNODATA, Suppressed: false, Timestamp: 1504509380, + Values: map[string]float64{}, }, "metric2": { EventTimestamp: 1504449789, State: moira.StateNODATA, Suppressed: false, Timestamp: 1504509380, + Values: map[string]float64{}, }, "metric3": { EventTimestamp: 1504449789, State: moira.StateNODATA, Suppressed: false, Timestamp: 1504509380, + Values: map[string]float64{}, }, "metric4": { EventTimestamp: 1504463770, State: moira.StateNODATA, Suppressed: false, Timestamp: 1504509380, + Values: map[string]float64{}, }, "metric5": { EventTimestamp: 1504463770, State: moira.StateNODATA, Suppressed: false, Timestamp: 1504509380, + Values: map[string]float64{}, }, "metric6": { EventTimestamp: 1504463770, State: "Ok", Suppressed: false, Timestamp: 1504509380, + Values: map[string]float64{}, + }, + "metric7": { + EventTimestamp: 1504463770, + State: "Ok", + Suppressed: false, + Timestamp: 1504509380, + Values: map[string]float64{}, }, }, + MetricsToTargetRelation: map[string]string{}, } var lastCheckWithNoMetrics = moira.CheckData{ @@ -516,6 +570,7 @@ var lastCheckWithNoMetrics = moira.CheckData{ State: moira.StateOK, Timestamp: 1504509981, Metrics: make(map[string]moira.MetricState), + MetricsToTargetRelation: map[string]string{}, } var lastCheckWithNoMetricsWithMaintenance = moira.CheckData{ @@ -524,4 +579,5 @@ var lastCheckWithNoMetricsWithMaintenance = moira.CheckData{ Timestamp: 1504509981, Maintenance: 1000, Metrics: make(map[string]moira.MetricState), + MetricsToTargetRelation: map[string]string{}, } diff --git a/database/redis/notification_event.go b/database/redis/notification_event.go index 5f59fd6f5..c96a07f82 100644 --- a/database/redis/notification_event.go +++ b/database/redis/notification_event.go @@ -21,6 +21,10 @@ func (connector *DbConnector) GetNotificationEvents(triggerID string, start int6 eventsData, err := reply.Events(c.Do("ZREVRANGE", triggerEventsKey(triggerID), start, start+size)) + for _, event := range eventsData { //TODO(litleleprikon): remove in moira v2.8.0. Compatibility with moira < v2.6.0 + notificationEventDidUnmarshal(event) + } + if err != nil { if err == redis.ErrNil { return make([]*moira.NotificationEvent, 0), nil @@ -34,6 +38,7 @@ func (connector *DbConnector) GetNotificationEvents(triggerID string, start int6 // PushNotificationEvent adds new NotificationEvent to events list and to given triggerID events list and deletes events who are older than 30 days // If ui=true, then add to ui events list func (connector *DbConnector) PushNotificationEvent(event *moira.NotificationEvent, ui bool) error { + notificationEventWillMarshal(event) //TODO(litleleprikon): remove in moira v2.8.0. Compatibility with moira < v2.6.0 eventBytes, err := json.Marshal(event) if err != nil { return err @@ -92,6 +97,13 @@ func (connector *DbConnector) FetchNotificationEvent() (moira.NotificationEvent, if err := json.Unmarshal(eventBytes, &event); err != nil { return event, fmt.Errorf("failed to parse event json %s: %s", eventBytes, err.Error()) } + if event.Values == nil { //TODO(litleleprikon): remove in moira v2.8.0. Compatibility with moira < v2.6.0 + event.Values = make(map[string]float64) + } + if event.Value != nil { + event.Values["t1"] = *event.Value + event.Value = nil + } return event, nil } diff --git a/database/redis/notification_event_test.go b/database/redis/notification_event_test.go index a777e1db1..f7a1cf665 100644 --- a/database/redis/notification_event_test.go +++ b/database/redis/notification_event_test.go @@ -3,7 +3,6 @@ package redis import ( "testing" - "github.com/gofrs/uuid" "github.com/op/go-logging" . "github.com/smartystreets/goconvey/convey" @@ -13,6 +12,14 @@ import ( "github.com/moira-alert/moira/database" ) +const triggerID = "81588c33-eab3-4ad4-aa03-82a9560adad9" +const triggerID1 = "7854DE02-0E4B-4430-A570-B0C0162755E4" +const triggerID2 = "26D3C4E4-507E-4930-9B1E-FD5AD369445C" +const triggerID3 = "F0F4A5B9-637C-4933-AA0D-88B9798A2630" +var now = time.Now().Unix() +var value = float64(0) + + func TestNotificationEvents(t *testing.T) { logger, _ := logging.GetLogger("dataBase") dataBase := newTestDatabase(logger, config) @@ -22,11 +29,11 @@ func TestNotificationEvents(t *testing.T) { Convey("Notification events manipulation", t, func() { Convey("Test push-get-get count-fetch", func() { Convey("Should no events", func() { - actual, err := dataBase.GetNotificationEvents(notificationEvent.TriggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) - total := dataBase.GetNotificationEventCount(notificationEvent.TriggerID, 0) + total := dataBase.GetNotificationEventCount(triggerID, 0) So(total, ShouldEqual, 0) actual1, err := dataBase.FetchNotificationEvent() @@ -36,27 +43,55 @@ func TestNotificationEvents(t *testing.T) { }) Convey("Should has one events after push", func() { - err := dataBase.PushNotificationEvent(¬ificationEvent, true) + err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID, + Metric: "my.metric", + Value: &value, + }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(notificationEvent.TriggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{¬ificationEvent}) - - total := dataBase.GetNotificationEventCount(notificationEvent.TriggerID, 0) + So(actual, ShouldResemble, []*moira.NotificationEvent{&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID, + Metric: "my.metric", + Values: map[string]float64{"t1": 0}, + }}) + + total := dataBase.GetNotificationEventCount(triggerID, 0) So(total, ShouldEqual, 1) actual1, err := dataBase.FetchNotificationEvent() So(err, ShouldBeNil) - So(actual1, ShouldResemble, notificationEvent) + So(actual1, ShouldResemble, moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID, + Metric: "my.metric", + Values: map[string]float64{"t1": 0}, + }) }) Convey("Should has event by triggerID after fetch", func() { - actual, err := dataBase.GetNotificationEvents(notificationEvent.TriggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{¬ificationEvent}) - - total := dataBase.GetNotificationEventCount(notificationEvent.TriggerID, 0) + So(actual, ShouldResemble, []*moira.NotificationEvent{&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID, + Metric: "my.metric", + Values: map[string]float64{"t1": 0}, + }}) + + total := dataBase.GetNotificationEventCount(triggerID, 0) So(total, ShouldEqual, 1) }) @@ -70,44 +105,92 @@ func TestNotificationEvents(t *testing.T) { Convey("Test push-fetch multiple event by differ triggerIDs", func() { Convey("Push events and get it by triggerIDs", func() { - err := dataBase.PushNotificationEvent(¬ificationEvent1, true) + err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateEXCEPTION, + OldState: moira.StateNODATA, + TriggerID: triggerID1, + Metric: "my.metric", + }, true) So(err, ShouldBeNil) - err = dataBase.PushNotificationEvent(¬ificationEvent2, true) + err = dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateOK, + OldState: moira.StateWARN, + TriggerID: triggerID2, + Metric: "my.metric1", + Value: &value, + }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(notificationEvent1.TriggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{¬ificationEvent1}) - - total := dataBase.GetNotificationEventCount(notificationEvent1.TriggerID, 0) + So(actual, ShouldResemble, []*moira.NotificationEvent{&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateEXCEPTION, + OldState: moira.StateNODATA, + TriggerID: triggerID1, + Metric: "my.metric", + Values: map[string]float64{}, + }}) + + total := dataBase.GetNotificationEventCount(triggerID1, 0) So(total, ShouldEqual, 1) - actual, err = dataBase.GetNotificationEvents(notificationEvent2.TriggerID, 0, 1) + actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1) So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{¬ificationEvent2}) - - total = dataBase.GetNotificationEventCount(notificationEvent2.TriggerID, 0) + So(actual, ShouldResemble, []*moira.NotificationEvent{&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateOK, + OldState: moira.StateWARN, + TriggerID: triggerID2, + Metric: "my.metric1", + Values: map[string]float64{"t1": 0}, + }}) + + total = dataBase.GetNotificationEventCount(triggerID2, 0) So(total, ShouldEqual, 1) }) Convey("Fetch one of them and check for existing again", func() { actual1, err := dataBase.FetchNotificationEvent() So(err, ShouldBeNil) - So(actual1, ShouldResemble, notificationEvent1) - - actual, err := dataBase.GetNotificationEvents(notificationEvent1.TriggerID, 0, 1) + So(actual1, ShouldResemble, moira.NotificationEvent{ + Timestamp: now, + State: moira.StateEXCEPTION, + OldState: moira.StateNODATA, + TriggerID: triggerID1, + Metric: "my.metric", + Values: map[string]float64{}, + }) + + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{¬ificationEvent1}) - - total := dataBase.GetNotificationEventCount(notificationEvent1.TriggerID, 0) + So(actual, ShouldResemble, []*moira.NotificationEvent{&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateEXCEPTION, + OldState: moira.StateNODATA, + TriggerID: triggerID1, + Metric: "my.metric", + Values: map[string]float64{}, + }}) + + total := dataBase.GetNotificationEventCount(triggerID1, 0) So(total, ShouldEqual, 1) }) Convey("Fetch second then fetch and and check for ErrNil", func() { actual, err := dataBase.FetchNotificationEvent() So(err, ShouldBeNil) - So(actual, ShouldResemble, notificationEvent2) + So(actual, ShouldResemble, moira.NotificationEvent{ + Timestamp: now, + State: moira.StateOK, + OldState: moira.StateWARN, + TriggerID: triggerID2, + Metric: "my.metric1", + Values: map[string]float64{"t1": 0}, + }) actual, err = dataBase.FetchNotificationEvent() So(err, ShouldBeError) @@ -117,48 +200,73 @@ func TestNotificationEvents(t *testing.T) { }) Convey("Test get by ranges", func() { - now := time.Now().Unix() - event := moira.NotificationEvent{ + err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ Timestamp: now, State: moira.StateNODATA, OldState: moira.StateNODATA, - TriggerID: uuid.Must(uuid.NewV4()).String(), + TriggerID: triggerID3, Metric: "my.metric", - } - - err := dataBase.PushNotificationEvent(&event, true) + }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(event.TriggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1) So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{&event}) + So(actual, ShouldResemble, []*moira.NotificationEvent{&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + Values: map[string]float64{}, + }}) - total := dataBase.GetNotificationEventCount(event.TriggerID, 0) + total := dataBase.GetNotificationEventCount(triggerID3, 0) So(total, ShouldEqual, 1) - total = dataBase.GetNotificationEventCount(event.TriggerID, now-1) + total = dataBase.GetNotificationEventCount(triggerID3, now-1) So(total, ShouldEqual, 1) - total = dataBase.GetNotificationEventCount(event.TriggerID, now) + total = dataBase.GetNotificationEventCount(triggerID3, now) So(total, ShouldEqual, 1) - total = dataBase.GetNotificationEventCount(event.TriggerID, now+1) + total = dataBase.GetNotificationEventCount(triggerID3, now+1) So(total, ShouldEqual, 0) - actual, err = dataBase.GetNotificationEvents(event.TriggerID, 1, 1) + actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) }) Convey("Test removing notification events", func() { Convey("Should remove all notifications", func() { - err := dataBase.PushNotificationEvent(¬ificationEvent, true) + err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID, + Metric: "my.metric", + Value: &value, + }, true) So(err, ShouldBeNil) - err = dataBase.PushNotificationEvent(¬ificationEvent1, true) + err = dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateEXCEPTION, + OldState: moira.StateNODATA, + TriggerID: triggerID1, + Metric: "my.metric", + Value: &value, + }, true) So(err, ShouldBeNil) - err = dataBase.PushNotificationEvent(¬ificationEvent2, true) + err = dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateOK, + OldState: moira.StateWARN, + TriggerID: triggerID2, + Metric: "my.metric1", + Value: &value, + }, true) So(err, ShouldBeNil) err = dataBase.RemoveAllNotificationEvents() @@ -203,26 +311,3 @@ func TestNotificationEventErrorConnection(t *testing.T) { So(err, ShouldNotBeNil) }) } - -var notificationEvent = moira.NotificationEvent{ - Timestamp: time.Now().Unix(), - State: moira.StateNODATA, - OldState: moira.StateNODATA, - TriggerID: "81588c33-eab3-4ad4-aa03-82a9560adad9", - Metric: "my.metric", -} - -var notificationEvent1 = moira.NotificationEvent{ - Timestamp: time.Now().Unix(), - State: moira.StateEXCEPTION, - OldState: moira.StateNODATA, - TriggerID: uuid.Must(uuid.NewV4()).String(), - Metric: "my.metric", -} -var notificationEvent2 = moira.NotificationEvent{ - Timestamp: time.Now().Unix(), - State: moira.StateOK, - OldState: moira.StateWARN, - TriggerID: uuid.Must(uuid.NewV4()).String(), - Metric: "my.metric1", -} diff --git a/database/redis/reply/trigger.go b/database/redis/reply/trigger.go index d2d5bf7e9..7fb89b459 100644 --- a/database/redis/reply/trigger.go +++ b/database/redis/reply/trigger.go @@ -28,6 +28,7 @@ type triggerStorageElement struct { TTL string `json:"ttl,omitempty"` IsRemote bool `json:"is_remote"` MuteNewMetrics bool `json:"mute_new_metrics,omitempty"` + AloneMetrics map[string]bool `json:"alone_metrics"` } func (storageElement *triggerStorageElement) toTrigger() moira.Trigger { @@ -48,6 +49,7 @@ func (storageElement *triggerStorageElement) toTrigger() moira.Trigger { TTL: getTriggerTTL(storageElement.TTL), IsRemote: storageElement.IsRemote, MuteNewMetrics: storageElement.MuteNewMetrics, + AloneMetrics: storageElement.AloneMetrics, } } @@ -69,6 +71,7 @@ func toTriggerStorageElement(trigger *moira.Trigger, triggerID string) *triggerS TTL: getTriggerTTLString(trigger.TTL), IsRemote: trigger.IsRemote, MuteNewMetrics: trigger.MuteNewMetrics, + AloneMetrics: trigger.AloneMetrics, } } diff --git a/database/redis/trigger.go b/database/redis/trigger.go index 6eca62c7f..f5a56414d 100644 --- a/database/redis/trigger.go +++ b/database/redis/trigger.go @@ -282,6 +282,7 @@ func (connector *DbConnector) GetTriggerChecks(triggerIDs []string) ([]*moira.Tr return nil, err } lastCheck, err := reply.Check(slice[3], nil) + checkDataDidUnmarshal(&lastCheck) //TODO(litleleprikon): remove in moira v2.8.0. Compatibility with moira < v2.6.0 if err != nil && err != database.ErrNil { return nil, err } @@ -300,6 +301,7 @@ func (connector *DbConnector) GetTriggerChecks(triggerIDs []string) ([]*moira.Tr func (connector *DbConnector) getTriggerWithTags(triggerRaw interface{}, tagsRaw interface{}, triggerID string) (moira.Trigger, error) { trigger, err := reply.Trigger(triggerRaw, nil) + triggerDidUnmarshal(&trigger) //TODO(litleleprikon): remove in moira v2.8.0. Compatibility with moira < v2.6.0 if err != nil { return trigger, err } diff --git a/database/redis/trigger_test.go b/database/redis/trigger_test.go index 71d300b59..e2db401fb 100644 --- a/database/redis/trigger_test.go +++ b/database/redis/trigger_test.go @@ -54,7 +54,7 @@ func TestTriggerStoring(t *testing.T) { //Check for not existing not written trigger actual, err := dataBase.GetTrigger(trigger.ID) So(err, ShouldResemble, database.ErrNil) - So(actual, ShouldResemble, moira.Trigger{}) + So(actual, ShouldResemble, moira.Trigger{AloneMetrics: map[string]bool{}}) err = dataBase.RemoveTrigger(trigger.ID) So(err, ShouldBeNil) @@ -187,7 +187,7 @@ func TestTriggerStoring(t *testing.T) { //And check for existing by several pointers like id or tag actual, err = dataBase.GetTrigger(changedAgainTrigger.ID) So(err, ShouldResemble, database.ErrNil) - So(actual, ShouldResemble, moira.Trigger{}) + So(actual, ShouldResemble, moira.Trigger{AloneMetrics: map[string]bool{}}) ids, err = dataBase.GetLocalTriggerIDs() So(err, ShouldBeNil) @@ -228,6 +228,7 @@ func TestTriggerStoring(t *testing.T) { trigger := triggers[5] triggerCheck := &moira.TriggerCheck{ Trigger: trigger, + LastCheck:moira.CheckData{MetricsToTargetRelation: map[string]string{}}, } err := dataBase.SaveTrigger(trigger.ID, &trigger) @@ -295,21 +296,23 @@ func TestTriggerStoring(t *testing.T) { metric2 := "my.new.test.super.metric2" triggerVer1 := &moira.Trigger{ - ID: "test-triggerID-id1", - Name: "test trigger 1 v1.0", - Targets: []string{pattern1}, - Tags: []string{"test-tag-1"}, - Patterns: []string{pattern1}, - TriggerType: moira.RisingTrigger, + ID: "test-triggerID-id1", + Name: "test trigger 1 v1.0", + Targets: []string{pattern1}, + Tags: []string{"test-tag-1"}, + Patterns: []string{pattern1}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, } triggerVer2 := &moira.Trigger{ - ID: "test-triggerID-id1", - Name: "test trigger 1 v2.0", - Targets: []string{pattern2}, - Tags: []string{"test-tag-1"}, - Patterns: []string{pattern2}, - TriggerType: moira.RisingTrigger, + ID: "test-triggerID-id1", + Name: "test trigger 1 v2.0", + Targets: []string{pattern2}, + Tags: []string{"test-tag-1"}, + Patterns: []string{pattern2}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, } val1 := &moira.MatchedMetric{ @@ -432,7 +435,7 @@ func TestTriggerStoring(t *testing.T) { actual, err = dataBase.GetTrigger(triggerVer2.ID) So(err, ShouldResemble, database.ErrNil) - So(actual, ShouldResemble, moira.Trigger{}) + So(actual, ShouldResemble, moira.Trigger{AloneMetrics: map[string]bool{}}) ids, err = dataBase.GetLocalTriggerIDs() So(err, ShouldBeNil) @@ -518,12 +521,13 @@ func TestRemoteTrigger(t *testing.T) { dataBase := newTestDatabase(logger, config) pattern := "test.pattern.remote1" trigger := &moira.Trigger{ - ID: "triggerID-0000000000010", - Name: "remote", - Targets: []string{"test.target.remote1"}, - Patterns: []string{pattern}, - IsRemote: true, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000010", + Name: "remote", + Targets: []string{"test.target.remote1"}, + Patterns: []string{pattern}, + IsRemote: true, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, } dataBase.flush() defer dataBase.flush() @@ -679,70 +683,79 @@ func TestTriggerErrorConnection(t *testing.T) { var triggers = []moira.Trigger{ { - ID: "triggerID-0000000000001", - Name: "test trigger 1 v1.0", - Targets: []string{"test.target.1"}, - Tags: []string{"test-tag-1"}, - Patterns: []string{"test.pattern.1"}, - TriggerType: moira.RisingTrigger, - TTLState: &moira.TTLStateNODATA, + ID: "triggerID-0000000000001", + Name: "test trigger 1 v1.0", + Targets: []string{"test.target.1"}, + Tags: []string{"test-tag-1"}, + Patterns: []string{"test.pattern.1"}, + TriggerType: moira.RisingTrigger, + TTLState: &moira.TTLStateNODATA, + AloneMetrics: map[string]bool{}, }, { - ID: "triggerID-0000000000001", - Name: "test trigger 1 v2.0", - Targets: []string{"test.target.1", "test.target.2"}, - Tags: []string{"test-tag-2", "test-tag-1"}, - Patterns: []string{"test.pattern.2", "test.pattern.1"}, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000001", + Name: "test trigger 1 v2.0", + Targets: []string{"test.target.1", "test.target.2"}, + Tags: []string{"test-tag-2", "test-tag-1"}, + Patterns: []string{"test.pattern.2", "test.pattern.1"}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{"t2": true}, }, { - ID: "triggerID-0000000000001", - Name: "test trigger 1 v3.0", - Targets: []string{"test.target.3"}, - Tags: []string{"test-tag-2", "test-tag-3"}, - Patterns: []string{"test.pattern.3", "test.pattern.2"}, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000001", + Name: "test trigger 1 v3.0", + Targets: []string{"test.target.3"}, + Tags: []string{"test-tag-2", "test-tag-3"}, + Patterns: []string{"test.pattern.3", "test.pattern.2"}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, }, { - ID: "triggerID-0000000000004", - Name: "test trigger 4", - Targets: []string{"test.target.4"}, - Tags: []string{"test-tag-4"}, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000004", + Name: "test trigger 4", + Targets: []string{"test.target.4"}, + Tags: []string{"test-tag-4"}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, }, { - ID: "triggerID-0000000000005", - Name: "test trigger 5 (nobody is subscribed)", - Targets: []string{"test.target.5"}, - Tags: []string{"test-tag-nosub"}, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000005", + Name: "test trigger 5 (nobody is subscribed)", + Targets: []string{"test.target.5"}, + Tags: []string{"test-tag-nosub"}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, }, { - ID: "triggerID-0000000000006", - Name: "test trigger 6 (throttling disabled)", - Targets: []string{"test.target.6"}, - Tags: []string{"test-tag-throttling-disabled"}, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000006", + Name: "test trigger 6 (throttling disabled)", + Targets: []string{"test.target.6"}, + Tags: []string{"test-tag-throttling-disabled"}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, }, { - ID: "triggerID-0000000000007", - Name: "test trigger 7 (multiple subscribers)", - Targets: []string{"test.target.7"}, - Tags: []string{"test-tag-multiple-subs"}, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000007", + Name: "test trigger 7 (multiple subscribers)", + Targets: []string{"test.target.7"}, + Tags: []string{"test-tag-multiple-subs"}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, }, { - ID: "triggerID-0000000000008", - Name: "test trigger 8 (duplicated contacts)", - Targets: []string{"test.target.8"}, - Tags: []string{"test-tag-dup-contacts"}, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000008", + Name: "test trigger 8 (duplicated contacts)", + Targets: []string{"test.target.8"}, + Tags: []string{"test-tag-dup-contacts"}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, }, { - ID: "triggerID-0000000000009", - Name: "test trigger 9 (pseudo tag)", - Targets: []string{"test.target.9"}, - Tags: []string{"test-degradation"}, - TriggerType: moira.RisingTrigger, + ID: "triggerID-0000000000009", + Name: "test trigger 9 (pseudo tag)", + Targets: []string{"test.target.9"}, + Tags: []string{"test-degradation"}, + TriggerType: moira.RisingTrigger, + AloneMetrics: map[string]bool{}, }, } diff --git a/datatypes.go b/datatypes.go index f45914d6e..dc32ac0b9 100644 --- a/datatypes.go +++ b/datatypes.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "math" + "sort" "strconv" "strings" "time" @@ -29,17 +30,18 @@ const ( // NotificationEvent represents trigger state changes event type NotificationEvent struct { - IsTriggerEvent bool `json:"trigger_event,omitempty"` - Timestamp int64 `json:"timestamp"` - Metric string `json:"metric"` - Value *float64 `json:"value,omitempty"` - State State `json:"state"` - TriggerID string `json:"trigger_id"` - SubscriptionID *string `json:"sub_id,omitempty"` - ContactID string `json:"contactId,omitempty"` - OldState State `json:"old_state"` - Message *string `json:"msg,omitempty"` - MessageEventInfo *EventInfo `json:"event_message"` + IsTriggerEvent bool `json:"trigger_event,omitempty"` + Timestamp int64 `json:"timestamp"` + Metric string `json:"metric"` + Value *float64 `json:"value,omitempty"` + Values map[string]float64 `json:"values,omitempty"` + State State `json:"state"` + TriggerID string `json:"trigger_id"` + SubscriptionID *string `json:"sub_id,omitempty"` + ContactID string `json:"contactId,omitempty"` + OldState State `json:"old_state"` + Message *string `json:"msg,omitempty"` + MessageEventInfo *EventInfo `json:"event_message"` } // EventInfo - a base for creating messages. @@ -205,22 +207,23 @@ const ( // Trigger represents trigger data object type Trigger struct { - ID string `json:"id"` - Name string `json:"name"` - Desc *string `json:"desc,omitempty"` - Targets []string `json:"targets"` - WarnValue *float64 `json:"warn_value"` - ErrorValue *float64 `json:"error_value"` - TriggerType string `json:"trigger_type"` - Tags []string `json:"tags"` - TTLState *TTLState `json:"ttl_state,omitempty"` - TTL int64 `json:"ttl,omitempty"` - Schedule *ScheduleData `json:"sched,omitempty"` - Expression *string `json:"expression,omitempty"` - PythonExpression *string `json:"python_expression,omitempty"` - Patterns []string `json:"patterns"` - IsRemote bool `json:"is_remote"` - MuteNewMetrics bool `json:"mute_new_metrics"` + ID string `json:"id"` + Name string `json:"name"` + Desc *string `json:"desc,omitempty"` + Targets []string `json:"targets"` + WarnValue *float64 `json:"warn_value"` + ErrorValue *float64 `json:"error_value"` + TriggerType string `json:"trigger_type"` + Tags []string `json:"tags"` + TTLState *TTLState `json:"ttl_state,omitempty"` + TTL int64 `json:"ttl,omitempty"` + Schedule *ScheduleData `json:"sched,omitempty"` + Expression *string `json:"expression,omitempty"` + PythonExpression *string `json:"python_expression,omitempty"` + Patterns []string `json:"patterns"` + IsRemote bool `json:"is_remote"` + MuteNewMetrics bool `json:"mute_new_metrics"` + AloneMetrics map[string]bool `json:"alone_metrics"` } // TriggerCheck represents trigger data with last check data and check timestamp @@ -240,6 +243,7 @@ type MaintenanceCheck interface { // CheckData represents last trigger check data type CheckData struct { Metrics map[string]MetricState `json:"metrics"` + MetricsToTargetRelation map[string]string `json:"metrics_to_target_relation"` Score int64 `json:"score"` State State `json:"state"` Maintenance int64 `json:"maintenance,omitempty"` @@ -254,14 +258,16 @@ type CheckData struct { // MetricState represents metric state data for given timestamp type MetricState struct { - EventTimestamp int64 `json:"event_timestamp"` - State State `json:"state"` - Suppressed bool `json:"suppressed"` - SuppressedState State `json:"suppressed_state,omitempty"` - Timestamp int64 `json:"timestamp"` - Value *float64 `json:"value,omitempty"` - Maintenance int64 `json:"maintenance,omitempty"` - MaintenanceInfo MaintenanceInfo `json:"maintenance_info"` + EventTimestamp int64 `json:"event_timestamp"` + State State `json:"state"` + Suppressed bool `json:"suppressed"` + SuppressedState State `json:"suppressed_state,omitempty"` + Timestamp int64 `json:"timestamp"` + Value *float64 `json:"value,omitempty"` + Values map[string]float64 `json:"values,omitempty"` + Maintenance int64 `json:"maintenance,omitempty"` + MaintenanceInfo MaintenanceInfo `json:"maintenance_info"` + // AloneMetrics map[string]string `json:"alone_metrics"` // represents a relation between name of alone metrics and their targets } // SetMaintenance set maintenance user, time for MetricState @@ -335,14 +341,14 @@ func (trigger *TriggerData) GetTags() string { // GetKey return notification key to prevent duplication to the same contact func (notification *ScheduledNotification) GetKey() string { - return fmt.Sprintf("%s:%s:%s:%s:%s:%d:%f:%d:%t:%d", + return fmt.Sprintf("%s:%s:%s:%s:%s:%d:%s:%d:%t:%d", notification.Contact.Type, notification.Contact.Value, notification.Event.TriggerID, notification.Event.Metric, notification.Event.State, notification.Event.Timestamp, - UseFloat64(notification.Event.Value), + notification.Event.GetMetricsValues(), notification.SendFail, notification.Throttled, notification.Timestamp, @@ -380,12 +386,27 @@ func (schedule *ScheduleData) IsScheduleAllows(ts int64) bool { } func (event NotificationEvent) String() string { - return fmt.Sprintf("TriggerId: %s, Metric: %s, Value: %v, OldState: %s, State: %s, Message: '%s', Timestamp: %v", event.TriggerID, event.Metric, UseFloat64(event.Value), event.OldState, event.State, event.CreateMessage(nil), event.Timestamp) + return fmt.Sprintf("TriggerId: %s, Metric: %s, Values: %s, OldState: %s, State: %s, Message: '%s', Timestamp: %v", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State, event.CreateMessage(nil), event.Timestamp) } -// GetMetricValue gets event metric value and format it to human readable presentation -func (event NotificationEvent) GetMetricValue() string { - return strconv.FormatFloat(UseFloat64(event.Value), 'f', -1, 64) +// GetMetricsValues gets event metric value and format it to human readable presentation +func (event NotificationEvent) GetMetricsValues() string { + var builder strings.Builder + var targetNames []string + for targetName := range event.Values { + targetNames = append(targetNames, targetName) + } + sort.Strings(targetNames) + for i, targetName := range targetNames { + builder.WriteString(targetName) + builder.WriteRune(':') + value := strconv.FormatFloat(event.Values[targetName], 'f', -1, 64) + builder.WriteString(value) + if i < len(targetNames)-1 { + builder.WriteString(", ") + } + } + return builder.String() } // FormatTimestamp gets event timestamp and format it using given location to human readable presentation diff --git a/datatypes_test.go b/datatypes_test.go index c1fb8812e..88b302ced 100644 --- a/datatypes_test.go +++ b/datatypes_test.go @@ -181,11 +181,10 @@ func TestNotificationEvent_CreateMessage(t *testing.T) { } func TestNotificationEvent_GetSubjectState(t *testing.T) { Convey("Get ERROR state", t, func() { - var value float64 = 1 - states := NotificationEvents{{State: StateOK}, {State: StateERROR, Value: &value}} + states := NotificationEvents{{State: StateOK, Values: map[string]float64{"t1": 0}}, {State: StateERROR, Values: map[string]float64{"t1": 1}}} So(states.GetSubjectState(), ShouldResemble, StateERROR) - So(states[0].String(), ShouldResemble, "TriggerId: , Metric: , Value: 0, OldState: , State: OK, Message: '', Timestamp: 0") - So(states[1].String(), ShouldResemble, "TriggerId: , Metric: , Value: 1, OldState: , State: ERROR, Message: '', Timestamp: 0") + So(states[0].String(), ShouldResemble, "TriggerId: , Metric: , Values: t1:0, OldState: , State: OK, Message: '', Timestamp: 0") + So(states[1].String(), ShouldResemble, "TriggerId: , Metric: , Values: t1:1, OldState: , State: ERROR, Message: '', Timestamp: 0") }) } @@ -202,24 +201,28 @@ func TestNotificationEvent_FormatTimestamp(t *testing.T) { } func TestNotificationEvent_GetValue(t *testing.T) { - event := NotificationEvent{} - value1 := float64(2.32) - value2 := float64(2.3222222) - value3 := float64(2) - value4 := float64(2.000001) - value5 := float64(2.33333333) - Convey("Test GetMetricValue", t, func() { - So(event.GetMetricValue(), ShouldResemble, "0") - event.Value = &value1 - So(event.GetMetricValue(), ShouldResemble, "2.32") - event.Value = &value2 - So(event.GetMetricValue(), ShouldResemble, "2.3222222") - event.Value = &value3 - So(event.GetMetricValue(), ShouldResemble, "2") - event.Value = &value4 - So(event.GetMetricValue(), ShouldResemble, "2.000001") - event.Value = &value5 - So(event.GetMetricValue(), ShouldResemble, "2.33333333") + Convey("Test GetMetricsValues", t, func() { + event := NotificationEvent{} + event.Values = make(map[string]float64) + Convey("One target with zero", func() { + event.Values["t1"] = 0 + So(event.GetMetricsValues(), ShouldResemble, "t1:0") + }) + + Convey("One target with short fraction", func() { + event.Values["t1"] = 2.32 + So(event.GetMetricsValues(), ShouldResemble, "t1:2.32") + }) + + Convey("One target with long fraction", func() { + event.Values["t1"] = 2.3222222 + So(event.GetMetricsValues(), ShouldResemble, "t1:2.3222222") + }) + Convey("Two targets", func() { + event.Values["t2"] = 0.12 + event.Values["t1"] = 2.3222222 + So(event.GetMetricsValues(), ShouldResemble, "t1:2.3222222, t2:0.12") + }) }) } @@ -248,10 +251,10 @@ func TestScheduledNotification_GetKey(t *testing.T) { Convey("Get key", t, func() { notification := ScheduledNotification{ Contact: ContactData{Type: "email", Value: "my@mail.com"}, - Event: NotificationEvent{Value: nil, State: StateNODATA, Metric: "my.metric"}, + Event: NotificationEvent{Values: map[string]float64{"t1": 0}, State: StateNODATA, Metric: "my.metric"}, Timestamp: 123456789, } - So(notification.GetKey(), ShouldResemble, "email:my@mail.com::my.metric:NODATA:0:0.000000:0:false:123456789") + So(notification.GetKey(), ShouldResemble, "email:my@mail.com::my.metric:NODATA:0:t1:0:0:false:123456789") }) } diff --git a/docker-compose.yml b/docker-compose.yml index 9f2a9a9fe..eebd636c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,9 @@ services: redis: image: redis:alpine volumes: - - data:/data + - /Users/emilsharifullin/projects/kontur/moira/redis_data:/data + ports: + - 6379:6379 graphite: image: graphiteapp/graphite-statsd diff --git a/go.mod b/go.mod index af9837b62..7c516e5bc 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 // indirect github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 // indirect github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a // indirect + github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31 github.com/go-chi/chi v3.1.1-0.20170712121200-4c5a584b324b+incompatible github.com/go-chi/render v1.0.0 github.com/go-graphite/carbonapi v0.0.0-20190604194342-5a4fa2112923 diff --git a/go.sum b/go.sum index 52a9d7933..ac1ae1553 100644 --- a/go.sum +++ b/go.sum @@ -464,6 +464,7 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911230505-6bfd74cf029c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190912215617-3720d1ec3678 h1:rM1Udd0CgtYI3KUIhu9ROz0QCqjW+n/ODp/hH7c60Xc= golang.org/x/tools v0.0.0-20190912215617-3720d1ec3678/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485 h1:OB/uP/Puiu5vS5QMRPrXCDWUPb+kt8f1KW8oQzFejQw= diff --git a/interfaces.go b/interfaces.go index 51b5ef4a5..05aedf9b2 100644 --- a/interfaces.go +++ b/interfaces.go @@ -145,7 +145,7 @@ type Logger interface { // Sender interface for implementing specified contact type sender type Sender interface { - SendEvents(events NotificationEvents, contact ContactData, trigger TriggerData, plot []byte, throttled bool) error + SendEvents(events NotificationEvents, contact ContactData, trigger TriggerData, plot [][]byte, throttled bool) error Init(senderSettings map[string]string, logger Logger, location *time.Location, dateTimeFormat string) error } diff --git a/metric_source/fetched_metrics.go b/metric_source/fetched_metrics.go new file mode 100644 index 000000000..7ad4ff68b --- /dev/null +++ b/metric_source/fetched_metrics.go @@ -0,0 +1,76 @@ +package metricSource + +import "fmt" + +const defaultStep int64 = 60 + +// FetchedPatternMetrics represents different metrics within one pattern. +type FetchedPatternMetrics []MetricData + +// NewFetchedPatternMetricsWithCapacity is a constructor function for patternMetrics +func NewFetchedPatternMetricsWithCapacity(capacity int) FetchedPatternMetrics { + return make(FetchedPatternMetrics, 0, capacity) +} + +// CleanWildcards is a function that removes all wildcarded metrics and returns new PatternMetrics +func (m FetchedPatternMetrics) CleanWildcards() FetchedPatternMetrics { + result := NewFetchedPatternMetricsWithCapacity(len(m)) + for _, metric := range m { + if !metric.Wildcard { + result = append(result, metric) + } + } + return result +} + +// Deduplicate is a function that checks if FetchedPatternMetrics have a two or more metrics with +// the same name and returns new FetchedPatternMetrics without duplicates and slice of duplicated metrics names. +func (m FetchedPatternMetrics) Deduplicate() (FetchedPatternMetrics, []string) { + deduplicated := NewFetchedPatternMetricsWithCapacity(len(m)) + collectedNames := make(setHelper, len(m)) + var duplicates []string + for _, metric := range m { + if collectedNames[metric.Name] { + duplicates = append(duplicates, metric.Name) + } else { + deduplicated = append(deduplicated, metric) + } + collectedNames[metric.Name] = true + } + return deduplicated, duplicates +} + +// HasOnlyWildcards is a function that checks PatternMetrics for only wildcards +func (m FetchedPatternMetrics) HasOnlyWildcards() bool { + for _, timeSeries := range m { + if !timeSeries.Wildcard { + return false + } + } + return true +} + +// FetchedMetrics represent collections of metrics associated with target name +// There is a map where keys are target names and values are maps of metrics with metric names as keys. +type FetchedMetrics map[string]FetchedPatternMetrics + +// NewFetchedMetricsWithCapacity is a constructor function that creates TriggerMetricsData with initialized empty fields +func NewFetchedMetricsWithCapacity(capacity int) FetchedMetrics { + return make(FetchedMetrics, capacity) +} + +// AddMetrics is a function to add a bunch of metrics sequences to TriggerMetricsData. +func (m FetchedMetrics) AddMetrics(target int, metrics FetchedPatternMetrics) { // NOTE(litleleprikon): Probably set metrics will be better + targetName := fmt.Sprintf("t%d", target) + m[targetName] = metrics +} + +// HasOnlyWildcards is a function that checks given targetTimeSeries for only wildcards +func (m FetchedMetrics) HasOnlyWildcards() bool { + for _, metric := range m { + if !metric.HasOnlyWildcards() { + return false + } + } + return true +} diff --git a/metric_source/fetched_metrics_test.go b/metric_source/fetched_metrics_test.go new file mode 100644 index 000000000..cd1405168 --- /dev/null +++ b/metric_source/fetched_metrics_test.go @@ -0,0 +1,231 @@ +package metricSource + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestNewFetchedPatternMetricsWithCapacity(t *testing.T) { + Convey("NewFetchedPatternMetricsWithCapacity", t, func() { + Convey("call", func() { + capacity := 10 + actual := NewFetchedPatternMetricsWithCapacity(capacity) + So(actual, ShouldNotBeNil) + So(actual, ShouldHaveLength, 0) + So(cap(actual), ShouldEqual, capacity) + }) + }) +} + +func TestFetchedPatternMetrics_CleanWildcards(t *testing.T) { + tests := []struct { + name string + m FetchedPatternMetrics + want FetchedPatternMetrics + }{ + { + name: "does not have wildcards", + m: FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: false}, + }, + want: FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: false}, + }, + }, + { + name: "has wildcards", + m: FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: false}, + {Name: "metric.test.2", Wildcard: true}, + }, + want: FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: false}, + }, + }, + } + Convey("FetchedPatternMetrics", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.CleanWildcards() + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func TestFetchedPatternMetrics_Deduplicate(t *testing.T) { + tests := []struct { + name string + m FetchedPatternMetrics + wantDeduplicated FetchedPatternMetrics + wantDuplicates []string + }{ + { + name: "does not have duplicates", + m: FetchedPatternMetrics{ + {Name: "metric.test.1"}, + {Name: "metric.test.2"}, + }, + wantDeduplicated: FetchedPatternMetrics{ + {Name: "metric.test.1"}, + {Name: "metric.test.2"}, + }, + wantDuplicates: nil, + }, + { + name: "has duplicates", + m: FetchedPatternMetrics{ + {Name: "metric.test.1"}, + {Name: "metric.test.1"}, + {Name: "metric.test.2"}, + }, + wantDeduplicated: FetchedPatternMetrics{ + {Name: "metric.test.1"}, + {Name: "metric.test.2"}, + }, + wantDuplicates: []string{"metric.test.1"}, + }, + { + name: "has multiple duplicates", + m: FetchedPatternMetrics{ + {Name: "metric.test.1"}, + {Name: "metric.test.1"}, + {Name: "metric.test.1"}, + {Name: "metric.test.2"}, + }, + wantDeduplicated: FetchedPatternMetrics{ + {Name: "metric.test.1"}, + {Name: "metric.test.2"}, + }, + wantDuplicates: []string{"metric.test.1", "metric.test.1"}, + }, + } + Convey("FetchedPatternMetrics", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + deduplicated, duplicates := tt.m.Deduplicate() + So(deduplicated, ShouldResemble, tt.wantDeduplicated) + So(duplicates, ShouldResemble, tt.wantDuplicates) + }) + } + }) +} + +func TestFetchedPatternMetrics_HasOnlyWildcards(t *testing.T) { + tests := []struct { + name string + m FetchedPatternMetrics + want bool + }{ + { + name: "does not have wildcards", + m: FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: false}, + {Name: "metric.test.2", Wildcard: false}, + }, + want: false, + }, + { + name: "has wildcards", + m: FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: false}, + {Name: "metric.test.2", Wildcard: true}, + }, + want: false, + }, + { + name: "has only wildcards", + m: FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: true}, + {Name: "metric.test.2", Wildcard: true}, + }, + want: true, + }, + } + Convey("FetchedPatternMetrics", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.HasOnlyWildcards() + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func TestNewFetchedMetricsWithCapacity(t *testing.T) { + Convey("NewNewFetchedMetricsWithCapacity", t, func() { + Convey("call", func() { + capacity := 10 + actual := NewFetchedMetricsWithCapacity(capacity) + So(actual, ShouldNotBeNil) + So(actual, ShouldHaveLength, 0) + }) + }) +} + +func TestFetchedMetrics_AddMetrics(t *testing.T) { + Convey("AddMetrics", t, func() { + m := FetchedMetrics{} + m.AddMetrics(1, FetchedPatternMetrics{{Name: "metric.test.1"}, {Name: "metric.test.2"}}) + m.AddMetrics(2, FetchedPatternMetrics{{Name: "metric.test.3"}}) + So(m, ShouldResemble, FetchedMetrics{ + "t1": FetchedPatternMetrics{{Name: "metric.test.1"}, {Name: "metric.test.2"}}, + "t2": FetchedPatternMetrics{{Name: "metric.test.3"}}, + }) + }) +} + +func TestFetchedMetrics_HasOnlyWildcards(t *testing.T) { + tests := []struct { + name string + m FetchedMetrics + want bool + }{ + { + name: "does not have wildcards", + m: FetchedMetrics{ + "t1": FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: false}, + {Name: "metric.test.2", Wildcard: false}, + }, + }, + want: false, + }, + { + name: "one target has wildcards", + m: FetchedMetrics{ + "t1": FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: true}, + {Name: "metric.test.2", Wildcard: true}, + }, + "t2": FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: false}, + {Name: "metric.test.2", Wildcard: true}, + }, + }, + want: false, + }, + { + name: "has only wildcards", + m: FetchedMetrics{ + "t1": FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: true}, + {Name: "metric.test.2", Wildcard: true}, + }, + "t2": FetchedPatternMetrics{ + {Name: "metric.test.1", Wildcard: true}, + {Name: "metric.test.2", Wildcard: true}, + }, + }, + want: true, + }} + Convey("FetchedMetrics", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.HasOnlyWildcards() + So(actual, ShouldResemble, tt.want) + }) + } + }) +} diff --git a/metric_source/local/fetch_result.go b/metric_source/local/fetch_result.go index 018146992..7ff015af3 100644 --- a/metric_source/local/fetch_result.go +++ b/metric_source/local/fetch_result.go @@ -7,7 +7,7 @@ import ( // FetchResult is implementation of metric_source.FetchResult interface, // which represent fetching result from moira data source in moira format type FetchResult struct { - MetricsData []*metricSource.MetricData + MetricsData []metricSource.MetricData Patterns []string Metrics []string } @@ -15,14 +15,14 @@ type FetchResult struct { // CreateEmptyFetchResult just creates FetchResult with initialized empty fields func CreateEmptyFetchResult() *FetchResult { return &FetchResult{ - MetricsData: make([]*metricSource.MetricData, 0), + MetricsData: make([]metricSource.MetricData, 0), Patterns: make([]string, 0), Metrics: make([]string, 0), } } // GetMetricsData return all metrics data from fetch result -func (fetchResult *FetchResult) GetMetricsData() []*metricSource.MetricData { +func (fetchResult *FetchResult) GetMetricsData() []metricSource.MetricData { return fetchResult.MetricsData } diff --git a/metric_source/local/fetch_result_test.go b/metric_source/local/fetch_result_test.go index 8143fae93..1df50a36e 100644 --- a/metric_source/local/fetch_result_test.go +++ b/metric_source/local/fetch_result_test.go @@ -10,7 +10,7 @@ import ( func TestCreateEmptyFetchResult(t *testing.T) { Convey("Just create fetch empty fetch result", t, func() { So(*(CreateEmptyFetchResult()), ShouldResemble, FetchResult{ - MetricsData: make([]*metricSource.MetricData, 0), + MetricsData: make([]metricSource.MetricData, 0), Patterns: make([]string, 0), Metrics: make([]string, 0), }) @@ -20,7 +20,7 @@ func TestCreateEmptyFetchResult(t *testing.T) { func TestFetchResult_GetMetricsData(t *testing.T) { Convey("Get empty metric data", t, func() { fetchResult := &FetchResult{ - MetricsData: make([]*metricSource.MetricData, 0), + MetricsData: make([]metricSource.MetricData, 0), Patterns: make([]string, 0), Metrics: make([]string, 0), } @@ -29,7 +29,7 @@ func TestFetchResult_GetMetricsData(t *testing.T) { Convey("Get not empty metric data", t, func() { fetchResult := &FetchResult{ - MetricsData: []*metricSource.MetricData{metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, + MetricsData: []metricSource.MetricData{*metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, Patterns: make([]string, 0), Metrics: make([]string, 0), } @@ -40,7 +40,7 @@ func TestFetchResult_GetMetricsData(t *testing.T) { func TestFetchResult_GetPatternMetrics(t *testing.T) { Convey("Get empty pattern metrics", t, func() { fetchResult := &FetchResult{ - MetricsData: make([]*metricSource.MetricData, 0), + MetricsData: make([]metricSource.MetricData, 0), Patterns: make([]string, 0), Metrics: make([]string, 0), } @@ -51,7 +51,7 @@ func TestFetchResult_GetPatternMetrics(t *testing.T) { Convey("Get not empty metric data", t, func() { fetchResult := &FetchResult{ - MetricsData: []*metricSource.MetricData{metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, + MetricsData: []metricSource.MetricData{*metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, Patterns: make([]string, 0), Metrics: []string{"123"}, } @@ -64,7 +64,7 @@ func TestFetchResult_GetPatternMetrics(t *testing.T) { func TestFetchResult_GetPatterns(t *testing.T) { Convey("Get empty pattern metrics", t, func() { fetchResult := &FetchResult{ - MetricsData: make([]*metricSource.MetricData, 0), + MetricsData: make([]metricSource.MetricData, 0), Patterns: make([]string, 0), Metrics: make([]string, 0), } @@ -75,7 +75,7 @@ func TestFetchResult_GetPatterns(t *testing.T) { Convey("Get not empty metric data", t, func() { fetchResult := &FetchResult{ - MetricsData: []*metricSource.MetricData{metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, + MetricsData: []metricSource.MetricData{*metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, Patterns: []string{"123"}, Metrics: []string{"123"}, } diff --git a/metric_source/local/local.go b/metric_source/local/local.go index e19e92387..9b8fdcb3b 100644 --- a/metric_source/local/local.go +++ b/metric_source/local/local.go @@ -83,7 +83,7 @@ func (local *Local) Fetch(target string, from int64, until int64, allowRealTimeA } for _, metricData := range metricsData { md := *metricData - result.MetricsData = append(result.MetricsData, &metricSource.MetricData{ + result.MetricsData = append(result.MetricsData, metricSource.MetricData{ Name: md.Name, StartTime: md.StartTime, StopTime: md.StopTime, diff --git a/metric_source/local/local_test.go b/metric_source/local/local_test.go index 3e68dbee0..de6676d5d 100644 --- a/metric_source/local/local_test.go +++ b/metric_source/local/local_test.go @@ -103,7 +103,7 @@ func TestEvaluateTarget(t *testing.T) { result, err := localSource.Fetch("aliasByNode(super.puper.pattern, 2)", from, until, true) So(err, ShouldBeNil) So(result, ShouldResemble, &FetchResult{ - MetricsData: []*metricSource.MetricData{{ + MetricsData: []metricSource.MetricData{{ Name: "pattern", StartTime: from, StopTime: until, @@ -123,7 +123,7 @@ func TestEvaluateTarget(t *testing.T) { result, err := localSource.Fetch("aliasByNode(super.puper.pattern, 2)", from, until, true) So(err, ShouldBeNil) So(result, ShouldResemble, &FetchResult{ - MetricsData: []*metricSource.MetricData{{ + MetricsData: []metricSource.MetricData{{ Name: "metric", StartTime: from, StopTime: until, @@ -143,7 +143,7 @@ func TestEvaluateTarget(t *testing.T) { result, err := localSource.Fetch("super.puper.pattern | scale(100) | aliasByNode(2)", from, until, true) So(err, ShouldBeNil) So(result, ShouldResemble, &FetchResult{ - MetricsData: []*metricSource.MetricData{{ + MetricsData: []metricSource.MetricData{{ Name: "metric", StartTime: from, StopTime: until, diff --git a/metric_source/remote/fetch_result.go b/metric_source/remote/fetch_result.go index ca6ca77ca..b8b22362a 100644 --- a/metric_source/remote/fetch_result.go +++ b/metric_source/remote/fetch_result.go @@ -9,11 +9,11 @@ import ( // FetchResult is implementation of metric_source.FetchResult interface, // which represent fetching result from remote graphite installation in moira format type FetchResult struct { - MetricsData []*metricSource.MetricData + MetricsData []metricSource.MetricData } // GetMetricsData return all metrics data from fetch result -func (fetchResult *FetchResult) GetMetricsData() []*metricSource.MetricData { +func (fetchResult *FetchResult) GetMetricsData() []metricSource.MetricData { return fetchResult.MetricsData } diff --git a/metric_source/remote/fetch_result_test.go b/metric_source/remote/fetch_result_test.go index a925f6bbe..7ad661c4b 100644 --- a/metric_source/remote/fetch_result_test.go +++ b/metric_source/remote/fetch_result_test.go @@ -10,7 +10,7 @@ import ( func TestFetchResult(t *testing.T) { Convey("Get empty metric data", t, func() { fetchResult := FetchResult{ - MetricsData: make([]*metricSource.MetricData, 0), + MetricsData: make([]metricSource.MetricData, 0), } So(fetchResult.GetMetricsData(), ShouldBeEmpty) patterns, err := fetchResult.GetPatterns() @@ -23,7 +23,7 @@ func TestFetchResult(t *testing.T) { Convey("Get not empty metric data", t, func() { fetchResult := &FetchResult{ - MetricsData: []*metricSource.MetricData{metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, + MetricsData: []metricSource.MetricData{*metricSource.MakeMetricData("123", []float64{1, 2, 3}, 60, 0)}, } So(fetchResult.GetMetricsData(), ShouldHaveLength, 1) patterns, err := fetchResult.GetPatterns() diff --git a/metric_source/remote/remote_test.go b/metric_source/remote/remote_test.go index 608cfd316..d98370b69 100644 --- a/metric_source/remote/remote_test.go +++ b/metric_source/remote/remote_test.go @@ -52,7 +52,7 @@ func TestFetch(t *testing.T) { server := createServer([]byte("[]"), http.StatusOK) remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} result, err := remote.Fetch(target, from, until, false) - So(result, ShouldResemble, &FetchResult{MetricsData: []*metricSource.MetricData{}}) + So(result, ShouldResemble, &FetchResult{MetricsData: []metricSource.MetricData{}}) So(err, ShouldBeEmpty) }) diff --git a/metric_source/remote/response.go b/metric_source/remote/response.go index 0458164a5..0af2fbaa0 100644 --- a/metric_source/remote/response.go +++ b/metric_source/remote/response.go @@ -12,24 +12,27 @@ type graphiteMetric struct { DataPoints [][2]*float64 } -func convertResponse(metricsData []*metricSource.MetricData, allowRealTimeAlerting bool) FetchResult { - if !allowRealTimeAlerting { - for _, metricData := range metricsData { - // remove last value - metricData.Values = metricData.Values[:len(metricData.Values)-1] - } +func convertResponse(metricsData []metricSource.MetricData, allowRealTimeAlerting bool) FetchResult { + if allowRealTimeAlerting { + return FetchResult{MetricsData: metricsData} } - return FetchResult{MetricsData: metricsData} + result := make([]metricSource.MetricData, 0, len(metricsData)) + for _, metricData := range metricsData { + // remove last value + metricData.Values = metricData.Values[:len(metricData.Values)-1] + result = append(result, metricData) + } + return FetchResult{MetricsData: result} } -func decodeBody(body []byte) ([]*metricSource.MetricData, error) { +func decodeBody(body []byte) ([]metricSource.MetricData, error) { var tmp []graphiteMetric err := json.Unmarshal(body, &tmp) if err != nil { return nil, err } - res := make([]*metricSource.MetricData, 0, len(tmp)) + res := make([]metricSource.MetricData, 0, len(tmp)) for _, m := range tmp { var stepTime int64 = 60 if len(m.DataPoints) > 1 { @@ -49,7 +52,7 @@ func decodeBody(body []byte) ([]*metricSource.MetricData, error) { metricData.Values[i] = *v[0] } } - res = append(res, &metricData) + res = append(res, metricData) } return res, nil } diff --git a/metric_source/remote/response_test.go b/metric_source/remote/response_test.go index 3f26126ca..42c5c44e6 100644 --- a/metric_source/remote/response_test.go +++ b/metric_source/remote/response_test.go @@ -59,8 +59,8 @@ func TestDecodeBody(t *testing.T) { } func TestConvertResponse(t *testing.T) { - d := metricSource.MakeMetricData("test", []float64{1, 2, 3}, 20, 0) - data := []*metricSource.MetricData{d} + d := *metricSource.MakeMetricData("test", []float64{1, 2, 3}, 20, 0) + data := []metricSource.MetricData{d} Convey("Given data and allowRealTimeAlerting is set", t, func() { fetchResult := convertResponse(data, true) Convey("response should contain last value", func() { diff --git a/metric_source/source.go b/metric_source/source.go index f932b28b6..42eea8a2a 100644 --- a/metric_source/source.go +++ b/metric_source/source.go @@ -8,7 +8,7 @@ type MetricSource interface { // FetchResult implements moira metric sources fetching result format type FetchResult interface { - GetMetricsData() []*MetricData + GetMetricsData() []MetricData GetPatterns() ([]string, error) GetPatternMetrics() ([]string, error) } diff --git a/metric_source/trigger_metric_data.go b/metric_source/trigger_metric_data.go deleted file mode 100644 index 7acd69073..000000000 --- a/metric_source/trigger_metric_data.go +++ /dev/null @@ -1,45 +0,0 @@ -package metricSource - -import "fmt" - -// TriggerMetricsData represent collection of Main target timeseries and collection of additions targets timeseries -type TriggerMetricsData struct { - Main []*MetricData - Additional []*MetricData -} - -// NewTriggerMetricsData is a constructor function that creates TriggerMetricsData with initialized empty fields -func NewTriggerMetricsData() *TriggerMetricsData { - return &TriggerMetricsData{ - Main: make([]*MetricData, 0), - Additional: make([]*MetricData, 0), - } -} - -// MakeTriggerMetricsData just creates TriggerMetricsData with given main and additional metrics data -func MakeTriggerMetricsData(main []*MetricData, additional []*MetricData) *TriggerMetricsData { - return &TriggerMetricsData{ - Main: main, - Additional: additional, - } -} - -// GetMainTargetName just gets triggers main targets name (always is 't1') -func (*TriggerMetricsData) GetMainTargetName() string { - return "t1" -} - -// GetAdditionalTargetName gets triggers additional target name -func (*TriggerMetricsData) GetAdditionalTargetName(targetIndex int) string { - return fmt.Sprintf("t%v", targetIndex+2) -} - -// HasOnlyWildcards checks given targetTimeSeries for only wildcards -func (triggerTimeSeries *TriggerMetricsData) HasOnlyWildcards() bool { - for _, timeSeries := range triggerTimeSeries.Main { - if !timeSeries.Wildcard { - return false - } - } - return true -} diff --git a/metric_source/trigger_metric_data_test.go b/metric_source/trigger_metric_data_test.go deleted file mode 100644 index 8869442c5..000000000 --- a/metric_source/trigger_metric_data_test.go +++ /dev/null @@ -1,93 +0,0 @@ -package metricSource - -import ( - "fmt" - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestNewTriggerMetricsData(t *testing.T) { - Convey("Just make empty TriggerMetricsData", t, func() { - So(*(NewTriggerMetricsData()), ShouldResemble, TriggerMetricsData{ - Main: make([]*MetricData, 0), - Additional: make([]*MetricData, 0), - }) - }) -} - -func TestMakeTriggerMetricsData(t *testing.T) { - Convey("Just make empty TriggerMetricsData", t, func() { - So(*(MakeTriggerMetricsData(make([]*MetricData, 0), make([]*MetricData, 0))), ShouldResemble, TriggerMetricsData{ - Main: make([]*MetricData, 0), - Additional: make([]*MetricData, 0), - }) - }) - - Convey("Just make TriggerMetricsData only with main", t, func() { - So(*(MakeTriggerMetricsData([]*MetricData{MakeMetricData("000", make([]float64, 0), 10, 0)}, make([]*MetricData, 0))), ShouldResemble, TriggerMetricsData{ - Main: []*MetricData{MakeMetricData("000", make([]float64, 0), 10, 0)}, - Additional: make([]*MetricData, 0), - }) - }) - - Convey("Just make TriggerMetricsData with main and additional", t, func() { - So(*(MakeTriggerMetricsData([]*MetricData{MakeMetricData("000", make([]float64, 0), 10, 0)}, []*MetricData{MakeMetricData("000", make([]float64, 0), 10, 0)})), ShouldResemble, TriggerMetricsData{ - Main: []*MetricData{MakeMetricData("000", make([]float64, 0), 10, 0)}, - Additional: []*MetricData{MakeMetricData("000", make([]float64, 0), 10, 0)}, - }) - }) -} - -func TestGetTargetName(t *testing.T) { - tts := TriggerMetricsData{} - - Convey("GetMainTargetName", t, func() { - So(tts.GetMainTargetName(), ShouldResemble, "t1") - }) - - Convey("GetAdditionalTargetName", t, func() { - for i := 0; i < 5; i++ { - So(tts.GetAdditionalTargetName(i), ShouldResemble, fmt.Sprintf("t%v", i+2)) - } - }) -} - -func TestTriggerTimeSeriesHasOnlyWildcards(t *testing.T) { - Convey("Main metrics data has wildcards only", t, func() { - tts := TriggerMetricsData{ - Main: []*MetricData{{Wildcard: true}}, - } - So(tts.HasOnlyWildcards(), ShouldBeTrue) - - tts1 := TriggerMetricsData{ - Main: []*MetricData{{Wildcard: true}, {Wildcard: true}}, - } - So(tts1.HasOnlyWildcards(), ShouldBeTrue) - }) - - Convey("Main metrics data has not only wildcards", t, func() { - tts := TriggerMetricsData{ - Main: []*MetricData{{Wildcard: false}}, - } - So(tts.HasOnlyWildcards(), ShouldBeFalse) - - tts1 := TriggerMetricsData{ - Main: []*MetricData{{Wildcard: false}, {Wildcard: true}}, - } - So(tts1.HasOnlyWildcards(), ShouldBeFalse) - - tts2 := TriggerMetricsData{ - Main: []*MetricData{{Wildcard: false}, {Wildcard: false}}, - } - So(tts2.HasOnlyWildcards(), ShouldBeFalse) - }) - - Convey("Additional metrics data has wildcards but Main not", t, func() { - tts := TriggerMetricsData{ - Main: []*MetricData{{Wildcard: false}}, - Additional: []*MetricData{{Wildcard: true}, {Wildcard: true}}, - } - So(tts.HasOnlyWildcards(), ShouldBeFalse) - }) -} diff --git a/metric_source/trigger_metrics.go b/metric_source/trigger_metrics.go new file mode 100644 index 000000000..47599c8f3 --- /dev/null +++ b/metric_source/trigger_metrics.go @@ -0,0 +1,301 @@ +package metricSource + +import ( + "github.com/moira-alert/moira" + "sort" +) + +// setHelper is a map that represents a set of strings with corresponding methods. +type setHelper map[string]bool + +// newSetHelperFromTriggerPatternMetrics is a constructor function for setHelper. +func newSetHelperFromTriggerPatternMetrics(metrics TriggerPatternMetrics) setHelper { + result := make(setHelper, len(metrics)) + for metricName := range metrics { + result[metricName] = true + } + return result +} + +// diff is a set relative complement operation that returns a new set with elements +// that appear only in second set. +func (h setHelper) diff(other setHelper) setHelper { + result := make(setHelper, len(h)) + for metricName := range other { + if _, ok := h[metricName]; !ok { + result[metricName] = true + } + } + return result +} + +// union is a sets union operation that return a new set with elements from both sets. +func (h setHelper) union(other setHelper) setHelper { + result := make(setHelper, len(h)+len(other)) + for metricName := range h { + result[metricName] = true + } + for metricName := range other { + result[metricName] = true + } + return result +} + +// isOneMetricMap is a function that checks that map have only one metric and if so returns that metric key. +func isOneMetricMap(metrics map[string]MetricData) (bool, string) { + if len(metrics) == 1 { + for metricName := range metrics { + return true, metricName + } + } + return false, "" +} + +// TriggerPatternMetrics is a map that contains metrics of one pattern. Keys of this map +// are metric names. This map have a methods that helps to prepare metrics for check. +type TriggerPatternMetrics map[string]MetricData + +// newTriggerPatternMetricsWithCapacity is a constructor function for TriggerPatternMetrics that creates +// a new map with given capacity. +func newTriggerPatternMetricsWithCapacity(capacity int) TriggerPatternMetrics { + return make(TriggerPatternMetrics, capacity) +} + +// NewTriggerPatternMetrics is a constructor function for TriggerPatternMetrics that creates +// a new empty map. +func NewTriggerPatternMetrics(source FetchedPatternMetrics) TriggerPatternMetrics { + result := newTriggerPatternMetricsWithCapacity(len(source)) + for _, m := range source { + result[m.Name] = m + } + return result +} + +// Populate is a function that takes the list of metric names that first appeared and +// adds metrics with this names and empty values. +func (m TriggerPatternMetrics) Populate(lastMetrics map[string]bool, from, to int64) TriggerPatternMetrics { + result := newTriggerPatternMetricsWithCapacity(len(m)) + + var firstMetric MetricData + + for _, metric := range m { + firstMetric = metric + break + } + + for metricName := range lastMetrics { + metric, ok := m[metricName] + if !ok { + step := defaultStep + if len(m) > 0 && firstMetric.StepTime != 0 { + step = firstMetric.StepTime + } + metric = *MakeEmptyMetricData(metricName, step, from, to) + } + result[metricName] = metric + } + return result +} + +// TriggerMetrics is a map of TriggerPatternMetrics that represents all metrics within trigger. +type TriggerMetrics map[string]TriggerPatternMetrics + +// NewTriggerMetricsWithCapacity is a constructor function that creates TriggerMetrics with given capacity. +func NewTriggerMetricsWithCapacity(capacity int) TriggerMetrics { + return make(TriggerMetrics, capacity) +} + +// Populate is a function that takes TriggerMetrics and populate targets +// that is missing metrics that appear in another targets except the targets that have +// only alone metrics. +func (m TriggerMetrics) Populate(lastCheck moira.CheckData, from int64, to int64) TriggerMetrics { + allMetrics := make(map[string]map[string]bool, len(m)) + lastAloneMetrics := make(map[string]bool, len(lastCheck.MetricsToTargetRelation)) + + for targetName, metricName := range lastCheck.MetricsToTargetRelation { + allMetrics[targetName] = map[string]bool{metricName: true} + lastAloneMetrics[metricName] = true + } + + for metricName, metricState := range lastCheck.Metrics { + if lastAloneMetrics[metricName] { + continue + } + for targetName := range metricState.Values { + if _, ok := lastCheck.MetricsToTargetRelation[targetName]; ok { + continue + } + if _, ok := allMetrics[targetName]; !ok { + allMetrics[targetName] = make(map[string]bool) + } + allMetrics[targetName][metricName] = true + } + } + for targetName, metrics := range m { + for metricName := range metrics { + if _, ok := allMetrics[targetName]; !ok { + allMetrics[targetName] = make(map[string]bool) + } + allMetrics[targetName][metricName] = true + } + } + + diff := m.Diff() + + for targetName, metrics := range diff { + for metricName := range metrics { + allMetrics[targetName][metricName] = true + } + } + + result := NewTriggerMetricsWithCapacity(len(allMetrics)) + for targetName, metrics := range allMetrics { + patternMetrics, ok := m[targetName] + if !ok { + patternMetrics = newTriggerPatternMetricsWithCapacity(len(metrics)) + } + patternMetrics = patternMetrics.Populate(metrics, from, to) + result[targetName] = patternMetrics + } + return result +} + +// FilterAloneMetrics is a function that remove alone metrics targets from TriggerMetrics +// and return this metrics in format map[targetName]MetricData. +func (m TriggerMetrics) FilterAloneMetrics() (TriggerMetrics, MetricsToCheck) { + result := NewTriggerMetricsWithCapacity(len(m)) + aloneMetrics := make(MetricsToCheck) + + for targetName, patternMetrics := range m { + if oneMetricMap, metricName := isOneMetricMap(patternMetrics); oneMetricMap { + aloneMetrics[targetName] = patternMetrics[metricName] + continue + } + result[targetName] = m[targetName] + } + return result, aloneMetrics +} + +// Diff is a function that returns a map of target names with metric names that are absent in +// current target but appear in another targets. +func (m TriggerMetrics) Diff() map[string]map[string]bool { + result := make(map[string]map[string]bool) + + if len(m) == 0 { + return result + } + + fullMetrics := make(setHelper) + + for _, patternMetrics := range m { + if oneMetricTarget, _ := isOneMetricMap(patternMetrics); oneMetricTarget { + continue + } + currentMetrics := newSetHelperFromTriggerPatternMetrics(patternMetrics) + fullMetrics = fullMetrics.union(currentMetrics) + } + + for targetName, patternMetrics := range m { + metricsSet := newSetHelperFromTriggerPatternMetrics(patternMetrics) + if oneMetricTarget, _ := isOneMetricMap(patternMetrics); oneMetricTarget { + continue + } + diff := metricsSet.diff(fullMetrics) + if len(diff) > 0 { + result[targetName] = diff + } + } + return result +} + +// multiMetricsTarget is a function that finds any first target with +// amount of metrics greater than one and returns set with names of this metrics. +func (m TriggerMetrics) multiMetricsTarget() (string, setHelper) { + commonMetrics := make(setHelper) + for targetName, metrics := range m { + if len(metrics) > 1 { + for metricName := range metrics { + commonMetrics[metricName] = true + } + return targetName, commonMetrics + } + } + return "", nil +} + +// ConvertForCheck is a function that converts TriggerMetrics with structure +// map[TargetName]map[MetricName]MetricData to ConvertedTriggerMetrics +// with structure map[MetricName]map[TargetName]MetricData and fill with +// duplicated metrics targets that have only one metric. Second return value is +// a map with names of targets that had only one metric as key and original metric name as value. +func (m TriggerMetrics) ConvertForCheck() TriggerMetricsToCheck { + result := make(TriggerMetricsToCheck) + _, commonMetrics := m.multiMetricsTarget() + + hasAtLeastOneMultiMetricsTarget := commonMetrics != nil + + if !hasAtLeastOneMultiMetricsTarget && len(m) <= 1 { + return result + } + + for targetName, targetMetrics := range m { + oneMetricTarget, oneMetricName := isOneMetricMap(targetMetrics) + + for metricName := range commonMetrics { + if _, ok := result[metricName]; !ok { + result[metricName] = make(MetricsToCheck, len(m)) + } + + if oneMetricTarget { + result[metricName][targetName] = m[targetName][oneMetricName] + continue + } + + result[metricName][targetName] = m[targetName][metricName] + } + } + return result +} + +// MetricsToCheck is a map where key is a target name and value is a MetricData. +type MetricsToCheck map[string]MetricData + +// MetricName is a function that returns a metric name from random metric in MetricsToCheck. +// Should be used with care if MetricsToCheck have metrics with different names. +func (m MetricsToCheck) MetricName() string { + if len(m) == 0 { + return "" + } + var metricNames []string + for _, metric := range m { + metricNames = append(metricNames, metric.Name) + } + sort.Strings(metricNames) + return metricNames[0] +} + +// GetRelations is a function that returns a map with relation between target name and metric +// name for this target. +func (m MetricsToCheck) GetRelations() map[string]string { + result := make(map[string]string, len(m)) + for targetName, metric := range m { + result[targetName] = metric.Name + } + return result +} + +// Merge is a function that merges two MetricsToCheck maps and returns a map +// where represented elements from both maps. +func (m MetricsToCheck) Merge(other MetricsToCheck) MetricsToCheck { + result := make(MetricsToCheck, len(m)+len(other)) + for k, v := range m { + result[k] = v + } + for k, v := range other { + result[k] = v + } + return result +} + +// TriggerMetricsToCheck is a map of maps of metrics that have a form map[metricName]map[targetName]MetricData. +type TriggerMetricsToCheck map[string]MetricsToCheck diff --git a/metric_source/trigger_metrics_test.go b/metric_source/trigger_metrics_test.go new file mode 100644 index 000000000..1d8234f60 --- /dev/null +++ b/metric_source/trigger_metrics_test.go @@ -0,0 +1,810 @@ +package metricSource + +import ( + "math" + "reflect" + "testing" + + "github.com/moira-alert/moira" + . "github.com/smartystreets/goconvey/convey" +) + +func Test_newSetHelperFromTriggerPatternMetrics(t *testing.T) { + type args struct { + metrics TriggerPatternMetrics + } + tests := []struct { + name string + args args + want setHelper + }{ + { + name: "is empty", + args: args{ + metrics: TriggerPatternMetrics{}, + }, + want: setHelper{}, + }, + { + name: "is not empty", + args: args{ + metrics: TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.name.1"}, + }, + }, + want: setHelper{"metric.test.1": true}, + }, + } + + Convey("TriggerPatterMetrics", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := newSetHelperFromTriggerPatternMetrics(tt.args.metrics) + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func Test_setHelper_union(t *testing.T) { + type args struct { + other setHelper + } + tests := []struct { + name string + h setHelper + args args + want setHelper + }{ + { + name: "Both empty", + h: setHelper{}, + args: args{ + other: setHelper{}, + }, + want: setHelper{}, + }, + { + name: "Target is empty, other is not empty", + h: setHelper{}, + args: args{ + other: setHelper{"metric.test.1": true}, + }, + want: setHelper{"metric.test.1": true}, + }, + { + name: "Target is not empty, other is empty", + h: setHelper{"metric.test.1": true}, + args: args{ + other: setHelper{}, + }, + want: setHelper{"metric.test.1": true}, + }, + { + name: "Both are not empty", + h: setHelper{"metric.test.1": true}, + args: args{ + other: setHelper{"metric.test.2": true}, + }, + want: setHelper{"metric.test.1": true, "metric.test.2": true}, + }, + { + name: "Both are not empty and have same names", + h: setHelper{"metric.test.1": true, "metric.test.2": true}, + args: args{ + other: setHelper{"metric.test.2": true, "metric.test.3": true}, + }, + want: setHelper{"metric.test.1": true, "metric.test.2": true, "metric.test.3": true}, + }, + } + Convey("union", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.h.union(tt.args.other) + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func Test_setHelper_diff(t *testing.T) { + type args struct { + other setHelper + } + tests := []struct { + name string + h setHelper + args args + want setHelper + }{ + { + name: "both have same elements", + h: setHelper{"t1": true, "t2": true}, + args: args{ + other: setHelper{"t1": true, "t2": true}, + }, + want: setHelper{}, + }, + { + name: "other have additional values", + h: setHelper{"t1": true, "t2": true}, + args: args{ + other: setHelper{"t1": true, "t2": true, "t3": true}, + }, + want: setHelper{"t3": true}, + }, + { + name: "origin have additional values", + h: setHelper{"t1": true, "t2": true, "t3": true}, + args: args{ + other: setHelper{"t1": true, "t2": true}, + }, + want: setHelper{}, + }, + } + Convey("diff", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.h.diff(tt.args.other) + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func Test_isOneMetricMap(t *testing.T) { + type args struct { + metrics map[string]MetricData + } + tests := []struct { + name string + args args + want bool + want1 string + }{ + { + name: "is one metric map", + args: args{ + metrics: map[string]MetricData{ + "metric.test.1": {}, + }, + }, + want: true, + want1: "metric.test.1", + }, + { + name: "is not one metric map", + args: args{ + metrics: map[string]MetricData{ + "metric.test.1": {}, + "metric.test.2": {}, + }, + }, + want: false, + want1: "", + }, + } + Convey("metrics map", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + ok, metricName := isOneMetricMap(tt.args.metrics) + So(ok, ShouldResemble, tt.want) + So(metricName, ShouldResemble, tt.want1) + }) + } + }) +} + +func Test_newTriggerPatternMetricsWithCapacity(t *testing.T) { + Convey("newTriggerPatternMetricsWithCapacity", t, func() { + Convey("call", func() { + capacity := 10 + actual := newTriggerPatternMetricsWithCapacity(capacity) + So(actual, ShouldNotBeNil) + So(actual, ShouldHaveLength, 0) + }) + }) +} + +func TestNewTriggerPatternMetrics(t *testing.T) { + Convey("NewTriggerPatternMetrics", t, func() { + fetched := FetchedPatternMetrics{ + {Name: "metric.test.1"}, + {Name: "metric.test.2"}, + } + actual := NewTriggerPatternMetrics(fetched) + So(actual, ShouldHaveLength, 2) + So(actual["metric.test.1"].Name, ShouldResemble, "metric.test.1") + So(actual["metric.test.2"].Name, ShouldResemble, "metric.test.2") + }) +} + +func TestTriggerPatternMetrics_Populate(t *testing.T) { + type args struct { + lastMetrics map[string]bool + from int64 + to int64 + } + tests := []struct { + name string + m TriggerPatternMetrics + args args + want TriggerPatternMetrics + }{ + { + name: "origin do not have missing metrics", + m: TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{0}}, + "metric.test.2": {Name: "metric.test.2", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{0}}, + }, + args: args{ + lastMetrics: map[string]bool{ + "metric.test.1": true, + "metric.test.2": true, + }, + from: 17, + to: 67, + }, + want: TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{0}}, + "metric.test.2": {Name: "metric.test.2", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{0}}, + }, + }, + { + name: "origin have missing metrics", + m: TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{0}}, + }, + args: args{ + lastMetrics: map[string]bool{ + "metric.test.1": true, + "metric.test.2": true, + }, + from: 17, + to: 67, + }, + want: TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{0}}, + "metric.test.2": {Name: "metric.test.2", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{math.NaN()}}, + }, + }, + } + Convey("Populate", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.Populate(tt.args.lastMetrics, tt.args.from, tt.args.to) + So(actual, ShouldHaveLength, len(tt.want)) + for metricName, actualMetric := range actual { + wantMetric, ok := tt.want[metricName] + So(ok, ShouldBeTrue) + So(actualMetric.StartTime, ShouldResemble, wantMetric.StartTime) + So(actualMetric.StopTime, ShouldResemble, wantMetric.StopTime) + So(actualMetric.StepTime, ShouldResemble, wantMetric.StepTime) + So(actualMetric.Values, ShouldHaveLength, len(wantMetric.Values)) + } + }) + } + }) +} +func TestNewTriggerMetricsWithCapacity(t *testing.T) { + Convey("NewTriggerMetricsWithCapacity", t, func() { + capacity := 10 + actual := NewTriggerMetricsWithCapacity(capacity) + So(actual, ShouldNotBeNil) + So(actual, ShouldHaveLength, 0) + }) +} + +func TestTriggerMetrics_Populate(t *testing.T) { + type args struct { + lastCheck moira.CheckData + from int64 + to int64 + } + tests := []struct { + name string + m TriggerMetrics + args args + want TriggerMetrics + }{ + { + name: "origin do not have missing metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + args: args{ + lastCheck: moira.CheckData{ + Metrics: map[string]moira.MetricState{ + "metric.test.1": {Values: map[string]float64{"t1": 0}}, + "metric.test.2": {Values: map[string]float64{"t1": 0}}, + }, + }, + from: 17, + to: 67, + }, + want: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + }, + { + name: "origin have missing alone metric", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + args: args{ + lastCheck: moira.CheckData{ + MetricsToTargetRelation: map[string]string{"t2": "metric.test.3"}, + Metrics: map[string]moira.MetricState{ + "metric.test.1": {Values: map[string]float64{"t1": 0, "t2": 0}}, + "metric.test.2": {Values: map[string]float64{"t1": 0, "t2": 0}}, + }, + }, + from: 17, + to: 67, + }, + want: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + "t2": { + "metric.test.3": {Name: "metric.test.3", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{math.NaN()}}, + }, + }, + }, + { + name: "origin have missing metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + }, + }, + args: args{ + lastCheck: moira.CheckData{ + Metrics: map[string]moira.MetricState{ + "metric.test.1": {Values: map[string]float64{"t1": 0}}, + "metric.test.2": {Values: map[string]float64{"t1": 0}}, + }, + }, + from: 17, + to: 67, + }, + want: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{math.NaN()}}, + }, + }, + }, + { + name: "origin have missing metrics and alone metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.3": {Name: "metric.test.3"}, + }, + }, + args: args{ + lastCheck: moira.CheckData{ + MetricsToTargetRelation: map[string]string{"t2": "metric.test.3"}, + Metrics: map[string]moira.MetricState{ + "metric.test.1": {Values: map[string]float64{"t1": 0, "t2": 0}}, + "metric.test.2": {Values: map[string]float64{"t1": 0, "t2": 0}}, + }, + }, + from: 17, + to: 67, + }, + want: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{math.NaN()}}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.3": {Name: "metric.test.3"}, + }, + }, + }, + { + name: "origin have target with missing metrics and alone metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + "metric.test.3": {Name: "metric.test.3"}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.4": {Name: "metric.test.4"}, + }, + "t3": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + args: args{ + lastCheck: moira.CheckData{ + MetricsToTargetRelation: map[string]string{"t2": "metric.test.4"}, + Metrics: map[string]moira.MetricState{ + "metric.test.1": {Values: map[string]float64{"t1": 0, "t2": 0, "t3": 0}}, + "metric.test.2": {Values: map[string]float64{"t1": 0, "t2": 0, "t3": 0}}, + "metric.test.3": {Values: map[string]float64{"t1": 0, "t2": 0, "t3": 0}}, + "metric.test.4": {Values: map[string]float64{"t1": 0, "t2": 0, "t3": 0}}, + }, + }, + from: 17, + to: 67, + }, + want: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + "metric.test.3": {Name: "metric.test.3"}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.4": {Name: "metric.test.4"}, + }, + "t3": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + "metric.test.3": {Name: "metric.test.3", StartTime: 17, StopTime: 67, StepTime: 60, Values: []float64{math.NaN()}}, + }, + }, + }, + } + Convey("Populate", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.Populate(tt.args.lastCheck, tt.args.from, tt.args.to) + So(actual, ShouldHaveLength, len(tt.want)) + for targetName, metrics := range actual { + wantMetrics, ok := tt.want[targetName] + So(metrics, ShouldHaveLength, len(wantMetrics)) + So(ok, ShouldBeTrue) + for metricName, actualMetric := range metrics { + wantMetric, ok := wantMetrics[metricName] + So(ok, ShouldBeTrue) + So(actualMetric.Name, ShouldResemble, wantMetric.Name) + So(actualMetric.StartTime, ShouldResemble, wantMetric.StartTime) + So(actualMetric.StopTime, ShouldResemble, wantMetric.StopTime) + So(actualMetric.StepTime, ShouldResemble, wantMetric.StepTime) + So(actualMetric.Values, ShouldHaveLength, len(wantMetric.Values)) + } + } + }) + } + }) +} + +func TestTriggerMetrics_FilterAloneMetrics(t *testing.T) { + tests := []struct { + name string + m TriggerMetrics + want TriggerMetrics + want1 MetricsToCheck + }{ + { + name: "origin does not have alone metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + want: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + want1: MetricsToCheck{}, + }, + { + name: "origin has alone metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.3": {Name: "metric.test.3"}, + }, + }, + want: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + want1: MetricsToCheck{"t2": {Name: "metric.test.3"}}, + }, + } + Convey("FilterAloneMetrics", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + filtered, alone := tt.m.FilterAloneMetrics() + So(filtered, ShouldResemble, tt.want) + So(alone, ShouldResemble, tt.want1) + }) + } + }) +} + +func TestTriggerMetrics_Diff(t *testing.T) { + tests := []struct { + name string + m TriggerMetrics + want map[string]map[string]bool + }{ + { + name: "all targets have same metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + "metric.test.3": {Name: "metric.test.3"}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + "metric.test.3": {Name: "metric.test.3"}, + }, + }, + want: map[string]map[string]bool{}, + }, + { + name: "one target have missed metric", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + "metric.test.3": {Name: "metric.test.3"}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + want: map[string]map[string]bool{"t2": {"metric.test.3": true}}, + }, + { + name: "one target is alone metric", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + "metric.test.3": {Name: "metric.test.3"}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + }, + }, + want: map[string]map[string]bool{}, + }, + } + Convey("Diff", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.Diff() + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func TestTriggerMetrics_multiMetricsTarget(t *testing.T) { + tests := []struct { + name string + m TriggerMetrics + want string + want1 setHelper + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := tt.m.multiMetricsTarget() + if got != tt.want { + t.Errorf("TriggerMetrics.multiMetricsTarget() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(got1, tt.want1) { + t.Errorf("TriggerMetrics.multiMetricsTarget() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func TestTriggerMetrics_ConvertForCheck(t *testing.T) { + tests := []struct { + name string + m TriggerMetrics + want TriggerMetricsToCheck + }{ + { + name: "origin is empty", + m: TriggerMetrics{}, + want: TriggerMetricsToCheck{}, + }, + { + name: "origin have metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + }, + want: TriggerMetricsToCheck{ + "metric.test.1": MetricsToCheck{ + "t1": {Name: "metric.test.1"}, + }, + "metric.test.2": MetricsToCheck{ + "t1": {Name: "metric.test.2"}, + }, + }, + }, + { + name: "origin have metrics and target with empty metrics", + m: TriggerMetrics{ + "t1": TriggerPatternMetrics{ + "metric.test.1": {Name: "metric.test.1"}, + "metric.test.2": {Name: "metric.test.2"}, + }, + "t2": TriggerPatternMetrics{ + "metric.test.3": {Name: "metric.test.3"}, + }, + }, + want: TriggerMetricsToCheck{ + "metric.test.1": MetricsToCheck{ + "t1": {Name: "metric.test.1"}, + "t2": {Name: "metric.test.3"}, + }, + "metric.test.2": MetricsToCheck{ + "t1": {Name: "metric.test.2"}, + "t2": {Name: "metric.test.3"}, + }, + }, + }, + } + Convey("ConvertForCheck", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.ConvertForCheck() + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func TestMetricsToCheck_MetricName(t *testing.T) { + tests := []struct { + name string + m MetricsToCheck + want string + }{ + { + name: "origin is empty", + m: MetricsToCheck{}, + want: "", + }, + { + name: "origin is not empty and all metrics have same name", + m: MetricsToCheck{ + "t1": MetricData{Name: "metric.test.1"}, + "t2": MetricData{Name: "metric.test.1"}, + }, + want: "metric.test.1", + }, + { + name: "origin is not empty and metrics have different names", + m: MetricsToCheck{ + "t1": MetricData{Name: "metric.test.2"}, + "t2": MetricData{Name: "metric.test.1"}, + }, + want: "metric.test.1", + }, + } + Convey("MetricName", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.MetricName() + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func TestMetricsToCheck_GetRelations(t *testing.T) { + tests := []struct { + name string + m MetricsToCheck + want map[string]string + }{ + { + name: "origin is empty", + m: MetricsToCheck{}, + want: map[string]string{}, + }, + { + name: "origin is not empty", + m: MetricsToCheck{ + "t1": MetricData{Name: "metric.test.1"}, + "t2": MetricData{Name: "metric.test.2"}, + }, + want: map[string]string{ + "t1": "metric.test.1", + "t2": "metric.test.2", + }, + }, + } + Convey("GetRelations", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.GetRelations() + So(actual, ShouldResemble, tt.want) + }) + } + }) +} + +func TestMetricsToCheck_Merge(t *testing.T) { + type args struct { + other MetricsToCheck + } + tests := []struct { + name string + m MetricsToCheck + args args + want MetricsToCheck + }{ + { + name: "origin and other are empty", + m: MetricsToCheck{}, + args: args{ + other: MetricsToCheck{}, + }, + want: MetricsToCheck{}, + }, + { + name: "origin is empty and other is not", + m: MetricsToCheck{}, + args: args{ + other: MetricsToCheck{"t1": MetricData{Name: "metric.test.1"}}, + }, + want: MetricsToCheck{"t1": MetricData{Name: "metric.test.1"}}, + }, + { + name: "origin is not empty and other is empty", + m: MetricsToCheck{"t1": MetricData{Name: "metric.test.1"}}, + args: args{ + other: MetricsToCheck{}, + }, + want: MetricsToCheck{"t1": MetricData{Name: "metric.test.1"}}, + }, + { + name: "origin and other have same targets", + m: MetricsToCheck{"t1": MetricData{Name: "metric.test.1"}}, + args: args{ + other: MetricsToCheck{"t1": MetricData{Name: "metric.test.2"}}, + }, + want: MetricsToCheck{"t1": MetricData{Name: "metric.test.2"}}, + }, + } + Convey("Merge", t, func() { + for _, tt := range tests { + Convey(tt.name, func() { + actual := tt.m.Merge(tt.args.other) + So(actual, ShouldResemble, tt.want) + }) + } + }) +} diff --git a/mock/metric_source/fetch_result.go b/mock/metric_source/fetch_result.go index b9e59d1cd..53bd27adf 100644 --- a/mock/metric_source/fetch_result.go +++ b/mock/metric_source/fetch_result.go @@ -35,10 +35,10 @@ func (m *MockFetchResult) EXPECT() *MockFetchResultMockRecorder { } // GetMetricsData mocks base method -func (m *MockFetchResult) GetMetricsData() []*metric_source.MetricData { +func (m *MockFetchResult) GetMetricsData() []metric_source.MetricData { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetMetricsData") - ret0, _ := ret[0].([]*metric_source.MetricData) + ret0, _ := ret[0].([]metric_source.MetricData) return ret0 } diff --git a/mock/moira-alert/sender.go b/mock/moira-alert/sender.go index 1ef2409d5..7c5f18c7c 100644 --- a/mock/moira-alert/sender.go +++ b/mock/moira-alert/sender.go @@ -50,7 +50,7 @@ func (mr *MockSenderMockRecorder) Init(arg0, arg1, arg2, arg3 interface{}) *gomo } // SendEvents mocks base method -func (m *MockSender) SendEvents(arg0 moira.NotificationEvents, arg1 moira.ContactData, arg2 moira.TriggerData, arg3 []byte, arg4 bool) error { +func (m *MockSender) SendEvents(arg0 moira.NotificationEvents, arg1 moira.ContactData, arg2 moira.TriggerData, arg3 [][]byte, arg4 bool) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendEvents", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(error) diff --git a/notifier/events/event.go b/notifier/events/event.go index 2ba96f82b..d983e3b8c 100644 --- a/notifier/events/event.go +++ b/notifier/events/event.go @@ -68,7 +68,7 @@ func (worker *FetchEventsWorker) processEvent(event moira.NotificationEvent) err ) if event.State != moira.StateTEST { - worker.Logger.Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, moira.UseFloat64(event.Value), event.OldState, event.State) + worker.Logger.Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State) trigger, err := worker.Database.GetTrigger(event.TriggerID) if err != nil { diff --git a/notifier/events/event_test.go b/notifier/events/event_test.go index b9630ad74..2cee8e879 100644 --- a/notifier/events/event_test.go +++ b/notifier/events/event_test.go @@ -152,7 +152,7 @@ func TestDisabledNotification(t *testing.T) { dataBase.EXPECT().GetTrigger(event.TriggerID).Return(trigger, nil) dataBase.EXPECT().GetTagsSubscriptions(triggerData.Tags).Times(1).Return([]*moira.SubscriptionData{&disabledSubscription}, nil) - logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, moira.UseFloat64(event.Value), event.OldState, event.State) + logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State) logger.EXPECT().Debugf("Getting subscriptions for tags %v", triggerData.Tags) logger.EXPECT().Debugf("Subscription %s is disabled", disabledSubscription.ID) @@ -185,7 +185,7 @@ func TestSubscriptionsManagedToIgnoreEvents(t *testing.T) { dataBase.EXPECT().GetTrigger(event.TriggerID).Return(trigger, nil) dataBase.EXPECT().GetTagsSubscriptions(triggerData.Tags).Times(1).Return([]*moira.SubscriptionData{&subscriptionToIgnoreWarnings}, nil) - logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, moira.UseFloat64(event.Value), event.OldState, event.State) + logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State) logger.EXPECT().Debugf("Getting subscriptions for tags %v", triggerData.Tags) logger.EXPECT().Debugf("Subscription %s is managed to ignore %s -> %s transitions", subscriptionToIgnoreWarnings.ID, event.OldState, event.State) @@ -210,7 +210,7 @@ func TestSubscriptionsManagedToIgnoreEvents(t *testing.T) { dataBase.EXPECT().GetTrigger(event.TriggerID).Return(trigger, nil) dataBase.EXPECT().GetTagsSubscriptions(triggerData.Tags).Times(1).Return([]*moira.SubscriptionData{&subscriptionToIgnoreRecoverings}, nil) - logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, moira.UseFloat64(event.Value), event.OldState, event.State) + logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State) logger.EXPECT().Debugf("Getting subscriptions for tags %v", triggerData.Tags) logger.EXPECT().Debugf("Subscription %s is managed to ignore %s -> %s transitions", subscriptionToIgnoreRecoverings.ID, event.OldState, event.State) @@ -235,7 +235,7 @@ func TestSubscriptionsManagedToIgnoreEvents(t *testing.T) { dataBase.EXPECT().GetTrigger(event.TriggerID).Return(trigger, nil) dataBase.EXPECT().GetTagsSubscriptions(triggerData.Tags).Times(1).Return([]*moira.SubscriptionData{&subscriptionToIgnoreWarningsAndRecoverings}, nil) - logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, moira.UseFloat64(event.Value), event.OldState, event.State) + logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State) logger.EXPECT().Debugf("Getting subscriptions for tags %v", triggerData.Tags) logger.EXPECT().Debugf("Subscription %s is managed to ignore %s -> %s transitions", subscriptionToIgnoreWarningsAndRecoverings.ID, event.OldState, event.State) @@ -343,7 +343,7 @@ func TestFailReadContact(t *testing.T) { getContactError := fmt.Errorf("Can not get contact") dataBase.EXPECT().GetContact(contact.ID).Times(1).Return(moira.ContactData{}, getContactError) - logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, moira.UseFloat64(event.Value), event.OldState, event.State) + logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State) logger.EXPECT().Debugf("Getting subscriptions for tags %v", triggerData.Tags) logger.EXPECT().Warningf("Failed to get contact: %s, skip handling it, error: %v", contact.ID, getContactError) @@ -375,7 +375,7 @@ func TestEmptySubscriptions(t *testing.T) { dataBase.EXPECT().GetTrigger(event.TriggerID).Return(trigger, nil) dataBase.EXPECT().GetTagsSubscriptions(triggerData.Tags).Times(1).Return([]*moira.SubscriptionData{{ThrottlingEnabled: true}}, nil) - logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, moira.UseFloat64(event.Value), event.OldState, event.State) + logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State) logger.EXPECT().Debugf("Getting subscriptions for tags %v", triggerData.Tags) logger.EXPECT().Debugf("Subscription %s is disabled", "") @@ -404,7 +404,7 @@ func TestEmptySubscriptions(t *testing.T) { dataBase.EXPECT().GetTrigger(event.TriggerID).Return(trigger, nil) dataBase.EXPECT().GetTagsSubscriptions(triggerData.Tags).Times(1).Return([]*moira.SubscriptionData{nil}, nil) - logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, moira.UseFloat64(event.Value), event.OldState, event.State) + logger.EXPECT().Debugf("Processing trigger id %s for metric %s == %f, %s -> %s", event.TriggerID, event.Metric, event.GetMetricsValues(), event.OldState, event.State) logger.EXPECT().Debugf("Getting subscriptions for tags %v", triggerData.Tags) logger.EXPECT().Debugf("Subscription is nil") diff --git a/notifier/notifier.go b/notifier/notifier.go index e9e5d0e6f..46541a797 100644 --- a/notifier/notifier.go +++ b/notifier/notifier.go @@ -150,7 +150,7 @@ func (notifier *StandardNotifier) resend(pkg *NotificationPackage, reason string func (notifier *StandardNotifier) runSender(sender moira.Sender, ch chan NotificationPackage) { defer notifier.waitGroup.Done() for pkg := range ch { - plot, err := notifier.buildNotificationPackagePlot(pkg) + plots, err := notifier.buildNotificationPackagePlots(pkg) if err != nil { buildErr := fmt.Sprintf("Can't build notification package plot for %s: %s", pkg.Trigger.ID, err.Error()) switch err.(type) { @@ -160,7 +160,7 @@ func (notifier *StandardNotifier) runSender(sender moira.Sender, ch chan Notific notifier.logger.Errorf(buildErr) } } - err = sender.SendEvents(pkg.Events, pkg.Contact, pkg.Trigger, plot, pkg.Throttled) + err = sender.SendEvents(pkg.Events, pkg.Contact, pkg.Trigger, plots, pkg.Throttled) if err == nil { if metric, found := notifier.metrics.SendersOkMetrics.GetRegisteredMeter(pkg.Contact.Type); found { metric.Mark(1) diff --git a/notifier/notifier_test.go b/notifier/notifier_test.go index c48ce8309..4922fc6b0 100644 --- a/notifier/notifier_test.go +++ b/notifier/notifier_test.go @@ -19,7 +19,7 @@ import ( ) var ( - plot = make([]byte, 0) + plots [][]byte shutdown = make(chan struct{}) ) @@ -107,7 +107,7 @@ func TestFailSendEvent(t *testing.T) { }, } notification := moira.ScheduledNotification{} - sender.EXPECT().SendEvents(eventsData, pkg.Contact, pkg.Trigger, plot, pkg.Throttled).Return(fmt.Errorf("Cant't send")) + sender.EXPECT().SendEvents(eventsData, pkg.Contact, pkg.Trigger, plots, pkg.Throttled).Return(fmt.Errorf("Cant't send")) scheduler.EXPECT().ScheduleNotification(gomock.Any(), event, pkg.Trigger, pkg.Contact, pkg.Plotting, pkg.Throttled, pkg.FailCount+1).Return(¬ification) dataBase.EXPECT().AddNotification(¬ification).Return(nil) @@ -132,7 +132,7 @@ func TestTimeout(t *testing.T) { }, } - sender.EXPECT().SendEvents(eventsData, pkg.Contact, pkg.Trigger, plot, pkg.Throttled).Return(nil).Do(func(f ...interface{}) { + sender.EXPECT().SendEvents(eventsData, pkg.Contact, pkg.Trigger, plots, pkg.Throttled).Return(nil).Do(func(f ...interface{}) { fmt.Print("Trying to send for 10 second") time.Sleep(time.Second * 10) }).Times(maxParallelSendsPerSender) diff --git a/notifier/plotting.go b/notifier/plotting.go index 4a9dfdd27..0dee8ba79 100644 --- a/notifier/plotting.go +++ b/notifier/plotting.go @@ -31,38 +31,42 @@ func (err errFetchAvailableSeriesFailed) Error() string { } // buildNotificationPackagePlot returns bytes slice containing package plot -func (notifier *StandardNotifier) buildNotificationPackagePlot(pkg NotificationPackage) ([]byte, error) { - buff := bytes.NewBuffer(make([]byte, 0)) +func (notifier *StandardNotifier) buildNotificationPackagePlots(pkg NotificationPackage) ([][]byte, error) { + result := make([][]byte, 0) if !pkg.Plotting.Enabled { - return buff.Bytes(), nil + return nil, nil } if pkg.Trigger.ID == "" { - return buff.Bytes(), nil + return nil, nil } metricsToShow := pkg.GetMetricNames() if len(metricsToShow) == 0 { - return buff.Bytes(), nil + return nil, nil } plotTemplate, err := plotting.GetPlotTemplate(pkg.Plotting.Theme, notifier.config.Location) if err != nil { - return buff.Bytes(), err + return nil, err } from, to := resolveMetricsWindow(notifier.logger, pkg.Trigger, pkg) metricsData, trigger, err := notifier.evaluateTriggerMetrics(from, to, pkg.Trigger.ID) if err != nil { - return buff.Bytes(), err + return nil, err } metricsData = getMetricDataToShow(metricsData, metricsToShow) notifier.logger.Debugf("Build plot for trigger: %s from MetricsData: %v", trigger.ID, metricsData) - renderable, err := plotTemplate.GetRenderable(trigger, metricsData) - if err != nil { - return buff.Bytes(), err - } - if err = renderable.Render(chart.PNG, buff); err != nil { - buff.Reset() - return buff.Bytes(), err + for _, targetName := range trigger.Targets { + metrics := metricsData[targetName] + renderable, err := plotTemplate.GetRenderable(targetName, trigger, metrics) + if err != nil { + return nil, err + } + buff := bytes.NewBuffer(make([]byte, 0)) + if err = renderable.Render(chart.PNG, buff); err != nil { + return nil, err + } + result = append(result, buff.Bytes()) } - return buff.Bytes(), nil + return result, nil } // resolveMetricsWindow returns from, to parameters depending on trigger type @@ -105,7 +109,7 @@ func alignToMinutes(unixTime int64) int64 { } // evaluateTriggerMetrics returns collection of MetricData -func (notifier *StandardNotifier) evaluateTriggerMetrics(from, to int64, triggerID string) ([]*metricSource.MetricData, *moira.Trigger, error) { +func (notifier *StandardNotifier) evaluateTriggerMetrics(from, to int64, triggerID string) (map[string][]metricSource.MetricData, *moira.Trigger, error) { trigger, err := notifier.database.GetTrigger(triggerID) if err != nil { return nil, nil, err @@ -114,19 +118,21 @@ func (notifier *StandardNotifier) evaluateTriggerMetrics(from, to int64, trigger if err != nil { return nil, &trigger, err } - var metricsData = make([]*metricSource.MetricData, 0) - for _, target := range trigger.Targets { + var result = make(map[string][]metricSource.MetricData) + for i, target := range trigger.Targets { + i++ // Increase + targetName := fmt.Sprintf("t%d", i) timeSeries, fetchErr := fetchAvailableSeries(metricsSource, target, from, to) if fetchErr != nil { return nil, &trigger, fetchErr } - metricsData = append(metricsData, timeSeries...) + result[targetName] = timeSeries } - return metricsData, &trigger, err + return result, &trigger, err } // fetchAvailableSeries calls fetch function with realtime alerting and retries on fail without -func fetchAvailableSeries(metricsSource metricSource.MetricSource, target string, from, to int64) ([]*metricSource.MetricData, error) { +func fetchAvailableSeries(metricsSource metricSource.MetricSource, target string, from, to int64) ([]metricSource.MetricData, error) { realtimeFetchResult, realtimeErr := metricsSource.Fetch(target, from, to, true) if realtimeErr == nil { return realtimeFetchResult.GetMetricsData(), realtimeErr @@ -142,20 +148,28 @@ func fetchAvailableSeries(metricsSource metricSource.MetricSource, target string } // getMetricDataToShow returns MetricData limited by whitelist -func getMetricDataToShow(metricsData []*metricSource.MetricData, metricsWhitelist []string) []*metricSource.MetricData { +func getMetricDataToShow(metricsData map[string][]metricSource.MetricData, metricsWhitelist []string) map[string][]metricSource.MetricData { + result := make(map[string][]metricSource.MetricData) if len(metricsWhitelist) == 0 { return metricsData } - metricsWhitelistHash := make(map[string]struct{}, len(metricsWhitelist)) + metricsWhitelistHash := make(map[string]bool, len(metricsWhitelist)) for _, whiteListed := range metricsWhitelist { - metricsWhitelistHash[whiteListed] = struct{}{} + metricsWhitelistHash[whiteListed] = true } - newMetricsData := make([]*metricSource.MetricData, 0, len(metricsWhitelist)) - for _, metricData := range metricsData { - if _, ok := metricsWhitelistHash[metricData.Name]; ok { - newMetricsData = append(newMetricsData, metricData) + for targetName, metrics := range metricsData { + newMetricsData := make([]metricSource.MetricData, 0, len(metricsWhitelist)) + if len(metrics) == 1 { + result[targetName] = metrics + continue + } + for _, metricData := range metrics { + if _, ok := metricsWhitelistHash[metricData.Name]; ok { + newMetricsData = append(newMetricsData, metricData) + } } + result[targetName] = newMetricsData } - return newMetricsData + return result } diff --git a/notifier/plotting_test.go b/notifier/plotting_test.go index beaf307c7..2acdb5e90 100644 --- a/notifier/plotting_test.go +++ b/notifier/plotting_test.go @@ -102,30 +102,42 @@ func TestResolveMetricsWindow(t *testing.T) { // TestGetMetricDataToShow tests to limited metricsData returns only necessary metricsData func TestGetMetricDataToShow(t *testing.T) { - givenSeries := []*metricSource.MetricData{ - metricSource.MakeMetricData("metricPrefix.metricName1", []float64{1}, 1, 1), - metricSource.MakeMetricData("metricPrefix.metricName2", []float64{2}, 2, 2), - metricSource.MakeMetricData("metricPrefix.metricName3", []float64{3}, 3, 3), + givenSeries := map[string][]metricSource.MetricData{ + "t1": []metricSource.MetricData{ + *metricSource.MakeMetricData("metricPrefix.metricName1", []float64{1}, 1, 1), + *metricSource.MakeMetricData("metricPrefix.metricName2", []float64{2}, 2, 2), + *metricSource.MakeMetricData("metricPrefix.metricName3", []float64{3}, 3, 3), + }, } Convey("Limit series by non-empty whitelist", t, func() { Convey("MetricsData has necessary series", func() { metricsWhiteList := []string{"metricPrefix.metricName1", "metricPrefix.metricName2"} metricsData := getMetricDataToShow(givenSeries, metricsWhiteList) - So(len(metricsData), ShouldEqual, len(metricsWhiteList)) - So(metricsData[0].Name, ShouldEqual, metricsWhiteList[0]) - So(metricsData[1].Name, ShouldEqual, metricsWhiteList[1]) + So(len(metricsData["t1"]), ShouldEqual, len(metricsWhiteList)) + So(metricsData["t1"][0].Name, ShouldEqual, metricsWhiteList[0]) + So(metricsData["t1"][1].Name, ShouldEqual, metricsWhiteList[1]) }) Convey("MetricsData has no necessary series", func() { metricsWhiteList := []string{"metricPrefix.metricName4"} metricsData := getMetricDataToShow(givenSeries, metricsWhiteList) - So(len(metricsData), ShouldEqual, 0) + So(len(metricsData["t1"]), ShouldEqual, 0) + }) + Convey("MetricsData has necessary series and alone metrics target", func() { + metricsWhiteList := []string{"metricPrefix.metricName1", "metricPrefix.metricName2"} + givenSeries["t2"] = []metricSource.MetricData{ + *metricSource.MakeMetricData("metricPrefix.metricName4", []float64{1}, 1, 1), + } + metricsData := getMetricDataToShow(givenSeries, metricsWhiteList) + So(len(metricsData["t1"]), ShouldEqual, 2) + So(len(metricsData["t2"]), ShouldEqual, 1) }) + }) Convey("Limit series by an empty whitelist", t, func() { metricsWhiteList := make([]string, 0) metricsData := getMetricDataToShow(givenSeries, metricsWhiteList) - for metricDataInd := range metricsData { - So(metricsData[metricDataInd].Name, ShouldEqual, givenSeries[metricDataInd].Name) + for metricDataInd := range metricsData["t1"] { + So(metricsData["t1"][metricDataInd].Name, ShouldEqual, givenSeries["t1"][metricDataInd].Name) } So(len(metricsData), ShouldEqual, len(givenSeries)) }) diff --git a/notifier/selfstate/selfstate.go b/notifier/selfstate/selfstate.go index 06471e095..226616369 100644 --- a/notifier/selfstate/selfstate.go +++ b/notifier/selfstate/selfstate.go @@ -168,13 +168,13 @@ func (selfCheck *SelfCheckWorker) check(nowTS int64, lastMetricReceivedTS, redis } func appendNotificationEvents(events *[]moira.NotificationEvent, message string, currentValue int64) { - val := float64(currentValue) + val := map[string]float64{"t1": float64(currentValue)} event := moira.NotificationEvent{ Timestamp: time.Now().Unix(), OldState: moira.StateNODATA, State: moira.StateERROR, Metric: message, - Value: &val, + Values: val, } *events = append(*events, event) diff --git a/plotting/_examples/dark.expression.example.png b/plotting/_examples/dark.expression.example.png index fa96b77d0..ed9b2096e 100644 Binary files a/plotting/_examples/dark.expression.example.png and b/plotting/_examples/dark.expression.example.png differ diff --git a/plotting/_examples/dark.expression.humanized.example.png b/plotting/_examples/dark.expression.humanized.example.png index 33a2f5a79..64c7aaf38 100644 Binary files a/plotting/_examples/dark.expression.humanized.example.png and b/plotting/_examples/dark.expression.humanized.example.png differ diff --git a/plotting/_examples/dark.falling.error.example.png b/plotting/_examples/dark.falling.error.example.png index 082704eef..72427a620 100644 Binary files a/plotting/_examples/dark.falling.error.example.png and b/plotting/_examples/dark.falling.error.example.png differ diff --git a/plotting/_examples/dark.falling.error.humanized.example.png b/plotting/_examples/dark.falling.error.humanized.example.png index da604ae86..9c2574e78 100644 Binary files a/plotting/_examples/dark.falling.error.humanized.example.png and b/plotting/_examples/dark.falling.error.humanized.example.png differ diff --git a/plotting/_examples/dark.falling.stateOk.example.png b/plotting/_examples/dark.falling.stateOk.example.png index c03d19e62..5e5f89d8e 100644 Binary files a/plotting/_examples/dark.falling.stateOk.example.png and b/plotting/_examples/dark.falling.stateOk.example.png differ diff --git a/plotting/_examples/dark.falling.stateOk.humanized.example.png b/plotting/_examples/dark.falling.stateOk.humanized.example.png index 975559ea8..ff8d029b1 100644 Binary files a/plotting/_examples/dark.falling.stateOk.humanized.example.png and b/plotting/_examples/dark.falling.stateOk.humanized.example.png differ diff --git a/plotting/_examples/dark.falling.warn.error.example.png b/plotting/_examples/dark.falling.warn.error.example.png index 7fcd8d923..8db52a1ef 100644 Binary files a/plotting/_examples/dark.falling.warn.error.example.png and b/plotting/_examples/dark.falling.warn.error.example.png differ diff --git a/plotting/_examples/dark.falling.warn.error.humanized.example.png b/plotting/_examples/dark.falling.warn.error.humanized.example.png index 49e2efa75..05789ed03 100644 Binary files a/plotting/_examples/dark.falling.warn.error.humanized.example.png and b/plotting/_examples/dark.falling.warn.error.humanized.example.png differ diff --git a/plotting/_examples/dark.falling.warn.example.png b/plotting/_examples/dark.falling.warn.example.png index 64345b43d..a7940457b 100644 Binary files a/plotting/_examples/dark.falling.warn.example.png and b/plotting/_examples/dark.falling.warn.example.png differ diff --git a/plotting/_examples/dark.falling.warn.humanized.example.png b/plotting/_examples/dark.falling.warn.humanized.example.png index 14cb4ffa6..3402ba3f5 100644 Binary files a/plotting/_examples/dark.falling.warn.humanized.example.png and b/plotting/_examples/dark.falling.warn.humanized.example.png differ diff --git a/plotting/_examples/dark.rising.error.example.png b/plotting/_examples/dark.rising.error.example.png index d23ec6291..a3f5f21bf 100644 Binary files a/plotting/_examples/dark.rising.error.example.png and b/plotting/_examples/dark.rising.error.example.png differ diff --git a/plotting/_examples/dark.rising.error.humanized.example.png b/plotting/_examples/dark.rising.error.humanized.example.png index eb5f4818c..04681b7c7 100644 Binary files a/plotting/_examples/dark.rising.error.humanized.example.png and b/plotting/_examples/dark.rising.error.humanized.example.png differ diff --git a/plotting/_examples/dark.rising.stateOk.example.png b/plotting/_examples/dark.rising.stateOk.example.png index 06c75840a..e98de12f1 100644 Binary files a/plotting/_examples/dark.rising.stateOk.example.png and b/plotting/_examples/dark.rising.stateOk.example.png differ diff --git a/plotting/_examples/dark.rising.stateOk.humanized.example.png b/plotting/_examples/dark.rising.stateOk.humanized.example.png index 148dddcd7..5bf064675 100644 Binary files a/plotting/_examples/dark.rising.stateOk.humanized.example.png and b/plotting/_examples/dark.rising.stateOk.humanized.example.png differ diff --git a/plotting/_examples/dark.rising.warn.error.example.png b/plotting/_examples/dark.rising.warn.error.example.png index b797e1487..8a32c5733 100644 Binary files a/plotting/_examples/dark.rising.warn.error.example.png and b/plotting/_examples/dark.rising.warn.error.example.png differ diff --git a/plotting/_examples/dark.rising.warn.error.humanized.example.png b/plotting/_examples/dark.rising.warn.error.humanized.example.png index 07924d962..5b405f185 100644 Binary files a/plotting/_examples/dark.rising.warn.error.humanized.example.png and b/plotting/_examples/dark.rising.warn.error.humanized.example.png differ diff --git a/plotting/_examples/dark.rising.warn.example.png b/plotting/_examples/dark.rising.warn.example.png index 75f6559a6..0eb0b9c68 100644 Binary files a/plotting/_examples/dark.rising.warn.example.png and b/plotting/_examples/dark.rising.warn.example.png differ diff --git a/plotting/_examples/dark.rising.warn.humanized.example.png b/plotting/_examples/dark.rising.warn.humanized.example.png index f9f5e30cf..068535a8b 100644 Binary files a/plotting/_examples/dark.rising.warn.humanized.example.png and b/plotting/_examples/dark.rising.warn.humanized.example.png differ diff --git a/plotting/_examples/light.expression.example.png b/plotting/_examples/light.expression.example.png index f8e9c0255..302d2ddee 100644 Binary files a/plotting/_examples/light.expression.example.png and b/plotting/_examples/light.expression.example.png differ diff --git a/plotting/_examples/light.expression.humanized.example.png b/plotting/_examples/light.expression.humanized.example.png index 06366feb1..3f8e7ede1 100644 Binary files a/plotting/_examples/light.expression.humanized.example.png and b/plotting/_examples/light.expression.humanized.example.png differ diff --git a/plotting/_examples/light.falling.error.example.png b/plotting/_examples/light.falling.error.example.png index c985244b5..8f17038b7 100644 Binary files a/plotting/_examples/light.falling.error.example.png and b/plotting/_examples/light.falling.error.example.png differ diff --git a/plotting/_examples/light.falling.error.humanized.example.png b/plotting/_examples/light.falling.error.humanized.example.png index 099bbb31e..5555eb574 100644 Binary files a/plotting/_examples/light.falling.error.humanized.example.png and b/plotting/_examples/light.falling.error.humanized.example.png differ diff --git a/plotting/_examples/light.falling.stateOk.example.png b/plotting/_examples/light.falling.stateOk.example.png index 29179d291..4c61a96fb 100644 Binary files a/plotting/_examples/light.falling.stateOk.example.png and b/plotting/_examples/light.falling.stateOk.example.png differ diff --git a/plotting/_examples/light.falling.stateOk.humanized.example.png b/plotting/_examples/light.falling.stateOk.humanized.example.png index 436a59680..d265c0f78 100644 Binary files a/plotting/_examples/light.falling.stateOk.humanized.example.png and b/plotting/_examples/light.falling.stateOk.humanized.example.png differ diff --git a/plotting/_examples/light.falling.warn.error.example.png b/plotting/_examples/light.falling.warn.error.example.png index c92f6ab6d..eb4f14276 100644 Binary files a/plotting/_examples/light.falling.warn.error.example.png and b/plotting/_examples/light.falling.warn.error.example.png differ diff --git a/plotting/_examples/light.falling.warn.error.humanized.example.png b/plotting/_examples/light.falling.warn.error.humanized.example.png index 5a56ee2a5..701f1ea4d 100644 Binary files a/plotting/_examples/light.falling.warn.error.humanized.example.png and b/plotting/_examples/light.falling.warn.error.humanized.example.png differ diff --git a/plotting/_examples/light.falling.warn.example.png b/plotting/_examples/light.falling.warn.example.png index 4496dd999..7a2cc7760 100644 Binary files a/plotting/_examples/light.falling.warn.example.png and b/plotting/_examples/light.falling.warn.example.png differ diff --git a/plotting/_examples/light.falling.warn.humanized.example.png b/plotting/_examples/light.falling.warn.humanized.example.png index 2ab9be95a..449ce6045 100644 Binary files a/plotting/_examples/light.falling.warn.humanized.example.png and b/plotting/_examples/light.falling.warn.humanized.example.png differ diff --git a/plotting/_examples/light.rising.error.example.png b/plotting/_examples/light.rising.error.example.png index f70c00b0a..ad6e1b0a1 100644 Binary files a/plotting/_examples/light.rising.error.example.png and b/plotting/_examples/light.rising.error.example.png differ diff --git a/plotting/_examples/light.rising.error.humanized.example.png b/plotting/_examples/light.rising.error.humanized.example.png index 4d1442308..dd60db9ad 100644 Binary files a/plotting/_examples/light.rising.error.humanized.example.png and b/plotting/_examples/light.rising.error.humanized.example.png differ diff --git a/plotting/_examples/light.rising.stateOk.example.png b/plotting/_examples/light.rising.stateOk.example.png index 914150523..ebdaf0c52 100644 Binary files a/plotting/_examples/light.rising.stateOk.example.png and b/plotting/_examples/light.rising.stateOk.example.png differ diff --git a/plotting/_examples/light.rising.stateOk.humanized.example.png b/plotting/_examples/light.rising.stateOk.humanized.example.png index 4cd43beac..0e38ba163 100644 Binary files a/plotting/_examples/light.rising.stateOk.humanized.example.png and b/plotting/_examples/light.rising.stateOk.humanized.example.png differ diff --git a/plotting/_examples/light.rising.warn.error.example.png b/plotting/_examples/light.rising.warn.error.example.png index 215ea8755..a1caf53d6 100644 Binary files a/plotting/_examples/light.rising.warn.error.example.png and b/plotting/_examples/light.rising.warn.error.example.png differ diff --git a/plotting/_examples/light.rising.warn.error.humanized.example.png b/plotting/_examples/light.rising.warn.error.humanized.example.png index 39ae97477..816b4db1c 100644 Binary files a/plotting/_examples/light.rising.warn.error.humanized.example.png and b/plotting/_examples/light.rising.warn.error.humanized.example.png differ diff --git a/plotting/_examples/light.rising.warn.example.png b/plotting/_examples/light.rising.warn.example.png index 2a3041b4c..107eccc6a 100644 Binary files a/plotting/_examples/light.rising.warn.example.png and b/plotting/_examples/light.rising.warn.example.png differ diff --git a/plotting/_examples/light.rising.warn.humanized.example.png b/plotting/_examples/light.rising.warn.humanized.example.png index cfba283ae..837396aa0 100644 Binary files a/plotting/_examples/light.rising.warn.humanized.example.png and b/plotting/_examples/light.rising.warn.humanized.example.png differ diff --git a/plotting/curve.go b/plotting/curve.go index 269f10ab3..231044248 100644 --- a/plotting/curve.go +++ b/plotting/curve.go @@ -16,7 +16,7 @@ type plotCurve struct { } // getCurveSeriesList returns curve series list -func getCurveSeriesList(metricsData []*metricSource.MetricData, theme moira.PlotTheme) []chart.TimeSeries { +func getCurveSeriesList(metricsData []metricSource.MetricData, theme moira.PlotTheme) []chart.TimeSeries { curveSeriesList := make([]chart.TimeSeries, 0) for metricDataInd := range metricsData { curveStyle, pointStyle := theme.GetSerieStyles(metricDataInd) @@ -27,7 +27,7 @@ func getCurveSeriesList(metricsData []*metricSource.MetricData, theme moira.Plot } // generatePlotCurves returns go-chart timeseries to generate plot curves -func generatePlotCurves(metricData *metricSource.MetricData, curveStyle chart.Style, pointStyle chart.Style) []chart.TimeSeries { +func generatePlotCurves(metricData metricSource.MetricData, curveStyle chart.Style, pointStyle chart.Style) []chart.TimeSeries { curves := describePlotCurves(metricData) curveSeries := make([]chart.TimeSeries, 0) for _, curve := range curves { @@ -53,7 +53,7 @@ func generatePlotCurves(metricData *metricSource.MetricData, curveStyle chart.St } // describePlotCurves returns parameters for required curves -func describePlotCurves(metricData *metricSource.MetricData) []plotCurve { +func describePlotCurves(metricData metricSource.MetricData) []plotCurve { curves := []plotCurve{{}} curvesInd := 0 @@ -77,7 +77,7 @@ func describePlotCurves(metricData *metricSource.MetricData) []plotCurve { } // resolveFirstPoint returns first point coordinates -func resolveFirstPoint(metricData *metricSource.MetricData) (int, int64) { +func resolveFirstPoint(metricData metricSource.MetricData) (int, int64) { start := 0 startTime := metricData.StartTime for _, metricVal := range metricData.Values { diff --git a/plotting/curve_test.go b/plotting/curve_test.go index 7dfcbb965..99847aeb3 100644 --- a/plotting/curve_test.go +++ b/plotting/curve_test.go @@ -32,7 +32,7 @@ func TestGeneratePlotCurves(t *testing.T) { metricName := "metric.firstValueIsAbsent" metricData.Name = metricName metricData.Values = firstValIsAbsentVals - curveSeries := generatePlotCurves(&metricData, chart.Style{}, chart.Style{}) + curveSeries := generatePlotCurves(metricData, chart.Style{}, chart.Style{}) So(len(curveSeries), ShouldEqual, 2) So(curveSeries[0].Name, ShouldEqual, metricName) So(curveSeries[0].YValues, ShouldResemble, []float64{32}) @@ -55,7 +55,7 @@ func TestGeneratePlotCurves(t *testing.T) { metricName := "metric.firstValueIsPresent" metricData.Name = metricName metricData.Values = firstValIsPresentVals - curveSeries := generatePlotCurves(&metricData, chart.Style{}, chart.Style{}) + curveSeries := generatePlotCurves(metricData, chart.Style{}, chart.Style{}) So(len(curveSeries), ShouldEqual, 3) So(curveSeries[0].Name, ShouldEqual, metricName) So(curveSeries[0].YValues, ShouldResemble, []float64{11, 23, 45}) @@ -86,7 +86,7 @@ func TestGeneratePlotCurves(t *testing.T) { func TestDescribePlotCurves(t *testing.T) { Convey("First value is absent", t, func() { metricData.Values = firstValIsAbsentVals - plotCurves := describePlotCurves(&metricData) + plotCurves := describePlotCurves(metricData) So(len(plotCurves), ShouldEqual, 2) So(plotCurves[0].values, ShouldResemble, []float64{32}) So(plotCurves[0].timeStamps, ShouldResemble, []time.Time{ @@ -105,7 +105,7 @@ func TestDescribePlotCurves(t *testing.T) { }) Convey("First value is present", t, func() { metricData.Values = firstValIsPresentVals - plotCurves := describePlotCurves(&metricData) + plotCurves := describePlotCurves(metricData) So(len(plotCurves), ShouldEqual, 3) So(plotCurves[0].values, ShouldResemble, []float64{11, 23, 45}) So(plotCurves[0].timeStamps, ShouldResemble, []time.Time{ @@ -133,13 +133,13 @@ func TestDescribePlotCurves(t *testing.T) { func TestResolveFirstPoint(t *testing.T) { Convey("First value is absent", t, func() { metricData.Values = firstValIsAbsentVals - firstPointInd, startTime := resolveFirstPoint(&metricData) + firstPointInd, startTime := resolveFirstPoint(metricData) So(firstPointInd, ShouldEqual, 2) So(startTime, ShouldEqual, 20) }) Convey("First value is present", t, func() { metricData.Values = firstValIsPresentVals - firstPointInd, startTime := resolveFirstPoint(&metricData) + firstPointInd, startTime := resolveFirstPoint(metricData) So(firstPointInd, ShouldEqual, 0) So(startTime, ShouldEqual, 0) }) diff --git a/plotting/limits.go b/plotting/limits.go index 2427cf2d3..227e4234c 100644 --- a/plotting/limits.go +++ b/plotting/limits.go @@ -29,7 +29,7 @@ type plotLimits struct { } // resolveLimits returns common plot limits -func resolveLimits(metricsData []*metricSource.MetricData) plotLimits { +func resolveLimits(metricsData []metricSource.MetricData) plotLimits { allValues := make([]float64, 0) allTimes := make([]time.Time, 0) for _, metricData := range metricsData { diff --git a/plotting/limits_test.go b/plotting/limits_test.go index 3ecb14063..b56dffcbb 100644 --- a/plotting/limits_test.go +++ b/plotting/limits_test.go @@ -18,14 +18,14 @@ func TestResolveLimits(t *testing.T) { stepTime := 60 elementsToUse := 10 startTime := time.Now().UTC().Unix() - var metricsData []*metricSource.MetricData + var metricsData []metricSource.MetricData // Fill MetricsData with random float64 values that higher than minValue and lower than maxValue for i := 0; i < int(elementsToUse); i++ { values := make([]float64, elementsToUse) for valInd := range values { values[valInd] = float64(rand.Intn(maxValue-1)) * rand.Float64() } - metricData := metricSource.MakeMetricData("test", values, int64(stepTime), int64(startTime)) + metricData := *metricSource.MakeMetricData("test", values, int64(stepTime), int64(startTime)) metricsData = append(metricsData, metricData) } // Change 2 first points of MetricsData to minValue and maxValue diff --git a/plotting/plot.go b/plotting/plot.go index ec810870e..f69bf4b77 100644 --- a/plotting/plot.go +++ b/plotting/plot.go @@ -9,6 +9,10 @@ import ( metricSource "github.com/moira-alert/moira/metric_source" ) +const ( + plotNameLen = 40 +) + // ErrNoPointsToRender is used to prevent unnecessary render calls type ErrNoPointsToRender struct { triggerID string @@ -45,7 +49,7 @@ func GetPlotTemplate(theme string, location *time.Location) (*Plot, error) { } // GetRenderable returns go-chart to render -func (plot *Plot) GetRenderable(trigger *moira.Trigger, metricsData []*metricSource.MetricData) (chart.Chart, error) { +func (plot *Plot) GetRenderable(targetName string, trigger *moira.Trigger, metricsData []metricSource.MetricData) (chart.Chart, error) { var renderable chart.Chart plotSeries := make([]chart.Series, 0) @@ -69,9 +73,10 @@ func (plot *Plot) GetRenderable(trigger *moira.Trigger, metricsData []*metricSou yAxisValuesFormatter, maxMarkLen := getYAxisValuesFormatter(limits) yAxisRange := limits.getThresholdAxisRange(trigger.TriggerType) + name := fmt.Sprintf("%s - %s", targetName, trigger.Name) renderable = chart.Chart{ - Title: sanitizeLabelName(trigger.Name, 40), + Title: sanitizeLabelName(name, plotNameLen), TitleStyle: plot.theme.GetTitleStyle(), Width: plot.width, diff --git a/plotting/plot_test.go b/plotting/plot_test.go index e96dd8798..496044d3d 100644 --- a/plotting/plot_test.go +++ b/plotting/plot_test.go @@ -440,7 +440,7 @@ var plotsHashDistancesTestCases = []plotsHashDistancesTestCase{ } // generateTestMetricsData generates metricData array for tests -func generateTestMetricsData(useHumanizedValues bool) []*metricSource.MetricData { +func generateTestMetricsData(useHumanizedValues bool) []metricSource.MetricData { metricData := metricSource.MetricData{ Name: "MetricName", StartTime: 0, @@ -483,19 +483,19 @@ func generateTestMetricsData(useHumanizedValues bool) []*metricSource.MetricData metricData4.Values[valInd] = plotTestOuterPointMultiplier * value } } - metricsData := []*metricSource.MetricData{&metricData, &metricData2, &metricData3, &metricData4} + metricsData := []metricSource.MetricData{metricData, metricData2, metricData3, metricData4} return metricsData } // renderTestMetricsDataToPNG renders and saves rendered plots to PNG func renderTestMetricsDataToPNG(trigger moira.Trigger, plotTheme string, - metricsData []*metricSource.MetricData, filePath string) error { + metricsData []metricSource.MetricData, filePath string) error { location, _ := time.LoadLocation("UTC") plotTemplate, err := GetPlotTemplate(plotTheme, location) if err != nil { return err } - renderable, err := plotTemplate.GetRenderable(&trigger, metricsData) + renderable, err := plotTemplate.GetRenderable("t1", &trigger, metricsData) if err != nil { return err } @@ -527,7 +527,7 @@ func calculateHashDistance(pathToOriginal, pathToRendered string) (*int, error) } // generateRandomTestMetricsData returns random test MetricsData by given numbers of values -func generateRandomTestMetricsData(numTotal int, numEmpty int) []*metricSource.MetricData { +func generateRandomTestMetricsData(numTotal int, numEmpty int) []metricSource.MetricData { startTime := int64(0) stepTime := int64(10) stopTime := int64(numTotal) * stepTime @@ -539,7 +539,7 @@ func generateRandomTestMetricsData(numTotal int, numEmpty int) []*metricSource.M metricDataValues = append(metricDataValues, rand.Float64()) } } - return []*metricSource.MetricData{ + return []metricSource.MetricData{ { Name: "RandomTestMetric", StartTime: startTime, @@ -651,7 +651,7 @@ func TestErrNoPointsToRender_Error(t *testing.T) { } fmt.Printf("MetricsData points: %#v", testMetricsPoints) for _, trigger := range testTriggers { - _, err = plotTemplate.GetRenderable(&trigger, testMetricsData) + _, err = plotTemplate.GetRenderable("t1", &trigger, testMetricsData) So(err.Error(), ShouldEqual, ErrNoPointsToRender{triggerID: trigger.ID}.Error()) } }) @@ -663,7 +663,7 @@ func TestErrNoPointsToRender_Error(t *testing.T) { } fmt.Printf("MetricsData points: %#v", testMetricsPoints) for _, trigger := range testTriggers { - _, err = plotTemplate.GetRenderable(&trigger, testMetricsData) + _, err = plotTemplate.GetRenderable("t1", &trigger, testMetricsData) So(err, ShouldBeNil) } }) diff --git a/senders/discord/send.go b/senders/discord/send.go index a02f9d6eb..91df26ab6 100644 --- a/senders/discord/send.go +++ b/senders/discord/send.go @@ -21,11 +21,11 @@ var ( ) // SendEvents implements pushover build and send message functionality -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { data := &discordgo.MessageSend{} data.Content = sender.buildMessage(events, trigger, throttled) - if len(plot) > 0 { - data.File = sender.buildPlot(plot) + if len(plots) > 0 { + data.File = sender.buildPlot(plots[0]) data.Embed = &discordgo.MessageEmbed{ Image: &discordgo.MessageEmbedImage{ URL: "attachment://Plot.jpg", @@ -114,7 +114,7 @@ func (sender *Sender) buildEventsString(events moira.NotificationEvents, charsFo eventsLenLimitReached := false eventsPrinted := 0 for _, event := range events { - line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricValue(), event.OldState, event.State) + line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricsValues(), event.OldState, event.State) if msg := event.CreateMessage(sender.location); len(msg) > 0 { line += fmt.Sprintf(". %s", msg) } diff --git a/senders/discord/send_test.go b/senders/discord/send_test.go index 4ad262231..e1b9965e8 100644 --- a/senders/discord/send_test.go +++ b/senders/discord/send_test.go @@ -12,12 +12,11 @@ import ( func TestBuildMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location, frontURI: "http://moira.url"} - value := float64(97.4458331200185) Convey("Build Moira Message tests", t, func() { event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 97.4458331200185}, Timestamp: 150000000, Metric: "Metric name", OldState: moira.StateOK, @@ -43,7 +42,7 @@ some other text _italic text_` actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false) expected := "NODATA Trigger Name [tag1][tag2] (1)\n" + desc + ` -02:40: Metric name = 97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) http://moira.url/trigger/TriggerID ` @@ -54,7 +53,7 @@ http://moira.url/trigger/TriggerID actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name"}, false) expected := `NODATA Name (1) -02:40: Metric name = 97.4458331200185 (OK to NODATA)` +02:40: Metric name = t1:97.4458331200185 (OK to NODATA)` So(actual, ShouldResemble, expected) }) @@ -62,7 +61,7 @@ http://moira.url/trigger/TriggerID actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{}, false) expected := `NODATA (1) -02:40: Metric name = 97.4458331200185 (OK to NODATA)` +02:40: Metric name = t1:97.4458331200185 (OK to NODATA)` So(actual, ShouldResemble, expected) }) @@ -74,7 +73,7 @@ http://moira.url/trigger/TriggerID actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false) expected := "NODATA Trigger Name [tag1][tag2] (1)\n" + desc + ` -02:40: Metric name = 97.4458331200185 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.` +02:40: Metric name = t1:97.4458331200185 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.` So(actual, ShouldResemble, expected) }) @@ -82,7 +81,7 @@ http://moira.url/trigger/TriggerID actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, true) expected := "NODATA Trigger Name [tag1][tag2] (1)\n" + desc + ` -02:40: Metric name = 97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) http://moira.url/trigger/TriggerID @@ -90,7 +89,7 @@ Please, fix your system or tune this trigger to generate less events.` So(actual, ShouldResemble, expected) }) - eventLine := "\n02:40: Metric name = 97.4458331200185 (OK to NODATA)" + eventLine := "\n02:40: Metric name = t1:97.4458331200185 (OK to NODATA)" oneEventLineLen := len([]rune(eventLine)) // Events list with chars less than half the message limit var shortEvents moira.NotificationEvents @@ -110,7 +109,7 @@ Please, fix your system or tune this trigger to generate less events.` Convey("Print moira message with desc + events < msgLimit", func() { actual := sender.buildMessage(shortEvents, moira.TriggerData{Desc: longDesc}, false) - expected := "NODATA (15)\n" + longDesc + "\n" + shortEventsString + expected := "NODATA (14)\n" + longDesc + "\n" + shortEventsString So(actual, ShouldResemble, expected) }) @@ -122,20 +121,85 @@ Please, fix your system or tune this trigger to generate less events.` eventsString += eventLine } actual := sender.buildMessage(events, moira.TriggerData{Desc: longDesc}, false) - expected := "NODATA (18)\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)" + expected := `NODATA (17) +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... + +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA)` + So(actual, ShouldResemble, expected) }) Convey("Print moira message events string > msgLimit/2", func() { desc := strings.Repeat("a", messageMaxCharacters/2-100) actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: desc}, false) - expected := "NODATA (22)\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n\n...and 2 more events." + expected := `NODATA (21) +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) + +...and 3 more events.` + So(actual, ShouldResemble, expected) }) Convey("Print moira message with both desc and events > msgLimit/2", func() { actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: longDesc}, false) - expected := "NODATA (22)\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n02:40: Metric name = 97.4458331200185 (OK to NODATA)\n\n...and 4 more events." + expected := `NODATA (21) +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa... + +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) + +...and 4 more events.` + So(actual, ShouldResemble, expected) }) diff --git a/senders/mail/send.go b/senders/mail/send.go index 8748b2a64..d9e225ccc 100644 --- a/senders/mail/send.go +++ b/senders/mail/send.go @@ -21,7 +21,7 @@ type templateRow struct { Timestamp string Oldstate moira.State State moira.State - Value string + Values string WarnValue string ErrorValue string Message string @@ -39,12 +39,12 @@ type triggerData struct { } // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { - message := sender.makeMessage(events, contact, trigger, plot, throttled) +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { + message := sender.makeMessage(events, contact, trigger, plots, throttled) return sender.dialAndSend(message) } -func (sender *Sender) makeMessage(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) *gomail.Message { +func (sender *Sender) makeMessage(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) *gomail.Message { state := events.GetSubjectState() tags := trigger.GetTags() @@ -66,7 +66,7 @@ func (sender *Sender) makeMessage(events moira.NotificationEvents, contact moira Timestamp: time.Unix(event.Timestamp, 0).In(sender.location).Format(sender.dateTimeFormat), Oldstate: event.OldState, State: event.State, - Value: strconv.FormatFloat(moira.UseFloat64(event.Value), 'f', -1, 64), + Values: event.GetMetricsValues(), WarnValue: strconv.FormatFloat(trigger.WarnValue, 'f', -1, 64), ErrorValue: strconv.FormatFloat(trigger.ErrorValue, 'f', -1, 64), Message: event.CreateMessage(sender.location), @@ -78,13 +78,15 @@ func (sender *Sender) makeMessage(events moira.NotificationEvents, contact moira m.SetHeader("To", contact.Value) m.SetHeader("Subject", subject) - if len(plot) > 0 { - plotCID := "plot.png" - templateData.PlotCID = plotCID - m.Embed(plotCID, gomail.SetCopyFunc(func(w io.Writer) error { - _, err := w.Write(plot) - return err - })) + if len(plots) > 0 { + for i, plot := range plots { + plotCID := fmt.Sprintf("plot-t%d.png", i) + templateData.PlotCID = plotCID + m.Embed(plotCID, gomail.SetCopyFunc(func(w io.Writer) error { + _, err := w.Write(plot) + return err + })) + } } m.AddAlternativeWriter("text/html", func(w io.Writer) error { diff --git a/senders/mail/send_test.go b/senders/mail/send_test.go index 34776b28f..0099ee819 100644 --- a/senders/mail/send_test.go +++ b/senders/mail/send_test.go @@ -48,7 +48,7 @@ some other text _italics text_`, } Convey("Make message", t, func() { - message := sender.makeMessage(generateTestEvents(10, trigger.ID), contact, trigger, []byte{1, 0, 1}, true) + message := sender.makeMessage(generateTestEvents(10, trigger.ID), contact, trigger, [][]byte{[]byte{1, 0, 1}}, true) So(message.GetHeader("From")[0], ShouldEqual, sender.From) So(message.GetHeader("To")[0], ShouldEqual, contact.Value) @@ -108,7 +108,7 @@ func TestEmptyTriggerID(t *testing.T) { } Convey("Make message", t, func() { - message := sender.makeMessage(generateTestEvents(10, trigger.ID), contact, trigger, []byte{1, 0, 1}, true) + message := sender.makeMessage(generateTestEvents(10, trigger.ID), contact, trigger, [][]byte{[]byte{1, 0, 1}}, true) So(message.GetHeader("From")[0], ShouldEqual, sender.From) So(message.GetHeader("To")[0], ShouldEqual, contact.Value) messageStr := new(bytes.Buffer) diff --git a/senders/mail/template.go b/senders/mail/template.go index 7accd977a..3a0baa10e 100644 --- a/senders/mail/template.go +++ b/senders/mail/template.go @@ -154,7 +154,7 @@ const defaultTemplate = `
- Value
+ Values @@ -179,7 +179,7 @@ const defaultTemplate = ` - {{ .Value }} + {{ .Values }} diff --git a/senders/msteams/msteams.go b/senders/msteams/msteams.go index 07d96d77d..f8002fbe1 100644 --- a/senders/msteams/msteams.go +++ b/senders/msteams/msteams.go @@ -61,14 +61,14 @@ func (sender *Sender) Init(senderSettings map[string]string, logger moira.Logger } // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { err := sender.isValidWebhookURL(contact.Value) if err != nil { return err } - request, err := sender.buildRequest(events, contact, trigger, plot, throttled) + request, err := sender.buildRequest(events, contact, trigger, plots, throttled) if err != nil { return fmt.Errorf("failed to build request: %w", err) @@ -140,7 +140,7 @@ func (sender *Sender) buildMessage(events moira.NotificationEvents, trigger moir } } -func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) (*http.Request, error) { +func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) (*http.Request, error) { messageCard := sender.buildMessage(events, trigger, throttled) requestURL := contact.Value @@ -184,7 +184,7 @@ func (sender *Sender) buildEventsFacts(events moira.NotificationEvents, maxEvent eventsPrinted := 0 for _, event := range events { - line := fmt.Sprintf("%s = %s (%s to %s)", event.Metric, event.GetMetricValue(), event.OldState, event.State) + line := fmt.Sprintf("%s = %s (%s to %s)", event.Metric, event.GetMetricsValues(), event.OldState, event.State) if len(moira.UseString(event.Message)) > 0 { line += fmt.Sprintf(". %s", moira.UseString(event.Message)) } diff --git a/senders/msteams/msteams_test.go b/senders/msteams/msteams_test.go index 542246756..830085922 100644 --- a/senders/msteams/msteams_test.go +++ b/senders/msteams/msteams_test.go @@ -37,10 +37,9 @@ func TestMSTeamsHttpResponse(t *testing.T) { _ = sender.Init(map[string]string{ "max_events": "-1", }, logger, location, "") - value := float64(123) event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -66,7 +65,7 @@ some other text _italic text_`, Reply(200). BodyString("1") contact := moira.ContactData{Value: "https://outlook.office.com/webhook/foo"} - err := sender.SendEvents([]moira.NotificationEvent{event}, contact, trigger, make([]byte, 0, 1), false) + err := sender.SendEvents([]moira.NotificationEvent{event}, contact, trigger, make([][]byte, 0, 1), false) So(err, ShouldResemble, nil) So(gock.IsDone(), ShouldBeTrue) }) @@ -77,7 +76,7 @@ some other text _italic text_`, Reply(200). BodyString("Some error") contact := moira.ContactData{Value: "https://outlook.office.com/webhook/foo"} - err := sender.SendEvents([]moira.NotificationEvent{event}, contact, trigger, make([]byte, 0, 1), false) + err := sender.SendEvents([]moira.NotificationEvent{event}, contact, trigger, make([][]byte, 0, 1), false) So(err.Error(), ShouldResemble, "teams endpoint responded with an error: Some error") So(gock.IsDone(), ShouldBeTrue) }) @@ -88,7 +87,7 @@ some other text _italic text_`, Reply(500). BodyString("Some error") contact := moira.ContactData{Value: "https://outlook.office.com/webhook/foo"} - err := sender.SendEvents([]moira.NotificationEvent{event}, contact, trigger, make([]byte, 0, 1), false) + err := sender.SendEvents([]moira.NotificationEvent{event}, contact, trigger, make([][]byte, 0, 1), false) So(err.Error(), ShouldResemble, "server responded with a non 2xx code: 500") So(gock.IsDone(), ShouldBeTrue) }) @@ -113,12 +112,11 @@ func TestValidWebhook(t *testing.T) { func TestBuildMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location, maxEvents: -1, frontURI: "http://moira.url"} - value := float64(123) Convey("Build Moira Message tests", t, func() { event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -140,7 +138,7 @@ some other text _italic text_`, Convey("State is Red for Error", func() { actual := sender.buildMessage([]moira.NotificationEvent{{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -160,7 +158,7 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (OK to ERROR)```", + Value: "```Metric = t1:123 (OK to ERROR)```", }, }, }, @@ -171,7 +169,7 @@ some other text _italic text_`, Convey("State is Orange for Warning", func() { actual := sender.buildMessage([]moira.NotificationEvent{{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -191,7 +189,7 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (OK to WARN)```", + Value: "```Metric = t1:123 (OK to WARN)```", }, }, }, @@ -202,7 +200,7 @@ some other text _italic text_`, Convey("State is Green for OK", func() { actual := sender.buildMessage([]moira.NotificationEvent{{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateWARN, @@ -222,7 +220,7 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (WARN to OK)```", + Value: "```Metric = t1:123 (WARN to OK)```", }, }, }, @@ -233,7 +231,7 @@ some other text _italic text_`, Convey("State is Black for NODATA", func() { actual := sender.buildMessage([]moira.NotificationEvent{{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateNODATA, @@ -253,7 +251,7 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (NODATA to NODATA)```", + Value: "```Metric = t1:123 (NODATA to NODATA)```", }, }, }, @@ -279,7 +277,7 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, }, }, @@ -314,7 +312,7 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, }, }, @@ -338,7 +336,7 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, { Name: "Warning", @@ -378,27 +376,27 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, }, }, @@ -434,7 +432,7 @@ some other text _italic text_`, Facts: []Fact{ { Name: "02:40", - Value: "```Metric = 123 (OK to NODATA)```", + Value: "```Metric = t1:123 (OK to NODATA)```", }, }, }, diff --git a/senders/opsgenie/send.go b/senders/opsgenie/send.go index 37f84e71d..d078c9862 100644 --- a/senders/opsgenie/send.go +++ b/senders/opsgenie/send.go @@ -18,8 +18,8 @@ const ( ) // SendEvents sends the events as an alert to opsgenie -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { - createAlertRequest := sender.makeCreateAlertRequest(events, contact, trigger, plot, throttled) +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { + createAlertRequest := sender.makeCreateAlertRequest(events, contact, trigger, plots, throttled) _, err := sender.client.Create(context.Background(), createAlertRequest) if err != nil { return fmt.Errorf("failed to send %s event message to opsgenie: %s", trigger.ID, err.Error()) @@ -27,7 +27,7 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. return nil } -func (sender *Sender) makeCreateAlertRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) *alert.CreateAlertRequest { +func (sender *Sender) makeCreateAlertRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) *alert.CreateAlertRequest { createAlertRequest := &alert.CreateAlertRequest{ Message: sender.buildTitle(events, trigger), Description: sender.buildMessage(events, throttled, trigger), @@ -40,8 +40,8 @@ func (sender *Sender) makeCreateAlertRequest(events moira.NotificationEvents, co Priority: sender.getMessagePriority(events), } - if len(plot) > 0 && sender.imageStoreConfigured { - imageLink, err := sender.imageStore.StoreImage(plot) + if len(plots) > 0 && sender.imageStoreConfigured { + imageLink, err := sender.imageStore.StoreImage(plots[0]) if err != nil { sender.logger.Warningf("could not store the plot image in the image store: %s", err) } else { @@ -94,7 +94,7 @@ func (sender *Sender) buildEventsString(events moira.NotificationEvents, charsFo eventsLenLimitReached := false eventsPrinted := 0 for _, event := range events { - line := fmt.Sprintf("%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricValue(), event.OldState, event.State) + line := fmt.Sprintf("%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricsValues(), event.OldState, event.State) if msg := event.CreateMessage(sender.location); len(msg) > 0 { line += fmt.Sprintf(". %s\n", msg) } else { diff --git a/senders/opsgenie/send_test.go b/senders/opsgenie/send_test.go index ce262c642..1f667ad7c 100644 --- a/senders/opsgenie/send_test.go +++ b/senders/opsgenie/send_test.go @@ -53,11 +53,10 @@ func TestGetPushoverPriority(t *testing.T) { func TestBuildMoiraMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location} - value := float64(123) Convey("Build Moira Message tests", t, func() { event := moira.NotificationEvent{ - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -71,13 +70,13 @@ func TestBuildMoiraMessage(t *testing.T) { Convey("Print moira message with one event", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, false, moira.TriggerData{}) - expected := "02:40: Metric = 123 (OK to NODATA)\n" + expected := "02:40: Metric = t1:123 (OK to NODATA)\n" So(actual, ShouldResemble, expected) }) Convey("Print moira message with one event and desc", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, false, trigger) - expected := "

header

\n\n

bold text italics\ncode

\n" + "02:40: Metric = 123 (OK to NODATA)\n" + expected := "

header

\n\n

bold text italics\ncode

\n" + "02:40: Metric = t1:123 (OK to NODATA)\n" So(actual, ShouldResemble, expected) }) @@ -85,13 +84,13 @@ func TestBuildMoiraMessage(t *testing.T) { var interval int64 = 24 event.MessageEventInfo = &moira.EventInfo{Interval: &interval} actual := sender.buildMessage([]moira.NotificationEvent{event}, false, moira.TriggerData{}) - expected := "02:40: Metric = 123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.\n" + expected := "02:40: Metric = t1:123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.\n" So(actual, ShouldResemble, expected) }) Convey("Print moira message with one event and throttled", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, true, moira.TriggerData{}) - expected := `02:40: Metric = 123 (OK to NODATA) + expected := `02:40: Metric = t1:123 (OK to NODATA) Please, fix your system or tune this trigger to generate less events.` So(actual, ShouldResemble, expected) @@ -126,7 +125,6 @@ func TestMakeCreateAlertRequest(t *testing.T) { defer mockCtrl.Finish() imageStore := mock_moira_alert.NewMockImageStore(mockCtrl) - value := float64(123) sender := Sender{ frontURI: "https://my-moira.com", location: location, @@ -137,7 +135,7 @@ func TestMakeCreateAlertRequest(t *testing.T) { imageStore.EXPECT().StoreImage([]byte(`test`)).Return("testlink", nil) Convey("Build CreateAlertRequest", t, func() { event := []moira.NotificationEvent{{ - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -152,7 +150,7 @@ func TestMakeCreateAlertRequest(t *testing.T) { contact := moira.ContactData{ Value: "123", } - actual := sender.makeCreateAlertRequest(event, contact, trigger, []byte(`test`), false) + actual := sender.makeCreateAlertRequest(event, contact, trigger, [][]byte{[]byte(`test`)}, false) expected := &alert.CreateAlertRequest{ Message: sender.buildTitle(event, trigger), Description: sender.buildMessage(event, false, trigger), diff --git a/senders/pagerduty/send.go b/senders/pagerduty/send.go index f2ad0025c..1eecc5592 100644 --- a/senders/pagerduty/send.go +++ b/senders/pagerduty/send.go @@ -15,8 +15,8 @@ import ( const summaryMaxChars = 1024 // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { - event := sender.buildEvent(events, contact, trigger, plot, throttled) +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { + event := sender.buildEvent(events, contact, trigger, plots, throttled) _, err := pagerduty.ManageEvent(event) if err != nil { return fmt.Errorf("failed to post the event to the pagerduty contact %s : %s. ", contact.Value, err) @@ -24,7 +24,7 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. return nil } -func (sender *Sender) buildEvent(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) pagerduty.V2Event { +func (sender *Sender) buildEvent(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) pagerduty.V2Event { summary := sender.buildSummary(events, trigger, throttled) details := make(map[string]interface{}) @@ -41,7 +41,7 @@ func (sender *Sender) buildEvent(events moira.NotificationEvents, contact moira. var eventList string for _, event := range events { - line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricValue(), event.OldState, event.State) + line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricsValues(), event.OldState, event.State) if msg := event.CreateMessage(sender.location); len(msg) > 0 { line += fmt.Sprintf(". %s", msg) } @@ -67,8 +67,8 @@ func (sender *Sender) buildEvent(events moira.NotificationEvents, contact moira. Payload: payload, } - if len(plot) > 0 && sender.imageStoreConfigured { - imageLink, err := sender.imageStore.StoreImage(plot) + if len(plots) > 0 && sender.imageStoreConfigured { + imageLink, err := sender.imageStore.StoreImage(plots[0]) if err != nil { sender.logger.Warningf("could not store the plot image in the image store: %s", err) } else { diff --git a/senders/pagerduty/send_test.go b/senders/pagerduty/send_test.go index 126fc3e16..163fcb600 100644 --- a/senders/pagerduty/send_test.go +++ b/senders/pagerduty/send_test.go @@ -15,7 +15,6 @@ import ( func TestBuildEvent(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location, frontURI: "http://moira.url"} - value := float64(97.4458331200185) mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() imageStore := mock_moira_alert.NewMockImageStore(mockCtrl) @@ -23,7 +22,7 @@ func TestBuildEvent(t *testing.T) { Convey("Build pagerduty event tests", t, func() { event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 97.4458331200185}, Timestamp: 150000000, Metric: "Metric name", OldState: moira.StateOK, @@ -57,10 +56,10 @@ func TestBuildEvent(t *testing.T) { } Convey("Build pagerduty event with one moira event", func() { - actual := sender.buildEvent(moira.NotificationEvents{event}, contact, trigger, []byte{}, false) + actual := sender.buildEvent(moira.NotificationEvents{event}, contact, trigger, [][]byte{}, false) expected := baseExpected details := map[string]interface{}{ - "Events": "\n02:40: Metric name = 97.4458331200185 (OK to NODATA)", + "Events": "\n02:40: Metric name = t1:97.4458331200185 (OK to NODATA)", "Trigger URI": "http://moira.url/trigger/TriggerID", "Trigger Name": "Trigger Name", "Description": "bold text italics code regular", @@ -73,10 +72,10 @@ func TestBuildEvent(t *testing.T) { imageStore.EXPECT().StoreImage([]byte("test")).Return("test", nil) sender.imageStore = imageStore sender.imageStoreConfigured = true - actual := sender.buildEvent(moira.NotificationEvents{event}, contact, trigger, []byte("test"), false) + actual := sender.buildEvent(moira.NotificationEvents{event}, contact, trigger, [][]byte{[]byte("test")}, false) expected := baseExpected details := map[string]interface{}{ - "Events": "\n02:40: Metric name = 97.4458331200185 (OK to NODATA)", + "Events": "\n02:40: Metric name = t1:97.4458331200185 (OK to NODATA)", "Trigger URI": "http://moira.url/trigger/TriggerID", "Trigger Name": "Trigger Name", "Description": "bold text italics code regular", @@ -92,10 +91,10 @@ func TestBuildEvent(t *testing.T) { }) Convey("Build pagerduty event with one event and throttled", func() { - actual := sender.buildEvent(moira.NotificationEvents{event}, contact, trigger, []byte{}, true) + actual := sender.buildEvent(moira.NotificationEvents{event}, contact, trigger, [][]byte{}, true) expected := baseExpected details := map[string]interface{}{ - "Events": "\n02:40: Metric name = 97.4458331200185 (OK to NODATA)", + "Events": "\n02:40: Metric name = t1:97.4458331200185 (OK to NODATA)", "Message": "Please, fix your system or tune this trigger to generate less events.", "Trigger URI": "http://moira.url/trigger/TriggerID", "Description": "bold text italics code regular", @@ -110,20 +109,20 @@ func TestBuildEvent(t *testing.T) { for i := 0; i < 10; i++ { events = append(events, event) } - actual := sender.buildEvent(events, contact, trigger, []byte{}, true) + actual := sender.buildEvent(events, contact, trigger, [][]byte{}, true) expected := baseExpected details := map[string]interface{}{ "Events": ` -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA)`, +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA)`, "Message": "Please, fix your system or tune this trigger to generate less events.", "Trigger URI": "http://moira.url/trigger/TriggerID", "Description": "bold text italics code regular", diff --git a/senders/pushover/pushover.go b/senders/pushover/pushover.go index 0add4db4a..10a2b4e37 100644 --- a/senders/pushover/pushover.go +++ b/senders/pushover/pushover.go @@ -38,8 +38,8 @@ func (sender *Sender) Init(senderSettings map[string]string, logger moira.Logger } // SendEvents implements pushover build and send message functionality -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { - pushoverMessage := sender.makePushoverMessage(events, contact, trigger, plot, throttled) +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { + pushoverMessage := sender.makePushoverMessage(events, contact, trigger, plots, throttled) sender.logger.Debugf("Calling pushover with message title %s, body %s", pushoverMessage.Title, pushoverMessage.Message) recipient := pushover.NewRecipient(contact.Value) @@ -50,7 +50,7 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. return nil } -func (sender *Sender) makePushoverMessage(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) *pushover.Message { +func (sender *Sender) makePushoverMessage(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) *pushover.Message { pushoverMessage := &pushover.Message{ Message: sender.buildMessage(events, throttled), Title: sender.buildTitle(events, trigger), @@ -63,8 +63,8 @@ func (sender *Sender) makePushoverMessage(events moira.NotificationEvents, conta if len(url) < urlLimit { pushoverMessage.URL = url } - if len(plot) > 0 { - reader := bytes.NewReader(plot) + if len(plots) > 0 { + reader := bytes.NewReader(plots[0]) pushoverMessage.AddAttachment(reader) } @@ -77,7 +77,7 @@ func (sender *Sender) buildMessage(events moira.NotificationEvents, throttled bo if i > printEventsCount-1 { break } - message.WriteString(fmt.Sprintf("%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricValue(), event.OldState, event.State)) + message.WriteString(fmt.Sprintf("%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricsValues(), event.OldState, event.State)) if msg := event.CreateMessage(sender.location); len(msg) > 0 { message.WriteString(fmt.Sprintf(". %s\n", msg)) } else { diff --git a/senders/pushover/pushover_test.go b/senders/pushover/pushover_test.go index aaef6bb50..b49a19873 100644 --- a/senders/pushover/pushover_test.go +++ b/senders/pushover/pushover_test.go @@ -78,11 +78,10 @@ func TestGetPushoverPriority(t *testing.T) { func TestBuildMoiraMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location} - value := float64(123) Convey("Build Moira Message tests", t, func() { event := moira.NotificationEvent{ - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -91,7 +90,7 @@ func TestBuildMoiraMessage(t *testing.T) { Convey("Print moira message with one event", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, false) - expected := "02:40: Metric = 123 (OK to NODATA)\n" + expected := "02:40: Metric = t1:123 (OK to NODATA)\n" So(actual, ShouldResemble, expected) }) @@ -99,13 +98,13 @@ func TestBuildMoiraMessage(t *testing.T) { var interval int64 = 24 event.MessageEventInfo = &moira.EventInfo{Interval: &interval} actual := sender.buildMessage([]moira.NotificationEvent{event}, false) - expected := "02:40: Metric = 123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.\n" + expected := "02:40: Metric = t1:123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.\n" So(actual, ShouldResemble, expected) }) Convey("Print moira message with one event and throttled", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, true) - expected := `02:40: Metric = 123 (OK to NODATA) + expected := `02:40: Metric = t1:123 (OK to NODATA) Please, fix your system or tune this trigger to generate less events.` So(actual, ShouldResemble, expected) @@ -113,11 +112,11 @@ Please, fix your system or tune this trigger to generate less events.` Convey("Print moira message with 6 events", func() { actual := sender.buildMessage([]moira.NotificationEvent{event, event, event, event, event, event}, false) - expected := `02:40: Metric = 123 (OK to NODATA) -02:40: Metric = 123 (OK to NODATA) -02:40: Metric = 123 (OK to NODATA) -02:40: Metric = 123 (OK to NODATA) -02:40: Metric = 123 (OK to NODATA) + expected := `02:40: Metric = t1:123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) ...and 1 more events.` So(actual, ShouldResemble, expected) @@ -149,7 +148,6 @@ func TestMakePushoverMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") logger, _ := logging.ConfigureLog("stdout", "debug", "test") - value := float64(123) sender := Sender{ frontURI: "https://my-moira.com", location: location, @@ -157,7 +155,7 @@ func TestMakePushoverMessage(t *testing.T) { } Convey("Just build PushoverMessage", t, func() { event := []moira.NotificationEvent{{ - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -179,9 +177,9 @@ func TestMakePushoverMessage(t *testing.T) { URL: "https://my-moira.com/trigger/SomeID", Priority: pushover.PriorityEmergency, Title: "ERROR TriggerName [tag1][tag2] (1)", - Message: "02:40: Metric = 123 (OK to ERROR)\n", + Message: "02:40: Metric = t1:123 (OK to ERROR)\n", } expected.AddAttachment(bytes.NewReader([]byte{1, 0, 1})) - So(sender.makePushoverMessage(event, contact, trigger, []byte{1, 0, 1}, false), ShouldResemble, expected) + So(sender.makePushoverMessage(event, contact, trigger, [][]byte{[]byte{1, 0, 1}}, false), ShouldResemble, expected) }) } diff --git a/senders/script/script.go b/senders/script/script.go index 4de9d1891..9065fb656 100644 --- a/senders/script/script.go +++ b/senders/script/script.go @@ -41,7 +41,7 @@ func (sender *Sender) Init(senderSettings map[string]string, logger moira.Logger } // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { scriptFile, args, scriptBody, err := sender.buildCommandData(events, contact, trigger, throttled) if err != nil { return err diff --git a/senders/selfstate/selfstate.go b/senders/selfstate/selfstate.go index 51ed3c23f..153efc9e8 100644 --- a/senders/selfstate/selfstate.go +++ b/senders/selfstate/selfstate.go @@ -20,7 +20,7 @@ func (sender *Sender) Init(senderSettings map[string]string, logger moira.Logger } // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { selfState, err := sender.Database.GetNotifierState() if err != nil { return fmt.Errorf("failed to get notifier state: %s", err.Error()) diff --git a/senders/selfstate/selfstate_test.go b/senders/selfstate/selfstate_test.go index 382621bfe..32e71f5bf 100644 --- a/senders/selfstate/selfstate_test.go +++ b/senders/selfstate/selfstate_test.go @@ -26,7 +26,7 @@ var ( Type: "selfstate", } testThrottled = false - testPlot = make([]byte, 0) + testPlots = make([][]byte, 0) ) var ( @@ -52,7 +52,7 @@ func TestSender_SendEvents(t *testing.T) { for _, subjectState := range ignorableSubjectStates { testEvents := []moira.NotificationEvent{{State: subjectState}} dataBase.EXPECT().GetNotifierState().Return(selfStateInitial, nil) - err := sender.SendEvents(testEvents, testContact, testTrigger, testPlot, testThrottled) + err := sender.SendEvents(testEvents, testContact, testTrigger, testPlots, testThrottled) So(err, ShouldBeNil) } }) @@ -63,7 +63,7 @@ func TestSender_SendEvents(t *testing.T) { dataBase.EXPECT().GetNotifierState().Return(selfStateInitial, nil) dataBase.EXPECT().SetNotifierState(selfStateFinal).Return(nil) testEvents := []moira.NotificationEvent{{State: subjectState}} - err := sender.SendEvents(testEvents, testContact, testTrigger, testPlot, testThrottled) + err := sender.SendEvents(testEvents, testContact, testTrigger, testPlots, testThrottled) So(err, ShouldBeNil) } }) @@ -75,7 +75,7 @@ func TestSender_SendEvents(t *testing.T) { for _, subjectState := range disablingSubjectStates { testEvents := []moira.NotificationEvent{{State: subjectState}} dataBase.EXPECT().GetNotifierState().Return(selfStateInitial, nil) - err := sender.SendEvents(testEvents, testContact, testTrigger, testPlot, testThrottled) + err := sender.SendEvents(testEvents, testContact, testTrigger, testPlots, testThrottled) So(err, ShouldBeNil) } }) @@ -87,7 +87,7 @@ func TestSender_SendEvents(t *testing.T) { for _, subjectState := range disablingSubjectStates { testEvents := []moira.NotificationEvent{{State: subjectState}} dataBase.EXPECT().GetNotifierState().Return("", fmt.Errorf("redis is down")) - err := sender.SendEvents(testEvents, testContact, testTrigger, testPlot, testThrottled) + err := sender.SendEvents(testEvents, testContact, testTrigger, testPlots, testThrottled) So(err, ShouldNotBeNil) So(err.Error(), ShouldEqual, "failed to get notifier state: redis is down") } diff --git a/senders/slack/slack.go b/senders/slack/slack.go index 47d4774c5..6f483cc53 100644 --- a/senders/slack/slack.go +++ b/senders/slack/slack.go @@ -59,7 +59,7 @@ func (sender *Sender) Init(senderSettings map[string]string, logger moira.Logger } // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { message := sender.buildMessage(events, trigger, throttled) useDirectMessaging := useDirectMessaging(contact.Value) emoji := sender.getStateEmoji(events.GetSubjectState()) @@ -67,8 +67,8 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. if err != nil { return err } - if channelID != "" && len(plot) > 0 { - sender.sendPlot(plot, channelID, threadTimestamp, trigger.ID) + if channelID != "" && len(plots) > 0 { + sender.sendPlots(plots, channelID, threadTimestamp, trigger.ID) } return nil } @@ -146,7 +146,7 @@ func (sender *Sender) buildEventsString(events moira.NotificationEvents, charsFo eventsLenLimitReached := false eventsPrinted := 0 for _, event := range events { - line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricValue(), event.OldState, event.State) + line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricsValues(), event.OldState, event.State) if msg := event.CreateMessage(sender.location); len(msg) > 0 { line += fmt.Sprintf(". %s", msg) } @@ -189,17 +189,22 @@ func (sender *Sender) sendMessage(message string, contact string, triggerID stri return channelID, threadTimestamp, nil } -func (sender *Sender) sendPlot(plot []byte, channelID, threadTimestamp, triggerID string) error { - reader := bytes.NewReader(plot) - uploadParameters := slack.FileUploadParameters{ - Channels: []string{channelID}, - ThreadTimestamp: threadTimestamp, - Reader: reader, - Filetype: "png", - Filename: fmt.Sprintf("%s.png", triggerID), - } - _, err := sender.client.UploadFile(uploadParameters) - return err +func (sender *Sender) sendPlots(plots [][]byte, channelID, threadTimestamp, triggerID string) error { + for _, plot := range plots { + reader := bytes.NewReader(plot) + uploadParameters := slack.FileUploadParameters{ + Channels: []string{channelID}, + ThreadTimestamp: threadTimestamp, + Reader: reader, + Filetype: "png", + Filename: fmt.Sprintf("%s.png", triggerID), + } + _, err := sender.client.UploadFile(uploadParameters) + if err != nil { + return err + } + } + return nil } // getStateEmoji returns corresponding state emoji diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index ae15222b0..dcbcbd120 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -86,12 +86,11 @@ func TestGetStateEmoji(t *testing.T) { func TestBuildMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location, frontURI: "http://moira.url"} - value := float64(123) Convey("Build Moira Message tests", t, func() { event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -118,13 +117,13 @@ some other text italic text Convey("Print moira message with one event", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false) expected := "*NODATA* [tag1][tag2]\n" + slackCompatibleMD + - "\n\n```\n02:40: Metric = 123 (OK to NODATA)```" + "\n\n```\n02:40: Metric = t1:123 (OK to NODATA)```" So(actual, ShouldResemble, expected) }) Convey("Print moira message with empty trigger", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{}, false) - expected := "*NODATA*\n```\n02:40: Metric = 123 (OK to NODATA)```" + expected := "*NODATA*\n```\n02:40: Metric = t1:123 (OK to NODATA)```" So(actual, ShouldResemble, expected) }) @@ -133,31 +132,31 @@ some other text italic text event.MessageEventInfo = &moira.EventInfo{Interval: &interval} actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false) expected := "*NODATA* [tag1][tag2]\n" + slackCompatibleMD + - "\n\n```\n02:40: Metric = 123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.```" + "\n\n```\n02:40: Metric = t1:123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.```" So(actual, ShouldResemble, expected) }) Convey("Print moira message with one event and throttled", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, true) expected := "*NODATA* [tag1][tag2]\n" + slackCompatibleMD + - "\n\n```\n02:40: Metric = 123 (OK to NODATA)```\nPlease, *fix your system or tune this trigger* to generate less events." + "\n\n```\n02:40: Metric = t1:123 (OK to NODATA)```\nPlease, *fix your system or tune this trigger* to generate less events." So(actual, ShouldResemble, expected) }) Convey("Print moira message with 6 events", func() { actual := sender.buildMessage([]moira.NotificationEvent{event, event, event, event, event, event}, trigger, false) expected := "*NODATA* [tag1][tag2]\n" + slackCompatibleMD + - "\n\n```\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)```" + "\n\n```\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)```" So(actual, ShouldResemble, expected) }) Convey("Print moira message with empty triggerID, but with trigger name", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name"}, false) - expected := "*NODATA* Name\n```\n02:40: Metric = 123 (OK to NODATA)```" + expected := "*NODATA* Name\n```\n02:40: Metric = t1:123 (OK to NODATA)```" So(actual, ShouldResemble, expected) }) - eventLine := "\n02:40: Metric = 123 (OK to NODATA)" + eventLine := "\n02:40: Metric = t1:123 (OK to NODATA)" oneEventLineLen := len([]rune(eventLine)) // Events list with chars less than half the message limit var shortEvents moira.NotificationEvents @@ -189,20 +188,20 @@ some other text italic text eventsString += eventLine } actual := sender.buildMessage(events, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)```" + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)```" So(actual, ShouldResemble, expected) }) Convey("Print moira message events string > msgLimit/2", func() { desc := strings.Repeat("a", messageMaxCharacters/2-100) actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: desc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n```\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)```\n...and 4 more events." + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n```\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)```\n...and 3 more events." So(actual, ShouldResemble, expected) }) Convey("Print moira message with both desc and events > msgLimit/2", func() { actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)```\n...and 6 more events." + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)```\n...and 6 more events." So(actual, ShouldResemble, expected) }) diff --git a/senders/telegram/send.go b/senders/telegram/send.go index 8be2a9c78..7789352a8 100644 --- a/senders/telegram/send.go +++ b/senders/telegram/send.go @@ -12,33 +12,33 @@ import ( type messageType string const ( - // Photo type used if notification has plot - Photo messageType = "photo" + // Album type used if notification has plots + Album messageType = "album" // Message type used if notification has not plot Message messageType = "message" ) const ( - photoCaptionMaxCharacters = 1024 + albumCaptionMaxCharacters = 1024 messageMaxCharacters = 4096 additionalInfoCharactersCount = 400 ) var characterLimits = map[messageType]int{ Message: messageMaxCharacters, - Photo: photoCaptionMaxCharacters, + Album: albumCaptionMaxCharacters, } // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { - msgType := getMessageType(plot) +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { + msgType := getMessageType(plots) message := sender.buildMessage(events, trigger, throttled, characterLimits[msgType]) sender.logger.Debugf("Calling telegram api with chat_id %s and message body %s", contact.Value, message) chat, err := sender.getChat(contact.Value) if err != nil { return err } - if err := sender.talk(chat, message, plot, msgType); err != nil { + if err := sender.talk(chat, message, plots, msgType); err != nil { return fmt.Errorf("failed to send message to telegram contact %s: %s. ", contact.Value, err) } return nil @@ -58,7 +58,7 @@ func (sender *Sender) buildMessage(events moira.NotificationEvents, trigger moir messageLimitReached := false for _, event := range events { - line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricValue(), event.OldState, event.State) + line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricsValues(), event.OldState, event.State) if msg := event.CreateMessage(sender.location); len(msg) > 0 { line += fmt.Sprintf(". %s", msg) } @@ -99,9 +99,9 @@ func (sender *Sender) getChat(username string) (*telebot.Chat, error) { } // talk processes one talk -func (sender *Sender) talk(chat *telebot.Chat, message string, plot []byte, messageType messageType) error { - if messageType == Photo { - return sender.sendAsPhoto(chat, plot, message) +func (sender *Sender) talk(chat *telebot.Chat, message string, plots [][]byte, messageType messageType) error { + if messageType == Album { + return sender.sendAsAlbum(chat, plots, message) } return sender.sendAsMessage(chat, message) } @@ -114,18 +114,27 @@ func (sender *Sender) sendAsMessage(chat *telebot.Chat, message string) error { return nil } -func (sender *Sender) sendAsPhoto(chat *telebot.Chat, plot []byte, caption string) error { - photo := telebot.Photo{File: telebot.FromReader(bytes.NewReader(plot)), Caption: caption} - _, err := photo.Send(sender.bot, chat, &telebot.SendOptions{}) +func (sender *Sender) sendAsAlbum(chat *telebot.Chat, plots [][]byte, caption string) error { + var album telebot.Album + firstPhoto := true + for _, plot := range plots { + photo := &telebot.Photo{File: telebot.FromReader(bytes.NewReader(plot)), Caption: caption} + album = append(album, photo) + if firstPhoto { + caption = "" // Caption should be defined only for first photo + } + } + + _, err := sender.bot.SendAlbum(chat, album) if err != nil { - return fmt.Errorf("can't send event plot to %v: %s", chat.ID, err.Error()) + return fmt.Errorf("can't send event plots to %v: %s", chat.ID, err.Error()) } return nil } -func getMessageType(plot []byte) messageType { - if len(plot) > 0 { - return Photo +func getMessageType(plots [][]byte) messageType { + if len(plots) > 0 { + return Album } return Message } diff --git a/senders/telegram/send_test.go b/senders/telegram/send_test.go index 66d37dfe6..1880408ec 100644 --- a/senders/telegram/send_test.go +++ b/senders/telegram/send_test.go @@ -12,12 +12,11 @@ import ( func TestBuildMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location, frontURI: "http://moira.url"} - value := float64(97.4458331200185) Convey("Build Moira Message tests", t, func() { event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 97.4458331200185}, Timestamp: 150000000, Metric: "Metric name", OldState: moira.StateOK, @@ -34,7 +33,7 @@ func TestBuildMessage(t *testing.T) { actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false, messageMaxCharacters) expected := `đź’ŁNODATA Trigger Name [tag1][tag2] (1) -02:40: Metric name = 97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) http://moira.url/trigger/TriggerID ` @@ -45,7 +44,7 @@ http://moira.url/trigger/TriggerID actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name"}, false, messageMaxCharacters) expected := `đź’ŁNODATA Name (1) -02:40: Metric name = 97.4458331200185 (OK to NODATA)` +02:40: Metric name = t1:97.4458331200185 (OK to NODATA)` So(actual, ShouldResemble, expected) }) @@ -53,7 +52,7 @@ http://moira.url/trigger/TriggerID actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{}, false, messageMaxCharacters) expected := `đź’ŁNODATA (1) -02:40: Metric name = 97.4458331200185 (OK to NODATA)` +02:40: Metric name = t1:97.4458331200185 (OK to NODATA)` So(actual, ShouldResemble, expected) }) @@ -65,7 +64,7 @@ http://moira.url/trigger/TriggerID actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false, messageMaxCharacters) expected := `đź’ŁNODATA Trigger Name [tag1][tag2] (1) -02:40: Metric name = 97.4458331200185 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.` +02:40: Metric name = t1:97.4458331200185 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix.` So(actual, ShouldResemble, expected) }) @@ -73,7 +72,7 @@ http://moira.url/trigger/TriggerID actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, true, messageMaxCharacters) expected := `đź’ŁNODATA Trigger Name [tag1][tag2] (1) -02:40: Metric name = 97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) http://moira.url/trigger/TriggerID @@ -86,22 +85,21 @@ Please, fix your system or tune this trigger to generate less events.` for i := 0; i < 18; i++ { events = append(events, event) } - actual := sender.buildMessage(events, trigger, false, photoCaptionMaxCharacters) + actual := sender.buildMessage(events, trigger, false, albumCaptionMaxCharacters) expected := `đź’ŁNODATA Trigger Name [tag1][tag2] (18) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) -02:40: Metric name = 97.4458331200185 (OK to NODATA) - -...and 7 more events. +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) +02:40: Metric name = t1:97.4458331200185 (OK to NODATA) + +...and 8 more events. http://moira.url/trigger/TriggerID ` diff --git a/senders/twilio/sms.go b/senders/twilio/sms.go index 1468a5462..b25ab0709 100644 --- a/senders/twilio/sms.go +++ b/senders/twilio/sms.go @@ -14,7 +14,7 @@ type twilioSenderSms struct { twilioSender } -func (sender *twilioSenderSms) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { +func (sender *twilioSenderSms) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { message := sender.buildMessage(events, trigger, throttled) sender.logger.Debugf("Calling twilio sms api to phone %s and message body %s", contact.Value, message) twilioMessage, err := twilio.NewMessage(sender.client, sender.APIFromPhone, contact.Value, twilio.Body(message)) @@ -35,7 +35,7 @@ func (sender *twilioSenderSms) buildMessage(events moira.NotificationEvents, tri if i > printEventsCount-1 { break } - message.WriteString(fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricValue(), event.OldState, event.State)) + message.WriteString(fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricsValues(), event.OldState, event.State)) if msg := event.CreateMessage(sender.location); len(msg) > 0 { message.WriteString(fmt.Sprintf(". %s", msg)) } diff --git a/senders/twilio/sms_test.go b/senders/twilio/sms_test.go index 3362f3c3e..156f3e4bb 100644 --- a/senders/twilio/sms_test.go +++ b/senders/twilio/sms_test.go @@ -16,11 +16,10 @@ func TestBuildMoiraMessage(t *testing.T) { twilioSender: twilioSender{ location: location, }} - value := float64(123) Convey("Build Moira Message tests", t, func() { event := moira.NotificationEvent{ - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -29,7 +28,7 @@ func TestBuildMoiraMessage(t *testing.T) { Convey("Print moira message with one event", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name", Tags: []string{"tag1"}}, false) - expected := "NODATA Name [tag1] (1)\n\n02:40: Metric = 123 (OK to NODATA)" + expected := "NODATA Name [tag1] (1)\n\n02:40: Metric = t1:123 (OK to NODATA)" So(actual, ShouldResemble, expected) }) @@ -37,7 +36,7 @@ func TestBuildMoiraMessage(t *testing.T) { var interval int64 = 24 event.MessageEventInfo = &moira.EventInfo{Interval: &interval} actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name", Tags: []string{"tag1"}}, false) - expected := "NODATA Name [tag1] (1)\n\n02:40: Metric = 123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix." + expected := "NODATA Name [tag1] (1)\n\n02:40: Metric = t1:123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix." So(actual, ShouldResemble, expected) }) @@ -45,7 +44,7 @@ func TestBuildMoiraMessage(t *testing.T) { actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name", Tags: []string{"tag1"}}, true) expected := `NODATA Name [tag1] (1) -02:40: Metric = 123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) Please, fix your system or tune this trigger to generate less events.` So(actual, ShouldResemble, expected) @@ -55,11 +54,11 @@ Please, fix your system or tune this trigger to generate less events.` actual := sender.buildMessage([]moira.NotificationEvent{event, event, event, event, event, event}, moira.TriggerData{Name: "Name", Tags: []string{"tag1"}}, false) expected := `NODATA Name [tag1] (6) -02:40: Metric = 123 (OK to NODATA) -02:40: Metric = 123 (OK to NODATA) -02:40: Metric = 123 (OK to NODATA) -02:40: Metric = 123 (OK to NODATA) -02:40: Metric = 123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) +02:40: Metric = t1:123 (OK to NODATA) ...and 1 more events.` So(actual, ShouldResemble, expected) @@ -78,10 +77,9 @@ func TestTwilioSenderSms_SendEvents(t *testing.T) { location: location, }, } - value := float64(123) event := moira.NotificationEvent{ - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -89,7 +87,7 @@ func TestTwilioSenderSms_SendEvents(t *testing.T) { } Convey("just send", t, func() { - err := sender.SendEvents([]moira.NotificationEvent{event}, moira.ContactData{}, moira.TriggerData{Name: "Name", Tags: []string{"tag1"}}, []byte{}, true) + err := sender.SendEvents([]moira.NotificationEvent{event}, moira.ContactData{}, moira.TriggerData{Name: "Name", Tags: []string{"tag1"}}, [][]byte{}, true) So(err, ShouldNotBeNil) }) } diff --git a/senders/twilio/twilio.go b/senders/twilio/twilio.go index 1d12b69c0..32528beb6 100644 --- a/senders/twilio/twilio.go +++ b/senders/twilio/twilio.go @@ -14,7 +14,7 @@ type Sender struct { } type sendEventsTwilio interface { - SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error + SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error } type twilioSender struct { @@ -80,6 +80,6 @@ func (sender *Sender) Init(senderSettings map[string]string, logger moira.Logger } // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { - return sender.sender.SendEvents(events, contact, trigger, plot, throttled) +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { + return sender.sender.SendEvents(events, contact, trigger, plots, throttled) } diff --git a/senders/twilio/voice.go b/senders/twilio/voice.go index d792e669d..116d9d569 100644 --- a/senders/twilio/voice.go +++ b/senders/twilio/voice.go @@ -17,7 +17,7 @@ type twilioSenderVoice struct { twimletsEcho bool } -func (sender *twilioSenderVoice) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { +func (sender *twilioSenderVoice) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { voiceURL := sender.buildVoiceURL(trigger) twilioCall, err := twilio.NewCall(sender.client, sender.APIFromPhone, contact.Value, twilio.Callback(voiceURL)) if err != nil { diff --git a/senders/twilio/voice_test.go b/senders/twilio/voice_test.go index a178f6c1b..2483ef381 100644 --- a/senders/twilio/voice_test.go +++ b/senders/twilio/voice_test.go @@ -26,7 +26,7 @@ func TestTwilioSenderVoice_SendEvents(t *testing.T) { } Convey("just send", t, func() { - err := sender.SendEvents(moira.NotificationEvents{}, moira.ContactData{}, moira.TriggerData{}, []byte{}, true) + err := sender.SendEvents(moira.NotificationEvents{}, moira.ContactData{}, moira.TriggerData{}, [][]byte{}, true) So(err, ShouldNotBeNil) }) } diff --git a/senders/victorops/send.go b/senders/victorops/send.go index 088681d74..0423b607d 100644 --- a/senders/victorops/send.go +++ b/senders/victorops/send.go @@ -12,8 +12,8 @@ import ( ) // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { - createAlertRequest := sender.buildCreateAlertRequest(events, trigger, throttled, plot, time.Now().Unix()) +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { + createAlertRequest := sender.buildCreateAlertRequest(events, trigger, throttled, plots, time.Now().Unix()) err := sender.client.CreateAlert(contact.Value, createAlertRequest) if err != nil { return fmt.Errorf("error while sending alert to victorops: %s", err) @@ -21,7 +21,7 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. return nil } -func (sender *Sender) buildCreateAlertRequest(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool, plot []byte, time int64) api.CreateAlertRequest { +func (sender *Sender) buildCreateAlertRequest(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool, plots [][]byte, time int64) api.CreateAlertRequest { triggerURI := trigger.GetTriggerURI(sender.frontURI) @@ -36,8 +36,8 @@ func (sender *Sender) buildCreateAlertRequest(events moira.NotificationEvents, t EntityID: trigger.ID, } - if len(plot) > 0 && sender.imageStoreConfigured { - imageLink, err := sender.imageStore.StoreImage(plot) + if len(plots) > 0 && sender.imageStoreConfigured { + imageLink, err := sender.imageStore.StoreImage(plots[0]) if err != nil { sender.logger.Warningf("could not store the plot image in the image store: %s", err) } else { @@ -99,7 +99,7 @@ func (sender *Sender) buildEventsString(events moira.NotificationEvents, charsFo eventsLenLimitReached := false eventsPrinted := 0 for _, event := range events { - line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricValue(), event.OldState, event.State) + line := fmt.Sprintf("\n%s: %s = %s (%s to %s)", event.FormatTimestamp(sender.location), event.Metric, event.GetMetricsValues(), event.OldState, event.State) if msg := event.CreateMessage(sender.location); len(msg) > 0 { line += fmt.Sprintf(". %s", msg) } diff --git a/senders/victorops/send_test.go b/senders/victorops/send_test.go index 0a756c51a..a923e0e64 100644 --- a/senders/victorops/send_test.go +++ b/senders/victorops/send_test.go @@ -15,12 +15,11 @@ import ( func TestBuildMessage(t *testing.T) { location, _ := time.LoadLocation("UTC") sender := Sender{location: location, frontURI: "http://moira.url"} - value := float64(123) Convey("Build Moira Message tests", t, func() { event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -37,13 +36,13 @@ func TestBuildMessage(t *testing.T) { strippedDesc := "test\n test test test\n" Convey("Print moira message with one event", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false) - expected := strippedDesc + "\n02:40: Metric = 123 (OK to NODATA)" + expected := strippedDesc + "\n02:40: Metric = t1:123 (OK to NODATA)" So(actual, ShouldResemble, expected) }) Convey("Print moira message with empty trigger", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{}, false) - expected := "\n02:40: Metric = 123 (OK to NODATA)" + expected := "\n02:40: Metric = t1:123 (OK to NODATA)" So(actual, ShouldResemble, expected) }) @@ -51,25 +50,25 @@ func TestBuildMessage(t *testing.T) { var interval int64 = 24 event.MessageEventInfo = &moira.EventInfo{Interval: &interval} actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, false) - expected := strippedDesc + "\n02:40: Metric = 123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix." + expected := strippedDesc + "\n02:40: Metric = t1:123 (OK to NODATA). This metric has been in bad state for more than 24 hours - please, fix." So(actual, ShouldResemble, expected) }) Convey("Print moira message with one event and throttled", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, trigger, true) - expected := strippedDesc + "\n02:40: Metric = 123 (OK to NODATA)\nPlease, fix your system or tune this trigger to generate less events." + expected := strippedDesc + "\n02:40: Metric = t1:123 (OK to NODATA)\nPlease, fix your system or tune this trigger to generate less events." So(actual, ShouldResemble, expected) }) Convey("Print moira message with 6 events", func() { actual := sender.buildMessage([]moira.NotificationEvent{event, event, event, event, event, event}, trigger, false) - expected := strippedDesc + "\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)\n02:40: Metric = 123 (OK to NODATA)" + expected := strippedDesc + "\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)\n02:40: Metric = t1:123 (OK to NODATA)" So(actual, ShouldResemble, expected) }) Convey("Print moira message with empty triggerID, but with trigger name", func() { actual := sender.buildMessage([]moira.NotificationEvent{event}, moira.TriggerData{Name: "Name"}, false) - expected := "\n02:40: Metric = 123 (OK to NODATA)" + expected := "\n02:40: Metric = t1:123 (OK to NODATA)" So(actual, ShouldResemble, expected) }) @@ -82,12 +81,11 @@ func TestBuildCreateAlertRequest(t *testing.T) { defer mockCtrl.Finish() imageStore := mock_moira_alert.NewMockImageStore(mockCtrl) sender := Sender{location: location, frontURI: "http://moira.url", imageStore: imageStore, imageStoreConfigured: true} - value := float64(123) Convey("Build CreateAlertRequest tests", t, func() { event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, @@ -102,7 +100,7 @@ func TestBuildCreateAlertRequest(t *testing.T) { Convey("Build CreateAlertRequest with one moira event and plot", func() { imageStore.EXPECT().StoreImage([]byte("test")).Return("test", nil) - actual := sender.buildCreateAlertRequest(moira.NotificationEvents{event}, trigger, false, []byte("test"), 150000000) + actual := sender.buildCreateAlertRequest(moira.NotificationEvents{event}, trigger, false, [][]byte{[]byte("test")}, 150000000) expected := api.CreateAlertRequest{ MessageType: api.Warning, StateMessage: sender.buildMessage(moira.NotificationEvents{event}, trigger, false), @@ -121,12 +119,11 @@ func TestBuildCreateAlertRequest(t *testing.T) { func TestBuildTitle(t *testing.T) { sender := Sender{} - value := float64(123) Convey("Build title test", t, func() { event := moira.NotificationEvent{ TriggerID: "TriggerID", - Value: &value, + Values: map[string]float64{"t1": 123}, Timestamp: 150000000, Metric: "Metric", OldState: moira.StateOK, diff --git a/senders/webhook/payload.go b/senders/webhook/payload.go index bfda16f8e..2e9441744 100644 --- a/senders/webhook/payload.go +++ b/senders/webhook/payload.go @@ -10,7 +10,7 @@ type payload struct { Trigger triggerData `json:"trigger"` Events []eventData `json:"events"` Contact contactData `json:"contact"` - Plot string `json:"plot"` + Plots []string `json:"plots"` Throttled bool `json:"throttled"` } @@ -22,12 +22,12 @@ type triggerData struct { } type eventData struct { - Metric string `json:"metric"` - Value float64 `json:"value"` - Timestamp int64 `json:"timestamp"` - IsTriggerEvent bool `json:"trigger_event"` - State string `json:"state"` - OldState string `json:"old_state"` + Metric string `json:"metric"` + Values map[string]float64 `json:"values"` + Timestamp int64 `json:"timestamp"` + IsTriggerEvent bool `json:"trigger_event"` + State string `json:"state"` + OldState string `json:"old_state"` } type contactData struct { @@ -55,7 +55,7 @@ func toEventsData(events moira.NotificationEvents) []eventData { for _, event := range events { result = append(result, eventData{ Metric: event.Metric, - Value: moira.UseFloat64(event.Value), + Values: event.Values, Timestamp: event.Timestamp, IsTriggerEvent: event.IsTriggerEvent, State: event.State.String(), diff --git a/senders/webhook/request.go b/senders/webhook/request.go index 72413df76..80c87cdd1 100644 --- a/senders/webhook/request.go +++ b/senders/webhook/request.go @@ -10,12 +10,12 @@ import ( "github.com/moira-alert/moira" ) -func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) (*http.Request, error) { +func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) (*http.Request, error) { if sender.url == moira.VariableContactValue { sender.log.Warningf("%s is potentially dangerous url template, api contact validation is advised", sender.url) } requestURL := buildRequestURL(sender.url, trigger, contact) - requestBody, err := buildRequestBody(events, contact, trigger, plot, throttled) + requestBody, err := buildRequestBody(events, contact, trigger, plots, throttled) if err != nil { return nil, err } @@ -33,7 +33,11 @@ func (sender *Sender) buildRequest(events moira.NotificationEvents, contact moir return request, nil } -func buildRequestBody(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) ([]byte, error) { +func buildRequestBody(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) ([]byte, error) { + encodedPlots := make([]string, 0, len(plots)) + for _, plot := range plots { + encodedPlots = append(encodedPlots, bytesToBase64(plot)) + } requestPayload := payload{ Trigger: toTriggerData(trigger), Events: toEventsData(events), @@ -43,7 +47,7 @@ func buildRequestBody(events moira.NotificationEvents, contact moira.ContactData ID: contact.ID, User: contact.User, }, - Plot: bytesToBase64(plot), + Plots: encodedPlots, Throttled: throttled, } return json.Marshal(requestPayload) diff --git a/senders/webhook/request_test.go b/senders/webhook/request_test.go index 486429c67..c5ad02d48 100644 --- a/senders/webhook/request_test.go +++ b/senders/webhook/request_test.go @@ -27,15 +27,15 @@ var ( Desc: "triggerDescription", Tags: []string{"triggerTag1", "triggerTag2"}, } - testEventsValue = float64(30) - testEvents = []moira.NotificationEvent{ - {Metric: "metricName1", Value: &testEventsValue, Timestamp: 15, IsTriggerEvent: false, State: "OK", OldState: "ERROR"}, - {Metric: "metricName2", Value: &testEventsValue, Timestamp: 11, IsTriggerEvent: false, State: "OK", OldState: "ERROR"}, - {Metric: "metricName3", Value: &testEventsValue, Timestamp: 31, IsTriggerEvent: false, State: "OK", OldState: "ERROR"}, - {Metric: "metricName4", Value: &testEventsValue, Timestamp: 179, IsTriggerEvent: true, State: "OK", OldState: "ERROR"}, - {Metric: "metricName5", Value: &testEventsValue, Timestamp: 12, IsTriggerEvent: false, State: "OK", OldState: "ERROR"}, + + testEvents = []moira.NotificationEvent{ + {Metric: "metricName1", Values: map[string]float64{"t1": 30}, Timestamp: 15, IsTriggerEvent: false, State: "OK", OldState: "ERROR"}, + {Metric: "metricName2", Values: map[string]float64{"t1": 30}, Timestamp: 11, IsTriggerEvent: false, State: "OK", OldState: "ERROR"}, + {Metric: "metricName3", Values: map[string]float64{"t1": 30}, Timestamp: 31, IsTriggerEvent: false, State: "OK", OldState: "ERROR"}, + {Metric: "metricName4", Values: map[string]float64{"t1": 30}, Timestamp: 179, IsTriggerEvent: true, State: "OK", OldState: "ERROR"}, + {Metric: "metricName5", Values: map[string]float64{"t1": 30}, Timestamp: 12, IsTriggerEvent: false, State: "OK", OldState: "ERROR"}, } - testPlot = make([]byte, 0) + testPlot = [][]byte{} testThrottled = false ) @@ -53,7 +53,7 @@ const expectedStateChangePayload = ` "events": [ { "metric": "metricName1", - "value": 30, + "values": {"t1":30}, "timestamp": 15, "trigger_event": false, "state": "OK", @@ -61,7 +61,7 @@ const expectedStateChangePayload = ` }, { "metric": "metricName2", - "value": 30, + "values": {"t1":30}, "timestamp": 11, "trigger_event": false, "state": "OK", @@ -69,7 +69,7 @@ const expectedStateChangePayload = ` }, { "metric": "metricName3", - "value": 30, + "values": {"t1":30}, "timestamp": 31, "trigger_event": false, "state": "OK", @@ -77,7 +77,7 @@ const expectedStateChangePayload = ` }, { "metric": "metricName4", - "value": 30, + "values": {"t1":30}, "timestamp": 179, "trigger_event": true, "state": "OK", @@ -85,7 +85,7 @@ const expectedStateChangePayload = ` }, { "metric": "metricName5", - "value": 30, + "values": {"t1":30}, "timestamp": 12, "trigger_event": false, "state": "OK", @@ -98,7 +98,7 @@ const expectedStateChangePayload = ` "id": "contactID", "user": "contactUser" }, - "plot": "", + "plots": [], "throttled": false } ` @@ -118,7 +118,7 @@ const expectedEmptyPayload = ` "id": "", "user": "" }, - "plot": "", + "plots": [], "throttled": false } ` @@ -187,8 +187,8 @@ func TestBuildRequestBody(t *testing.T) { So(err, ShouldBeNil) }) Convey("Empty notification", func() { - events, contact, trigger, plot, throttled := moira.NotificationEvents{}, moira.ContactData{}, moira.TriggerData{}, make([]byte, 0), false - requestBody, err := buildRequestBody(events, contact, trigger, plot, throttled) + events, contact, trigger, plots, throttled := moira.NotificationEvents{}, moira.ContactData{}, moira.TriggerData{}, make([][]byte, 0), false + requestBody, err := buildRequestBody(events, contact, trigger, plots, throttled) actual, expected := prepareStrings(string(requestBody), expectedEmptyPayload) So(actual, ShouldEqual, expected) So(actual, ShouldNotContainSubstring, "null") diff --git a/senders/webhook/webhook.go b/senders/webhook/webhook.go index 9239a66d7..07c421204 100644 --- a/senders/webhook/webhook.go +++ b/senders/webhook/webhook.go @@ -57,9 +57,9 @@ func (sender *Sender) Init(senderSettings map[string]string, logger moira.Logger } // SendEvents implements Sender interface Send -func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plot []byte, throttled bool) error { +func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira.ContactData, trigger moira.TriggerData, plots [][]byte, throttled bool) error { - request, err := sender.buildRequest(events, contact, trigger, plot, throttled) + request, err := sender.buildRequest(events, contact, trigger, plots, throttled) if request != nil { defer request.Body.Close() }