Skip to content

Commit

Permalink
feat: Improve advanced mode
Browse files Browse the repository at this point in the history
Datatype that stores metrics for check is changed to multiple datatypes
that handle different operations on data. Fetcher interface is changed to
return new implemented datatypes. Checker is changed to handle triggers
with multiple targets in new way. API, notifier are changed to fit new
CheckData struct.

Relates #428
  • Loading branch information
litleleprikon committed Apr 6, 2020
1 parent 31a117e commit d1ef079
Show file tree
Hide file tree
Showing 138 changed files with 3,972 additions and 1,953 deletions.
3 changes: 1 addition & 2 deletions api/controller/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
3 changes: 1 addition & 2 deletions api/controller/subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
54 changes: 22 additions & 32 deletions api/controller/trigger_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,26 @@ 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) (map[string][]metricSource.MetricData, *moira.Trigger, error) {
trigger, err := dataBase.GetTrigger(triggerID)
if err != nil {
return nil, nil, err
}
triggerMetrics := metricSource.NewTriggerMetricsData()
triggerMetrics := make(map[string][]metricSource.MetricData)
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...)
}

targetName := fmt.Sprintf("t%d", i)
triggerMetrics[targetName] = metricData
}
return triggerMetrics, &trigger, nil
}
Expand All @@ -57,31 +56,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
}
Expand Down
4 changes: 2 additions & 2 deletions api/controller/trigger_metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
25 changes: 21 additions & 4 deletions api/dto/triggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package dto
import (
"fmt"
"net/http"
"regexp"
"strconv"
"time"

"github.com/moira-alert/moira"
Expand All @@ -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"`
Expand Down Expand Up @@ -62,6 +66,8 @@ 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 map[string]bool `json:"alone_metrics"`
}

// ToMoiraTrigger transforms TriggerModel to moira.Trigger
Expand All @@ -82,6 +88,7 @@ func (model *TriggerModel) ToMoiraTrigger() *moira.Trigger {
Patterns: model.Patterns,
IsRemote: model.IsRemote,
MuteNewMetrics: model.MuteNewMetrics,
AloneMetrics: model.AloneMetrics,
}
}

Expand Down Expand Up @@ -120,6 +127,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),
Expand Down Expand Up @@ -323,10 +343,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
Expand Down
2 changes: 1 addition & 1 deletion api/dto/triggers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func TestExpressionModeMultipleTargetsWarnValue(t *testing.T) {
localSource.EXPECT().GetMetricsTTLSeconds().Return(int64(3600)).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")
Expand Down
2 changes: 1 addition & 1 deletion api/handler/trigger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 20 additions & 14 deletions api/handler/trigger_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,43 +46,43 @@ 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 == "" {
return
}
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) (map[string][]metricSource.MetricData, *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 {
Expand All @@ -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
}
Expand Down
14 changes: 14 additions & 0 deletions api/middleware/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,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))
})
}
}
6 changes: 6 additions & 0 deletions api/middleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ var (
loginKey ContextKey = "login"
timeSeriesNamesKey ContextKey = "timeSeriesNames"
metricSourceProvider ContextKey = "metricSourceProvider"
targetNameKey ContextKey = "target"
)

// GetDatabase gets moira.Database realization from request context
Expand Down Expand Up @@ -108,3 +109,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)
}
Loading

0 comments on commit d1ef079

Please sign in to comment.