diff --git a/api/config.go b/api/config.go index 7750b68d5..75617e6b5 100644 --- a/api/config.go +++ b/api/config.go @@ -5,6 +5,7 @@ import ( "time" "github.com/moira-alert/moira" + "github.com/moira-alert/moira/plotting" ) // WebContact is container for web ui contact validation. @@ -38,6 +39,7 @@ type Config struct { MetricsTTL map[moira.ClusterKey]time.Duration Flags FeatureFlags Authorization Authorization + PlotCfg plotting.PlotConfig } // WebConfig is container for web ui configuration parameters. diff --git a/api/handler/handler.go b/api/handler/handler.go index 27647a8c3..7e5cedc2c 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -104,7 +104,7 @@ func NewHandler( router.Route("/user", user) router.With(moiramiddle.Triggers( apiConfig.MetricsTTL, - )).Route("/trigger", triggers(metricSourceProvider, searchIndex)) + )).Route("/trigger", triggers(metricSourceProvider, searchIndex, apiConfig.PlotCfg)) router.Route("/tag", tag) router.Route("/pattern", pattern) router.Route("/event", event) diff --git a/api/handler/trigger.go b/api/handler/trigger.go index d17b09cc9..9d9d288cb 100644 --- a/api/handler/trigger.go +++ b/api/handler/trigger.go @@ -12,22 +12,25 @@ import ( "github.com/moira-alert/moira/api/controller" "github.com/moira-alert/moira/api/dto" "github.com/moira-alert/moira/api/middleware" + "github.com/moira-alert/moira/plotting" ) -func trigger(router chi.Router) { - router.Use(middleware.TriggerContext) - router.Put("/", updateTrigger) - router.With(middleware.TriggerContext, middleware.Populate(false)).Get("/", getTrigger) - router.Delete("/", removeTrigger) - router.Get("/state", getTriggerState) - router.Route("/throttling", func(router chi.Router) { - router.Get("/", getTriggerThrottling) - router.Delete("/", deleteThrottling) - }) - router.Route("/metrics", triggerMetrics) - router.Put("/setMaintenance", setTriggerMaintenance) - router.With(middleware.DateRange("-1hour", "now")).With(middleware.TargetName("t1")).Get("/render", renderTrigger) - router.Get("/dump", triggerDump) +func trigger(plotCfg plotting.PlotConfig) func(chi.Router) { + return func(router chi.Router) { + router.Use(middleware.TriggerContext) + router.Put("/", updateTrigger) + router.With(middleware.TriggerContext, middleware.Populate(false)).Get("/", getTrigger) + router.Delete("/", removeTrigger) + router.Get("/state", getTriggerState) + router.Route("/throttling", func(router chi.Router) { + router.Get("/", getTriggerThrottling) + router.Delete("/", deleteThrottling) + }) + router.Route("/metrics", triggerMetrics) + router.Put("/setMaintenance", setTriggerMaintenance) + router.With(middleware.DateRange("-1hour", "now")).With(middleware.TargetName("t1")).Get("/render", renderTrigger(plotCfg)) + router.Get("/dump", triggerDump) + } } // nolint: gofmt,goimports diff --git a/api/handler/trigger_render.go b/api/handler/trigger_render.go index 02794c759..d64292651 100644 --- a/api/handler/trigger_render.go +++ b/api/handler/trigger_render.go @@ -36,36 +36,40 @@ import ( // @failure 404 {object} api.ErrorNotFoundExample "Resource not found" // @failure 500 {object} api.ErrorInternalServerExample "Internal server error" // @router /trigger/{triggerID}/render [get] -func renderTrigger(writer http.ResponseWriter, request *http.Request) { - sourceProvider, targetName, from, to, triggerID, fetchRealtimeData, err := getEvaluationParameters(request) - if err != nil { - render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint - return - } - metricsData, trigger, err := evaluateTargetMetrics(sourceProvider, from, to, triggerID, fetchRealtimeData) - if err != nil { - if trigger == nil { - render.Render(writer, request, api.ErrorNotFound(fmt.Sprintf("trigger with ID = '%s' does not exists", triggerID))) //nolint - } else { - render.Render(writer, request, api.ErrorInternalServer(err)) //nolint +func renderTrigger(plotCfg plotting.PlotConfig) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + sourceProvider, targetName, from, to, triggerID, fetchRealtimeData, err := getEvaluationParameters(r) + if err != nil { + render.Render(w, r, api.ErrorInvalidRequest(err)) //nolint + return } - return - } - targetMetrics, ok := metricsData[targetName] - if !ok { - render.Render(writer, request, api.ErrorNotFound(fmt.Sprintf("Cannot find target %s", targetName))) //nolint - } + metricsData, trigger, err := evaluateTargetMetrics(sourceProvider, from, to, triggerID, fetchRealtimeData) + if err != nil { + if trigger == nil { + render.Render(w, r, api.ErrorNotFound(fmt.Sprintf("trigger with ID = '%s' does not exists", triggerID))) //nolint + } else { + render.Render(w, r, api.ErrorInternalServer(err)) //nolint + } + return + } - renderable, err := buildRenderable(request, trigger, targetMetrics, targetName) - if err != nil { - render.Render(writer, request, api.ErrorInternalServer(err)) //nolint - return - } - writer.Header().Set("Content-Type", "image/png") - err = renderable.Render(chart.PNG, writer) - if err != nil { - render.Render(writer, request, api.ErrorInternalServer(fmt.Errorf("can not render plot %s", err.Error()))) //nolint + targetMetrics, ok := metricsData[targetName] + if !ok { + render.Render(w, r, api.ErrorNotFound(fmt.Sprintf("Cannot find target %s", targetName))) //nolint + } + + renderable, err := buildRenderable(plotCfg, r, trigger, targetMetrics, targetName) + if err != nil { + render.Render(w, r, api.ErrorInternalServer(err)) //nolint + return + } + + w.Header().Set("Content-Type", "image/png") + err = renderable.Render(chart.PNG, w) + if err != nil { + render.Render(w, r, api.ErrorInternalServer(fmt.Errorf("can not render plot %w", err))) //nolint + } } } @@ -113,7 +117,7 @@ func evaluateTargetMetrics(metricSourceProvider *metricSource.SourceProvider, fr return tts, trigger, err } -func buildRenderable(request *http.Request, trigger *moira.Trigger, metricsData []metricSource.MetricData, targetName string) (*chart.Chart, error) { +func buildRenderable(plotCfg plotting.PlotConfig, request *http.Request, trigger *moira.Trigger, metricsData []metricSource.MetricData, targetName string) (*chart.Chart, error) { urlValues, err := url.ParseQuery(request.URL.RawQuery) if err != nil { return nil, fmt.Errorf("failed to parse query string: %w", err) @@ -126,7 +130,7 @@ func buildRenderable(request *http.Request, trigger *moira.Trigger, metricsData } plotTheme := urlValues.Get("theme") - plotTemplate, err := plotting.GetPlotTemplate(plotTheme, location) + plotTemplate, err := plotting.GetPlotTemplate(plotCfg, plotTheme, location) if err != nil { return nil, fmt.Errorf("can not initialize plot theme %s", err.Error()) } diff --git a/api/handler/trigger_render_test.go b/api/handler/trigger_render_test.go index d31de3d53..b267fa044 100644 --- a/api/handler/trigger_render_test.go +++ b/api/handler/trigger_render_test.go @@ -11,6 +11,7 @@ import ( 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" + "github.com/moira-alert/moira/plotting" . "github.com/smartystreets/goconvey/convey" "go.uber.org/mock/gomock" ) @@ -27,6 +28,8 @@ func TestRenderTrigger(t *testing.T) { responseWriter := httptest.NewRecorder() mockDb := mock_moira_alert.NewMockDatabase(mockCtrl) + plotConfig := plotting.PlotConfig{} + Convey("with the wrong realtime parameter", func() { testRequest := httptest.NewRequest(http.MethodGet, "/trigger/triggerID-0000000000001/render?realtime=test", nil) testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "triggerID", "triggerID-0000000000001")) @@ -35,7 +38,8 @@ func TestRenderTrigger(t *testing.T) { testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "from", "-1hour")) testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "to", "now")) - renderTrigger(responseWriter, testRequest) + render := renderTrigger(plotConfig) + render(responseWriter, testRequest) response := responseWriter.Result() defer response.Body.Close() @@ -68,7 +72,8 @@ func TestRenderTrigger(t *testing.T) { testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "from", "-1hour")) testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "to", "now")) - renderTrigger(responseWriter, testRequest) + render := renderTrigger(plotConfig) + render(responseWriter, testRequest) response := responseWriter.Result() defer response.Body.Close() @@ -101,7 +106,8 @@ func TestRenderTrigger(t *testing.T) { testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "from", "-1hour")) testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "to", "now")) - renderTrigger(responseWriter, testRequest) + render := renderTrigger(plotConfig) + render(responseWriter, testRequest) response := responseWriter.Result() defer response.Body.Close() @@ -122,7 +128,8 @@ func TestRenderTrigger(t *testing.T) { testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "from", "-1hour")) testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "to", "now")) - renderTrigger(responseWriter, testRequest) + render := renderTrigger(plotConfig) + render(responseWriter, testRequest) response := responseWriter.Result() defer response.Body.Close() diff --git a/api/handler/triggers.go b/api/handler/triggers.go index 090ed6607..d2bc6aa3c 100644 --- a/api/handler/triggers.go +++ b/api/handler/triggers.go @@ -15,6 +15,7 @@ import ( metricSource "github.com/moira-alert/moira/metric_source" "github.com/moira-alert/moira/metric_source/local" "github.com/moira-alert/moira/metric_source/remote" + "github.com/moira-alert/moira/plotting" "github.com/moira-alert/moira/api" "github.com/moira-alert/moira/api/controller" @@ -23,7 +24,7 @@ import ( "github.com/moira-alert/moira/expression" ) -func triggers(metricSourceProvider *metricSource.SourceProvider, searcher moira.Searcher) func(chi.Router) { +func triggers(metricSourceProvider *metricSource.SourceProvider, searcher moira.Searcher, plotCfg plotting.PlotConfig) func(chi.Router) { return func(router chi.Router) { router.Use(middleware.MetricSourceProvider(metricSourceProvider)) router.Use(middleware.SearchIndexContext(searcher)) @@ -33,7 +34,7 @@ func triggers(metricSourceProvider *metricSource.SourceProvider, searcher moira. router.Put("/", createTrigger) router.Put("/check", triggerCheck) - router.Route("/{triggerId}", trigger) + router.Route("/{triggerId}", trigger(plotCfg)) router.With(middleware.Paginate(0, 10)).With(middleware.Pager(false, "")).Get("/search", searchTriggers) router.With(middleware.Pager(false, "")).Delete("/search/pager", deletePager) // ToDo: DEPRECATED method. Remove in Moira 2.6 diff --git a/cmd/api/config.go b/cmd/api/config.go index bfd5ca3eb..047ddfe10 100644 --- a/cmd/api/config.go +++ b/cmd/api/config.go @@ -48,6 +48,8 @@ type apiConfig struct { EnableCORS bool `yaml:"enable_cors"` // Authorization contains authorization configuration. Authorization authorization `yaml:"authorization"` + // PlotCfg sets the configuration for the plots, such as size. + PlotCfg cmd.PlotConfig `yaml:"plot"` } type authorization struct { @@ -116,6 +118,7 @@ func (config *apiConfig) getSettings( MetricsTTL: metricsTTL, Flags: flags, Authorization: config.Authorization.toApiConfig(webConfig), + PlotCfg: config.PlotCfg.GetSettings(), } } @@ -217,6 +220,13 @@ func getDefault() config { API: apiConfig{ Listen: ":8081", EnableCORS: false, + PlotCfg: cmd.PlotConfig{ + Width: 800, + Height: 400, + YAxisSecondaryCfg: cmd.YAxisSecondaryConfig{ + EnablePrettyTicks: false, + }, + }, }, Web: webConfig{ RemoteAllowed: false, diff --git a/cmd/api/config_test.go b/cmd/api/config_test.go index 782fcf301..b9bac7b87 100644 --- a/cmd/api/config_test.go +++ b/cmd/api/config_test.go @@ -12,6 +12,14 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +var defaultPlotCfg = cmd.PlotConfig{ + Width: 800, + Height: 400, + YAxisSecondaryCfg: cmd.YAxisSecondaryConfig{ + EnablePrettyTicks: false, + }, +} + func Test_apiConfig_getSettings(t *testing.T) { Convey("Settings successfully filled", t, func() { metricTTLs := map[moira.ClusterKey]time.Duration{ @@ -30,6 +38,7 @@ func Test_apiConfig_getSettings(t *testing.T) { apiConf := apiConfig{ Listen: "0000", EnableCORS: true, + PlotCfg: defaultPlotCfg, } expectedResult := &api.Config{ @@ -43,6 +52,7 @@ func Test_apiConfig_getSettings(t *testing.T) { "test": {}, }, }, + PlotCfg: defaultPlotCfg.GetSettings(), } result := apiConf.getSettings(metricTTLs, api.FeatureFlags{IsReadonlyEnabled: true}, webConfig) @@ -88,6 +98,13 @@ func Test_webConfig_getDefault(t *testing.T) { API: apiConfig{ Listen: ":8081", EnableCORS: false, + PlotCfg: cmd.PlotConfig{ + Width: 800, + Height: 400, + YAxisSecondaryCfg: cmd.YAxisSecondaryConfig{ + EnablePrettyTicks: false, + }, + }, }, Web: webConfig{ RemoteAllowed: false, diff --git a/cmd/config.go b/cmd/config.go index 7a5ce2321..3054a746b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -8,6 +8,7 @@ import ( "github.com/moira-alert/moira" "github.com/moira-alert/moira/metrics" + "github.com/moira-alert/moira/plotting" "github.com/moira-alert/moira/image_store/s3" prometheusRemoteSource "github.com/moira-alert/moira/metric_source/prometheus" @@ -120,6 +121,29 @@ func (notificationConfig *NotificationConfig) GetSettings() redis.NotificationCo } } +// PlotConfig sets the configuration for the plots, such as size. +type PlotConfig struct { + Width int `yaml:"width"` + Height int `yaml:"height"` + YAxisSecondaryCfg YAxisSecondaryConfig `yaml:"y_axis_secondary"` +} + +// YAxisSecondaryConfig defines the setting for the secondary y-axis. +type YAxisSecondaryConfig struct { + EnablePrettyTicks bool `yaml:"enable_pretty_ticks"` +} + +// GetSettings returns plot configuration. +func (pcfg PlotConfig) GetSettings() plotting.PlotConfig { + return plotting.PlotConfig{ + Width: pcfg.Width, + Height: pcfg.Height, + YAxisSecondaryCfg: plotting.YAxisSecondaryConfig{ + EnablePrettyTicks: pcfg.YAxisSecondaryCfg.EnablePrettyTicks, + }, + } +} + // GraphiteConfig is graphite metrics config structure that initialises at the start of moira. type GraphiteConfig struct { // If true, graphite sender will be enabled. diff --git a/cmd/notifier/config.go b/cmd/notifier/config.go index 8277024b9..ca7519e84 100644 --- a/cmd/notifier/config.go +++ b/cmd/notifier/config.go @@ -56,6 +56,8 @@ type notifierConfig struct { MaxFailAttemptToSendAvailable int `yaml:"max_fail_attempt_to_send_available"` // Specify log level by entities SetLogLevel setLogLevelConfig `yaml:"set_log_level"` + // PlotCfg sets the configuration for the plots, such as size + PlotCfg cmd.PlotConfig `yaml:"plot"` } type selfStateConfig struct { @@ -117,6 +119,13 @@ func getDefault() config { Timezone: "UTC", ReadBatchSize: int(notifier.NotificationsLimitUnlimited), MaxFailAttemptToSendAvailable: 3, + PlotCfg: cmd.PlotConfig{ + Width: 800, + Height: 400, + YAxisSecondaryCfg: cmd.YAxisSecondaryConfig{ + EnablePrettyTicks: false, + }, + }, }, Telemetry: cmd.TelemetryConfig{ Listen: ":8093", @@ -203,6 +212,7 @@ func (config *notifierConfig) getSettings(logger moira.Logger) notifier.Config { MaxFailAttemptToSendAvailable: config.MaxFailAttemptToSendAvailable, LogContactsToLevel: contacts, LogSubscriptionsToLevel: subscriptions, + PlotCfg: config.PlotCfg.GetSettings(), } } diff --git a/go.mod b/go.mod index af4fb7c7e..de08e2631 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.14.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 - github.com/rs/cors v1.9.0 + github.com/rs/cors v1.11.0 github.com/rs/zerolog v1.29.0 github.com/russross/blackfriday/v2 v2.1.0 github.com/slack-go/slack v0.12.1 @@ -158,7 +158,7 @@ require ( go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.24.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/image v0.13.0 // indirect + golang.org/x/image v0.18.0 // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sys v0.21.0 // indirect diff --git a/go.sum b/go.sum index acccd9382..3ff9af507 100644 --- a/go.sum +++ b/go.sum @@ -1080,8 +1080,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po= +github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= @@ -1279,8 +1279,8 @@ golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXy golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= -golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= diff --git a/local/api.yml b/local/api.yml index a48bddd83..f018c92d3 100644 --- a/local/api.yml +++ b/local/api.yml @@ -37,6 +37,11 @@ prometheus_remote: api: listen: ":8081" enable_cors: false + plot: + width: 800 + height: 400 + y_axis_secondary: + enable_pretty_ticks: false web: contacts_template: - type: mail diff --git a/local/notifier.yml b/local/notifier.yml index 1fd44427a..8385787d7 100644 --- a/local/notifier.yml +++ b/local/notifier.yml @@ -42,6 +42,11 @@ notifier: front_uri: http://localhost timezone: UTC date_time_format: "15:04 02.01.2006" + plot: + width: 800 + height: 400 + y_axis_secondary: + enable_pretty_ticks: false notification_history: ttl: 48h query_limit: 10000 diff --git a/notifier/config.go b/notifier/config.go index a89b4b7ec..edf25284a 100644 --- a/notifier/config.go +++ b/notifier/config.go @@ -2,6 +2,8 @@ package notifier import ( "time" + + "github.com/moira-alert/moira/plotting" ) const NotificationsLimitUnlimited = int64(-1) @@ -24,4 +26,5 @@ type Config struct { MaxFailAttemptToSendAvailable int LogContactsToLevel map[string]string LogSubscriptionsToLevel map[string]string + PlotCfg plotting.PlotConfig } diff --git a/notifier/plotting.go b/notifier/plotting.go index 041d1d318..19bf1adab 100644 --- a/notifier/plotting.go +++ b/notifier/plotting.go @@ -66,7 +66,7 @@ func (notifier *StandardNotifier) buildNotificationPackagePlots(pkg Notification if len(metricsToShow) == 0 { return nil, 0, nil } - plotTemplate, err := plotting.GetPlotTemplate(pkg.Plotting.Theme, notifier.config.Location) + plotTemplate, err := plotting.GetPlotTemplate(notifier.config.PlotCfg, pkg.Plotting.Theme, notifier.config.Location) if err != nil { return nil, 0, err } diff --git a/notifier/plotting_test.go b/notifier/plotting_test.go index b91f341b0..88b93f477 100644 --- a/notifier/plotting_test.go +++ b/notifier/plotting_test.go @@ -18,6 +18,14 @@ import ( "go.uber.org/mock/gomock" ) +var defaultPlotCfg = plotting.PlotConfig{ + Width: 800, + Height: 400, + YAxisSecondaryCfg: plotting.YAxisSecondaryConfig{ + EnablePrettyTicks: false, + }, +} + // generateTestMetricsData generates metricsData map for tests. func generateTestMetricsData() map[string][]metricSource.MetricData { metricData1 := metricSource.MetricData{ @@ -248,7 +256,9 @@ func TestBuildTriggerPlots(t *testing.T) { Convey("Run buildTriggerPlots", t, func() { triggerID := uuid.Must(uuid.NewV4()).String() trigger := moira.Trigger{ID: triggerID} - plotTemplate, _ := plotting.GetPlotTemplate("", location) + plotCfg := defaultPlotCfg + + plotTemplate, _ := plotting.GetPlotTemplate(plotCfg, "", location) Convey("without errors", func() { testMetricsData := generateTestMetricsData() diff --git a/plotting/curve.go b/plotting/curve.go index 5db9c70bc..6c8a6b032 100644 --- a/plotting/curve.go +++ b/plotting/curve.go @@ -16,19 +16,22 @@ type plotCurve struct { // getCurveSeriesList returns curve series list. func getCurveSeriesList(metricsData []metricSource.MetricData, theme moira.PlotTheme) []chart.TimeSeries { - curveSeriesList := make([]chart.TimeSeries, 0) + curveSeriesList := make([]chart.TimeSeries, 0, len(metricsData)) + for metricDataInd := range metricsData { curveStyle, pointStyle := theme.GetSerieStyles(metricDataInd) curveSeries := generatePlotCurves(metricsData[metricDataInd], curveStyle, pointStyle) curveSeriesList = append(curveSeriesList, curveSeries...) } + return curveSeriesList } // generatePlotCurves returns go-chart timeseries to generate plot curves. func generatePlotCurves(metricData metricSource.MetricData, curveStyle chart.Style, pointStyle chart.Style) []chart.TimeSeries { curves := describePlotCurves(metricData) - curveSeries := make([]chart.TimeSeries, 0) + curveSeries := make([]chart.TimeSeries, 0, len(curves)) + for _, curve := range curves { var serieStyle chart.Style switch len(curve.values) { @@ -39,15 +42,18 @@ func generatePlotCurves(metricData metricSource.MetricData, curveStyle chart.Sty default: serieStyle = curveStyle } - curveSerie := chart.TimeSeries{ + + curve := chart.TimeSeries{ Name: metricData.Name, YAxis: chart.YAxisSecondary, Style: serieStyle, XValues: curve.timeStamps, YValues: curve.values, } - curveSeries = append(curveSeries, curveSerie) + + curveSeries = append(curveSeries, curve) } + return curveSeries } @@ -74,9 +80,9 @@ func describePlotCurves(metricData metricSource.MetricData) []plotCurve { } // resolveFirstPoint returns first point coordinates. -func resolveFirstPoint(metricData metricSource.MetricData) (int, int64) { - start := 0 - startTime := metricData.StartTime +func resolveFirstPoint(metricData metricSource.MetricData) (start int, startTime int64) { + startTime = metricData.StartTime + for _, metricVal := range metricData.Values { if !moira.IsFiniteNumber(metricVal) { start++ @@ -85,5 +91,6 @@ func resolveFirstPoint(metricData metricSource.MetricData) (int, int64) { break } } + return start, startTime } diff --git a/plotting/limits.go b/plotting/limits.go index 28aa370e7..52753e8b9 100644 --- a/plotting/limits.go +++ b/plotting/limits.go @@ -30,7 +30,7 @@ type plotLimits struct { // resolveLimits returns common plot limits. func resolveLimits(metricsData []metricSource.MetricData) plotLimits { allValues := make([]float64, 0) - allTimes := make([]time.Time, 0) + allTimes := make([]time.Time, 0, len(metricsData)*2) for _, metricData := range metricsData { for _, metricValue := range metricData.Values { if moira.IsFiniteNumber(metricValue) { @@ -40,19 +40,23 @@ func resolveLimits(metricsData []metricSource.MetricData) plotLimits { allTimes = append(allTimes, moira.Int64ToTime(metricData.StartTime)) allTimes = append(allTimes, moira.Int64ToTime(metricData.StopTime)) } + from, to := util.Time.StartAndEnd(allTimes...) lowest, highest := util.Math.MinAndMax(allValues...) if highest == lowest { highest += defaultRangeDelta / 2 lowest -= defaultRangeDelta / 2 } + yAxisIncrement := percentsOfRange(lowest, highest, defaultYAxisRangePercent) if highest > 0 { highest += yAxisIncrement } + if lowest < 0 { lowest -= yAxisIncrement } + return plotLimits{ from: from, to: to, diff --git a/plotting/plot.go b/plotting/plot.go index fa58e9517..c9fe6ade3 100644 --- a/plotting/plot.go +++ b/plotting/plot.go @@ -23,28 +23,40 @@ func (err ErrNoPointsToRender) Error() string { return fmt.Sprintf("no points found to render trigger: %s", err.triggerID) } +// PlotConfig sets the configuration for plots, such as size. +type PlotConfig struct { + Width int + Height int + YAxisSecondaryCfg YAxisSecondaryConfig +} + +// YAxisSecondaryConfig defines the setting for the secondary y-axis. +type YAxisSecondaryConfig struct { + EnablePrettyTicks bool +} + // Plot represents plot structure to render. type Plot struct { + cfg PlotConfig theme moira.PlotTheme location *time.Location - width int - height int } // GetPlotTemplate returns plot template. -func GetPlotTemplate(theme string, location *time.Location) (*Plot, error) { +func GetPlotTemplate(cfg PlotConfig, theme string, location *time.Location) (*Plot, error) { plotTheme, err := getPlotTheme(theme) if err != nil { return nil, err } + if location == nil { return nil, fmt.Errorf("location not specified") } + return &Plot{ + cfg: cfg, theme: plotTheme, location: location, - width: 800, //nolint - height: 400, //nolint }, nil } @@ -52,8 +64,6 @@ func GetPlotTemplate(theme string, location *time.Location) (*Plot, 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) - limits := resolveLimits(metricsData) curveSeriesList := getCurveSeriesList(metricsData, plot.theme) @@ -61,6 +71,8 @@ func (plot *Plot) GetRenderable(targetName string, trigger *moira.Trigger, metri return renderable, ErrNoPointsToRender{triggerID: trigger.ID} } + plotSeries := make([]chart.Series, 0, len(curveSeriesList)) + for _, curveSeries := range curveSeriesList { plotSeries = append(plotSeries, curveSeries) } @@ -78,8 +90,8 @@ func (plot *Plot) GetRenderable(targetName string, trigger *moira.Trigger, metri Title: sanitizeLabelName(name, plotNameLen), TitleStyle: plot.theme.GetTitleStyle(), - Width: plot.width, - Height: plot.height, + Width: plot.cfg.Width, + Height: plot.cfg.Height, Canvas: plot.theme.GetCanvasStyle(), Background: plot.theme.GetBackgroundStyle(maxMarkLen), @@ -113,14 +125,14 @@ func (plot *Plot) GetRenderable(targetName string, trigger *moira.Trigger, metri Max: limits.highest, Min: limits.lowest, }, - EnablePrettyTicks: true, + EnablePrettyTicks: plot.cfg.YAxisSecondaryCfg.EnablePrettyTicks, }, Series: plotSeries, } renderable.Elements = []chart.Renderable{ - getPlotLegend(&renderable, plot.theme.GetLegendStyle(), plot.width), + getPlotLegend(&renderable, plot.theme.GetLegendStyle(), plot.cfg.Width), } return renderable, nil diff --git a/plotting/plot_test.go b/plotting/plot_test.go index 13efc58f7..f9b359327 100644 --- a/plotting/plot_test.go +++ b/plotting/plot_test.go @@ -521,23 +521,36 @@ func renderTestMetricsDataToPNG( filePath string, ) error { location, _ := time.LoadLocation("UTC") - plotTemplate, err := GetPlotTemplate(plotTheme, location) + plotCfg := PlotConfig{ + Width: 800, + Height: 400, + YAxisSecondaryCfg: YAxisSecondaryConfig{ + EnablePrettyTicks: true, + }, + } + + plotTemplate, err := GetPlotTemplate(plotCfg, plotTheme, location) if err != nil { return err } + renderable, err := plotTemplate.GetRenderable("t1", &trigger, metricsData) if err != nil { return err } + f, err := os.Create(filePath) if err != nil { return err } + w := bufio.NewWriter(f) if err := renderable.Render(chart.PNG, w); err != nil { return err } + w.Flush() + return nil } @@ -548,11 +561,14 @@ func calculateHashDistance(pathToOriginal, pathToRendered string) (*int, error) if err != nil { return nil, err } + rendered, err := util.Open(pathToRendered) if err != nil { return nil, err } + distance := hash.Compare(original, rendered) + return &distance, nil } @@ -589,6 +605,7 @@ func TestGetRenderable(t *testing.T) { Name: testCase.getTriggerName(), TriggerType: testCase.triggerType, } + if testCase.errorValue != nil { errorValue := testCase.errorValue.(float64) if !testCase.useHumanizedValues { @@ -596,6 +613,7 @@ func TestGetRenderable(t *testing.T) { } trigger.ErrorValue = &errorValue } + if testCase.warnValue != nil { warnValue := testCase.warnValue.(float64) if !testCase.useHumanizedValues { @@ -603,23 +621,28 @@ func TestGetRenderable(t *testing.T) { } trigger.WarnValue = &warnValue } + metricsData := generateTestMetricsData(testCase.useHumanizedValues) pathToOriginal, err := testCase.getFilePath(true) if err != nil { t.Fatal(err) } + pathToRendered, err := testCase.getFilePath(false) if err != nil { t.Fatal(err) } + err = renderTestMetricsDataToPNG(trigger, testCase.plotTheme, metricsData, pathToRendered) if err != nil { t.Fatal(err) } + hashDistance, err := calculateHashDistance(pathToOriginal, pathToRendered) if err != nil { t.Fatal(err) } + os.Remove(pathToRendered) So(*hashDistance, ShouldBeLessThanOrEqualTo, testCase.expected) }) @@ -630,7 +653,12 @@ func TestGetRenderable(t *testing.T) { // TestErrNoPointsToRender_Error asserts conditions which leads to ErrNoPointsToRender. func TestErrNoPointsToRender_Error(t *testing.T) { location, _ := time.LoadLocation("UTC") - plotTemplate, err := GetPlotTemplate("", location) + plotCfg := PlotConfig{ + Width: 800, + Height: 400, + } + + plotTemplate, err := GetPlotTemplate(plotCfg, "", location) if err != nil { t.Fatalf("Test initialization failed: %s", err.Error()) } diff --git a/plotting/theme.go b/plotting/theme.go index 632b0ce20..1f1a09e78 100644 --- a/plotting/theme.go +++ b/plotting/theme.go @@ -17,26 +17,25 @@ const ( // getPlotTheme returns plot theme. func getPlotTheme(plotTheme string) (moira.PlotTheme, error) { // TODO: rewrite light theme - var err error var theme moira.PlotTheme themeFont, err := getDefaultFont() if err != nil { return nil, err } + switch plotTheme { case darkPlotTheme: theme, err = dark.NewTheme(themeFont) if err != nil { return nil, err } - case lightPlotTheme: - fallthrough default: theme, err = light.NewTheme(themeFont) if err != nil { return nil, err } } + return theme, nil } @@ -46,5 +45,6 @@ func getDefaultFont() (*truetype.Font, error) { if err != nil { return nil, err } + return ttf, nil } diff --git a/plotting/threshold.go b/plotting/threshold.go index 86b1a1228..976e33f38 100644 --- a/plotting/threshold.go +++ b/plotting/threshold.go @@ -39,16 +39,19 @@ func newThreshold(triggerType, thresholdType string, thresholdValue, higherLimit // getThresholdSeriesList returns collection of thresholds and annotations. func getThresholdSeriesList(trigger *moira.Trigger, theme moira.PlotTheme, limits plotLimits) []chart.Series { - thresholdSeriesList := make([]chart.Series, 0) if trigger.TriggerType == moira.ExpressionTrigger { - return thresholdSeriesList + return []chart.Series{} } + plotThresholds := generateThresholds(trigger, limits) + thresholdSeriesList := make([]chart.Series, 0, len(plotThresholds)) + for _, plotThreshold := range plotThresholds { thresholdSeriesList = append(thresholdSeriesList, plotThreshold.generateThresholdSeries(theme, limits)) // TODO: uncomment to use annotations if necessary, remove otherwise // thresholdSeriesList = append(thresholdSeriesList, plotThreshold.generateAnnotationSeries(theme, limits)) } + return thresholdSeriesList } @@ -105,9 +108,11 @@ func (threshold *threshold) generateThresholdSeries(theme moira.PlotTheme, limit XValues: []time.Time{limits.from, limits.to}, YValues: []float64{}, } + for j := 0; j < len(thresholdSeries.XValues); j++ { thresholdSeries.YValues = append(thresholdSeries.YValues, threshold.yCoordinate) } + return thresholdSeries }