From 9bb5bb7c52a7ec5b28f7d06a1904e6c6c88a16a9 Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Thu, 8 Aug 2024 17:30:52 +0700 Subject: [PATCH 01/36] fix(api): contact notification history (#1051) --- api/controller/contact_events.go | 10 +- api/controller/contact_events_test.go | 14 +- api/handler/constants.go | 8 + api/handler/contact_events.go | 24 +- cmd/api/config.go | 5 +- cmd/api/config_test.go | 3 +- cmd/cli/config.go | 18 +- cmd/cli/from_2.12_to_2.13.go | 32 ++ cmd/cli/main.go | 56 ++- cmd/cli/notification_history.go | 244 ++++++++++++ cmd/cli/notification_history_test.go | 375 ++++++++++++++++++ cmd/config.go | 5 +- cmd/notifier/config.go | 3 +- database/redis/config.go | 3 +- .../redis/contact_notification_history.go | 104 ++++- .../contact_notification_history_test.go | 273 ++++++++++++- database/redis/database.go | 6 +- interfaces.go | 3 +- local/cli.yml | 2 + local/notifier.yml | 1 - mock/moira-alert/database.go | 26 +- 21 files changed, 1112 insertions(+), 103 deletions(-) create mode 100644 api/handler/constants.go create mode 100644 cmd/cli/from_2.12_to_2.13.go create mode 100644 cmd/cli/notification_history.go create mode 100644 cmd/cli/notification_history_test.go diff --git a/api/controller/contact_events.go b/api/controller/contact_events.go index a6e5f3174..d40e2ff98 100644 --- a/api/controller/contact_events.go +++ b/api/controller/contact_events.go @@ -8,14 +8,16 @@ import ( "github.com/moira-alert/moira/api/dto" ) -func GetContactEventsByIdWithLimit(database moira.Database, contactID string, from int64, to int64) (*dto.ContactEventItemList, *api.ErrorResponse) { - events, err := database.GetNotificationsByContactIdWithLimit(contactID, from, to) +// GetContactEventsHistoryByID is a controller that fetches events from database by using moira.Database.GetNotificationsHistoryByContactID. +func GetContactEventsHistoryByID(database moira.Database, contactID string, from, to, page, size int64, +) (*dto.ContactEventItemList, *api.ErrorResponse) { + events, err := database.GetNotificationsHistoryByContactID(contactID, from, to, page, size) if err != nil { - return nil, api.ErrorInternalServer(fmt.Errorf("GetContactEventsByIdWithLimit: can't get notifications for contact with id %v", contactID)) + return nil, api.ErrorInternalServer(fmt.Errorf("GetContactEventsHistoryByID: can't get notifications for contact with id %v", contactID)) } eventsList := dto.ContactEventItemList{ - List: make([]dto.ContactEventItem, 0), + List: make([]dto.ContactEventItem, 0, len(events)), } for _, i := range events { contactEventItem := &dto.ContactEventItem{ diff --git a/api/controller/contact_events_test.go b/api/controller/contact_events_test.go index 9c594dae1..c208beaf6 100644 --- a/api/controller/contact_events_test.go +++ b/api/controller/contact_events_test.go @@ -76,12 +76,14 @@ func TestGetContactEventsByIdWithLimit(t *testing.T) { defaultToParameter := now.Unix() defaultFromParameter := defaultToParameter - int64((3 * time.Hour).Seconds()) + defaultPage := int64(0) + defaultSize := int64(100) Convey("Ensure that request with default parameters would return both event items (no url params specified)", t, func() { dataBase.EXPECT().GetContact(contact.ID).Return(contactExpect, nil).AnyTimes() - dataBase.EXPECT().GetNotificationsByContactIdWithLimit(contact.ID, defaultFromParameter, defaultToParameter).Return(items, nil) + dataBase.EXPECT().GetNotificationsHistoryByContactId(contact.ID, defaultFromParameter, defaultToParameter, defaultPage, defaultSize).Return(items, nil) - actualEvents, err := GetContactEventsByIdWithLimit(dataBase, contact.ID, defaultFromParameter, defaultToParameter) + actualEvents, err := GetContactEventsHistoryByID(dataBase, contact.ID, defaultFromParameter, defaultToParameter, defaultPage, defaultSize) So(err, ShouldBeNil) So(actualEvents, ShouldResemble, &itemsExpected) @@ -89,9 +91,9 @@ func TestGetContactEventsByIdWithLimit(t *testing.T) { Convey("Ensure that request with only 'from' parameter given and 'to' default will return only one (newest) event", t, func() { dataBase.EXPECT().GetContact(contact.ID).Return(contactExpect, nil).AnyTimes() - dataBase.EXPECT().GetNotificationsByContactIdWithLimit(contact.ID, defaultFromParameter-20, defaultToParameter).Return(items[:1], nil) + dataBase.EXPECT().GetNotificationsHistoryByContactId(contact.ID, defaultFromParameter-20, defaultToParameter, defaultPage, defaultSize).Return(items[:1], nil) - actualEvents, err := GetContactEventsByIdWithLimit(dataBase, contact.ID, defaultFromParameter-20, defaultToParameter) + actualEvents, err := GetContactEventsHistoryByID(dataBase, contact.ID, defaultFromParameter-20, defaultToParameter, defaultPage, defaultSize) So(err, ShouldBeNil) So(actualEvents, ShouldResemble, &dto.ContactEventItemList{ List: []dto.ContactEventItem{ @@ -102,9 +104,9 @@ func TestGetContactEventsByIdWithLimit(t *testing.T) { Convey("Ensure that request with only 'to' parameter given and 'from' default will return only one (oldest) event", t, func() { dataBase.EXPECT().GetContact(contact.ID).Return(contactExpect, nil).AnyTimes() - dataBase.EXPECT().GetNotificationsByContactIdWithLimit(contact.ID, defaultFromParameter, defaultToParameter-30).Return(items[1:], nil) + dataBase.EXPECT().GetNotificationsHistoryByContactId(contact.ID, defaultFromParameter, defaultToParameter-30, defaultPage, defaultSize).Return(items[1:], nil) - actualEvents, err := GetContactEventsByIdWithLimit(dataBase, contact.ID, defaultFromParameter, defaultToParameter-30) + actualEvents, err := GetContactEventsHistoryByID(dataBase, contact.ID, defaultFromParameter, defaultToParameter-30, defaultPage, defaultSize) So(err, ShouldBeNil) So(actualEvents, ShouldResemble, &dto.ContactEventItemList{ List: []dto.ContactEventItem{ diff --git a/api/handler/constants.go b/api/handler/constants.go new file mode 100644 index 000000000..413204120 --- /dev/null +++ b/api/handler/constants.go @@ -0,0 +1,8 @@ +package handler + +const ( + contactEventsDefaultFrom = "-3hour" + contactEventsDefaultTo = "now" + contactEventsDefaultPage = 0 + contactEventsDefaultSize = -1 +) diff --git a/api/handler/contact_events.go b/api/handler/contact_events.go index 7c57b409e..d5e90333a 100644 --- a/api/handler/contact_events.go +++ b/api/handler/contact_events.go @@ -20,7 +20,10 @@ func contactEvents(router chi.Router) { router.Route("/{contactId}/events", func(router chi.Router) { router.Use(middleware.ContactContext) router.Use(contactFilter) - router.With(middleware.DateRange("-3hour", "now")).Get("/", getContactByIdWithEvents) + router.With( + middleware.DateRange(contactEventsDefaultFrom, contactEventsDefaultTo), + middleware.Paginate(contactEventsDefaultPage, contactEventsDefaultSize), + ).Get("/", getContactEventHistoryByID) }) } @@ -30,9 +33,11 @@ func contactEvents(router chi.Router) { // @id get-contact-events-by-id // @tags contact // @produce json -// @param contactID path string true "Contact ID" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) -// @param from query string false "Start time of the time range" default(-3hour) -// @param to query string false "End time of the time range" default(now) +// @param contactID path string true "Contact ID" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) +// @param from query string false "Start time of the time range" default(-3hour) +// @param to query string false "End time of the time range" default(now) +// @param size query int false "Number of items to return or all items if size == -1 (if size == -1 p should be zero for correct work)" default(100) +// @param p query int false "Defines the index of data portion (combined with size). E.g, p=2, size=100 will return records from 200 (including), to 300 (not including)" default(0) // @success 200 {object} dto.ContactEventItemList "Successfully received contact events" // @failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" // @failure 403 {object} api.ErrorForbiddenExample "Forbidden" @@ -40,7 +45,7 @@ func contactEvents(router chi.Router) { // @failure 422 {object} api.ErrorRenderExample "Render error" // @failure 500 {object} api.ErrorInternalServerExample "Internal server error" // @router /contact/{contactID}/events [get] -func getContactByIdWithEvents(writer http.ResponseWriter, request *http.Request) { +func getContactEventHistoryByID(writer http.ResponseWriter, request *http.Request) { contactData := request.Context().Value(contactKey).(moira.ContactData) fromStr := middleware.GetFromStr(request) toStr := middleware.GetToStr(request) @@ -54,7 +59,14 @@ func getContactByIdWithEvents(writer http.ResponseWriter, request *http.Request) render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse to: %v", to))) //nolint return } - contactWithEvents, err := controller.GetContactEventsByIdWithLimit(database, contactData.ID, from, to) + + contactWithEvents, err := controller.GetContactEventsHistoryByID( + database, + contactData.ID, + from, + to, + middleware.GetPage(request), + middleware.GetSize(request)) if err != nil { render.Render(writer, request, err) //nolint } diff --git a/cmd/api/config.go b/cmd/api/config.go index bfd5ca3eb..4647bb3f1 100644 --- a/cmd/api/config.go +++ b/cmd/api/config.go @@ -4,8 +4,6 @@ import ( "time" "github.com/moira-alert/moira" - "github.com/moira-alert/moira/notifier" - "github.com/xiam/to" "github.com/moira-alert/moira/api" @@ -206,8 +204,7 @@ func getDefault() config { MaxRetries: 3, }, NotificationHistory: cmd.NotificationHistoryConfig{ - NotificationHistoryTTL: "48h", - NotificationHistoryQueryLimit: int(notifier.NotificationsLimitUnlimited), + NotificationHistoryTTL: "48h", }, Logger: cmd.LoggerConfig{ LogFile: "stdout", diff --git a/cmd/api/config_test.go b/cmd/api/config_test.go index 782fcf301..4777e70ea 100644 --- a/cmd/api/config_test.go +++ b/cmd/api/config_test.go @@ -110,8 +110,7 @@ func Test_webConfig_getDefault(t *testing.T) { }, Remotes: cmd.RemotesConfig{}, NotificationHistory: cmd.NotificationHistoryConfig{ - NotificationHistoryTTL: "48h", - NotificationHistoryQueryLimit: -1, + NotificationHistoryTTL: "48h", }, } diff --git a/cmd/cli/config.go b/cmd/cli/config.go index 0870ecaf1..5e3ac3997 100644 --- a/cmd/cli/config.go +++ b/cmd/cli/config.go @@ -13,11 +13,12 @@ type config struct { } type cleanupConfig struct { - Whitelist []string `yaml:"whitelist"` - Delete bool `yaml:"delete"` - AddAnonymousToWhitelist bool `json:"add_anonymous_to_whitelist"` - CleanupMetricsDuration string `yaml:"cleanup_metrics_duration"` - CleanupFutureMetricsDuration string `yaml:"cleanup_future_metrics_duration"` + Whitelist []string `yaml:"whitelist"` + Delete bool `yaml:"delete"` + AddAnonymousToWhitelist bool `json:"add_anonymous_to_whitelist"` + CleanupMetricsDuration string `yaml:"cleanup_metrics_duration"` + CleanupFutureMetricsDuration string `yaml:"cleanup_future_metrics_duration"` + CleanupNotificationHistoryDuration string `yaml:"cleanup_notification_history_duration"` } func getDefault() config { @@ -31,9 +32,10 @@ func getDefault() config { DialTimeout: "500ms", }, Cleanup: cleanupConfig{ - Whitelist: []string{}, - CleanupMetricsDuration: "-168h", - CleanupFutureMetricsDuration: "60m", + Whitelist: []string{}, + CleanupMetricsDuration: "-168h", + CleanupFutureMetricsDuration: "60m", + CleanupNotificationHistoryDuration: "48h", }, } } diff --git a/cmd/cli/from_2.12_to_2.13.go b/cmd/cli/from_2.12_to_2.13.go new file mode 100644 index 000000000..a7d40260e --- /dev/null +++ b/cmd/cli/from_2.12_to_2.13.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + + "github.com/moira-alert/moira" +) + +func updateFrom212(logger moira.Logger, database moira.Database) error { + logger.Info().Msg("Update 2.12 -> 2.13 was started") + + ctx := context.Background() + err := splitNotificationHistoryByContactID(ctx, logger, database) + if err != nil { + return err + } + + logger.Info().Msg("Update 2.12 -> 2.13 was finished") + return nil +} + +func downgradeTo212(logger moira.Logger, database moira.Database) error { + logger.Info().Msg("Downgrade 2.13 -> 2.12 started") + + err := mergeNotificationHistory(logger, database) + if err != nil { + return err + } + + logger.Info().Msg("Downgrade 2.13 -> 2.12 was finished") + return nil +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 54a054c96..e1b6bf899 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -25,7 +25,7 @@ var ( GoVersion = "unknown" ) -var moiraValidVersions = []string{"2.3", "2.6", "2.7", "2.9", "2.11"} +var moiraValidVersions = []string{"2.3", "2.6", "2.7", "2.9", "2.11", "2.12"} var ( configFileName = flag.String("config", "/etc/moira/cli.yml", "Path to configuration file") @@ -48,16 +48,17 @@ var plotting = flag.Bool("plotting", false, "enable images in all notifications" var removeSubscriptions = flag.String("remove-subscriptions", "", "Remove given subscriptions separated by semicolons.") var ( - cleanupUsers = flag.Bool("cleanup-users", false, "Disable/delete contacts and subscriptions of missing users") - cleanupLastChecks = flag.Bool("cleanup-last-checks", false, "Delete abandoned triggers last checks.") - cleanupTags = flag.Bool("cleanup-tags", false, "Delete abandoned tags.") - cleanupMetrics = flag.Bool("cleanup-metrics", false, "Delete outdated metrics.") - cleanupPatternMetrics = flag.Bool("cleanup-pattern-metrics", false, "Delete outdated pattern metrics.") - cleanupFutureMetrics = flag.Bool("cleanup-future-metrics", false, "Delete metrics with future timestamps.") - cleanupRetentions = flag.Bool("cleanup-retentions", false, "Delete abandoned retentions.") - userDel = flag.String("user-del", "", "Delete all contacts and subscriptions for a user") - fromUser = flag.String("from-user", "", "Transfer subscriptions and contacts from user.") - toUser = flag.String("to-user", "", "Transfer subscriptions and contacts to user.") + cleanupUsers = flag.Bool("cleanup-users", false, "Disable/delete contacts and subscriptions of missing users") + cleanupLastChecks = flag.Bool("cleanup-last-checks", false, "Delete abandoned triggers last checks.") + cleanupTags = flag.Bool("cleanup-tags", false, "Delete abandoned tags.") + cleanupMetrics = flag.Bool("cleanup-metrics", false, "Delete outdated metrics.") + cleanupPatternMetrics = flag.Bool("cleanup-pattern-metrics", false, "Delete outdated pattern metrics.") + cleanupFutureMetrics = flag.Bool("cleanup-future-metrics", false, "Delete metrics with future timestamps.") + cleanupRetentions = flag.Bool("cleanup-retentions", false, "Delete abandoned retentions.") + cleanupNotificationHistory = flag.Bool("cleanup-notification-history", false, "Delete notifications which were created more then ttl ago from history for each contact (ttl is set in config)") + userDel = flag.String("user-del", "", "Delete all contacts and subscriptions for a user") + fromUser = flag.String("from-user", "", "Transfer subscriptions and contacts from user.") + toUser = flag.String("to-user", "", "Transfer subscriptions and contacts to user.") ) var ( @@ -117,6 +118,13 @@ func main() { //nolint Error(err). Msg("Fail to update from version 2.11") } + case "2.12": + err := updateFrom212(logger, database) + if err != nil { + logger.Fatal(). + Error(err). + Msg("Fail to update from version 2.12") + } } } @@ -158,6 +166,13 @@ func main() { //nolint Error(err). Msg("Fail to update to version 2.11") } + case "2.12": + err := downgradeTo212(logger, database) + if err != nil { + logger.Fatal(). + Error(err). + Msg("Fail to update to version 2.12") + } } } @@ -411,6 +426,25 @@ func main() { //nolint Int("deleted", deleted). Msg("Deletion of subscriptions finished") } + + if *cleanupNotificationHistory { + logger.Info(). + Msg("Start cleaning up of notification history") + + ttl := int64(to.Duration(confCleanup.CleanupNotificationHistoryDuration).Seconds()) + logger.Info(). + Int64("cleanup_notification_history_duration", ttl). + Msg("cleaning history older then") + + if err := handleCleanupNotificationHistoryWithTTL(database, ttl); err != nil { + logger.Error(). + Error(err). + Msg("Failed to clean up notification history") + } + + logger.Info(). + Msg("Cleanup of notification history finished") + } } func GetDumpBriefInfo(dump *dto.TriggerDump) string { diff --git a/cmd/cli/notification_history.go b/cmd/cli/notification_history.go new file mode 100644 index 000000000..32502b4b8 --- /dev/null +++ b/cmd/cli/notification_history.go @@ -0,0 +1,244 @@ +package main + +import ( + "context" + "fmt" + "strconv" + + goredis "github.com/go-redis/redis/v8" + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/database/redis" +) + +const ( + contactNotificationKey = "moira-contact-notifications" +) + +func splitNotificationHistoryByContactID(ctx context.Context, logger moira.Logger, database moira.Database) error { + logger.Info().Msg("Start splitNotificationHistoryByContactID") + + switch d := database.(type) { + case *redis.DbConnector: + client := d.Client() + var splitCount int64 + + pipe := client.TxPipeline() + + iterator := client.ZScan(ctx, contactNotificationKey, 0, "", 0).Iterator() + for iterator.Next(ctx) { + eventStr := iterator.Val() + + // On 1, 3, 5, ... indexes witch have scores, not json + _, err := strconv.Atoi(eventStr) + if err == nil { + continue + } + + notification, err := redis.GetNotificationStruct(eventStr) + if err != nil { + return fmt.Errorf("failed to deserialize event: %w", err) + } + + notificationBytes, err := redis.GetNotificationBytes(¬ification) + if err != nil { + return fmt.Errorf("failed to serialize event: %w", err) + } + + pipe.ZAdd( + ctx, + contactNotificationKeyWithID(notification.ContactID), + &goredis.Z{ + Score: float64(notification.TimeStamp), + Member: notificationBytes, + }) + splitCount += 1 + } + + err := iterator.Err() + if err != nil { + return fmt.Errorf("error while iterating over notification history: %w", err) + } + + _, err = pipe.Exec(ctx) + if err != nil { + return fmt.Errorf("error while applying changes: %w", err) + } + + client.Del(ctx, contactNotificationKey) + + logger.Info(). + Int64("split_events", splitCount). + Msg("Number of contact notifications divided into separate keys") + + default: + return makeUnknownDBError(database) + } + + logger.Info().Msg("Successfully finished splitNotificationHistoryByContactID") + + return nil +} + +func mergeNotificationHistory(logger moira.Logger, database moira.Database) error { + logger.Info().Msg("Start mergeNotificationHistory") + + switch d := database.(type) { + case *redis.DbConnector: + if err := callFunc(d, func(connector *redis.DbConnector, client goredis.UniversalClient) error { + contactIDs, err := scanContactIDs(connector.Context(), client) + if err != nil { + return err + } + + if len(contactIDs) == 0 { + return nil + } + + events, err := fetchNotificationHistoryFromRedisNode(connector, client, logger, contactIDs) + if err != nil { + return err + } + + _, err = d.Client().Pipelined(connector.Context(), func(pipe goredis.Pipeliner) error { + for _, event := range events { + var eventBytes []byte + + eventBytes, err = redis.GetNotificationBytes(&event) + if err != nil { + return fmt.Errorf("failed to serialize notification event: %w", err) + } + + pipe.ZAdd(d.Context(), contactNotificationKey, &goredis.Z{ + Score: float64(event.TimeStamp), + Member: eventBytes, + }) + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to add notification history: %w", err) + } + + logger.Info(). + Msg("successfully added history") + + cmds, err := client.Pipelined(connector.Context(), func(pipe goredis.Pipeliner) error { + for _, id := range contactIDs { + pipe.Del(connector.Context(), id) + } + return nil + }) + if err != nil { + return fmt.Errorf("failed to delete previous notification history: %w", err) + } + + var totalDelCount int64 + for i, cmd := range cmds { + deleted, err := cmd.(*goredis.IntCmd).Result() + if err != nil { + logger.Warning(). + String("fail_contact_key", contactIDs[i]). + Error(err). + Msg("failed to delete") + } + totalDelCount += deleted + } + + logger.Info(). + Int64("delete_count", totalDelCount). + Msg("Number of deleted notification history events from node") + + return nil + }); err != nil { + return err + } + + default: + return makeUnknownDBError(database) + } + + logger.Info().Msg("Successfully finished mergeNotificationHistory") + + return nil +} + +func fetchNotificationHistoryFromRedisNode(connector *redis.DbConnector, client goredis.UniversalClient, logger moira.Logger, contactIDs []string) ([]moira.NotificationEventHistoryItem, error) { + ctx := connector.Context() + + logger.Info(). + Int("contact_ids", len(contactIDs)). + Msg("Number of contacts in notifications history") + + if len(contactIDs) == 0 { + return make([]moira.NotificationEventHistoryItem, 0), nil + } + + var eventStrings []string + + for _, id := range contactIDs { + iterator := client.ZScan(ctx, id, 0, "", 0).Iterator() + for iterator.Next(ctx) { + eventStr := iterator.Val() + + // On 1, 3, 5, ... indexes witch have scores, not json + _, err := strconv.Atoi(eventStr) + if err == nil { + continue + } + + eventStrings = append(eventStrings, eventStr) + } + + if err := iterator.Err(); err != nil { + return nil, fmt.Errorf("error while iterating over contact with id: %s, error: %w", id, err) + } + } + + notificationEvents, err := deserializeEvents(eventStrings) + if err != nil { + return nil, err + } + + return notificationEvents, nil +} + +func scanContactIDs(ctx context.Context, client goredis.UniversalClient) ([]string, error) { + var contactIDs []string + + iterator := client.Scan(ctx, 0, contactNotificationKeyWithID("*"), 0).Iterator() + for iterator.Next(ctx) { + contactIDs = append(contactIDs, iterator.Val()) + } + + err := iterator.Err() + if err != nil { + return nil, fmt.Errorf("error while iterating over notification history: %w", err) + } + + return contactIDs, nil +} + +func contactNotificationKeyWithID(contactID string) string { + return contactNotificationKey + ":" + contactID +} + +func handleCleanupNotificationHistoryWithTTL(db moira.Database, ttl int64) error { + err := db.CleanUpOutdatedNotificationHistory(ttl) + if err != nil { + return fmt.Errorf("database error: %w", err) + } + return nil +} + +func deserializeEvents(eventStrings []string) ([]moira.NotificationEventHistoryItem, error) { + notificationEvents := make([]moira.NotificationEventHistoryItem, 0, len(eventStrings)) + for _, str := range eventStrings { + notification, err := redis.GetNotificationStruct(str) + if err != nil { + return nil, fmt.Errorf("failed to deserialize notification events: %w", err) + } + + notificationEvents = append(notificationEvents, notification) + } + return notificationEvents, nil +} diff --git a/cmd/cli/notification_history_test.go b/cmd/cli/notification_history_test.go new file mode 100644 index 000000000..6eaedf084 --- /dev/null +++ b/cmd/cli/notification_history_test.go @@ -0,0 +1,375 @@ +package main + +import ( + "context" + "fmt" + "testing" + "time" + + goredis "github.com/go-redis/redis/v8" + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/database/redis" + logging "github.com/moira-alert/moira/logging/zerolog_adapter" + . "github.com/smartystreets/goconvey/convey" +) + +var testTimestamp = time.Now().Unix() + +const ( + defaultTestMetric = "test.notification.events" +) + +var testNotificationHistoryEvents = []*moira.NotificationEventHistoryItem{ + { + TimeStamp: testTimestamp - 1, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-4", + }, + { + TimeStamp: testTimestamp, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-1", + }, + { + TimeStamp: testTimestamp + 1, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-2", + }, + { + TimeStamp: testTimestamp + 2, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-3", + }, + { + TimeStamp: testTimestamp + 3, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-3", + }, + { + TimeStamp: testTimestamp + 4, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-2", + }, + { + TimeStamp: testTimestamp + 5, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-3", + }, +} + +var additionalTestNotificationHistoryEvents = []*moira.NotificationEventHistoryItem{ + { + TimeStamp: testTimestamp + 6, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-4", + }, + { + TimeStamp: testTimestamp + 7, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-5", + }, +} + +func TestSplitNotificationHistory(t *testing.T) { + conf := getDefault() + logger, err := logging.ConfigureLog(conf.LogFile, conf.LogLevel, "test", conf.LogPrettyFormat) + if err != nil { + t.Fatal(err) + } + db := redis.NewTestDatabase(logger) + db.Flush() + defer db.Flush() + + ctx := context.Background() + client := db.Client() + + Convey("Test split notification history", t, func() { + Convey("with empty contactNotificationKey", func() { + err = splitNotificationHistoryByContactID(ctx, logger, db) + So(err, ShouldBeNil) + + keys, err := client.Keys(ctx, contactNotificationKeyWithID("*")).Result() + So(err, ShouldBeNil) + So(keys, ShouldHaveLength, 0) + }) + + Convey("with empty split history", func() { + toInsert, err := prepareNotSplitItemsToInsert(testNotificationHistoryEvents) + So(err, ShouldBeNil) + + err = storeNotificationHistoryBySingleKey(ctx, db, toInsert) + So(err, ShouldBeNil) + + eventsMap := eventsByKey(testNotificationHistoryEvents) + + testSplitNotificationHistory(ctx, db, logger, eventsMap) + + db.Flush() + }) + + Convey("with not empty split history", func() { + toInsert, err := prepareNotSplitItemsToInsert(testNotificationHistoryEvents) + So(err, ShouldBeNil) + + err = storeNotificationHistoryBySingleKey(ctx, db, toInsert) + So(err, ShouldBeNil) + + toInsertMap, err := prepareSplitItemsToInsert(eventsByKey(additionalTestNotificationHistoryEvents)) + So(err, ShouldBeNil) + + err = storeSplitNotifications(ctx, db, toInsertMap) + So(err, ShouldBeNil) + + testSplitNotificationHistory( + ctx, db, logger, eventsByKey(append(testNotificationHistoryEvents, additionalTestNotificationHistoryEvents...))) + + db.Flush() + }) + }) +} + +func testSplitNotificationHistory( + ctx context.Context, + db *redis.DbConnector, + logger moira.Logger, + eventsMap map[string][]*moira.NotificationEventHistoryItem, +) { + client := db.Client() + + Convey("with prepared history", func() { + errExists := splitNotificationHistoryByContactID(ctx, logger, db) + So(errExists, ShouldBeNil) + + for contactID, expectedEvents := range eventsMap { + Convey(fmt.Sprintf("check contact with id: %s", contactID), func() { + gotEvents, errAfterZRange := client.ZRangeByScore( + ctx, + contactNotificationKeyWithID(contactID), + &goredis.ZRangeBy{ + Min: "-inf", + Max: "+inf", + Offset: 0, + Count: -1, + }).Result() + So(errAfterZRange, ShouldBeNil) + So(gotEvents, ShouldHaveLength, len(expectedEvents)) + + for i, gotEventStr := range gotEvents { + notificationEvent, err := redis.GetNotificationStruct(gotEventStr) + So(err, ShouldBeNil) + So(notificationEvent, ShouldResemble, *expectedEvents[i]) + } + }) + } + + res, errExists := client.Exists(ctx, contactNotificationKey).Result() + So(errExists, ShouldBeNil) + So(res, ShouldEqual, 0) + }) +} + +func TestMergeNotificationHistory(t *testing.T) { + conf := getDefault() + logger, err := logging.ConfigureLog(conf.LogFile, conf.LogLevel, "test", conf.LogPrettyFormat) + if err != nil { + t.Fatal(err) + } + db := redis.NewTestDatabase(logger) + db.Flush() + defer db.Flush() + + ctx := context.Background() + client := db.Client() + + Convey("Test merge notification history", t, func() { + Convey("with empty database", func() { + err = mergeNotificationHistory(logger, db) + So(err, ShouldBeNil) + + keys, err := client.Keys(ctx, contactNotificationKey).Result() + So(err, ShouldBeNil) + So(keys, ShouldHaveLength, 0) + }) + + Convey("with empty history by single key", func() { + eventsMap := eventsByKey(testNotificationHistoryEvents) + + toInsertMap, err := prepareSplitItemsToInsert(eventsMap) + So(err, ShouldBeNil) + + err = storeSplitNotifications(ctx, db, toInsertMap) + So(err, ShouldBeNil) + + testMergeNotificationHistory(ctx, db, logger, testNotificationHistoryEvents) + + db.Flush() + }) + + Convey("with not empty history by single key", func() { + eventsMap := eventsByKey(testNotificationHistoryEvents) + + toInsertMap, err := prepareSplitItemsToInsert(eventsMap) + So(err, ShouldBeNil) + + err = storeSplitNotifications(ctx, db, toInsertMap) + So(err, ShouldBeNil) + + toInsert, err := prepareNotSplitItemsToInsert(additionalTestNotificationHistoryEvents) + So(err, ShouldBeNil) + + err = storeNotificationHistoryBySingleKey(ctx, db, toInsert) + So(err, ShouldBeNil) + + testMergeNotificationHistory( + ctx, db, logger, append(testNotificationHistoryEvents, additionalTestNotificationHistoryEvents...)) + + db.Flush() + }) + }) +} + +func testMergeNotificationHistory( + ctx context.Context, + db *redis.DbConnector, + logger moira.Logger, + eventsList []*moira.NotificationEventHistoryItem, +) { + client := db.Client() + + Convey("with split history", func() { + err := mergeNotificationHistory(logger, db) + So(err, ShouldBeNil) + + gotEventsStrs, errAfterZRange := client.ZRangeByScore( + ctx, + contactNotificationKey, + &goredis.ZRangeBy{ + Min: "-inf", + Max: "+inf", + Offset: 0, + Count: -1, + }).Result() + So(errAfterZRange, ShouldBeNil) + So(gotEventsStrs, ShouldHaveLength, len(eventsList)) + + for i, gotEventStr := range gotEventsStrs { + notificationEvent, errDeserialize := redis.GetNotificationStruct(gotEventStr) + So(errDeserialize, ShouldBeNil) + So(notificationEvent, ShouldResemble, *eventsList[i]) + } + + contactKeys, errAfterKeys := client.Keys(ctx, contactNotificationKeyWithID("*")).Result() + So(errAfterKeys, ShouldBeNil) + So(contactKeys, ShouldHaveLength, 0) + }) +} + +func prepareNotSplitItemsToInsert(notificationEvents []*moira.NotificationEventHistoryItem) ([]*goredis.Z, error) { + resList := make([]*goredis.Z, 0, len(notificationEvents)) + for _, notificationEvent := range notificationEvents { + toInsert, err := toInsertableItem(notificationEvent) + if err != nil { + return nil, err + } + resList = append(resList, toInsert) + } + return resList, nil +} + +func toInsertableItem(notificationEvent *moira.NotificationEventHistoryItem) (*goredis.Z, error) { + notificationBytes, err := redis.GetNotificationBytes(notificationEvent) + if err != nil { + return nil, err + } + return &goredis.Z{Score: float64(notificationEvent.TimeStamp), Member: notificationBytes}, nil +} + +func storeNotificationHistoryBySingleKey(ctx context.Context, database moira.Database, toInsert []*goredis.Z) error { + switch db := database.(type) { + case *redis.DbConnector: + client := db.Client() + + _, err := client.ZAdd(ctx, contactNotificationKey, toInsert...).Result() + if err != nil { + return err + } + default: + return makeUnknownDBError(database) + } + + return nil +} + +func eventsByKey(notificationEvents []*moira.NotificationEventHistoryItem) map[string][]*moira.NotificationEventHistoryItem { + statistics := make(map[string][]*moira.NotificationEventHistoryItem, len(notificationEvents)) + for _, event := range notificationEvents { + statistics[event.ContactID] = append(statistics[event.ContactID], event) + } + return statistics +} + +func prepareSplitItemsToInsert(eventsMap map[string][]*moira.NotificationEventHistoryItem) (map[string][]*goredis.Z, error) { + resMap := make(map[string][]*goredis.Z, len(eventsMap)) + for contactID, notificationEvents := range eventsMap { + for _, notificationEvent := range notificationEvents { + toInsert, err := toInsertableItem(notificationEvent) + if err != nil { + return nil, err + } + resMap[contactID] = append(resMap[contactID], toInsert) + } + } + return resMap, nil +} + +func storeSplitNotifications(ctx context.Context, database moira.Database, toInsertMap map[string][]*goredis.Z) error { + switch db := database.(type) { + case *redis.DbConnector: + client := db.Client() + + pipe := client.TxPipeline() + + for contactID, insertItems := range toInsertMap { + key := contactNotificationKeyWithID(contactID) + for _, z := range insertItems { + pipe.ZAdd(ctx, key, z) + } + } + + _, err := pipe.Exec(ctx) + if err != nil { + return err + } + default: + return makeUnknownDBError(database) + } + + return nil +} diff --git a/cmd/config.go b/cmd/config.go index 7a5ce2321..bb83ea439 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -80,15 +80,12 @@ func (config *RedisConfig) GetSettings() redis.DatabaseConfig { type NotificationHistoryConfig struct { // Time which moira should store contacts and theirs events history NotificationHistoryTTL string `yaml:"ttl"` - // Max count of events which moira may send as response of contact and its events history - NotificationHistoryQueryLimit int `yaml:"query_limit"` } // GetSettings returns notification history storage policy configuration. func (notificationHistoryConfig *NotificationHistoryConfig) GetSettings() redis.NotificationHistoryConfig { return redis.NotificationHistoryConfig{ - NotificationHistoryTTL: to.Duration(notificationHistoryConfig.NotificationHistoryTTL), - NotificationHistoryQueryLimit: notificationHistoryConfig.NotificationHistoryQueryLimit, + NotificationHistoryTTL: to.Duration(notificationHistoryConfig.NotificationHistoryTTL), } } diff --git a/cmd/notifier/config.go b/cmd/notifier/config.go index 8277024b9..eb737a798 100644 --- a/cmd/notifier/config.go +++ b/cmd/notifier/config.go @@ -92,8 +92,7 @@ func getDefault() config { LogPrettyFormat: false, }, NotificationHistory: cmd.NotificationHistoryConfig{ - NotificationHistoryTTL: "48h", - NotificationHistoryQueryLimit: int(notifier.NotificationsLimitUnlimited), + NotificationHistoryTTL: "48h", }, Notification: cmd.NotificationConfig{ DelayedTime: "50s", diff --git a/database/redis/config.go b/database/redis/config.go index 7c840c15b..9ad11e26b 100644 --- a/database/redis/config.go +++ b/database/redis/config.go @@ -21,8 +21,7 @@ type DatabaseConfig struct { } type NotificationHistoryConfig struct { - NotificationHistoryTTL time.Duration - NotificationHistoryQueryLimit int + NotificationHistoryTTL time.Duration } // Notifier configuration in redis. diff --git a/database/redis/contact_notification_history.go b/database/redis/contact_notification_history.go index 6b15c27aa..dc3cb53d9 100644 --- a/database/redis/contact_notification_history.go +++ b/database/redis/contact_notification_history.go @@ -12,45 +12,56 @@ import ( const contactNotificationKey = "moira-contact-notifications" -func getNotificationBytes(notification *moira.NotificationEventHistoryItem) ([]byte, error) { +// GetNotificationBytes marshals moira.NotificationHistoryItem to json. +func GetNotificationBytes(notification *moira.NotificationEventHistoryItem) ([]byte, error) { bytes, err := json.Marshal(notification) if err != nil { - return nil, fmt.Errorf("failed to marshal notification event: %s", err.Error()) + return nil, fmt.Errorf("failed to marshal notification event: %w", err) } return bytes, nil } -func getNotificationStruct(notificationString string) (moira.NotificationEventHistoryItem, error) { +// GetNotificationStruct unmarshals moira.NotificationEventHistoryItem from json represented by string. +func GetNotificationStruct(notificationString string) (moira.NotificationEventHistoryItem, error) { var object moira.NotificationEventHistoryItem err := json.Unmarshal([]byte(notificationString), &object) if err != nil { - return object, fmt.Errorf("failed to umarshall event: %s", err.Error()) + return object, fmt.Errorf("failed to umarshal event: %w", err) } return object, nil } -func (connector *DbConnector) GetNotificationsByContactIdWithLimit(contactID string, from int64, to int64) ([]*moira.NotificationEventHistoryItem, error) { +func contactNotificationKeyWithID(contactID string) string { + return contactNotificationKey + ":" + contactID +} + +// GetNotificationsHistoryByContactID returns `size` (or all if `size` is -1) notification events with timestamp between `from` and `to`. +// The offset for fetching events may be changed by using `page` parameter, it is calculated as page * size. +func (connector *DbConnector) GetNotificationsHistoryByContactID(contactID string, from, to, page, size int64, +) ([]*moira.NotificationEventHistoryItem, error) { c := *connector.client - var notifications []*moira.NotificationEventHistoryItem - notificationStings, err := c.ZRangeByScore(connector.context, contactNotificationKey, &redis.ZRangeBy{ - Min: strconv.FormatInt(from, 10), - Max: strconv.FormatInt(to, 10), - Count: int64(connector.notificationHistory.NotificationHistoryQueryLimit), - }).Result() + notificationStings, err := c.ZRangeByScore( + connector.context, + contactNotificationKeyWithID(contactID), + &redis.ZRangeBy{ + Min: strconv.FormatInt(from, 10), + Max: strconv.FormatInt(to, 10), + Offset: page * size, + Count: size, + }).Result() if err != nil { - return notifications, err + return nil, err } + notifications := make([]*moira.NotificationEventHistoryItem, 0, len(notificationStings)) + for _, notification := range notificationStings { - notificationObj, err := getNotificationStruct(notification) + notificationObj, err := GetNotificationStruct(notification) if err != nil { return notifications, err } - - if notificationObj.ContactID == contactID { - notifications = append(notifications, ¬ificationObj) - } + notifications = append(notifications, ¬ificationObj) } return notifications, nil @@ -68,10 +79,10 @@ func (connector *DbConnector) PushContactNotificationToHistory(notification *moi TimeStamp: notification.Timestamp, } - notificationBytes, serializationErr := getNotificationBytes(notificationItemToSave) + notificationBytes, serializationErr := GetNotificationBytes(notificationItemToSave) if serializationErr != nil { - return fmt.Errorf("failed to serialize notification to contact event history item: %s", serializationErr.Error()) + return fmt.Errorf("failed to serialize notification to contact event history item: %w", serializationErr) } to := int(time.Now().Unix() - int64(connector.notificationHistory.NotificationHistoryTTL.Seconds())) @@ -80,7 +91,7 @@ func (connector *DbConnector) PushContactNotificationToHistory(notification *moi pipe.ZAdd( connector.context, - contactNotificationKey, + contactNotificationKeyWithID(notificationItemToSave.ContactID), &redis.Z{ Score: float64(notification.Timestamp), Member: notificationBytes, @@ -88,15 +99,64 @@ func (connector *DbConnector) PushContactNotificationToHistory(notification *moi pipe.ZRemRangeByScore( connector.context, - contactNotificationKey, + contactNotificationKeyWithID(notificationItemToSave.ContactID), "-inf", strconv.Itoa(to), ) _, err := pipe.Exec(connector.Context()) if err != nil { - return fmt.Errorf("failed to push contact event history item: %s", err.Error()) + return fmt.Errorf("failed to push contact event history item: %w", err) } return nil } + +// CleanUpOutdatedNotificationHistory is used for deleting notification history events which have been created more than ttl ago. +func (connector *DbConnector) CleanUpOutdatedNotificationHistory(ttl int64) error { + return connector.callFunc(func(dbConn *DbConnector, client redis.UniversalClient) error { + from := "-inf" + to := strconv.Itoa(int(time.Now().Unix() - ttl)) + + ctx := dbConn.Context() + + cmds, err := client.Pipelined(ctx, func(pipe redis.Pipeliner) error { + iterator := client.Scan(ctx, 0, contactNotificationKeyWithID("*"), 0).Iterator() + for iterator.Next(ctx) { + pipe.ZRemRangeByScore( + ctx, + iterator.Val(), + from, + to, + ) + } + + if err := iterator.Err(); err != nil { + return fmt.Errorf("failed to iterate over notification history keys: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("failed to pipeline deleting: %w", err) + } + + var totalDelCount int64 + + for _, cmd := range cmds { + count, err := cmd.(*redis.IntCmd).Result() + if err != nil { + connector.logger.Info(). + Error(err). + Msg("failed to remove outdated") + } + totalDelCount += count + } + + connector.logger.Info(). + Int64("delete_count", totalDelCount). + Msg("Cleaned up notification history") + + return nil + }) +} diff --git a/database/redis/contact_notification_history_test.go b/database/redis/contact_notification_history_test.go index b57e60c17..fed362967 100644 --- a/database/redis/contact_notification_history_test.go +++ b/database/redis/contact_notification_history_test.go @@ -1,20 +1,25 @@ package redis import ( + "fmt" "testing" "time" + "github.com/go-redis/redis/v8" + "github.com/moira-alert/moira" logging "github.com/moira-alert/moira/logging/zerolog_adapter" . "github.com/smartystreets/goconvey/convey" ) +const defaultTestMetric = "some_metric" + var inputScheduledNotification = moira.ScheduledNotification{ Event: moira.NotificationEvent{ IsTriggerEvent: true, Timestamp: time.Now().Unix(), - Metric: "some_metric", + Metric: defaultTestMetric, State: moira.StateERROR, OldState: moira.StateOK, TriggerID: "1111-2222-33-4444-5555", @@ -60,72 +65,160 @@ var eventsShouldBeInDb = []*moira.NotificationEventHistoryItem{ }, } -func TestGetNotificationsByContactIdWithLimit(t *testing.T) { +func TestGetNotificationsHistoryByContactID(t *testing.T) { logger, _ := logging.GetLogger("dataBase") dataBase := NewTestDatabase(logger) + var defaultPage int64 = 0 + var defaultSize int64 = 100 Convey("Notification history items manipulation", t, func() { dataBase.Flush() defer dataBase.Flush() Convey("While no data then notification items should be empty", func() { - items, err := dataBase.GetNotificationsByContactIdWithLimit( + items, err := dataBase.GetNotificationsHistoryByContactID( "id", eventsShouldBeInDb[0].TimeStamp, - eventsShouldBeInDb[0].TimeStamp) + eventsShouldBeInDb[0].TimeStamp, + defaultPage, + defaultSize) So(err, ShouldBeNil) So(items, ShouldHaveLength, 0) }) Convey("Write event and check for success write", func() { - err := dataBase.PushContactNotificationToHistory(&inputScheduledNotification) - So(err, ShouldBeNil) + errPushEvents := dataBase.PushContactNotificationToHistory(&inputScheduledNotification) + So(errPushEvents, ShouldBeNil) Convey("Ensure that we can find event on +- 5 seconds interval", func() { - eventFromDb, err := dataBase.GetNotificationsByContactIdWithLimit( + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( eventsShouldBeInDb[0].ContactID, eventsShouldBeInDb[0].TimeStamp-5, - eventsShouldBeInDb[0].TimeStamp+5) + eventsShouldBeInDb[0].TimeStamp+5, + defaultPage, + defaultSize) So(err, ShouldBeNil) So(eventFromDb, ShouldResemble, eventsShouldBeInDb) }) Convey("Ensure that we can find event exactly by its timestamp", func() { - eventFromDb, err := dataBase.GetNotificationsByContactIdWithLimit( + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( eventsShouldBeInDb[0].ContactID, eventsShouldBeInDb[0].TimeStamp, - eventsShouldBeInDb[0].TimeStamp) + eventsShouldBeInDb[0].TimeStamp, + defaultPage, + defaultSize) So(err, ShouldBeNil) So(eventFromDb, ShouldResemble, eventsShouldBeInDb) }) Convey("Ensure that we can find event if 'from' border equals its timestamp", func() { - eventFromDb, err := dataBase.GetNotificationsByContactIdWithLimit( + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( eventsShouldBeInDb[0].ContactID, eventsShouldBeInDb[0].TimeStamp, - eventsShouldBeInDb[0].TimeStamp+5) + eventsShouldBeInDb[0].TimeStamp+5, + defaultPage, + defaultSize) So(err, ShouldBeNil) So(eventFromDb, ShouldResemble, eventsShouldBeInDb) }) Convey("Ensure that we can find event if 'to' border equals its timestamp", func() { - eventFromDb, err := dataBase.GetNotificationsByContactIdWithLimit( + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( eventsShouldBeInDb[0].ContactID, eventsShouldBeInDb[0].TimeStamp-5, - eventsShouldBeInDb[0].TimeStamp) + eventsShouldBeInDb[0].TimeStamp, + defaultPage, + defaultSize) So(err, ShouldBeNil) So(eventFromDb, ShouldResemble, eventsShouldBeInDb) }) Convey("Ensure that we can't find event time borders don't fit event timestamp", func() { - eventFromDb, err := dataBase.GetNotificationsByContactIdWithLimit( + veryOldFrom := int64(928930626) // 09.06.1999, 12:17:06 + veryOldTo := int64(992089026) // 09.06.2001, 12:17:06 + + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( eventsShouldBeInDb[0].ContactID, - 928930626, - 992089026) + veryOldFrom, + veryOldTo, + defaultPage, + defaultSize) So(err, ShouldBeNil) So(eventFromDb, ShouldNotResemble, eventsShouldBeInDb) }) + + Convey("Ensure that with negative page and positive size empty slice returned", func() { + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( + eventsShouldBeInDb[0].ContactID, + eventsShouldBeInDb[0].TimeStamp, + eventsShouldBeInDb[0].TimeStamp, + -1, + 1) + So(err, ShouldBeNil) + So(eventFromDb, ShouldHaveLength, 0) + }) + + Convey("Ensure that with positive page and negative size empty slice returned", func() { + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( + eventsShouldBeInDb[0].ContactID, + eventsShouldBeInDb[0].TimeStamp, + eventsShouldBeInDb[0].TimeStamp, + 1, + -1) + So(err, ShouldBeNil) + So(eventFromDb, ShouldHaveLength, 0) + }) + + otherScheduledNotification := inputScheduledNotification + otherScheduledNotification.Timestamp += 1 + errPushEvents = dataBase.PushContactNotificationToHistory(&otherScheduledNotification) + So(errPushEvents, ShouldBeNil) + + Convey("Ensure that with page=0 size=1 returns first event", func() { + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( + eventsShouldBeInDb[0].ContactID, + eventsShouldBeInDb[0].TimeStamp-5, + eventsShouldBeInDb[0].TimeStamp+5, + 0, + 1) + So(err, ShouldBeNil) + So(eventFromDb, ShouldResemble, eventsShouldBeInDb) + }) + + otherEventShouldBeInDb := []*moira.NotificationEventHistoryItem{ + { + TimeStamp: otherScheduledNotification.Timestamp, + Metric: otherScheduledNotification.Event.Metric, + State: otherScheduledNotification.Event.State, + OldState: otherScheduledNotification.Event.OldState, + TriggerID: otherScheduledNotification.Trigger.ID, + ContactID: otherScheduledNotification.Contact.ID, + }, + } + + Convey("Ensure that with page=1 size=1 returns another event", func() { + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( + eventsShouldBeInDb[0].ContactID, + eventsShouldBeInDb[0].TimeStamp-5, + eventsShouldBeInDb[0].TimeStamp+5, + 1, + 1) + So(err, ShouldBeNil) + So(eventFromDb, ShouldResemble, otherEventShouldBeInDb) + }) + + Convey("Ensure that with page=0 size=-1 returns all events", func() { + eventFromDb, err := dataBase.GetNotificationsHistoryByContactID( + otherEventShouldBeInDb[0].ContactID, + eventsShouldBeInDb[0].TimeStamp, + otherEventShouldBeInDb[0].TimeStamp, + 0, + -1) + So(err, ShouldBeNil) + So(eventFromDb, ShouldHaveLength, 2) + }) }) }) } @@ -133,7 +226,6 @@ func TestGetNotificationsByContactIdWithLimit(t *testing.T) { func TestPushNotificationToHistory(t *testing.T) { logger, _ := logging.GetLogger("dataBase") dataBase := NewTestDatabase(logger) - dataBase.notificationHistory.NotificationHistoryQueryLimit = 500 Convey("Ensure that event would not have duplicates", t, func() { dataBase.Flush() @@ -145,12 +237,153 @@ func TestPushNotificationToHistory(t *testing.T) { err2 := dataBase.PushContactNotificationToHistory(&inputScheduledNotification) So(err2, ShouldBeNil) - dbContent, err3 := dataBase.GetNotificationsByContactIdWithLimit( + dbContent, err3 := dataBase.GetNotificationsHistoryByContactID( inputScheduledNotification.Contact.ID, inputScheduledNotification.Timestamp, - inputScheduledNotification.Timestamp) + inputScheduledNotification.Timestamp, + 0, + 100) So(err3, ShouldBeNil) So(dbContent, ShouldHaveLength, 1) }) } + +var ( + testTTL = int64(48 * time.Hour) + testNow = time.Now().Unix() +) + +var outdatedEvents = []*moira.NotificationEventHistoryItem{ + { + TimeStamp: testNow - testTTL - 1, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-1", + }, + { + TimeStamp: testNow - testTTL, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-2", + }, + { + TimeStamp: testNow - testTTL, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-1", + }, + { + TimeStamp: testNow - testTTL, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-3", + }, +} + +var notOutdatedEvents = []*moira.NotificationEventHistoryItem{ + { + TimeStamp: testNow, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-2", + }, + { + TimeStamp: testNow, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-1", + }, + { + TimeStamp: testNow, + Metric: defaultTestMetric, + State: moira.StateTEST, + OldState: "", + TriggerID: "", + ContactID: "contact-id-3", + }, +} + +func TestCleanUpOutdatedNotificationHistory(t *testing.T) { + logger, _ := logging.GetLogger("dataBase") + dataBase := NewTestDatabase(logger) + dataBase.Flush() + defer dataBase.Flush() + + Convey("Test clean up notification history", t, func() { + Convey("with empty database", func() { + err := dataBase.CleanUpOutdatedNotificationHistory(testTTL) + So(err, ShouldBeNil) + }) + + Convey("with prepared events", func() { + storeErr := storeOutdatedNotificationHistoryItems(dataBase, append(outdatedEvents, notOutdatedEvents...)) + So(storeErr, ShouldBeNil) + + err := dataBase.CleanUpOutdatedNotificationHistory(testTTL) + So(err, ShouldBeNil) + + client := dataBase.Client() + + contactIDs, errKeys := client.Keys(dataBase.context, contactNotificationKeyWithID("*")).Result() + So(errKeys, ShouldBeNil) + So(contactIDs, ShouldHaveLength, len(notOutdatedEvents)) + + eventsMap := toEventsMap(notOutdatedEvents) + + for _, contactID := range contactIDs { + Convey(fmt.Sprintf("for contact with id: %s", contactID), func() { + events, errGet := dataBase.GetNotificationsHistoryByContactID(contactID, testNow-testTTL, testNow, 0, -1) + So(errGet, ShouldBeNil) + So(events, ShouldHaveLength, len(eventsMap[contactID])) + + for i := range events { + So(events[i], ShouldResemble, eventsMap[contactID][i]) + } + }) + } + }) + }) +} + +func storeOutdatedNotificationHistoryItems(connector *DbConnector, notificationEvents []*moira.NotificationEventHistoryItem) error { + client := connector.Client() + + pipe := client.TxPipeline() + for _, notification := range notificationEvents { + notificationBytes, err := GetNotificationBytes(notification) + if err != nil { + return err + } + pipe.ZAdd( + connector.context, + contactNotificationKeyWithID(notification.ContactID), + &redis.Z{ + Score: float64(notification.TimeStamp), + Member: notificationBytes, + }) + } + + _, err := pipe.Exec(connector.context) + return err +} + +func toEventsMap(events []*moira.NotificationEventHistoryItem) map[string][]*moira.NotificationEventHistoryItem { + m := make(map[string][]*moira.NotificationEventHistoryItem, len(events)) + for _, event := range events { + m[event.ContactID] = append(m[event.ContactID], event) + } + return m +} diff --git a/database/redis/database.go b/database/redis/database.go index 0633b3098..5e3497080 100644 --- a/database/redis/database.go +++ b/database/redis/database.go @@ -95,8 +95,7 @@ func NewTestDatabase(logger moira.Logger) *DbConnector { Addrs: []string{"0.0.0.0:6379"}, }, NotificationHistoryConfig{ - NotificationHistoryTTL: time.Hour * 48, - NotificationHistoryQueryLimit: 1000, + NotificationHistoryTTL: time.Hour * 48, }, NotificationConfig{ DelayedTime: time.Minute, @@ -113,8 +112,7 @@ func NewTestDatabaseWithIncorrectConfig(logger moira.Logger) *DbConnector { return NewDatabase(logger, DatabaseConfig{Addrs: []string{"0.0.0.0:0000"}}, NotificationHistoryConfig{ - NotificationHistoryTTL: time.Hour * 48, - NotificationHistoryQueryLimit: 1000, + NotificationHistoryTTL: time.Hour * 48, }, NotificationConfig{ DelayedTime: time.Minute, diff --git a/interfaces.go b/interfaces.go index bd3ba84a9..fc9693b6f 100644 --- a/interfaces.go +++ b/interfaces.go @@ -87,13 +87,14 @@ type Database interface { // ScheduledNotification storing GetNotifications(start, end int64) ([]*ScheduledNotification, int64, error) - GetNotificationsByContactIdWithLimit(contactID string, from int64, to int64) ([]*NotificationEventHistoryItem, error) + GetNotificationsHistoryByContactID(contactID string, from, to, page, size int64) ([]*NotificationEventHistoryItem, error) RemoveNotification(notificationKey string) (int64, error) RemoveAllNotifications() error FetchNotifications(to int64, limit int64) ([]*ScheduledNotification, error) AddNotification(notification *ScheduledNotification) error AddNotifications(notification []*ScheduledNotification, timestamp int64) error PushContactNotificationToHistory(notification *ScheduledNotification) error + CleanUpOutdatedNotificationHistory(ttl int64) error // Patterns and metrics storing GetPatterns() ([]string, error) diff --git a/local/cli.yml b/local/cli.yml index 21ef388c8..a2e708dc8 100644 --- a/local/cli.yml +++ b/local/cli.yml @@ -10,3 +10,5 @@ cleanup: # Specifies the time from which metrics written to the future will be deleted # Defaults to 1 hour cleanup_future_metrics_duration: "60m" + # Default notification cleanup ttl (according to max ttl of notification history = 48h) + cleanup_notification_history_duration: "48h" diff --git a/local/notifier.yml b/local/notifier.yml index 1fd44427a..6a5cf57a7 100644 --- a/local/notifier.yml +++ b/local/notifier.yml @@ -44,7 +44,6 @@ notifier: date_time_format: "15:04 02.01.2006" notification_history: ttl: 48h - query_limit: 10000 notification: delayed_time: 50s transaction_timeout: 100ms diff --git a/mock/moira-alert/database.go b/mock/moira-alert/database.go index b0687cb78..43cb828ad 100644 --- a/mock/moira-alert/database.go +++ b/mock/moira-alert/database.go @@ -182,6 +182,20 @@ func (mr *MockDatabaseMockRecorder) CleanUpOutdatedMetrics(arg0 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpOutdatedMetrics", reflect.TypeOf((*MockDatabase)(nil).CleanUpOutdatedMetrics), arg0) } +// CleanUpOutdatedNotificationHistory mocks base method. +func (m *MockDatabase) CleanUpOutdatedNotificationHistory(arg0 int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CleanUpOutdatedNotificationHistory", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CleanUpOutdatedNotificationHistory indicates an expected call of CleanUpOutdatedNotificationHistory. +func (mr *MockDatabaseMockRecorder) CleanUpOutdatedNotificationHistory(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpOutdatedNotificationHistory", reflect.TypeOf((*MockDatabase)(nil).CleanUpOutdatedNotificationHistory), arg0) +} + // CleanupOutdatedPatternMetrics mocks base method. func (m *MockDatabase) CleanupOutdatedPatternMetrics() (int64, error) { m.ctrl.T.Helper() @@ -506,19 +520,19 @@ func (mr *MockDatabaseMockRecorder) GetNotifications(arg0, arg1 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifications", reflect.TypeOf((*MockDatabase)(nil).GetNotifications), arg0, arg1) } -// GetNotificationsByContactIdWithLimit mocks base method. -func (m *MockDatabase) GetNotificationsByContactIdWithLimit(arg0 string, arg1, arg2 int64) ([]*moira.NotificationEventHistoryItem, error) { +// GetNotificationsHistoryByContactId mocks base method. +func (m *MockDatabase) GetNotificationsHistoryByContactID(arg0 string, arg1, arg2, arg3, arg4 int64) ([]*moira.NotificationEventHistoryItem, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationsByContactIdWithLimit", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetNotificationsHistoryByContactID", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*moira.NotificationEventHistoryItem) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetNotificationsByContactIdWithLimit indicates an expected call of GetNotificationsByContactIdWithLimit. -func (mr *MockDatabaseMockRecorder) GetNotificationsByContactIdWithLimit(arg0, arg1, arg2 any) *gomock.Call { +// GetNotificationsHistoryByContactId indicates an expected call of GetNotificationsHistoryByContactId. +func (mr *MockDatabaseMockRecorder) GetNotificationsHistoryByContactId(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationsByContactIdWithLimit", reflect.TypeOf((*MockDatabase)(nil).GetNotificationsByContactIdWithLimit), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationsHistoryByContactID", reflect.TypeOf((*MockDatabase)(nil).GetNotificationsHistoryByContactID), arg0, arg1, arg2, arg3, arg4) } // GetNotifierState mocks base method. From a5ba594e2b596aaf50a67aebdf3b4d1196250c9e Mon Sep 17 00:00:00 2001 From: Xenia N Date: Mon, 12 Aug 2024 12:22:45 +0500 Subject: [PATCH 02/36] fix(filter): fix support custom retention for tagged metrics (#950) --- filter/cache_storage.go | 39 ++++++++++++++++++++++++++---------- filter/cache_storage_test.go | 38 ++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/filter/cache_storage.go b/filter/cache_storage.go index 077c5839d..077a8c306 100644 --- a/filter/cache_storage.go +++ b/filter/cache_storage.go @@ -2,6 +2,8 @@ package filter import ( "bufio" + "errors" + "fmt" "io" "regexp" "strconv" @@ -11,7 +13,12 @@ import ( "github.com/moira-alert/moira/metrics" ) -var defaultRetention = 60 +const defaultRetention = 60 + +var ( + invalidRetentionsFormatErr = errors.New("Invalid retentions format, it is correct to write in the format 'retentions = timePerPoint:timeToStore, timePerPoint:timeToStore, ...'") + invalidPatternFormatErr = errors.New("Invalid pattern format, it is correct to write in the format 'pattern = regex'") +) type retentionMatcher struct { pattern *regexp.Regexp @@ -82,33 +89,43 @@ func (storage *Storage) buildRetentions(retentionScanner *bufio.Scanner) error { storage.retentions = make([]retentionMatcher, 0, 100) for retentionScanner.Scan() { - line1 := retentionScanner.Text() - if strings.HasPrefix(line1, "#") || strings.Count(line1, "=") != 1 { + patternLine := retentionScanner.Text() + if strings.HasPrefix(patternLine, "#") || strings.Count(patternLine, "=") < 1 { continue } - patternString := strings.TrimSpace(strings.Split(line1, "=")[1]) + _, after, found := strings.Cut(patternLine, "=") + if !found { + storage.logger.Error(). + Error(invalidPatternFormatErr). + String("pattern_line", patternLine). + Msg("Invalid pattern format") + continue + } + + patternString := strings.TrimSpace(after) pattern, err := regexp.Compile(patternString) if err != nil { - return err + return fmt.Errorf("failed to compile regexp pattern '%s': %w", patternString, err) } retentionScanner.Scan() - line2 := retentionScanner.Text() - splitted := strings.Split(line2, "=") + retentionsLine := retentionScanner.Text() + splitted := strings.Split(retentionsLine, "=") if len(splitted) < 2 { //nolint storage.logger.Error(). - String("pattern", patternString). - Msg("Invalid pattern found") - + Error(invalidRetentionsFormatErr). + String("pattern_line", patternLine). + String("retentions_line", retentionsLine). + Msg("Invalid retentions format") continue } retentions := strings.TrimSpace(splitted[1]) retention, err := rawRetentionToSeconds(retentions[0:strings.Index(retentions, ":")]) if err != nil { - return err + return fmt.Errorf("failed to convert raw retentions '%s' to seconds: %w", retentions, err) } storage.retentions = append(storage.retentions, retentionMatcher{ diff --git a/filter/cache_storage_test.go b/filter/cache_storage_test.go index c0c3271e5..d3b3904e8 100644 --- a/filter/cache_storage_test.go +++ b/filter/cache_storage_test.go @@ -35,12 +35,16 @@ var testRetentions = ` pattern = yearly$ retentions = 1y:100y + [tagged metrics] + pattern = ;tag1=val1(;.*)?$ + retentions = 10s:2d,1m:30d,15m:1y + [default] pattern = .* retentions = 120:7d ` -var expectedRetentionIntervals = []int{60, 1200, 3600, 86400, 604800, 31536000, 120} +var expectedRetentionIntervals = []int{60, 1200, 3600, 86400, 604800, 31536000, 10, 120} var matchedMetrics = []moira.MatchedMetric{ { @@ -229,4 +233,36 @@ func TestRetentions(t *testing.T) { So(metr.Retention, ShouldEqual, 120) So(metr.RetentionTimestamp, ShouldEqual, 120) }) + + Convey("Tagged metrics", t, func() { + Convey("should be 10sec", func() { + matchedMetric := moira.MatchedMetric{ + Metric: "my_super_metric;tag1=val1;tag2=val2", + Value: 12, + Timestamp: 151, + RetentionTimestamp: 0, + Retention: 10, + } + buffer := make(map[string]*moira.MatchedMetric) + storage.EnrichMatchedMetric(buffer, &matchedMetric) + So(len(buffer), ShouldEqual, 1) + So(matchedMetric.Retention, ShouldEqual, 10) + So(matchedMetric.RetentionTimestamp, ShouldEqual, 150) + }) + + Convey("should be default 120sec", func() { + matchedMetric := moira.MatchedMetric{ + Metric: "my_super_metric;tag2=val2", + Value: 12, + Timestamp: 151, + RetentionTimestamp: 0, + Retention: 60, + } + buffer := make(map[string]*moira.MatchedMetric) + storage.EnrichMatchedMetric(buffer, &matchedMetric) + So(len(buffer), ShouldEqual, 1) + So(matchedMetric.Retention, ShouldEqual, 120) + So(matchedMetric.RetentionTimestamp, ShouldEqual, 120) + }) + }) } From 8e70ae2e44b8d7b7507d2f1e85ec280dfbfba27e Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:29:03 +0300 Subject: [PATCH 03/36] chore: update go chart version (#1070) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index af4fb7c7e..7bafdacc9 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/google/go-cmp v0.6.0 github.com/gotokatsuya/ipare v0.0.0-20161202043954-fd52c5b6c44b github.com/gregdel/pushover v1.1.0 - github.com/moira-alert/go-chart v0.0.0-20231107064049-444c44a558ef + github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19 github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.13 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/prometheus/client_golang v1.14.0 diff --git a/go.sum b/go.sum index acccd9382..29ce7ac2f 100644 --- a/go.sum +++ b/go.sum @@ -977,8 +977,8 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/moira-alert/blackfriday-slack v0.1.2 h1:W6VbDlHDBxoB7X+OJ+3xZZuzMcQ0qTcblhLLnm/xQ7U= github.com/moira-alert/blackfriday-slack v0.1.2/go.mod h1:tYMK3laTzU1wgxeOpUPdw36KHD3eTyQNDfxtg1nXLWI= -github.com/moira-alert/go-chart v0.0.0-20231107064049-444c44a558ef h1:hSEQ/9B23MTYQCxx+GTRW5P1eWaqtgEMEqOxXs/YNKE= -github.com/moira-alert/go-chart v0.0.0-20231107064049-444c44a558ef/go.mod h1:ktrkvZGboMQfYyBXAV05imlVxGIvVdeCn5vz91Fw1vE= +github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19 h1:dV1yczr6ndr5fCnBvj2SjBJxJNtnBtfZye0gDwTrPLs= +github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19/go.mod h1:ktrkvZGboMQfYyBXAV05imlVxGIvVdeCn5vz91Fw1vE= github.com/msaf1980/go-stringutils v0.1.4 h1:UwsIT0hplHVucqbknk3CoNqKkmIuSHhsbBldXxyld5U= github.com/msaf1980/go-stringutils v0.1.4/go.mod h1:AxmV/6JuQUAtZJg5XmYATB5ZwCWgtpruVHY03dswRf8= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= From 8560887ae228558d567e26f6a06f978f3f7a71ff Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:02:21 +0700 Subject: [PATCH 04/36] feat(api): query parameters to trigger events (#1064) --- api/controller/events.go | 78 +++++++++++++- api/controller/events_test.go | 122 ++++++++++++++++++++-- api/handler/constants.go | 10 ++ api/handler/event.go | 47 ++++++++- api/handler/trigger.go | 15 ++- api/handler/trigger_test.go | 6 +- api/middleware/context.go | 54 ++++++++++ api/middleware/context_test.go | 89 ++++++++++++++++ api/middleware/middleware.go | 18 +++- database/redis/notification_event.go | 27 +++-- database/redis/notification_event_test.go | 65 ++++++++++-- interfaces.go | 2 +- mock/moira-alert/database.go | 8 +- mock/scheduler/scheduler.go | 2 +- state.go | 10 ++ 15 files changed, 506 insertions(+), 47 deletions(-) diff --git a/api/controller/events.go b/api/controller/events.go index 218c8ecfe..5a45e2821 100644 --- a/api/controller/events.go +++ b/api/controller/events.go @@ -1,14 +1,23 @@ package controller import ( + "regexp" + "github.com/moira-alert/moira" "github.com/moira-alert/moira/api" "github.com/moira-alert/moira/api/dto" ) -// GetTriggerEvents gets trigger event from current page and all trigger event count. -func GetTriggerEvents(database moira.Database, triggerID string, page int64, size int64) (*dto.EventsList, *api.ErrorResponse) { - events, err := database.GetNotificationEvents(triggerID, page*size, size-1) +// GetTriggerEvents gets trigger event from current page and all trigger event count. Events list is filtered by time range +// (`from` and `to` params), metric (regular expression) and states. If `states` map is empty or nil then all states are accepted. +func GetTriggerEvents( + database moira.Database, + triggerID string, + page, size, from, to int64, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) (*dto.EventsList, *api.ErrorResponse) { + events, err := getFilteredNotificationEvents(database, triggerID, page, size, from, to, metricRegexp, states) if err != nil { return nil, api.ErrorInternalServer(err) } @@ -18,7 +27,7 @@ func GetTriggerEvents(database moira.Database, triggerID string, page int64, siz Size: size, Page: page, Total: eventCount, - List: make([]moira.NotificationEvent, 0), + List: make([]moira.NotificationEvent, 0, len(events)), } for _, event := range events { if event != nil { @@ -28,6 +37,67 @@ func GetTriggerEvents(database moira.Database, triggerID string, page int64, siz return eventsList, nil } +func getFilteredNotificationEvents( + database moira.Database, + triggerID string, + page, size, from, to int64, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) ([]*moira.NotificationEvent, error) { + // fetch all events + if size < 0 { + events, err := database.GetNotificationEvents(triggerID, page, size, from, to) + if err != nil { + return nil, err + } + + return filterNotificationEvents(events, metricRegexp, states), nil + } + + // fetch at most `size` events + filtered := make([]*moira.NotificationEvent, 0, size) + var count int64 + + for int64(len(filtered)) < size { + eventsData, err := database.GetNotificationEvents(triggerID, page+count, size, from, to) + if err != nil { + return nil, err + } + + if len(eventsData) == 0 { + break + } + + filtered = append(filtered, filterNotificationEvents(eventsData, metricRegexp, states)...) + count += 1 + + if int64(len(eventsData)) < size { + break + } + } + + return filtered, nil +} + +func filterNotificationEvents( + notificationEvents []*moira.NotificationEvent, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) []*moira.NotificationEvent { + filteredNotificationEvents := make([]*moira.NotificationEvent, 0) + + for _, event := range notificationEvents { + if metricRegexp.MatchString(event.Metric) { + _, ok := states[string(event.State)] + if len(states) == 0 || ok { + filteredNotificationEvents = append(filteredNotificationEvents, event) + } + } + } + + return filteredNotificationEvents +} + // DeleteAllEvents deletes all notification events. func DeleteAllEvents(database moira.Database) *api.ErrorResponse { if err := database.RemoveAllNotificationEvents(); err != nil { diff --git a/api/controller/events_test.go b/api/controller/events_test.go index 5cf3e3f34..3113f5203 100644 --- a/api/controller/events_test.go +++ b/api/controller/events_test.go @@ -2,7 +2,9 @@ package controller import ( "fmt" + "regexp" "testing" + "time" "github.com/gofrs/uuid" "github.com/moira-alert/moira" @@ -13,6 +15,11 @@ import ( "go.uber.org/mock/gomock" ) +var ( + allMetrics = regexp.MustCompile(``) + allStates map[string]struct{} +) + func TestGetEvents(t *testing.T) { mockCtrl := gomock.NewController(t) dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) @@ -20,12 +27,25 @@ func TestGetEvents(t *testing.T) { triggerID := uuid.Must(uuid.NewV4()).String() var page int64 = 10 var size int64 = 100 + var from int64 = 0 + to := time.Now().Unix() Convey("Test has events", t, func() { var total int64 = 6000000 - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return([]*moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, nil) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to). + Return([]*moira.NotificationEvent{ + { + State: moira.StateNODATA, + OldState: moira.StateOK, + }, + { + State: moira.StateOK, + OldState: moira.StateNODATA, + }, + }, nil) dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ List: []moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, @@ -37,9 +57,9 @@ func TestGetEvents(t *testing.T) { Convey("Test no events", t, func() { var total int64 - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return(make([]*moira.NotificationEvent, 0), nil) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(make([]*moira.NotificationEvent, 0), nil) dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ List: make([]moira.NotificationEvent, 0), @@ -51,11 +71,101 @@ func TestGetEvents(t *testing.T) { Convey("Test error", t, func() { expected := fmt.Errorf("oooops! Can not get all contacts") - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return(nil, expected) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(nil, expected) + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(list, ShouldBeNil) }) + + Convey("Test filtering", t, func() { + Convey("by metric regex", func() { + page = 0 + size = 2 + Convey("with same pattern", func() { + filtered := []*moira.NotificationEvent{ + {Metric: "metric.test.event1"}, + {Metric: "a.metric.test.event2"}, + } + notFiltered := []*moira.NotificationEvent{ + {Metric: "another.mEtric.test.event"}, + {Metric: "metric.test"}, + } + firstPortion := append(make([]*moira.NotificationEvent, 0), notFiltered[0], filtered[0]) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(firstPortion, nil) + + secondPortion := append(make([]*moira.NotificationEvent, 0), filtered[1], notFiltered[1]) + dataBase.EXPECT().GetNotificationEvents(triggerID, page+1, size, from, to).Return(secondPortion, nil) + + total := int64(len(firstPortion) + len(secondPortion)) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, regexp.MustCompile(`metric\.test\.event`), allStates) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(filtered), + }) + }) + }) + page = 0 + size = -1 + + Convey("by state", func() { + filtered := []*moira.NotificationEvent{ + {State: moira.StateOK}, + {State: moira.StateTEST}, + {State: moira.StateEXCEPTION}, + } + notFiltered := []*moira.NotificationEvent{ + {State: moira.StateWARN}, + {State: moira.StateNODATA}, + {State: moira.StateERROR}, + } + Convey("with empty map all allowed", func() { + total := int64(len(filtered) + len(notFiltered)) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(append(filtered, notFiltered...)), + }) + }) + + Convey("with given states", func() { + total := int64(len(filtered) + len(notFiltered)) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, map[string]struct{}{ + string(moira.StateOK): {}, + string(moira.StateEXCEPTION): {}, + string(moira.StateTEST): {}, + }) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(filtered), + }) + }) + }) + }) +} + +func toDTOList(eventPtrs []*moira.NotificationEvent) []moira.NotificationEvent { + events := make([]moira.NotificationEvent, 0, len(eventPtrs)) + for _, ptr := range eventPtrs { + events = append(events, *ptr) + } + return events } func TestDeleteAllNotificationEvents(t *testing.T) { diff --git a/api/handler/constants.go b/api/handler/constants.go index 413204120..2625a8d46 100644 --- a/api/handler/constants.go +++ b/api/handler/constants.go @@ -1,5 +1,15 @@ package handler +const allMetricsPattern = ".*" + +const ( + eventDefaultPage = 0 + eventDefaultSize = -1 + eventDefaultFrom = "-3hour" + eventDefaultTo = "now" + eventDefaultMetric = allMetricsPattern +) + const ( contactEventsDefaultFrom = "-3hour" contactEventsDefaultTo = "now" diff --git a/api/handler/event.go b/api/handler/event.go index c5d8602d5..231abd50e 100644 --- a/api/handler/event.go +++ b/api/handler/event.go @@ -1,7 +1,12 @@ package handler import ( + "fmt" "net/http" + "regexp" + "time" + + "github.com/go-graphite/carbonapi/date" "github.com/go-chi/chi" "github.com/go-chi/render" @@ -11,7 +16,13 @@ import ( ) func event(router chi.Router) { - router.With(middleware.TriggerContext, middleware.Paginate(0, 100)).Get("/{triggerId}", getEventsList) + router.With( + middleware.TriggerContext, + middleware.Paginate(eventDefaultPage, eventDefaultSize), + middleware.DateRange(eventDefaultFrom, eventDefaultTo), + middleware.MetricContext(eventDefaultMetric), + middleware.StatesContext(), + ).Get("/{triggerId}", getEventsList) router.With(middleware.AdminOnlyMiddleware()).Delete("/all", deleteAllEvents) } @@ -22,8 +33,12 @@ func event(router chi.Router) { // @tags event // @produce json // @param triggerID path string true "The ID of updated trigger" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) -// @param size query int false "Number of items to be displayed on one page" default(100) -// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) +// @param size query int false "Number of items to be displayed on one page. if size = -1 then all events returned" default(-1) +// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) +// @param from query string false "Start time of the time range" default(-3hour) +// @param to query string false "End time of the time range" default(now) +// @param metric query string false "Regular expression that will be used to filter events" default(.*) +// @param states query []string false "String of ',' separated state names. If empty then all states will be used." collectionFormat(csv) // @success 200 {object} dto.EventsList "Events fetched successfully" // @Failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" // @Failure 404 {object} api.ErrorNotFoundExample "Resource not found" @@ -34,7 +49,31 @@ func getEventsList(writer http.ResponseWriter, request *http.Request) { triggerID := middleware.GetTriggerID(request) size := middleware.GetSize(request) page := middleware.GetPage(request) - eventsList, err := controller.GetTriggerEvents(database, triggerID, page, size) + fromStr := middleware.GetFromStr(request) + toStr := middleware.GetToStr(request) + + from := date.DateParamToEpoch(fromStr, "UTC", 0, time.UTC) + if from == 0 { + render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse from: %s", fromStr))) //nolint + return + } + + to := date.DateParamToEpoch(toStr, "UTC", 0, time.UTC) + if to == 0 { + render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse to: %v", to))) //nolint + return + } + + metricStr := middleware.GetMetric(request) + metricRegexp, errCompile := regexp.Compile(metricStr) + if errCompile != nil { + _ = render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse metric \"%s\": %w", metricStr, errCompile))) + return + } + + states := middleware.GetStates(request) + + eventsList, err := controller.GetTriggerEvents(database, triggerID, page, size, from, to, metricRegexp, states) if err != nil { render.Render(writer, request, err) //nolint return diff --git a/api/handler/trigger.go b/api/handler/trigger.go index d17b09cc9..9d3cb9aff 100644 --- a/api/handler/trigger.go +++ b/api/handler/trigger.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "regexp" "time" "github.com/go-chi/chi" @@ -179,7 +180,19 @@ func checkingTemplateFilling(request *http.Request, trigger dto.Trigger) *api.Er return nil } - eventsList, err := controller.GetTriggerEvents(database, trigger.ID, 0, 3) + const ( + page = 0 + size = 3 + from = 0 + ) + + var ( + to = time.Now().Unix() + allMetricRegexp = regexp.MustCompile(allMetricsPattern) + allStates map[string]struct{} + ) + + eventsList, err := controller.GetTriggerEvents(database, trigger.ID, page, size, from, to, allMetricRegexp, allStates) if err != nil { return err } diff --git a/api/handler/trigger_test.go b/api/handler/trigger_test.go index 1e32507d8..f9d96ec3c 100644 --- a/api/handler/trigger_test.go +++ b/api/handler/trigger_test.go @@ -420,7 +420,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() @@ -464,7 +464,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() @@ -508,7 +508,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() diff --git a/api/middleware/context.go b/api/middleware/context.go index 5dbc88e34..0d9e26f31 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/go-chi/chi" @@ -277,3 +278,56 @@ func AuthorizationContext(auth *api.Authorization) func(next http.Handler) http. }) } } + +// MetricContext is a function that gets `metric` value from query string and places it in context. If query does not have value sets given value. +func MetricContext(defaultMetric string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + urlValues, err := url.ParseQuery(request.URL.RawQuery) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + metric := urlValues.Get("metric") + if metric == "" { + metric = defaultMetric + } + + ctx := context.WithValue(request.Context(), metricContextKey, metric) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} + +const statesArraySeparator = "," + +// StatesContext is a function that gets `states` value from query string and places it in context. If query does not have value empty map will be used. +func StatesContext() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + urlValues, err := url.ParseQuery(request.URL.RawQuery) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + states := make(map[string]struct{}, 0) + + statesStr := urlValues.Get("states") + if statesStr != "" { + statesList := strings.Split(statesStr, statesArraySeparator) + for _, state := range statesList { + if !moira.State(state).IsValid() { + _ = render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("bad state in query parameter: %s", state))) + return + } + states[state] = struct{}{} + } + } + + ctx := context.WithValue(request.Context(), statesContextKey, states) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} diff --git a/api/middleware/context_test.go b/api/middleware/context_test.go index 1c4e9e7c9..6031e77ee 100644 --- a/api/middleware/context_test.go +++ b/api/middleware/context_test.go @@ -216,3 +216,92 @@ func TestTargetNameMiddleware(t *testing.T) { }) }) } + +func TestMetricProviderMiddleware(t *testing.T) { + Convey("Check metric provider", t, func() { + responseWriter := httptest.NewRecorder() + defaultMetric := ".*" + + Convey("status ok with correct query paramete", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?metric=test%5C.metric.*", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := MetricContext(defaultMetric) + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("status bad request with wrong url query parameter", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?metric%=test", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := MetricContext(defaultMetric) + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + + So(contents, ShouldEqual, expectedBadRequest) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + }) +} + +func TestStatesProviderMiddleware(t *testing.T) { + Convey("Checking states provide", t, func() { + responseWriter := httptest.NewRecorder() + + Convey("ok with correct states list", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("bad request with bad states list", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR%2Cwarn", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("bad request with wrong url query parameter", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states%=test", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + + So(contents, ShouldEqual, expectedBadRequest) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + }) +} diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index a520f01b4..80d65066d 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -39,6 +39,8 @@ var ( teamIDKey ContextKey = "teamID" teamUserIDKey ContextKey = "teamUserIDKey" authKey ContextKey = "auth" + metricContextKey ContextKey = "metric" + statesContextKey ContextKey = "states" anonymousUser = "anonymous" ) @@ -63,7 +65,7 @@ func GetTriggerID(request *http.Request) string { return request.Context().Value(triggerIDKey).(string) } -// GetLocalMetricTTL gets local metric ttl duration time from request context, which was sets in TriggerContext middleware. +// GetMetricTTL gets local metric ttl duration time from request context, which was sets in TriggerContext middleware. func GetMetricTTL(request *http.Request) map[moira.ClusterKey]time.Duration { return request.Context().Value(clustersMetricTTLKey).(map[moira.ClusterKey]time.Duration) } @@ -118,13 +120,13 @@ func GetToStr(request *http.Request) string { return request.Context().Value(toKey).(string) } -// SetTimeSeriesNames sets to requests context timeSeriesNames from saved trigger. +// SetTimeSeriesNames sets to request's context timeSeriesNames from saved trigger. func SetTimeSeriesNames(request *http.Request, timeSeriesNames map[string]bool) { ctx := context.WithValue(request.Context(), timeSeriesNamesKey, timeSeriesNames) *request = *request.WithContext(ctx) } -// GetTimeSeriesNames gets from requests context timeSeriesNames from saved trigger. +// GetTimeSeriesNames gets from request's context timeSeriesNames from saved trigger. func GetTimeSeriesNames(request *http.Request) map[string]bool { return request.Context().Value(timeSeriesNamesKey).(map[string]bool) } @@ -162,3 +164,13 @@ func SetContextValueForTest(ctx context.Context, key string, value interface{}) func GetAuth(request *http.Request) *api.Authorization { return request.Context().Value(authKey).(*api.Authorization) } + +// GetMetric is used to retrieve metric name. +func GetMetric(request *http.Request) string { + return request.Context().Value(metricContextKey).(string) +} + +// GetStates is used to retrieve trigger state. +func GetStates(request *http.Request) map[string]struct{} { + return request.Context().Value(statesContextKey).(map[string]struct{}) +} diff --git a/database/redis/notification_event.go b/database/redis/notification_event.go index 4a5974969..8f9502f4e 100644 --- a/database/redis/notification_event.go +++ b/database/redis/notification_event.go @@ -14,19 +14,24 @@ import ( var eventsTTL int64 = 3600 * 24 * 30 -// GetNotificationEvents gets NotificationEvents by given triggerID and interval. -func (connector *DbConnector) GetNotificationEvents(triggerID string, start int64, size int64) ([]*moira.NotificationEvent, error) { - ctx := connector.context - c := *connector.client - - eventsData, err := reply.Events(c.ZRevRange(ctx, triggerEventsKey(triggerID), start, start+size)) +// GetNotificationEvents gets NotificationEvents by given triggerID and interval. The events are also filtered by time range +// (`from`, `to` params). +func (connector *DbConnector) GetNotificationEvents(triggerID string, page, size, from, to int64) ([]*moira.NotificationEvent, error) { + ctx := connector.Context() + client := connector.Client() + + eventsData, err := reply.Events(client.ZRevRangeByScore(ctx, triggerEventsKey(triggerID), &redis.ZRangeBy{ + Min: strconv.FormatInt(from, 10), + Max: strconv.FormatInt(to, 10), + Offset: page * size, + Count: size, + })) if err != nil { if errors.Is(err, redis.Nil) { return make([]*moira.NotificationEvent, 0), nil } - return nil, fmt.Errorf("failed to get range for trigger events, triggerID: %s, error: %s", triggerID, err.Error()) + return nil, fmt.Errorf("failed to get range of trigger events, triggerID: %s, error: %w", triggerID, err) } - return eventsData, nil } @@ -85,7 +90,7 @@ func (connector *DbConnector) FetchNotificationEvent() (moira.NotificationEvent, } if err != nil { - return event, fmt.Errorf("failed to fetch event: %s", err.Error()) + return event, fmt.Errorf("failed to fetch event: %w", err) } event, _ = reply.BRPopToEvent(response) @@ -108,13 +113,13 @@ func (connector *DbConnector) RemoveAllNotificationEvents() error { c := *connector.client if _, err := c.Del(ctx, notificationEventsList).Result(); err != nil { - return fmt.Errorf("failed to remove %s: %s", notificationEventsList, err.Error()) + return fmt.Errorf("failed to remove %s: %w", notificationEventsList, err) } return nil } -var ( +const ( notificationEventsList = "moira-trigger-events" notificationEventsUIList = "moira-trigger-events-ui" ) diff --git a/database/redis/notification_event_test.go b/database/redis/notification_event_test.go index c6ee30dd2..269b9d023 100644 --- a/database/redis/notification_event_test.go +++ b/database/redis/notification_event_test.go @@ -33,7 +33,7 @@ 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(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) @@ -57,7 +57,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -86,7 +86,7 @@ func TestNotificationEvents(t *testing.T) { }) Convey("Should has event by triggerID after fetch", func() { - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -132,7 +132,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -148,7 +148,7 @@ func TestNotificationEvents(t *testing.T) { total := dataBase.GetNotificationEventCount(triggerID1, 0) So(total, ShouldEqual, 1) - actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1) + actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -177,7 +177,7 @@ func TestNotificationEvents(t *testing.T) { Values: map[string]float64{}, }) - actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -223,7 +223,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -248,11 +248,58 @@ func TestNotificationEvents(t *testing.T) { total = dataBase.GetNotificationEventCount(triggerID3, now+1) So(total, ShouldEqual, 0) - actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1) + actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1, 0, now) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) }) + Convey("Test `from` and `to` params", func() { + err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + }, true) + So(err, ShouldBeNil) + + Convey("returns event on exact time", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now, now) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{ + { + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + Values: map[string]float64{}, + }, + }) + }) + + Convey("not return event out of time range", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now-2, now-1) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{}) + }) + + Convey("returns event in time range", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now-1, now+1) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{ + { + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + Values: map[string]float64{}, + }, + }) + }) + }) + Convey("Test removing notification events", func() { Convey("Should remove all notifications", func() { err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ @@ -312,7 +359,7 @@ func TestNotificationEventErrorConnection(t *testing.T) { } Convey("Should throw error when no connection", t, func() { - actual1, err := dataBase.GetNotificationEvents("123", 0, 1) + actual1, err := dataBase.GetNotificationEvents("123", 0, 1, 0, time.Now().Unix()) So(actual1, ShouldBeNil) So(err, ShouldNotBeNil) diff --git a/interfaces.go b/interfaces.go index fc9693b6f..d16bf5f1e 100644 --- a/interfaces.go +++ b/interfaces.go @@ -60,7 +60,7 @@ type Database interface { DeleteTriggerThrottling(triggerID string) error // NotificationEvent storing - GetNotificationEvents(triggerID string, start, size int64) ([]*NotificationEvent, error) + GetNotificationEvents(triggerID string, start, size, from, to int64) ([]*NotificationEvent, error) PushNotificationEvent(event *NotificationEvent, ui bool) error GetNotificationEventCount(triggerID string, from int64) int64 FetchNotificationEvent() (NotificationEvent, error) diff --git a/mock/moira-alert/database.go b/mock/moira-alert/database.go index 43cb828ad..7356c8a1f 100644 --- a/mock/moira-alert/database.go +++ b/mock/moira-alert/database.go @@ -490,18 +490,18 @@ func (mr *MockDatabaseMockRecorder) GetNotificationEventCount(arg0, arg1 any) *g } // GetNotificationEvents mocks base method. -func (m *MockDatabase) GetNotificationEvents(arg0 string, arg1, arg2 int64) ([]*moira.NotificationEvent, error) { +func (m *MockDatabase) GetNotificationEvents(arg0 string, arg1, arg2, arg3, arg4 int64) ([]*moira.NotificationEvent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationEvents", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetNotificationEvents", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*moira.NotificationEvent) ret1, _ := ret[1].(error) return ret0, ret1 } // GetNotificationEvents indicates an expected call of GetNotificationEvents. -func (mr *MockDatabaseMockRecorder) GetNotificationEvents(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockDatabaseMockRecorder) GetNotificationEvents(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationEvents", reflect.TypeOf((*MockDatabase)(nil).GetNotificationEvents), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationEvents", reflect.TypeOf((*MockDatabase)(nil).GetNotificationEvents), arg0, arg1, arg2, arg3, arg4) } // GetNotifications mocks base method. diff --git a/mock/scheduler/scheduler.go b/mock/scheduler/scheduler.go index 8a146a314..3274a1760 100644 --- a/mock/scheduler/scheduler.go +++ b/mock/scheduler/scheduler.go @@ -48,7 +48,7 @@ func (m *MockScheduler) ScheduleNotification(arg0 moira.SchedulerParams, arg1 mo } // ScheduleNotification indicates an expected call of ScheduleNotification. -func (mr *MockSchedulerMockRecorder) ScheduleNotification(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockSchedulerMockRecorder) ScheduleNotification(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleNotification", reflect.TypeOf((*MockScheduler)(nil).ScheduleNotification), arg0, arg1) } diff --git a/state.go b/state.go index 8f00e8fb7..8c66624a1 100644 --- a/state.go +++ b/state.go @@ -61,6 +61,16 @@ func (state State) ToSelfState() string { return SelfStateOK } +// IsValid checks if valid State. +func (state State) IsValid() bool { + for _, allowedState := range eventStatesPriority { + if state == allowedState { + return true + } + } + return false +} + // ToMetricState is an auxiliary function to handle metric state properly. func (state TTLState) ToMetricState() State { if state == TTLStateDEL { From 6683a4a3fb290d8c73924a05d6ace341b62acb05 Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Wed, 14 Aug 2024 12:31:48 +0300 Subject: [PATCH 05/36] Revert "feat(api): query parameters to trigger events (#1064)" (#1071) This reverts commit 8560887ae228558d567e26f6a06f978f3f7a71ff. --- api/controller/events.go | 78 +------------- api/controller/events_test.go | 122 ++-------------------- api/handler/constants.go | 10 -- api/handler/event.go | 47 +-------- api/handler/trigger.go | 15 +-- api/handler/trigger_test.go | 6 +- api/middleware/context.go | 54 ---------- api/middleware/context_test.go | 89 ---------------- api/middleware/middleware.go | 18 +--- database/redis/notification_event.go | 27 ++--- database/redis/notification_event_test.go | 65 ++---------- interfaces.go | 2 +- mock/moira-alert/database.go | 8 +- mock/scheduler/scheduler.go | 2 +- state.go | 10 -- 15 files changed, 47 insertions(+), 506 deletions(-) diff --git a/api/controller/events.go b/api/controller/events.go index 5a45e2821..218c8ecfe 100644 --- a/api/controller/events.go +++ b/api/controller/events.go @@ -1,23 +1,14 @@ package controller import ( - "regexp" - "github.com/moira-alert/moira" "github.com/moira-alert/moira/api" "github.com/moira-alert/moira/api/dto" ) -// GetTriggerEvents gets trigger event from current page and all trigger event count. Events list is filtered by time range -// (`from` and `to` params), metric (regular expression) and states. If `states` map is empty or nil then all states are accepted. -func GetTriggerEvents( - database moira.Database, - triggerID string, - page, size, from, to int64, - metricRegexp *regexp.Regexp, - states map[string]struct{}, -) (*dto.EventsList, *api.ErrorResponse) { - events, err := getFilteredNotificationEvents(database, triggerID, page, size, from, to, metricRegexp, states) +// GetTriggerEvents gets trigger event from current page and all trigger event count. +func GetTriggerEvents(database moira.Database, triggerID string, page int64, size int64) (*dto.EventsList, *api.ErrorResponse) { + events, err := database.GetNotificationEvents(triggerID, page*size, size-1) if err != nil { return nil, api.ErrorInternalServer(err) } @@ -27,7 +18,7 @@ func GetTriggerEvents( Size: size, Page: page, Total: eventCount, - List: make([]moira.NotificationEvent, 0, len(events)), + List: make([]moira.NotificationEvent, 0), } for _, event := range events { if event != nil { @@ -37,67 +28,6 @@ func GetTriggerEvents( return eventsList, nil } -func getFilteredNotificationEvents( - database moira.Database, - triggerID string, - page, size, from, to int64, - metricRegexp *regexp.Regexp, - states map[string]struct{}, -) ([]*moira.NotificationEvent, error) { - // fetch all events - if size < 0 { - events, err := database.GetNotificationEvents(triggerID, page, size, from, to) - if err != nil { - return nil, err - } - - return filterNotificationEvents(events, metricRegexp, states), nil - } - - // fetch at most `size` events - filtered := make([]*moira.NotificationEvent, 0, size) - var count int64 - - for int64(len(filtered)) < size { - eventsData, err := database.GetNotificationEvents(triggerID, page+count, size, from, to) - if err != nil { - return nil, err - } - - if len(eventsData) == 0 { - break - } - - filtered = append(filtered, filterNotificationEvents(eventsData, metricRegexp, states)...) - count += 1 - - if int64(len(eventsData)) < size { - break - } - } - - return filtered, nil -} - -func filterNotificationEvents( - notificationEvents []*moira.NotificationEvent, - metricRegexp *regexp.Regexp, - states map[string]struct{}, -) []*moira.NotificationEvent { - filteredNotificationEvents := make([]*moira.NotificationEvent, 0) - - for _, event := range notificationEvents { - if metricRegexp.MatchString(event.Metric) { - _, ok := states[string(event.State)] - if len(states) == 0 || ok { - filteredNotificationEvents = append(filteredNotificationEvents, event) - } - } - } - - return filteredNotificationEvents -} - // DeleteAllEvents deletes all notification events. func DeleteAllEvents(database moira.Database) *api.ErrorResponse { if err := database.RemoveAllNotificationEvents(); err != nil { diff --git a/api/controller/events_test.go b/api/controller/events_test.go index 3113f5203..5cf3e3f34 100644 --- a/api/controller/events_test.go +++ b/api/controller/events_test.go @@ -2,9 +2,7 @@ package controller import ( "fmt" - "regexp" "testing" - "time" "github.com/gofrs/uuid" "github.com/moira-alert/moira" @@ -15,11 +13,6 @@ import ( "go.uber.org/mock/gomock" ) -var ( - allMetrics = regexp.MustCompile(``) - allStates map[string]struct{} -) - func TestGetEvents(t *testing.T) { mockCtrl := gomock.NewController(t) dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) @@ -27,25 +20,12 @@ func TestGetEvents(t *testing.T) { triggerID := uuid.Must(uuid.NewV4()).String() var page int64 = 10 var size int64 = 100 - var from int64 = 0 - to := time.Now().Unix() Convey("Test has events", t, func() { var total int64 = 6000000 - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to). - Return([]*moira.NotificationEvent{ - { - State: moira.StateNODATA, - OldState: moira.StateOK, - }, - { - State: moira.StateOK, - OldState: moira.StateNODATA, - }, - }, nil) + dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return([]*moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, nil) dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - - list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) + list, err := GetTriggerEvents(dataBase, triggerID, page, size) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ List: []moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, @@ -57,9 +37,9 @@ func TestGetEvents(t *testing.T) { Convey("Test no events", t, func() { var total int64 - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(make([]*moira.NotificationEvent, 0), nil) + dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return(make([]*moira.NotificationEvent, 0), nil) dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) + list, err := GetTriggerEvents(dataBase, triggerID, page, size) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ List: make([]moira.NotificationEvent, 0), @@ -71,101 +51,11 @@ func TestGetEvents(t *testing.T) { Convey("Test error", t, func() { expected := fmt.Errorf("oooops! Can not get all contacts") - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(nil, expected) - list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) + dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return(nil, expected) + list, err := GetTriggerEvents(dataBase, triggerID, page, size) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(list, ShouldBeNil) }) - - Convey("Test filtering", t, func() { - Convey("by metric regex", func() { - page = 0 - size = 2 - Convey("with same pattern", func() { - filtered := []*moira.NotificationEvent{ - {Metric: "metric.test.event1"}, - {Metric: "a.metric.test.event2"}, - } - notFiltered := []*moira.NotificationEvent{ - {Metric: "another.mEtric.test.event"}, - {Metric: "metric.test"}, - } - firstPortion := append(make([]*moira.NotificationEvent, 0), notFiltered[0], filtered[0]) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(firstPortion, nil) - - secondPortion := append(make([]*moira.NotificationEvent, 0), filtered[1], notFiltered[1]) - dataBase.EXPECT().GetNotificationEvents(triggerID, page+1, size, from, to).Return(secondPortion, nil) - - total := int64(len(firstPortion) + len(secondPortion)) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - - actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, regexp.MustCompile(`metric\.test\.event`), allStates) - So(err, ShouldBeNil) - So(actual, ShouldResemble, &dto.EventsList{ - Page: page, - Size: size, - Total: total, - List: toDTOList(filtered), - }) - }) - }) - page = 0 - size = -1 - - Convey("by state", func() { - filtered := []*moira.NotificationEvent{ - {State: moira.StateOK}, - {State: moira.StateTEST}, - {State: moira.StateEXCEPTION}, - } - notFiltered := []*moira.NotificationEvent{ - {State: moira.StateWARN}, - {State: moira.StateNODATA}, - {State: moira.StateERROR}, - } - Convey("with empty map all allowed", func() { - total := int64(len(filtered) + len(notFiltered)) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - - actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) - So(err, ShouldBeNil) - So(actual, ShouldResemble, &dto.EventsList{ - Page: page, - Size: size, - Total: total, - List: toDTOList(append(filtered, notFiltered...)), - }) - }) - - Convey("with given states", func() { - total := int64(len(filtered) + len(notFiltered)) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - - actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, map[string]struct{}{ - string(moira.StateOK): {}, - string(moira.StateEXCEPTION): {}, - string(moira.StateTEST): {}, - }) - So(err, ShouldBeNil) - So(actual, ShouldResemble, &dto.EventsList{ - Page: page, - Size: size, - Total: total, - List: toDTOList(filtered), - }) - }) - }) - }) -} - -func toDTOList(eventPtrs []*moira.NotificationEvent) []moira.NotificationEvent { - events := make([]moira.NotificationEvent, 0, len(eventPtrs)) - for _, ptr := range eventPtrs { - events = append(events, *ptr) - } - return events } func TestDeleteAllNotificationEvents(t *testing.T) { diff --git a/api/handler/constants.go b/api/handler/constants.go index 2625a8d46..413204120 100644 --- a/api/handler/constants.go +++ b/api/handler/constants.go @@ -1,15 +1,5 @@ package handler -const allMetricsPattern = ".*" - -const ( - eventDefaultPage = 0 - eventDefaultSize = -1 - eventDefaultFrom = "-3hour" - eventDefaultTo = "now" - eventDefaultMetric = allMetricsPattern -) - const ( contactEventsDefaultFrom = "-3hour" contactEventsDefaultTo = "now" diff --git a/api/handler/event.go b/api/handler/event.go index 231abd50e..c5d8602d5 100644 --- a/api/handler/event.go +++ b/api/handler/event.go @@ -1,12 +1,7 @@ package handler import ( - "fmt" "net/http" - "regexp" - "time" - - "github.com/go-graphite/carbonapi/date" "github.com/go-chi/chi" "github.com/go-chi/render" @@ -16,13 +11,7 @@ import ( ) func event(router chi.Router) { - router.With( - middleware.TriggerContext, - middleware.Paginate(eventDefaultPage, eventDefaultSize), - middleware.DateRange(eventDefaultFrom, eventDefaultTo), - middleware.MetricContext(eventDefaultMetric), - middleware.StatesContext(), - ).Get("/{triggerId}", getEventsList) + router.With(middleware.TriggerContext, middleware.Paginate(0, 100)).Get("/{triggerId}", getEventsList) router.With(middleware.AdminOnlyMiddleware()).Delete("/all", deleteAllEvents) } @@ -33,12 +22,8 @@ func event(router chi.Router) { // @tags event // @produce json // @param triggerID path string true "The ID of updated trigger" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) -// @param size query int false "Number of items to be displayed on one page. if size = -1 then all events returned" default(-1) -// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) -// @param from query string false "Start time of the time range" default(-3hour) -// @param to query string false "End time of the time range" default(now) -// @param metric query string false "Regular expression that will be used to filter events" default(.*) -// @param states query []string false "String of ',' separated state names. If empty then all states will be used." collectionFormat(csv) +// @param size query int false "Number of items to be displayed on one page" default(100) +// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) // @success 200 {object} dto.EventsList "Events fetched successfully" // @Failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" // @Failure 404 {object} api.ErrorNotFoundExample "Resource not found" @@ -49,31 +34,7 @@ func getEventsList(writer http.ResponseWriter, request *http.Request) { triggerID := middleware.GetTriggerID(request) size := middleware.GetSize(request) page := middleware.GetPage(request) - fromStr := middleware.GetFromStr(request) - toStr := middleware.GetToStr(request) - - from := date.DateParamToEpoch(fromStr, "UTC", 0, time.UTC) - if from == 0 { - render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse from: %s", fromStr))) //nolint - return - } - - to := date.DateParamToEpoch(toStr, "UTC", 0, time.UTC) - if to == 0 { - render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse to: %v", to))) //nolint - return - } - - metricStr := middleware.GetMetric(request) - metricRegexp, errCompile := regexp.Compile(metricStr) - if errCompile != nil { - _ = render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse metric \"%s\": %w", metricStr, errCompile))) - return - } - - states := middleware.GetStates(request) - - eventsList, err := controller.GetTriggerEvents(database, triggerID, page, size, from, to, metricRegexp, states) + eventsList, err := controller.GetTriggerEvents(database, triggerID, page, size) if err != nil { render.Render(writer, request, err) //nolint return diff --git a/api/handler/trigger.go b/api/handler/trigger.go index 9d3cb9aff..d17b09cc9 100644 --- a/api/handler/trigger.go +++ b/api/handler/trigger.go @@ -2,7 +2,6 @@ package handler import ( "net/http" - "regexp" "time" "github.com/go-chi/chi" @@ -180,19 +179,7 @@ func checkingTemplateFilling(request *http.Request, trigger dto.Trigger) *api.Er return nil } - const ( - page = 0 - size = 3 - from = 0 - ) - - var ( - to = time.Now().Unix() - allMetricRegexp = regexp.MustCompile(allMetricsPattern) - allStates map[string]struct{} - ) - - eventsList, err := controller.GetTriggerEvents(database, trigger.ID, page, size, from, to, allMetricRegexp, allStates) + eventsList, err := controller.GetTriggerEvents(database, trigger.ID, 0, 3) if err != nil { return err } diff --git a/api/handler/trigger_test.go b/api/handler/trigger_test.go index f9d96ec3c..1e32507d8 100644 --- a/api/handler/trigger_test.go +++ b/api/handler/trigger_test.go @@ -420,7 +420,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() @@ -464,7 +464,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() @@ -508,7 +508,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() diff --git a/api/middleware/context.go b/api/middleware/context.go index 0d9e26f31..5dbc88e34 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -6,7 +6,6 @@ import ( "net/http" "net/url" "strconv" - "strings" "time" "github.com/go-chi/chi" @@ -278,56 +277,3 @@ func AuthorizationContext(auth *api.Authorization) func(next http.Handler) http. }) } } - -// MetricContext is a function that gets `metric` value from query string and places it in context. If query does not have value sets given value. -func MetricContext(defaultMetric string) func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - urlValues, err := url.ParseQuery(request.URL.RawQuery) - if err != nil { - render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint - return - } - - metric := urlValues.Get("metric") - if metric == "" { - metric = defaultMetric - } - - ctx := context.WithValue(request.Context(), metricContextKey, metric) - next.ServeHTTP(writer, request.WithContext(ctx)) - }) - } -} - -const statesArraySeparator = "," - -// StatesContext is a function that gets `states` value from query string and places it in context. If query does not have value empty map will be used. -func StatesContext() func(next http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { - urlValues, err := url.ParseQuery(request.URL.RawQuery) - if err != nil { - render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint - return - } - - states := make(map[string]struct{}, 0) - - statesStr := urlValues.Get("states") - if statesStr != "" { - statesList := strings.Split(statesStr, statesArraySeparator) - for _, state := range statesList { - if !moira.State(state).IsValid() { - _ = render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("bad state in query parameter: %s", state))) - return - } - states[state] = struct{}{} - } - } - - ctx := context.WithValue(request.Context(), statesContextKey, states) - next.ServeHTTP(writer, request.WithContext(ctx)) - }) - } -} diff --git a/api/middleware/context_test.go b/api/middleware/context_test.go index 6031e77ee..1c4e9e7c9 100644 --- a/api/middleware/context_test.go +++ b/api/middleware/context_test.go @@ -216,92 +216,3 @@ func TestTargetNameMiddleware(t *testing.T) { }) }) } - -func TestMetricProviderMiddleware(t *testing.T) { - Convey("Check metric provider", t, func() { - responseWriter := httptest.NewRecorder() - defaultMetric := ".*" - - Convey("status ok with correct query paramete", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?metric=test%5C.metric.*", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := MetricContext(defaultMetric) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusOK) - }) - - Convey("status bad request with wrong url query parameter", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?metric%=test", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := MetricContext(defaultMetric) - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) - - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) - }) - }) -} - -func TestStatesProviderMiddleware(t *testing.T) { - Convey("Checking states provide", t, func() { - responseWriter := httptest.NewRecorder() - - Convey("ok with correct states list", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := StatesContext() - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusOK) - }) - - Convey("bad request with bad states list", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR%2Cwarn", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := StatesContext() - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) - }) - - Convey("bad request with wrong url query parameter", func() { - testRequest := httptest.NewRequest(http.MethodGet, "/test?states%=test", nil) - handler := func(w http.ResponseWriter, r *http.Request) {} - - middlewareFunc := StatesContext() - wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) - - wrappedHandler.ServeHTTP(responseWriter, testRequest) - response := responseWriter.Result() - defer response.Body.Close() - contentBytes, _ := io.ReadAll(response.Body) - contents := string(contentBytes) - - So(contents, ShouldEqual, expectedBadRequest) - So(response.StatusCode, ShouldEqual, http.StatusBadRequest) - }) - }) -} diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index 80d65066d..a520f01b4 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -39,8 +39,6 @@ var ( teamIDKey ContextKey = "teamID" teamUserIDKey ContextKey = "teamUserIDKey" authKey ContextKey = "auth" - metricContextKey ContextKey = "metric" - statesContextKey ContextKey = "states" anonymousUser = "anonymous" ) @@ -65,7 +63,7 @@ func GetTriggerID(request *http.Request) string { return request.Context().Value(triggerIDKey).(string) } -// GetMetricTTL gets local metric ttl duration time from request context, which was sets in TriggerContext middleware. +// GetLocalMetricTTL gets local metric ttl duration time from request context, which was sets in TriggerContext middleware. func GetMetricTTL(request *http.Request) map[moira.ClusterKey]time.Duration { return request.Context().Value(clustersMetricTTLKey).(map[moira.ClusterKey]time.Duration) } @@ -120,13 +118,13 @@ func GetToStr(request *http.Request) string { return request.Context().Value(toKey).(string) } -// SetTimeSeriesNames sets to request's context timeSeriesNames from saved trigger. +// SetTimeSeriesNames sets to requests context timeSeriesNames from saved trigger. func SetTimeSeriesNames(request *http.Request, timeSeriesNames map[string]bool) { ctx := context.WithValue(request.Context(), timeSeriesNamesKey, timeSeriesNames) *request = *request.WithContext(ctx) } -// GetTimeSeriesNames gets from request's context timeSeriesNames from saved trigger. +// GetTimeSeriesNames gets from requests context timeSeriesNames from saved trigger. func GetTimeSeriesNames(request *http.Request) map[string]bool { return request.Context().Value(timeSeriesNamesKey).(map[string]bool) } @@ -164,13 +162,3 @@ func SetContextValueForTest(ctx context.Context, key string, value interface{}) func GetAuth(request *http.Request) *api.Authorization { return request.Context().Value(authKey).(*api.Authorization) } - -// GetMetric is used to retrieve metric name. -func GetMetric(request *http.Request) string { - return request.Context().Value(metricContextKey).(string) -} - -// GetStates is used to retrieve trigger state. -func GetStates(request *http.Request) map[string]struct{} { - return request.Context().Value(statesContextKey).(map[string]struct{}) -} diff --git a/database/redis/notification_event.go b/database/redis/notification_event.go index 8f9502f4e..4a5974969 100644 --- a/database/redis/notification_event.go +++ b/database/redis/notification_event.go @@ -14,24 +14,19 @@ import ( var eventsTTL int64 = 3600 * 24 * 30 -// GetNotificationEvents gets NotificationEvents by given triggerID and interval. The events are also filtered by time range -// (`from`, `to` params). -func (connector *DbConnector) GetNotificationEvents(triggerID string, page, size, from, to int64) ([]*moira.NotificationEvent, error) { - ctx := connector.Context() - client := connector.Client() - - eventsData, err := reply.Events(client.ZRevRangeByScore(ctx, triggerEventsKey(triggerID), &redis.ZRangeBy{ - Min: strconv.FormatInt(from, 10), - Max: strconv.FormatInt(to, 10), - Offset: page * size, - Count: size, - })) +// GetNotificationEvents gets NotificationEvents by given triggerID and interval. +func (connector *DbConnector) GetNotificationEvents(triggerID string, start int64, size int64) ([]*moira.NotificationEvent, error) { + ctx := connector.context + c := *connector.client + + eventsData, err := reply.Events(c.ZRevRange(ctx, triggerEventsKey(triggerID), start, start+size)) if err != nil { if errors.Is(err, redis.Nil) { return make([]*moira.NotificationEvent, 0), nil } - return nil, fmt.Errorf("failed to get range of trigger events, triggerID: %s, error: %w", triggerID, err) + return nil, fmt.Errorf("failed to get range for trigger events, triggerID: %s, error: %s", triggerID, err.Error()) } + return eventsData, nil } @@ -90,7 +85,7 @@ func (connector *DbConnector) FetchNotificationEvent() (moira.NotificationEvent, } if err != nil { - return event, fmt.Errorf("failed to fetch event: %w", err) + return event, fmt.Errorf("failed to fetch event: %s", err.Error()) } event, _ = reply.BRPopToEvent(response) @@ -113,13 +108,13 @@ func (connector *DbConnector) RemoveAllNotificationEvents() error { c := *connector.client if _, err := c.Del(ctx, notificationEventsList).Result(); err != nil { - return fmt.Errorf("failed to remove %s: %w", notificationEventsList, err) + return fmt.Errorf("failed to remove %s: %s", notificationEventsList, err.Error()) } return nil } -const ( +var ( notificationEventsList = "moira-trigger-events" notificationEventsUIList = "moira-trigger-events-ui" ) diff --git a/database/redis/notification_event_test.go b/database/redis/notification_event_test.go index 269b9d023..c6ee30dd2 100644 --- a/database/redis/notification_event_test.go +++ b/database/redis/notification_event_test.go @@ -33,7 +33,7 @@ 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(triggerID, 0, 1, 0, now) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) @@ -57,7 +57,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, 0, now) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -86,7 +86,7 @@ func TestNotificationEvents(t *testing.T) { }) Convey("Should has event by triggerID after fetch", func() { - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, 0, now) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -132,7 +132,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1, 0, now) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -148,7 +148,7 @@ func TestNotificationEvents(t *testing.T) { total := dataBase.GetNotificationEventCount(triggerID1, 0) So(total, ShouldEqual, 1) - actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1, 0, now) + actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -177,7 +177,7 @@ func TestNotificationEvents(t *testing.T) { Values: map[string]float64{}, }) - actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1, 0, now) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -223,7 +223,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, 0, now) + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -248,58 +248,11 @@ func TestNotificationEvents(t *testing.T) { total = dataBase.GetNotificationEventCount(triggerID3, now+1) So(total, ShouldEqual, 0) - actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1, 0, now) + actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) }) - Convey("Test `from` and `to` params", func() { - err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ - Timestamp: now, - State: moira.StateNODATA, - OldState: moira.StateNODATA, - TriggerID: triggerID3, - Metric: "my.metric", - }, true) - So(err, ShouldBeNil) - - Convey("returns event on exact time", func() { - actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now, now) - So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{ - { - Timestamp: now, - State: moira.StateNODATA, - OldState: moira.StateNODATA, - TriggerID: triggerID3, - Metric: "my.metric", - Values: map[string]float64{}, - }, - }) - }) - - Convey("not return event out of time range", func() { - actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now-2, now-1) - So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{}) - }) - - Convey("returns event in time range", func() { - actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, now-1, now+1) - So(err, ShouldBeNil) - So(actual, ShouldResemble, []*moira.NotificationEvent{ - { - Timestamp: now, - State: moira.StateNODATA, - OldState: moira.StateNODATA, - TriggerID: triggerID3, - Metric: "my.metric", - Values: map[string]float64{}, - }, - }) - }) - }) - Convey("Test removing notification events", func() { Convey("Should remove all notifications", func() { err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ @@ -359,7 +312,7 @@ func TestNotificationEventErrorConnection(t *testing.T) { } Convey("Should throw error when no connection", t, func() { - actual1, err := dataBase.GetNotificationEvents("123", 0, 1, 0, time.Now().Unix()) + actual1, err := dataBase.GetNotificationEvents("123", 0, 1) So(actual1, ShouldBeNil) So(err, ShouldNotBeNil) diff --git a/interfaces.go b/interfaces.go index d16bf5f1e..fc9693b6f 100644 --- a/interfaces.go +++ b/interfaces.go @@ -60,7 +60,7 @@ type Database interface { DeleteTriggerThrottling(triggerID string) error // NotificationEvent storing - GetNotificationEvents(triggerID string, start, size, from, to int64) ([]*NotificationEvent, error) + GetNotificationEvents(triggerID string, start, size int64) ([]*NotificationEvent, error) PushNotificationEvent(event *NotificationEvent, ui bool) error GetNotificationEventCount(triggerID string, from int64) int64 FetchNotificationEvent() (NotificationEvent, error) diff --git a/mock/moira-alert/database.go b/mock/moira-alert/database.go index 7356c8a1f..43cb828ad 100644 --- a/mock/moira-alert/database.go +++ b/mock/moira-alert/database.go @@ -490,18 +490,18 @@ func (mr *MockDatabaseMockRecorder) GetNotificationEventCount(arg0, arg1 any) *g } // GetNotificationEvents mocks base method. -func (m *MockDatabase) GetNotificationEvents(arg0 string, arg1, arg2, arg3, arg4 int64) ([]*moira.NotificationEvent, error) { +func (m *MockDatabase) GetNotificationEvents(arg0 string, arg1, arg2 int64) ([]*moira.NotificationEvent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationEvents", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "GetNotificationEvents", arg0, arg1, arg2) ret0, _ := ret[0].([]*moira.NotificationEvent) ret1, _ := ret[1].(error) return ret0, ret1 } // GetNotificationEvents indicates an expected call of GetNotificationEvents. -func (mr *MockDatabaseMockRecorder) GetNotificationEvents(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { +func (mr *MockDatabaseMockRecorder) GetNotificationEvents(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationEvents", reflect.TypeOf((*MockDatabase)(nil).GetNotificationEvents), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationEvents", reflect.TypeOf((*MockDatabase)(nil).GetNotificationEvents), arg0, arg1, arg2) } // GetNotifications mocks base method. diff --git a/mock/scheduler/scheduler.go b/mock/scheduler/scheduler.go index 3274a1760..8a146a314 100644 --- a/mock/scheduler/scheduler.go +++ b/mock/scheduler/scheduler.go @@ -48,7 +48,7 @@ func (m *MockScheduler) ScheduleNotification(arg0 moira.SchedulerParams, arg1 mo } // ScheduleNotification indicates an expected call of ScheduleNotification. -func (mr *MockSchedulerMockRecorder) ScheduleNotification(arg0, arg1 any) *gomock.Call { +func (mr *MockSchedulerMockRecorder) ScheduleNotification(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleNotification", reflect.TypeOf((*MockScheduler)(nil).ScheduleNotification), arg0, arg1) } diff --git a/state.go b/state.go index 8c66624a1..8f00e8fb7 100644 --- a/state.go +++ b/state.go @@ -61,16 +61,6 @@ func (state State) ToSelfState() string { return SelfStateOK } -// IsValid checks if valid State. -func (state State) IsValid() bool { - for _, allowedState := range eventStatesPriority { - if state == allowedState { - return true - } - } - return false -} - // ToMetricState is an auxiliary function to handle metric state properly. func (state TTLState) ToMetricState() State { if state == TTLStateDEL { From 64f2f54ca05057d45d12ef8cf89461daafd10cef Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Wed, 14 Aug 2024 13:37:12 +0300 Subject: [PATCH 06/36] fix(notifier): fixed sending of notifications with schedule between different days (#1069) --- notifier/scheduler.go | 17 +++++- notifier/scheduler_test.go | 118 ++++++++++++++++++++++++++++++++++++- 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/notifier/scheduler.go b/notifier/scheduler.go index 0868c7f8a..8b69f0a35 100644 --- a/notifier/scheduler.go +++ b/notifier/scheduler.go @@ -162,16 +162,25 @@ func calculateNextDelivery(schedule *moira.ScheduleData, nextTime time.Time) (ti if len(schedule.Days) == 0 { return nextTime, nil } + beginOffset := time.Duration(schedule.StartOffset) * time.Minute endOffset := time.Duration(schedule.EndOffset) * time.Minute - if schedule.EndOffset < schedule.StartOffset { - endOffset += time.Hour * 24 - } tzOffset := time.Duration(schedule.TimezoneOffset) * time.Minute localNextTime := nextTime.Add(-tzOffset).Truncate(time.Minute) localNextTimeDay := localNextTime.Truncate(24 * time.Hour) //nolint localNextWeekday := int(localNextTimeDay.Weekday()+6) % 7 //nolint + timeOfDay := localNextTime.Sub(localNextTimeDay) + + if schedule.EndOffset < schedule.StartOffset { + // The condition can only be fulfilled if the begin offset should be on the past day and not on the current day. + // In other variants end offset must be on the next day + if timeOfDay < beginOffset && timeOfDay < endOffset { + beginOffset -= time.Hour * 24 + } else { + endOffset += time.Hour * 24 + } + } if schedule.Days[localNextWeekday].Enabled && (localNextTime.Equal(localNextTimeDay.Add(beginOffset)) || localNextTime.After(localNextTimeDay.Add(beginOffset))) && @@ -186,9 +195,11 @@ func calculateNextDelivery(schedule *moira.ScheduleData, nextTime time.Time) (ti if localNextTime.After(nextLocalDayBegin.Add(beginOffset)) { continue } + if !schedule.Days[nextLocalWeekDay].Enabled { continue } + return nextLocalDayBegin.Add(beginOffset + tzOffset), nil } diff --git a/notifier/scheduler_test.go b/notifier/scheduler_test.go index 930955f59..cf1e75942 100644 --- a/notifier/scheduler_test.go +++ b/notifier/scheduler_test.go @@ -256,7 +256,7 @@ func TestSubscriptionSchedule(t *testing.T) { }) }) - Convey("Test advanced schedule (e.g. 02:00 - 00:00)", t, func() { + Convey("Test advanced schedule during current day (e.g. 02:00 - 00:00)", t, func() { // Schedule: 02:00 - 00:00 (GTM +3) Convey("Time is out of range, nextTime should resemble now", func() { // 2015-09-02, 14:00:00 GMT+03:00 @@ -342,6 +342,107 @@ func TestSubscriptionSchedule(t *testing.T) { So(throttled, ShouldBeFalse) }) }) + + Convey("Test advanced schedule between different days (e.g. 23:30 - 18:00)", t, func() { + // Schedule: 23:30 - 18:00 (GTM +3) + Convey("Time is out of range within the current day, nextTime should resemble now", func() { + // 2015-09-02, 23:45:00 GMT+03:00 + now := time.Unix(1441140300, 0) + subscription.ThrottlingEnabled = false + subscription.Schedule = schedule4 + dataBase.EXPECT().GetTriggerThrottling(event.TriggerID).Return(time.Unix(0, 0), time.Unix(0, 0)) + dataBase.EXPECT().GetSubscription(*event.SubscriptionID).Return(subscription, nil) + + next, throttled := scheduler.calculateNextDelivery(now, &event, logger) + // 2015-09-02, 23:45:00 GMT+03:00 + So(next, ShouldResemble, now) + So(throttled, ShouldBeFalse) + }) + + Convey("Time is out of range on the next day, nextTime should resemble now", func() { + // 2015-09-02, 00:35:00 GMT+03:00 + now := time.Unix(1441143300, 0) + subscription.ThrottlingEnabled = false + subscription.Schedule = schedule4 + dataBase.EXPECT().GetTriggerThrottling(event.TriggerID).Return(time.Unix(0, 0), time.Unix(0, 0)) + dataBase.EXPECT().GetSubscription(*event.SubscriptionID).Return(subscription, nil) + + next, throttled := scheduler.calculateNextDelivery(now, &event, logger) + // 2015-09-02, 00:35:00 GMT+03:00 + So(next, ShouldResemble, now) + So(throttled, ShouldBeFalse) + }) + + Convey("Time is in range, nextTime should resemble start of new period", func() { + // 2015-09-02, 20:00:00 GMT+03:00 + now := time.Unix(1441213200, 0) + subscription.ThrottlingEnabled = false + subscription.Schedule = schedule4 + dataBase.EXPECT().GetTriggerThrottling(event.TriggerID).Return(time.Unix(0, 0), time.Unix(0, 0)) + dataBase.EXPECT().GetSubscription(*event.SubscriptionID).Return(subscription, nil) + + next, throttled := scheduler.calculateNextDelivery(now, &event, logger) + // 2015-09-02, 23:30:00 GMT+03:00 + So(next, ShouldResemble, time.Unix(1441225800, 0)) + So(throttled, ShouldBeFalse) + }) + + Convey("Up border case, nextTime should resemble now", func() { + // 2015-09-02, 23:30:00 GMT+03:00 + now := time.Unix(1441225800, 0) + subscription.ThrottlingEnabled = false + subscription.Schedule = schedule4 + dataBase.EXPECT().GetTriggerThrottling(event.TriggerID).Return(time.Unix(0, 0), time.Unix(0, 0)) + dataBase.EXPECT().GetSubscription(*event.SubscriptionID).Return(subscription, nil) + + next, throttled := scheduler.calculateNextDelivery(now, &event, logger) + // 2015-09-02, 23:30:00 GMT+03:00 + So(next, ShouldResemble, now) + So(throttled, ShouldBeFalse) + }) + + Convey("Low border case, nextTime should resemble start of new period", func() { + // 2015-09-02, 18:00:00 GMT+03:00 + now := time.Unix(1441206000, 0) + subscription.ThrottlingEnabled = false + subscription.Schedule = schedule4 + dataBase.EXPECT().GetTriggerThrottling(event.TriggerID).Return(time.Unix(0, 0), time.Unix(0, 0)) + dataBase.EXPECT().GetSubscription(*event.SubscriptionID).Return(subscription, nil) + + next, throttled := scheduler.calculateNextDelivery(now, &event, logger) + // 2015-09-02, 23:30:00 GMT+03:00 + So(next, ShouldResemble, time.Unix(1441225800, 0)) + So(throttled, ShouldBeFalse) + }) + + Convey("Low border case - 1 minute, nextTime should resemble now", func() { + // 2015-09-01, 17:59:00 GMT+03:00 + now := time.Unix(1441205940, 0) + subscription.ThrottlingEnabled = false + subscription.Schedule = schedule4 + dataBase.EXPECT().GetTriggerThrottling(event.TriggerID).Return(time.Unix(0, 0), time.Unix(0, 0)) + dataBase.EXPECT().GetSubscription(*event.SubscriptionID).Return(subscription, nil) + + next, throttled := scheduler.calculateNextDelivery(now, &event, logger) + // 2015-09-01, 17:59:00 GMT+03:00 + So(next, ShouldResemble, now) + So(throttled, ShouldBeFalse) + }) + + Convey("Up border case - 1 minute, nextTime should resemble start of new period", func() { + // 2015-09-02, 23:29:00 GMT+03:00 + now := time.Unix(1441225740, 0) + subscription.ThrottlingEnabled = false + subscription.Schedule = schedule4 + dataBase.EXPECT().GetTriggerThrottling(event.TriggerID).Return(time.Unix(0, 0), time.Unix(0, 0)) + dataBase.EXPECT().GetSubscription(*event.SubscriptionID).Return(subscription, nil) + + next, throttled := scheduler.calculateNextDelivery(now, &event, logger) + // 2015-09-02, 23:30:00 GMT+03:00 + So(next, ShouldResemble, time.Unix(1441225800, 0)) + So(throttled, ShouldBeFalse) + }) + }) } var schedule1 = moira.ScheduleData{ @@ -388,3 +489,18 @@ var schedule3 = moira.ScheduleData{ {Enabled: true}, }, } + +var schedule4 = moira.ScheduleData{ + StartOffset: 1410, // 23:30 + EndOffset: 1080, // 18:00 + TimezoneOffset: -180, // (GMT +3) + Days: []moira.ScheduleDataDay{ + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + }, +} From 0197720a100fc900a5fb5dc72bee7f42e8366acc Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Thu, 15 Aug 2024 16:39:33 +0700 Subject: [PATCH 07/36] feat(api): query params to event history (#1072) --- api/controller/contact_events_test.go | 6 +- api/controller/events.go | 81 +++++++++++++- api/controller/events_test.go | 122 ++++++++++++++++++++-- api/handler/constants.go | 10 ++ api/handler/contact_events.go | 10 +- api/handler/event.go | 56 +++++++++- api/handler/trigger.go | 13 ++- api/handler/trigger_test.go | 6 +- api/middleware/context.go | 54 ++++++++++ api/middleware/context_test.go | 89 ++++++++++++++++ api/middleware/middleware.go | 18 +++- database/redis/notification_event.go | 27 +++-- database/redis/notification_event_test.go | 72 +++++++++++-- interfaces.go | 2 +- mock/moira-alert/database.go | 16 +-- mock/scheduler/scheduler.go | 2 +- state.go | 10 ++ 17 files changed, 532 insertions(+), 62 deletions(-) diff --git a/api/controller/contact_events_test.go b/api/controller/contact_events_test.go index c208beaf6..11308dbf7 100644 --- a/api/controller/contact_events_test.go +++ b/api/controller/contact_events_test.go @@ -81,7 +81,7 @@ func TestGetContactEventsByIdWithLimit(t *testing.T) { Convey("Ensure that request with default parameters would return both event items (no url params specified)", t, func() { dataBase.EXPECT().GetContact(contact.ID).Return(contactExpect, nil).AnyTimes() - dataBase.EXPECT().GetNotificationsHistoryByContactId(contact.ID, defaultFromParameter, defaultToParameter, defaultPage, defaultSize).Return(items, nil) + dataBase.EXPECT().GetNotificationsHistoryByContactID(contact.ID, defaultFromParameter, defaultToParameter, defaultPage, defaultSize).Return(items, nil) actualEvents, err := GetContactEventsHistoryByID(dataBase, contact.ID, defaultFromParameter, defaultToParameter, defaultPage, defaultSize) @@ -91,7 +91,7 @@ func TestGetContactEventsByIdWithLimit(t *testing.T) { Convey("Ensure that request with only 'from' parameter given and 'to' default will return only one (newest) event", t, func() { dataBase.EXPECT().GetContact(contact.ID).Return(contactExpect, nil).AnyTimes() - dataBase.EXPECT().GetNotificationsHistoryByContactId(contact.ID, defaultFromParameter-20, defaultToParameter, defaultPage, defaultSize).Return(items[:1], nil) + dataBase.EXPECT().GetNotificationsHistoryByContactID(contact.ID, defaultFromParameter-20, defaultToParameter, defaultPage, defaultSize).Return(items[:1], nil) actualEvents, err := GetContactEventsHistoryByID(dataBase, contact.ID, defaultFromParameter-20, defaultToParameter, defaultPage, defaultSize) So(err, ShouldBeNil) @@ -104,7 +104,7 @@ func TestGetContactEventsByIdWithLimit(t *testing.T) { Convey("Ensure that request with only 'to' parameter given and 'from' default will return only one (oldest) event", t, func() { dataBase.EXPECT().GetContact(contact.ID).Return(contactExpect, nil).AnyTimes() - dataBase.EXPECT().GetNotificationsHistoryByContactId(contact.ID, defaultFromParameter, defaultToParameter-30, defaultPage, defaultSize).Return(items[1:], nil) + dataBase.EXPECT().GetNotificationsHistoryByContactID(contact.ID, defaultFromParameter, defaultToParameter-30, defaultPage, defaultSize).Return(items[1:], nil) actualEvents, err := GetContactEventsHistoryByID(dataBase, contact.ID, defaultFromParameter, defaultToParameter-30, defaultPage, defaultSize) So(err, ShouldBeNil) diff --git a/api/controller/events.go b/api/controller/events.go index 218c8ecfe..c38fd9152 100644 --- a/api/controller/events.go +++ b/api/controller/events.go @@ -1,14 +1,25 @@ package controller import ( + "regexp" + "github.com/moira-alert/moira" "github.com/moira-alert/moira/api" "github.com/moira-alert/moira/api/dto" ) -// GetTriggerEvents gets trigger event from current page and all trigger event count. -func GetTriggerEvents(database moira.Database, triggerID string, page int64, size int64) (*dto.EventsList, *api.ErrorResponse) { - events, err := database.GetNotificationEvents(triggerID, page*size, size-1) +// GetTriggerEvents gets trigger event from current page and all trigger event count. Events list is filtered by time range +// with `from` and `to` params (`from` and `to` should be "+inf", "-inf" or int64 converted to string), +// by metric (regular expression) and by states. If `states` map is empty or nil then all states are accepted. +func GetTriggerEvents( + database moira.Database, + triggerID string, + page, size int64, + from, to string, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) (*dto.EventsList, *api.ErrorResponse) { + events, err := getFilteredNotificationEvents(database, triggerID, page, size, from, to, metricRegexp, states) if err != nil { return nil, api.ErrorInternalServer(err) } @@ -18,7 +29,7 @@ func GetTriggerEvents(database moira.Database, triggerID string, page int64, siz Size: size, Page: page, Total: eventCount, - List: make([]moira.NotificationEvent, 0), + List: make([]moira.NotificationEvent, 0, len(events)), } for _, event := range events { if event != nil { @@ -28,6 +39,68 @@ func GetTriggerEvents(database moira.Database, triggerID string, page int64, siz return eventsList, nil } +func getFilteredNotificationEvents( + database moira.Database, + triggerID string, + page, size int64, + from, to string, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) ([]*moira.NotificationEvent, error) { + // fetch all events + if size < 0 { + events, err := database.GetNotificationEvents(triggerID, page, size, from, to) + if err != nil { + return nil, err + } + + return filterNotificationEvents(events, metricRegexp, states), nil + } + + // fetch at most `size` events + filtered := make([]*moira.NotificationEvent, 0, size) + var count int64 + + for int64(len(filtered)) < size { + eventsData, err := database.GetNotificationEvents(triggerID, page+count, size, from, to) + if err != nil { + return nil, err + } + + if len(eventsData) == 0 { + break + } + + filtered = append(filtered, filterNotificationEvents(eventsData, metricRegexp, states)...) + count += 1 + + if int64(len(eventsData)) < size { + break + } + } + + return filtered, nil +} + +func filterNotificationEvents( + notificationEvents []*moira.NotificationEvent, + metricRegexp *regexp.Regexp, + states map[string]struct{}, +) []*moira.NotificationEvent { + filteredNotificationEvents := make([]*moira.NotificationEvent, 0) + + for _, event := range notificationEvents { + if metricRegexp.MatchString(event.Metric) { + _, ok := states[string(event.State)] + if len(states) == 0 || ok { + filteredNotificationEvents = append(filteredNotificationEvents, event) + } + } + } + + return filteredNotificationEvents +} + // DeleteAllEvents deletes all notification events. func DeleteAllEvents(database moira.Database) *api.ErrorResponse { if err := database.RemoveAllNotificationEvents(); err != nil { diff --git a/api/controller/events_test.go b/api/controller/events_test.go index 5cf3e3f34..4c84f7fc2 100644 --- a/api/controller/events_test.go +++ b/api/controller/events_test.go @@ -2,6 +2,7 @@ package controller import ( "fmt" + "regexp" "testing" "github.com/gofrs/uuid" @@ -13,19 +14,38 @@ import ( "go.uber.org/mock/gomock" ) +var ( + allMetrics = regexp.MustCompile(``) + allStates map[string]struct{} +) + func TestGetEvents(t *testing.T) { mockCtrl := gomock.NewController(t) dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) defer mockCtrl.Finish() triggerID := uuid.Must(uuid.NewV4()).String() + var page int64 = 10 var size int64 = 100 + from := "-inf" + to := "+inf" Convey("Test has events", t, func() { var total int64 = 6000000 - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return([]*moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, nil) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to). + Return([]*moira.NotificationEvent{ + { + State: moira.StateNODATA, + OldState: moira.StateOK, + }, + { + State: moira.StateOK, + OldState: moira.StateNODATA, + }, + }, nil) dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ List: []moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, @@ -37,9 +57,9 @@ func TestGetEvents(t *testing.T) { Convey("Test no events", t, func() { var total int64 - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return(make([]*moira.NotificationEvent, 0), nil) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(make([]*moira.NotificationEvent, 0), nil) dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ List: make([]moira.NotificationEvent, 0), @@ -51,11 +71,101 @@ func TestGetEvents(t *testing.T) { Convey("Test error", t, func() { expected := fmt.Errorf("oooops! Can not get all contacts") - dataBase.EXPECT().GetNotificationEvents(triggerID, page*size, size-1).Return(nil, expected) - list, err := GetTriggerEvents(dataBase, triggerID, page, size) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(nil, expected) + list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(list, ShouldBeNil) }) + + Convey("Test filtering", t, func() { + Convey("by metric regex", func() { + page = 0 + size = 2 + Convey("with same pattern", func() { + filtered := []*moira.NotificationEvent{ + {Metric: "metric.test.event1"}, + {Metric: "a.metric.test.event2"}, + } + notFiltered := []*moira.NotificationEvent{ + {Metric: "another.mEtric.test.event"}, + {Metric: "metric.test"}, + } + firstPortion := append(make([]*moira.NotificationEvent, 0), notFiltered[0], filtered[0]) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(firstPortion, nil) + + secondPortion := append(make([]*moira.NotificationEvent, 0), filtered[1], notFiltered[1]) + dataBase.EXPECT().GetNotificationEvents(triggerID, page+1, size, from, to).Return(secondPortion, nil) + + total := int64(len(firstPortion) + len(secondPortion)) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, regexp.MustCompile(`metric\.test\.event`), allStates) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(filtered), + }) + }) + }) + page = 0 + size = -1 + + Convey("by state", func() { + filtered := []*moira.NotificationEvent{ + {State: moira.StateOK}, + {State: moira.StateTEST}, + {State: moira.StateEXCEPTION}, + } + notFiltered := []*moira.NotificationEvent{ + {State: moira.StateWARN}, + {State: moira.StateNODATA}, + {State: moira.StateERROR}, + } + Convey("with empty map all allowed", func() { + total := int64(len(filtered) + len(notFiltered)) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(append(filtered, notFiltered...)), + }) + }) + + Convey("with given states", func() { + total := int64(len(filtered) + len(notFiltered)) + dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) + dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + + actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, map[string]struct{}{ + string(moira.StateOK): {}, + string(moira.StateEXCEPTION): {}, + string(moira.StateTEST): {}, + }) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: page, + Size: size, + Total: total, + List: toDTOList(filtered), + }) + }) + }) + }) +} + +func toDTOList(eventPtrs []*moira.NotificationEvent) []moira.NotificationEvent { + events := make([]moira.NotificationEvent, 0, len(eventPtrs)) + for _, ptr := range eventPtrs { + events = append(events, *ptr) + } + return events } func TestDeleteAllNotificationEvents(t *testing.T) { diff --git a/api/handler/constants.go b/api/handler/constants.go index 413204120..16585b0f1 100644 --- a/api/handler/constants.go +++ b/api/handler/constants.go @@ -1,5 +1,15 @@ package handler +const allMetricsPattern = ".*" + +const ( + eventDefaultPage = 0 + eventDefaultSize = 100 + eventDefaultFrom = "-inf" + eventDefaultTo = "+inf" + eventDefaultMetric = allMetricsPattern +) + const ( contactEventsDefaultFrom = "-3hour" contactEventsDefaultTo = "now" diff --git a/api/handler/contact_events.go b/api/handler/contact_events.go index d5e90333a..605165a86 100644 --- a/api/handler/contact_events.go +++ b/api/handler/contact_events.go @@ -33,11 +33,11 @@ func contactEvents(router chi.Router) { // @id get-contact-events-by-id // @tags contact // @produce json -// @param contactID path string true "Contact ID" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) -// @param from query string false "Start time of the time range" default(-3hour) -// @param to query string false "End time of the time range" default(now) -// @param size query int false "Number of items to return or all items if size == -1 (if size == -1 p should be zero for correct work)" default(100) -// @param p query int false "Defines the index of data portion (combined with size). E.g, p=2, size=100 will return records from 200 (including), to 300 (not including)" default(0) +// @param contactID path string true "Contact ID" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) +// @param from query string false "Start time of the time range" default(-3hour) +// @param to query string false "End time of the time range" default(now) +// @param size query int false "Number of items to return or all items if size == -1 (if size == -1 p should be zero for correct work)" default(100) +// @param p query int false "Defines the index of data portion (combined with size). E.g, p=2, size=100 will return records from 200 (including), to 300 (not including)" default(0) // @success 200 {object} dto.ContactEventItemList "Successfully received contact events" // @failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" // @failure 403 {object} api.ErrorForbiddenExample "Forbidden" diff --git a/api/handler/event.go b/api/handler/event.go index c5d8602d5..61ef68104 100644 --- a/api/handler/event.go +++ b/api/handler/event.go @@ -1,7 +1,13 @@ package handler import ( + "fmt" "net/http" + "regexp" + "strconv" + "time" + + "github.com/go-graphite/carbonapi/date" "github.com/go-chi/chi" "github.com/go-chi/render" @@ -11,7 +17,13 @@ import ( ) func event(router chi.Router) { - router.With(middleware.TriggerContext, middleware.Paginate(0, 100)).Get("/{triggerId}", getEventsList) + router.With( + middleware.TriggerContext, + middleware.Paginate(eventDefaultPage, eventDefaultSize), + middleware.DateRange(eventDefaultFrom, eventDefaultTo), + middleware.MetricContext(eventDefaultMetric), + middleware.StatesContext(), + ).Get("/{triggerId}", getEventsList) router.With(middleware.AdminOnlyMiddleware()).Delete("/all", deleteAllEvents) } @@ -21,9 +33,13 @@ func event(router chi.Router) { // @id get-events-list // @tags event // @produce json -// @param triggerID path string true "The ID of updated trigger" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) -// @param size query int false "Number of items to be displayed on one page" default(100) -// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) +// @param triggerID path string true "The ID of updated trigger" default(bcba82f5-48cf-44c0-b7d6-e1d32c64a88c) +// @param size query int false "Number of items to be displayed on one page. if size = -1 then all events returned" default(100) +// @param p query int false "Defines the number of the displayed page. E.g, p=2 would display the 2nd page" default(0) +// @param from query string false "Start time of the time range" default(-3hours) +// @param to query string false "End time of the time range" default(now) +// @param metric query string false "Regular expression that will be used to filter events" default(.*) +// @param states query []string false "String of ',' separated state names. If empty then all states will be used." collectionFormat(csv) // @success 200 {object} dto.EventsList "Events fetched successfully" // @Failure 400 {object} api.ErrorInvalidRequestExample "Bad request from client" // @Failure 404 {object} api.ErrorNotFoundExample "Resource not found" @@ -34,7 +50,37 @@ func getEventsList(writer http.ResponseWriter, request *http.Request) { triggerID := middleware.GetTriggerID(request) size := middleware.GetSize(request) page := middleware.GetPage(request) - eventsList, err := controller.GetTriggerEvents(database, triggerID, page, size) + fromStr := middleware.GetFromStr(request) + toStr := middleware.GetToStr(request) + + if fromStr != "-inf" { + from := date.DateParamToEpoch(fromStr, "UTC", 0, time.UTC) + if from == 0 { + render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse from: %s", fromStr))) //nolint + return + } + fromStr = strconv.FormatInt(from, 10) + } + + if toStr != "+inf" { + to := date.DateParamToEpoch(toStr, "UTC", 0, time.UTC) + if to == 0 { + render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse to: %v", to))) //nolint + return + } + toStr = strconv.FormatInt(to, 10) + } + + metricStr := middleware.GetMetric(request) + metricRegexp, errCompile := regexp.Compile(metricStr) + if errCompile != nil { + _ = render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("can not parse metric \"%s\": %w", metricStr, errCompile))) + return + } + + states := middleware.GetStates(request) + + eventsList, err := controller.GetTriggerEvents(database, triggerID, page, size, fromStr, toStr, metricRegexp, states) if err != nil { render.Render(writer, request, err) //nolint return diff --git a/api/handler/trigger.go b/api/handler/trigger.go index d17b09cc9..1356ecbe1 100644 --- a/api/handler/trigger.go +++ b/api/handler/trigger.go @@ -2,6 +2,7 @@ package handler import ( "net/http" + "regexp" "time" "github.com/go-chi/chi" @@ -179,7 +180,17 @@ func checkingTemplateFilling(request *http.Request, trigger dto.Trigger) *api.Er return nil } - eventsList, err := controller.GetTriggerEvents(database, trigger.ID, 0, 3) + const ( + page = 0 + size = 3 + ) + + var ( + allMetricRegexp = regexp.MustCompile(allMetricsPattern) + allStates map[string]struct{} + ) + + eventsList, err := controller.GetTriggerEvents(database, trigger.ID, page, size, eventDefaultFrom, eventDefaultTo, allMetricRegexp, allStates) if err != nil { return err } diff --git a/api/handler/trigger_test.go b/api/handler/trigger_test.go index 1e32507d8..f9d96ec3c 100644 --- a/api/handler/trigger_test.go +++ b/api/handler/trigger_test.go @@ -420,7 +420,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() @@ -464,7 +464,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() @@ -508,7 +508,7 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) - db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) + db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() diff --git a/api/middleware/context.go b/api/middleware/context.go index 5dbc88e34..0d9e26f31 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/go-chi/chi" @@ -277,3 +278,56 @@ func AuthorizationContext(auth *api.Authorization) func(next http.Handler) http. }) } } + +// MetricContext is a function that gets `metric` value from query string and places it in context. If query does not have value sets given value. +func MetricContext(defaultMetric string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + urlValues, err := url.ParseQuery(request.URL.RawQuery) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + metric := urlValues.Get("metric") + if metric == "" { + metric = defaultMetric + } + + ctx := context.WithValue(request.Context(), metricContextKey, metric) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} + +const statesArraySeparator = "," + +// StatesContext is a function that gets `states` value from query string and places it in context. If query does not have value empty map will be used. +func StatesContext() func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + urlValues, err := url.ParseQuery(request.URL.RawQuery) + if err != nil { + render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint + return + } + + states := make(map[string]struct{}, 0) + + statesStr := urlValues.Get("states") + if statesStr != "" { + statesList := strings.Split(statesStr, statesArraySeparator) + for _, state := range statesList { + if !moira.State(state).IsValid() { + _ = render.Render(writer, request, api.ErrorInvalidRequest(fmt.Errorf("bad state in query parameter: %s", state))) + return + } + states[state] = struct{}{} + } + } + + ctx := context.WithValue(request.Context(), statesContextKey, states) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} diff --git a/api/middleware/context_test.go b/api/middleware/context_test.go index 1c4e9e7c9..6031e77ee 100644 --- a/api/middleware/context_test.go +++ b/api/middleware/context_test.go @@ -216,3 +216,92 @@ func TestTargetNameMiddleware(t *testing.T) { }) }) } + +func TestMetricProviderMiddleware(t *testing.T) { + Convey("Check metric provider", t, func() { + responseWriter := httptest.NewRecorder() + defaultMetric := ".*" + + Convey("status ok with correct query paramete", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?metric=test%5C.metric.*", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := MetricContext(defaultMetric) + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("status bad request with wrong url query parameter", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?metric%=test", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := MetricContext(defaultMetric) + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + + So(contents, ShouldEqual, expectedBadRequest) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + }) +} + +func TestStatesProviderMiddleware(t *testing.T) { + Convey("Checking states provide", t, func() { + responseWriter := httptest.NewRecorder() + + Convey("ok with correct states list", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("bad request with bad states list", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states=OK%2CERROR%2Cwarn", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("bad request with wrong url query parameter", func() { + testRequest := httptest.NewRequest(http.MethodGet, "/test?states%=test", nil) + handler := func(w http.ResponseWriter, r *http.Request) {} + + middlewareFunc := StatesContext() + wrappedHandler := middlewareFunc(http.HandlerFunc(handler)) + + wrappedHandler.ServeHTTP(responseWriter, testRequest) + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, _ := io.ReadAll(response.Body) + contents := string(contentBytes) + + So(contents, ShouldEqual, expectedBadRequest) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + }) +} diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index a520f01b4..80d65066d 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -39,6 +39,8 @@ var ( teamIDKey ContextKey = "teamID" teamUserIDKey ContextKey = "teamUserIDKey" authKey ContextKey = "auth" + metricContextKey ContextKey = "metric" + statesContextKey ContextKey = "states" anonymousUser = "anonymous" ) @@ -63,7 +65,7 @@ func GetTriggerID(request *http.Request) string { return request.Context().Value(triggerIDKey).(string) } -// GetLocalMetricTTL gets local metric ttl duration time from request context, which was sets in TriggerContext middleware. +// GetMetricTTL gets local metric ttl duration time from request context, which was sets in TriggerContext middleware. func GetMetricTTL(request *http.Request) map[moira.ClusterKey]time.Duration { return request.Context().Value(clustersMetricTTLKey).(map[moira.ClusterKey]time.Duration) } @@ -118,13 +120,13 @@ func GetToStr(request *http.Request) string { return request.Context().Value(toKey).(string) } -// SetTimeSeriesNames sets to requests context timeSeriesNames from saved trigger. +// SetTimeSeriesNames sets to request's context timeSeriesNames from saved trigger. func SetTimeSeriesNames(request *http.Request, timeSeriesNames map[string]bool) { ctx := context.WithValue(request.Context(), timeSeriesNamesKey, timeSeriesNames) *request = *request.WithContext(ctx) } -// GetTimeSeriesNames gets from requests context timeSeriesNames from saved trigger. +// GetTimeSeriesNames gets from request's context timeSeriesNames from saved trigger. func GetTimeSeriesNames(request *http.Request) map[string]bool { return request.Context().Value(timeSeriesNamesKey).(map[string]bool) } @@ -162,3 +164,13 @@ func SetContextValueForTest(ctx context.Context, key string, value interface{}) func GetAuth(request *http.Request) *api.Authorization { return request.Context().Value(authKey).(*api.Authorization) } + +// GetMetric is used to retrieve metric name. +func GetMetric(request *http.Request) string { + return request.Context().Value(metricContextKey).(string) +} + +// GetStates is used to retrieve trigger state. +func GetStates(request *http.Request) map[string]struct{} { + return request.Context().Value(statesContextKey).(map[string]struct{}) +} diff --git a/database/redis/notification_event.go b/database/redis/notification_event.go index 4a5974969..c094794e4 100644 --- a/database/redis/notification_event.go +++ b/database/redis/notification_event.go @@ -14,19 +14,24 @@ import ( var eventsTTL int64 = 3600 * 24 * 30 -// GetNotificationEvents gets NotificationEvents by given triggerID and interval. -func (connector *DbConnector) GetNotificationEvents(triggerID string, start int64, size int64) ([]*moira.NotificationEvent, error) { - ctx := connector.context - c := *connector.client - - eventsData, err := reply.Events(c.ZRevRange(ctx, triggerEventsKey(triggerID), start, start+size)) +// GetNotificationEvents gets NotificationEvents by given triggerID and interval. The events are also filtered by time range +// with `from`, `to` params (`from` and `to` should be "+inf", "-inf" or int64 converted to string). +func (connector *DbConnector) GetNotificationEvents(triggerID string, page, size int64, from, to string) ([]*moira.NotificationEvent, error) { + ctx := connector.Context() + client := connector.Client() + + eventsData, err := reply.Events(client.ZRevRangeByScore(ctx, triggerEventsKey(triggerID), &redis.ZRangeBy{ + Min: from, + Max: to, + Offset: page * size, + Count: size, + })) if err != nil { if errors.Is(err, redis.Nil) { return make([]*moira.NotificationEvent, 0), nil } - return nil, fmt.Errorf("failed to get range for trigger events, triggerID: %s, error: %s", triggerID, err.Error()) + return nil, fmt.Errorf("failed to get range of trigger events, triggerID: %s, error: %w", triggerID, err) } - return eventsData, nil } @@ -85,7 +90,7 @@ func (connector *DbConnector) FetchNotificationEvent() (moira.NotificationEvent, } if err != nil { - return event, fmt.Errorf("failed to fetch event: %s", err.Error()) + return event, fmt.Errorf("failed to fetch event: %w", err) } event, _ = reply.BRPopToEvent(response) @@ -108,13 +113,13 @@ func (connector *DbConnector) RemoveAllNotificationEvents() error { c := *connector.client if _, err := c.Del(ctx, notificationEventsList).Result(); err != nil { - return fmt.Errorf("failed to remove %s: %s", notificationEventsList, err.Error()) + return fmt.Errorf("failed to remove %s: %w", notificationEventsList, err) } return nil } -var ( +const ( notificationEventsList = "moira-trigger-events" notificationEventsUIList = "moira-trigger-events-ui" ) diff --git a/database/redis/notification_event_test.go b/database/redis/notification_event_test.go index c6ee30dd2..0266ed24e 100644 --- a/database/redis/notification_event_test.go +++ b/database/redis/notification_event_test.go @@ -1,6 +1,7 @@ package redis import ( + "strconv" "testing" "time" @@ -19,8 +20,10 @@ const ( ) var ( - now = time.Now().Unix() - value = float64(0) + allTimeFrom = "-inf" + allTimeTo = "+inf" + now = time.Now().Unix() + value = float64(0) ) // nolint @@ -33,7 +36,7 @@ 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(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, allTimeFrom, allTimeTo) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) @@ -57,7 +60,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, allTimeFrom, allTimeTo) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -86,7 +89,7 @@ func TestNotificationEvents(t *testing.T) { }) Convey("Should has event by triggerID after fetch", func() { - actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID, 0, 1, allTimeFrom, allTimeTo) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -132,7 +135,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1, allTimeFrom, allTimeTo) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -148,7 +151,7 @@ func TestNotificationEvents(t *testing.T) { total := dataBase.GetNotificationEventCount(triggerID1, 0) So(total, ShouldEqual, 1) - actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1) + actual, err = dataBase.GetNotificationEvents(triggerID2, 0, 1, allTimeFrom, allTimeTo) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -177,7 +180,7 @@ func TestNotificationEvents(t *testing.T) { Values: map[string]float64{}, }) - actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID1, 0, 1, allTimeFrom, allTimeTo) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -223,7 +226,7 @@ func TestNotificationEvents(t *testing.T) { }, true) So(err, ShouldBeNil) - actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1) + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, allTimeFrom, allTimeTo) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.NotificationEvent{ { @@ -248,11 +251,58 @@ func TestNotificationEvents(t *testing.T) { total = dataBase.GetNotificationEventCount(triggerID3, now+1) So(total, ShouldEqual, 0) - actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1) + actual, err = dataBase.GetNotificationEvents(triggerID3, 1, 1, allTimeFrom, allTimeTo) So(err, ShouldBeNil) So(actual, ShouldResemble, make([]*moira.NotificationEvent, 0)) }) + Convey("Test `from` and `to` params", func() { + err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + }, true) + So(err, ShouldBeNil) + + Convey("returns event on exact time", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, strconv.FormatInt(now, 10), strconv.FormatInt(now, 10)) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{ + { + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + Values: map[string]float64{}, + }, + }) + }) + + Convey("not return event out of time range", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, strconv.FormatInt(now-2, 10), strconv.FormatInt(now-1, 10)) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{}) + }) + + Convey("returns event in time range", func() { + actual, err := dataBase.GetNotificationEvents(triggerID3, 0, 1, strconv.FormatInt(now-1, 10), strconv.FormatInt(now+1, 10)) + So(err, ShouldBeNil) + So(actual, ShouldResemble, []*moira.NotificationEvent{ + { + Timestamp: now, + State: moira.StateNODATA, + OldState: moira.StateNODATA, + TriggerID: triggerID3, + Metric: "my.metric", + Values: map[string]float64{}, + }, + }) + }) + }) + Convey("Test removing notification events", func() { Convey("Should remove all notifications", func() { err := dataBase.PushNotificationEvent(&moira.NotificationEvent{ @@ -312,7 +362,7 @@ func TestNotificationEventErrorConnection(t *testing.T) { } Convey("Should throw error when no connection", t, func() { - actual1, err := dataBase.GetNotificationEvents("123", 0, 1) + actual1, err := dataBase.GetNotificationEvents("123", 0, 1, allTimeFrom, allTimeTo) So(actual1, ShouldBeNil) So(err, ShouldNotBeNil) diff --git a/interfaces.go b/interfaces.go index fc9693b6f..f3f1559ef 100644 --- a/interfaces.go +++ b/interfaces.go @@ -60,7 +60,7 @@ type Database interface { DeleteTriggerThrottling(triggerID string) error // NotificationEvent storing - GetNotificationEvents(triggerID string, start, size int64) ([]*NotificationEvent, error) + GetNotificationEvents(triggerID string, start, size int64, from, to string) ([]*NotificationEvent, error) PushNotificationEvent(event *NotificationEvent, ui bool) error GetNotificationEventCount(triggerID string, from int64) int64 FetchNotificationEvent() (NotificationEvent, error) diff --git a/mock/moira-alert/database.go b/mock/moira-alert/database.go index 43cb828ad..a9953446c 100644 --- a/mock/moira-alert/database.go +++ b/mock/moira-alert/database.go @@ -191,7 +191,7 @@ func (m *MockDatabase) CleanUpOutdatedNotificationHistory(arg0 int64) error { } // CleanUpOutdatedNotificationHistory indicates an expected call of CleanUpOutdatedNotificationHistory. -func (mr *MockDatabaseMockRecorder) CleanUpOutdatedNotificationHistory(arg0 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) CleanUpOutdatedNotificationHistory(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanUpOutdatedNotificationHistory", reflect.TypeOf((*MockDatabase)(nil).CleanUpOutdatedNotificationHistory), arg0) } @@ -490,18 +490,18 @@ func (mr *MockDatabaseMockRecorder) GetNotificationEventCount(arg0, arg1 any) *g } // GetNotificationEvents mocks base method. -func (m *MockDatabase) GetNotificationEvents(arg0 string, arg1, arg2 int64) ([]*moira.NotificationEvent, error) { +func (m *MockDatabase) GetNotificationEvents(arg0 string, arg1, arg2 int64, arg3, arg4 string) ([]*moira.NotificationEvent, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNotificationEvents", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetNotificationEvents", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].([]*moira.NotificationEvent) ret1, _ := ret[1].(error) return ret0, ret1 } // GetNotificationEvents indicates an expected call of GetNotificationEvents. -func (mr *MockDatabaseMockRecorder) GetNotificationEvents(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockDatabaseMockRecorder) GetNotificationEvents(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationEvents", reflect.TypeOf((*MockDatabase)(nil).GetNotificationEvents), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationEvents", reflect.TypeOf((*MockDatabase)(nil).GetNotificationEvents), arg0, arg1, arg2, arg3, arg4) } // GetNotifications mocks base method. @@ -520,7 +520,7 @@ func (mr *MockDatabaseMockRecorder) GetNotifications(arg0, arg1 any) *gomock.Cal return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotifications", reflect.TypeOf((*MockDatabase)(nil).GetNotifications), arg0, arg1) } -// GetNotificationsHistoryByContactId mocks base method. +// GetNotificationsHistoryByContactID mocks base method. func (m *MockDatabase) GetNotificationsHistoryByContactID(arg0 string, arg1, arg2, arg3, arg4 int64) ([]*moira.NotificationEventHistoryItem, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetNotificationsHistoryByContactID", arg0, arg1, arg2, arg3, arg4) @@ -529,8 +529,8 @@ func (m *MockDatabase) GetNotificationsHistoryByContactID(arg0 string, arg1, arg return ret0, ret1 } -// GetNotificationsHistoryByContactId indicates an expected call of GetNotificationsHistoryByContactId. -func (mr *MockDatabaseMockRecorder) GetNotificationsHistoryByContactId(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +// GetNotificationsHistoryByContactID indicates an expected call of GetNotificationsHistoryByContactID. +func (mr *MockDatabaseMockRecorder) GetNotificationsHistoryByContactID(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNotificationsHistoryByContactID", reflect.TypeOf((*MockDatabase)(nil).GetNotificationsHistoryByContactID), arg0, arg1, arg2, arg3, arg4) } diff --git a/mock/scheduler/scheduler.go b/mock/scheduler/scheduler.go index 8a146a314..3274a1760 100644 --- a/mock/scheduler/scheduler.go +++ b/mock/scheduler/scheduler.go @@ -48,7 +48,7 @@ func (m *MockScheduler) ScheduleNotification(arg0 moira.SchedulerParams, arg1 mo } // ScheduleNotification indicates an expected call of ScheduleNotification. -func (mr *MockSchedulerMockRecorder) ScheduleNotification(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockSchedulerMockRecorder) ScheduleNotification(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScheduleNotification", reflect.TypeOf((*MockScheduler)(nil).ScheduleNotification), arg0, arg1) } diff --git a/state.go b/state.go index 8f00e8fb7..8c66624a1 100644 --- a/state.go +++ b/state.go @@ -61,6 +61,16 @@ func (state State) ToSelfState() string { return SelfStateOK } +// IsValid checks if valid State. +func (state State) IsValid() bool { + for _, allowedState := range eventStatesPriority { + if state == allowedState { + return true + } + } + return false +} + // ToMetricState is an auxiliary function to handle metric state properly. func (state TTLState) ToMetricState() State { if state == TTLStateDEL { From d880235a8390ee0fcc0e9b655a10e0a0091a039e Mon Sep 17 00:00:00 2001 From: Xenia N Date: Mon, 26 Aug 2024 12:53:39 +0500 Subject: [PATCH 08/36] feat(build): Upgrade go 1.22 (#1062) Co-authored-by: almostinf --- Dockerfile.api | 2 +- Dockerfile.checker | 2 +- Dockerfile.cli | 2 +- Dockerfile.filter | 2 +- Dockerfile.notifier | 2 +- go.mod | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dockerfile.api b/Dockerfile.api index 4d3226d3d..b72840155 100644 --- a/Dockerfile.api +++ b/Dockerfile.api @@ -1,4 +1,4 @@ -FROM golang:1.21 as builder +FROM golang:1.22 as builder COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ WORKDIR /go/src/github.com/moira-alert/moira diff --git a/Dockerfile.checker b/Dockerfile.checker index db0f03494..098486cff 100644 --- a/Dockerfile.checker +++ b/Dockerfile.checker @@ -1,4 +1,4 @@ -FROM golang:1.21 as builder +FROM golang:1.22 as builder COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ WORKDIR /go/src/github.com/moira-alert/moira diff --git a/Dockerfile.cli b/Dockerfile.cli index c256f5b7a..b97f4fd6b 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -1,4 +1,4 @@ -FROM golang:1.21 as builder +FROM golang:1.22 as builder COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ WORKDIR /go/src/github.com/moira-alert/moira diff --git a/Dockerfile.filter b/Dockerfile.filter index 6aa7dc9fb..76cfd5355 100644 --- a/Dockerfile.filter +++ b/Dockerfile.filter @@ -1,4 +1,4 @@ -FROM golang:1.21 as builder +FROM golang:1.22 as builder COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ WORKDIR /go/src/github.com/moira-alert/moira diff --git a/Dockerfile.notifier b/Dockerfile.notifier index b936ba814..f2d4395fc 100644 --- a/Dockerfile.notifier +++ b/Dockerfile.notifier @@ -1,4 +1,4 @@ -FROM golang:1.21 as builder +FROM golang:1.22 as builder COPY go.mod go.sum /go/src/github.com/moira-alert/moira/ WORKDIR /go/src/github.com/moira-alert/moira diff --git a/go.mod b/go.mod index 7bafdacc9..ab02cbfde 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/moira-alert/moira -go 1.21 +go 1.22 require ( github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible From 02621ac7b9b17039287fc28c086b67ee6abdc129 Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Mon, 26 Aug 2024 15:14:59 +0300 Subject: [PATCH 09/36] feat(notifier): add ability to move contacts and subscriptions between users and teams (#1067) --- api/controller/contact.go | 16 +++++++--- api/controller/contact_test.go | 54 ++++++++++++++++++++++++++++------ api/controller/subscription.go | 10 ++++++- api/dto/contact.go | 3 ++ api/dto/subscription.go | 4 +++ api/handler/contact_test.go | 51 ++++++++++++++++++++++++++++++-- 6 files changed, 121 insertions(+), 17 deletions(-) diff --git a/api/controller/contact.go b/api/controller/contact.go index d829343b9..4874624be 100644 --- a/api/controller/contact.go +++ b/api/controller/contact.go @@ -57,10 +57,6 @@ func CreateContact( userLogin, teamID string, ) *api.ErrorResponse { - if userLogin != "" && teamID != "" { - return api.ErrorInternalServer(fmt.Errorf("CreateContact: cannot create contact when both userLogin and teamID specified")) - } - if !isAllowedToUseContactType(auth, userLogin, contact.Type) { return api.ErrorInvalidRequest(ErrNotAllowedContactType) } @@ -117,12 +113,20 @@ func UpdateContact( contactData.Type = contactDTO.Type contactData.Value = contactDTO.Value contactData.Name = contactDTO.Name + + if contactDTO.User != "" || contactDTO.TeamID != "" { + contactData.User = contactDTO.User + contactData.Team = contactDTO.TeamID + } + if err := dataBase.SaveContact(&contactData); err != nil { return contactDTO, api.ErrorInternalServer(err) } + contactDTO.User = contactData.User contactDTO.TeamID = contactData.Team contactDTO.ID = contactData.ID + return contactDTO, nil } @@ -221,9 +225,11 @@ func CheckUserPermissionsForContact( } return moira.ContactData{}, api.ErrorInternalServer(err) } + if auth.IsAdmin(userLogin) { return contactData, nil } + if contactData.Team != "" { teamContainsUser, err := dataBase.IsTeamContainUser(contactData.Team, userLogin) if err != nil { @@ -233,9 +239,11 @@ func CheckUserPermissionsForContact( return contactData, nil } } + if contactData.User == userLogin { return contactData, nil } + return moira.ContactData{}, api.ErrorForbidden("you are not permitted") } diff --git a/api/controller/contact_test.go b/api/controller/contact_test.go index 961deba9f..8c398b528 100644 --- a/api/controller/contact_test.go +++ b/api/controller/contact_test.go @@ -366,15 +366,6 @@ func TestCreateContact(t *testing.T) { }) }) }) - - Convey("Error on create with both: userLogin and teamID specified", t, func() { - contact := &dto.Contact{ - Value: contactValue, - Type: contactType, - } - err := CreateContact(dataBase, auth, contact, userLogin, teamID) - So(err, ShouldResemble, api.ErrorInternalServer(fmt.Errorf("CreateContact: cannot create contact when both userLogin and teamID specified"))) - }) } func TestAdminsCreatesContact(t *testing.T) { @@ -504,6 +495,30 @@ func TestUpdateContact(t *testing.T) { So(expectedContact.Name, ShouldResemble, contactDTO.Name) }) + Convey("Success with rewrite user", func() { + newUser := "testUser" + contactDTO := dto.Contact{ + Value: contactValue, + Name: "some-name", + Type: contactType, + User: newUser, + } + contactID := uuid.Must(uuid.NewV4()).String() + contact := moira.ContactData{ + Value: contactDTO.Value, + Type: contactDTO.Type, + Name: contactDTO.Name, + ID: contactID, + User: newUser, + } + dataBase.EXPECT().SaveContact(&contact).Return(nil) + expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + So(err, ShouldBeNil) + So(expectedContact.User, ShouldResemble, newUser) + So(expectedContact.ID, ShouldResemble, contactID) + So(expectedContact.Name, ShouldResemble, contactDTO.Name) + }) + Convey("Error update not allowed contact", func() { contactDTO := dto.Contact{ Value: contactValue, @@ -587,6 +602,27 @@ func TestUpdateContact(t *testing.T) { So(expectedContact.ID, ShouldResemble, contactID) }) + Convey("Success with rewrite team", func() { + newTeam := "testTeam" + contactDTO := dto.Contact{ + Value: contactValue, + Type: contactType, + TeamID: newTeam, + } + contactID := uuid.Must(uuid.NewV4()).String() + contact := moira.ContactData{ + Value: contactDTO.Value, + Type: contactDTO.Type, + ID: contactID, + Team: newTeam, + } + dataBase.EXPECT().SaveContact(&contact).Return(nil) + expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) + So(err, ShouldBeNil) + So(expectedContact.TeamID, ShouldResemble, newTeam) + So(expectedContact.ID, ShouldResemble, contactID) + }) + Convey("Error save", func() { contactDTO := dto.Contact{ Value: contactValue, diff --git a/api/controller/subscription.go b/api/controller/subscription.go index be6754307..c9fceec6c 100644 --- a/api/controller/subscription.go +++ b/api/controller/subscription.go @@ -81,13 +81,16 @@ func GetSubscription(dataBase moira.Database, subscriptionID string) (*dto.Subsc // UpdateSubscription updates existing subscription. func UpdateSubscription(dataBase moira.Database, subscriptionID string, userLogin string, subscription *dto.Subscription) *api.ErrorResponse { subscription.ID = subscriptionID - if subscription.TeamID == "" { + if subscription.TeamID == "" && subscription.User == "" { subscription.User = userLogin } + data := moira.SubscriptionData(*subscription) + if err := dataBase.SaveSubscription(&data); err != nil { return api.ErrorInternalServer(err) } + return nil } @@ -131,21 +134,26 @@ func CheckUserPermissionsForSubscription( } return moira.SubscriptionData{}, api.ErrorInternalServer(err) } + if auth.IsAdmin(userLogin) { return subscription, nil } + if subscription.TeamID != "" { teamContainsUser, err := dataBase.IsTeamContainUser(subscription.TeamID, userLogin) if err != nil { return moira.SubscriptionData{}, api.ErrorInternalServer(err) } + if teamContainsUser { return subscription, nil } } + if subscription.User == userLogin { return subscription, nil } + return moira.SubscriptionData{}, api.ErrorForbidden("you are not permitted") } diff --git a/api/dto/contact.go b/api/dto/contact.go index 84ffe2f57..376b190c5 100644 --- a/api/dto/contact.go +++ b/api/dto/contact.go @@ -36,5 +36,8 @@ func (contact *Contact) Bind(r *http.Request) error { if contact.Value == "" { return fmt.Errorf("contact value of type %s can not be empty", contact.Type) } + if contact.User != "" && contact.TeamID != "" { + return fmt.Errorf("contact cannot have both the user field and the team_id field filled in") + } return nil } diff --git a/api/dto/subscription.go b/api/dto/subscription.go index 38178db3a..ebbe7f0a9 100644 --- a/api/dto/subscription.go +++ b/api/dto/subscription.go @@ -84,6 +84,7 @@ func (subscription *Subscription) checkContacts(request *http.Request) error { if subscription.User != "" && teamID != "" { return ErrSubscriptionContainsTeamAndUser{} } + var contactIDs []string var err error if teamID != "" { @@ -112,6 +113,7 @@ func (subscription *Subscription) checkContacts(request *http.Request) error { if err != nil { return ErrProvidedContactsForbidden{contactIds: subscriptionContactIDs} } + anotherUserContactValues := make([]string, 0) anotherUserContactIDs := make([]string, 0) for i, contact := range contacts { @@ -121,11 +123,13 @@ func (subscription *Subscription) checkContacts(request *http.Request) error { anotherUserContactValues = append(anotherUserContactValues, contact.Value) } } + return ErrProvidedContactsForbidden{ contactNames: anotherUserContactValues, contactIds: subscriptionContactIDs, } } + return nil } diff --git a/api/handler/contact_test.go b/api/handler/contact_test.go index 6f469b23b..cc2ebc028 100644 --- a/api/handler/contact_test.go +++ b/api/handler/contact_test.go @@ -358,8 +358,8 @@ func TestCreateNewContact(t *testing.T) { }() expected := &api.ErrorResponse{ - StatusText: "Internal Server Error", - ErrorText: "CreateContact: cannot create contact when both userLogin and teamID specified", + StatusText: "Invalid request", + ErrorText: "contact cannot have both the user field and the team_id field filled in", } jsonContact, err := json.Marshal(newContactDto) So(err, ShouldBeNil) @@ -381,7 +381,7 @@ func TestCreateNewContact(t *testing.T) { So(err, ShouldBeNil) So(actual, ShouldResemble, expected) - So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) }) }) } @@ -450,6 +450,51 @@ func TestUpdateContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusOK) }) + Convey("Failed to update a contact with the specified user and team field", func() { + updatedContactDto.TeamID = defaultTeamID + defer func() { + updatedContactDto.TeamID = "" + }() + + jsonContact, err := json.Marshal(updatedContactDto) + So(err, ShouldBeNil) + + testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact)) + + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + ID: contactID, + Name: updatedContactDto.Name, + Type: updatedContactDto.Type, + Value: updatedContactDto.Value, + User: updatedContactDto.User, + })) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{ + AllowedContactTypes: map[string]struct{}{ + updatedContactDto.Type: {}, + }, + })) + + testRequest.Header.Add("content-type", "application/json") + + updateContact(responseWriter, testRequest) + + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "contact cannot have both the user field and the team_id field filled in", + } + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &api.ErrorResponse{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + Convey("Internal error when trying to update contact", func() { expected := &api.ErrorResponse{ StatusText: "Internal Server Error", From 78b22935060c9a66ab904b8839becfd7a6f820b7 Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Mon, 2 Sep 2024 11:40:07 +0300 Subject: [PATCH 10/36] fix(checker): fix skip first metric value (#1007) --- checker/check.go | 35 +++--- checker/check_test.go | 179 ++++++++++++++++++---------- checker/trigger_checker.go | 12 +- checker/trigger_checker_test.go | 25 ++-- clock/clock.go | 9 +- database/redis/last_check_test.go | 14 +++ database/redis/metric.go | 2 +- database/redis/metric_test.go | 4 +- database/redis/notification_test.go | 6 +- database/redis/reply/check.go | 2 + database/redis/trigger.go | 2 +- database/redis/trigger_test.go | 8 +- datatypes.go | 41 ++++--- datatypes_test.go | 173 ++++++++++++++++++++------- filter/patterns_storage.go | 2 +- filter/patterns_storage_test.go | 2 +- go.mod | 1 + go.sum | 1 + interfaces.go | 3 +- mock/clock/clock.go | 26 +++- notifier/events/event_test.go | 8 +- notifier/scheduler.go | 2 +- notifier/scheduler_test.go | 8 +- 23 files changed, 388 insertions(+), 177 deletions(-) diff --git a/checker/check.go b/checker/check.go index 7c295792e..698869cb8 100644 --- a/checker/check.go +++ b/checker/check.go @@ -12,7 +12,6 @@ import ( ) const ( - secondsInHour int64 = 3600 checkPointGap int64 = 120 ) @@ -222,6 +221,7 @@ func newCheckData(lastCheck *moira.CheckData, checkTimeStamp int64) moira.CheckD newCheckData.Timestamp = checkTimeStamp newCheckData.MetricsToTargetRelation = metricsToTargetRelation newCheckData.Message = "" + return newCheckData } @@ -465,34 +465,33 @@ func (triggerChecker *TriggerChecker) getMetricStepsStates( metrics map[string]metricSource.MetricData, logger moira.Logger, ) ( - last moira.MetricState, - current []moira.MetricState, + lastMetricState moira.MetricState, + newMetricStates []moira.MetricState, err error, ) { var startTime int64 var stepTime int64 for _, metric := range metrics { // Taking values from any metric - last = triggerChecker.lastCheck.GetOrCreateMetricState( + lastMetricState = triggerChecker.lastCheck.GetOrCreateMetricState( metricName, - metric.StartTime-secondsInHour, triggerChecker.trigger.MuteNewMetrics, + checkPointGap, ) + startTime = metric.StartTime stepTime = metric.StepTime break } - checkPoint := last.GetCheckPoint(checkPointGap) + checkPoint := lastMetricState.GetCheckPoint(checkPointGap) logger.Debug(). Int64(moira.LogFieldNameCheckpoint, checkPoint). Msg("Checkpoint got") - current = make([]moira.MetricState, 0) - // DO NOT CHANGE // Specific optimization magic - previousState := last + previousMetricState := lastMetricState difference := moira.MaxInt64(checkPoint-startTime, 0) stepsDifference := difference / stepTime if (difference % stepTime) > 0 { @@ -500,18 +499,24 @@ func (triggerChecker *TriggerChecker) getMetricStepsStates( } valueTimestamp := startTime + stepTime*stepsDifference endTimestamp := triggerChecker.until + stepTime + + newMetricStates = make([]moira.MetricState, 0) + for ; valueTimestamp < endTimestamp; valueTimestamp += stepTime { - metricNewState, err := triggerChecker.getMetricDataState(metrics, &previousState, &valueTimestamp, &checkPoint, logger) + newMetricState, err := triggerChecker.getMetricDataState(metrics, &previousMetricState, &valueTimestamp, &checkPoint, logger) if err != nil { - return last, current, err + return lastMetricState, newMetricStates, err } - if metricNewState == nil { + + if newMetricState == nil { continue } - previousState = *metricNewState - current = append(current, *metricNewState) + + previousMetricState = *newMetricState + newMetricStates = append(newMetricStates, *newMetricState) } - return last, current, nil + + return lastMetricState, newMetricStates, nil } func (triggerChecker *TriggerChecker) getMetricDataState( diff --git a/checker/check_test.go b/checker/check_test.go index 772f1eb0b..8a5ae0b1e 100644 --- a/checker/check_test.go +++ b/checker/check_test.go @@ -15,6 +15,7 @@ import ( "go.uber.org/mock/gomock" "github.com/moira-alert/moira/metrics" + mock_clock "github.com/moira-alert/moira/mock/clock" mock_metric_source "github.com/moira-alert/moira/mock/metric_source" mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" . "github.com/smartystreets/goconvey/convey" @@ -63,9 +64,9 @@ func TestGetMetricDataState(t *testing.T) { Suppressed: true, } - var valueTimestamp int64 = 37 - var checkPoint int64 = 47 Convey("Checkpoint more than valueTimestamp", t, func() { + var valueTimestamp int64 = 37 + var checkPoint int64 = 47 metricState, err := triggerChecker.getMetricDataState(metrics, &metricLastState, &valueTimestamp, &checkPoint, logger) So(err, ShouldBeNil) So(metricState, ShouldBeNil) @@ -136,6 +137,7 @@ func TestTriggerChecker_PrepareMetrics(t *testing.T) { AloneMetrics: map[string]bool{}, }, } + Convey("last check has no metrics", func() { Convey("fetched metrics is empty", func() { prepared, alone, err := triggerChecker.prepareMetrics(map[string][]metricSource.MetricData{}) @@ -251,6 +253,7 @@ func TestTriggerChecker_PrepareMetrics(t *testing.T) { So(err, ShouldBeNil) }) }) + Convey("fetched metrics is empty", func() { prepared, alone, err := triggerChecker.prepareMetrics(map[string][]metricSource.MetricData{}) So(prepared, ShouldHaveLength, 3) @@ -263,6 +266,7 @@ func TestTriggerChecker_PrepareMetrics(t *testing.T) { So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) + Convey("fetched metrics has only wildcards, step is 0", func() { prepared, alone, err := triggerChecker.prepareMetrics( map[string][]metricSource.MetricData{ @@ -283,6 +287,7 @@ func TestTriggerChecker_PrepareMetrics(t *testing.T) { So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) + Convey("fetched metrics has only wildcards, step is 10", func() { prepared, alone, err := triggerChecker.prepareMetrics( map[string][]metricSource.MetricData{ @@ -304,6 +309,7 @@ func TestTriggerChecker_PrepareMetrics(t *testing.T) { So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) + Convey("fetched metrics has one of last check metrics", func() { prepared, alone, err := triggerChecker.prepareMetrics( map[string][]metricSource.MetricData{ @@ -321,6 +327,7 @@ func TestTriggerChecker_PrepareMetrics(t *testing.T) { So(alone, ShouldBeEmpty) So(err, ShouldBeNil) }) + Convey("fetched metrics has one of last check metrics and one new", func() { prepared, alone, err := triggerChecker.prepareMetrics( map[string][]metricSource.MetricData{ @@ -443,6 +450,7 @@ func TestGetMetricStepsStates(t *testing.T) { EventTimestamp: 11, }, } + Convey("Metric has all valid values", func() { _, metricStates, err := triggerChecker.getMetricStepsStates("main.metric", map[string]metricSource.MetricData{"t1": metricData2, "t2": addMetricData}, logger) So(err, ShouldBeNil) @@ -532,6 +540,7 @@ func TestCheckForNODATA(t *testing.T) { Maintenance: 11111, Suppressed: true, } + Convey("No TTL", t, func() { triggerChecker := TriggerChecker{} needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricLastState, logger) @@ -560,6 +569,7 @@ func TestCheckForNODATA(t *testing.T) { So(needToDeleteMetric, ShouldBeFalse) So(currentState, ShouldBeNil) }) + Convey("2", func() { metricLastState.Timestamp = 401 needToDeleteMetric, currentState := triggerChecker.checkForNoData(metricLastState, logger) @@ -662,6 +672,8 @@ func TestCheck(t *testing.T) { messageException := `Unknown graphite function: "WrongFunction"` unknownFunctionExc := local.ErrorUnknownFunction(fmt.Errorf(messageException)) + testTime := time.Date(2022, time.June, 6, 10, 0, 0, 0, time.UTC).Unix() + var ttl int64 = 30 checkerMetrics, _ := metrics. @@ -674,8 +686,8 @@ func TestCheck(t *testing.T) { logger: logger, config: &Config{}, metrics: checkerMetrics, - from: 17, - until: 67, + from: testTime - 5*retention, + until: testTime, ttl: ttl, ttlState: moira.TTLStateNODATA, trigger: &moira.Trigger{ @@ -689,11 +701,11 @@ func TestCheck(t *testing.T) { }, lastCheck: &moira.CheckData{ State: moira.StateOK, - Timestamp: 57, + Timestamp: testTime - retention, Metrics: map[string]moira.MetricState{ metric: { State: moira.StateOK, - Timestamp: 26, + Timestamp: testTime - 4*retention - 1, }, }, }, @@ -717,7 +729,7 @@ func TestCheck(t *testing.T) { TriggerID: triggerChecker.triggerID, State: moira.StateEXCEPTION, OldState: moira.StateOK, - Timestamp: int64(67), + Timestamp: testTime, Metric: triggerChecker.trigger.Name, }, true).Return(nil), dataBase.EXPECT().SetTriggerLastCheck( @@ -737,7 +749,7 @@ func TestCheck(t *testing.T) { TriggerID: triggerChecker.triggerID, State: moira.StateEXCEPTION, OldState: moira.StateOK, - Timestamp: 67, + Timestamp: testTime, Metric: triggerChecker.trigger.Name, } @@ -767,14 +779,14 @@ func TestCheck(t *testing.T) { Convey("Switch state to OK. Event should be created", func() { triggerChecker.lastCheck.State = moira.StateEXCEPTION - triggerChecker.lastCheck.EventTimestamp = 67 + triggerChecker.lastCheck.EventTimestamp = testTime triggerChecker.lastCheck.LastSuccessfulCheckTimestamp = triggerChecker.until eventMetrics := map[string]moira.MetricState{ metric: { - EventTimestamp: 17, + EventTimestamp: testTime - 5*retention, State: moira.StateOK, Suppressed: false, - Timestamp: 57, + Timestamp: testTime - retention, Values: map[string]float64{"t1": 4}, }, } @@ -784,7 +796,7 @@ func TestCheck(t *testing.T) { TriggerID: triggerChecker.triggerID, State: moira.StateOK, OldState: moira.StateEXCEPTION, - Timestamp: 67, + Timestamp: testTime, Metric: triggerChecker.trigger.Name, } @@ -797,6 +809,7 @@ func TestCheck(t *testing.T) { 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)}), @@ -819,9 +832,9 @@ func TestCheck(t *testing.T) { lastCheck := moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { - EventTimestamp: 57, + EventTimestamp: testTime - retention, State: moira.StateERROR, - Timestamp: 57, + Timestamp: testTime - retention, MaintenanceInfo: moira.MaintenanceInfo{}, Values: map[string]float64{"t1": 25}, }, @@ -838,7 +851,7 @@ func TestCheck(t *testing.T) { TriggerID: triggerChecker.triggerID, State: moira.StateERROR, OldState: moira.StateOK, - Timestamp: 57, + Timestamp: testTime - retention, Metric: metric, Values: map[string]float64{"t1": 25}, } @@ -861,13 +874,14 @@ func TestCheck(t *testing.T) { err := triggerChecker.Check() So(err, ShouldBeNil) }) + Convey("Duplicate error", func() { lastCheck := moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { - EventTimestamp: 17, + EventTimestamp: testTime - 5*retention, State: moira.StateOK, - Timestamp: 57, + Timestamp: testTime - retention, MaintenanceInfo: moira.MaintenanceInfo{}, Values: map[string]float64{"t1": 4}, }, @@ -885,7 +899,7 @@ func TestCheck(t *testing.T) { TriggerID: triggerChecker.triggerID, State: moira.StateEXCEPTION, OldState: moira.StateOK, - Timestamp: 67, + Timestamp: testTime, Metric: triggerChecker.trigger.Name, } @@ -908,6 +922,7 @@ func TestCheck(t *testing.T) { }) Convey("Alone metrics error", func() { + mockTime := mock_clock.NewMockClock(mockCtrl) metricName1 := "test.metric.1" metricName2 := "test.metric.2" metricNameAlone := "test.metric.alone" @@ -917,18 +932,16 @@ func TestCheck(t *testing.T) { lastCheck := moira.CheckData{ Metrics: map[string]moira.MetricState{ metricName1: { - EventTimestamp: -3533, + EventTimestamp: testTime - checkPointGap, State: moira.StateNODATA, - Timestamp: -3533, + Timestamp: testTime, MaintenanceInfo: moira.MaintenanceInfo{}, - Values: map[string]float64{}, }, metricName2: { - EventTimestamp: -3533, + EventTimestamp: testTime - checkPointGap, State: moira.StateNODATA, - Timestamp: -3533, + Timestamp: testTime, MaintenanceInfo: moira.MaintenanceInfo{}, - Values: map[string]float64{}, }, }, MetricsToTargetRelation: map[string]string{"t2": metricNameAlone}, @@ -938,6 +951,7 @@ func TestCheck(t *testing.T) { EventTimestamp: triggerChecker.until, LastSuccessfulCheckTimestamp: triggerChecker.until, Message: "", + Clock: mockTime, } expression := "OK" triggerChecker.trigger.AloneMetrics = map[string]bool{"t2": true} @@ -947,7 +961,8 @@ func TestCheck(t *testing.T) { triggerChecker.lastCheck = &moira.CheckData{ Metrics: map[string]moira.MetricState{}, State: moira.StateOK, - Timestamp: triggerChecker.until - 3600, + Timestamp: triggerChecker.until - metricsTTL, + Clock: mockTime, } gomock.InOrder( @@ -972,6 +987,8 @@ func TestCheck(t *testing.T) { dataBase.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL), dataBase.EXPECT().RemoveMetricsValues([]string{metricName1, metricNameAlone, metricName2}, triggerChecker.until-metricsTTL).Return(nil), + mockTime.EXPECT().NowUnix().Return(testTime).Times(4), + dataBase.EXPECT().SetTriggerLastCheck( triggerChecker.triggerID, &lastCheck, @@ -1038,6 +1055,10 @@ func TestIgnoreNodataToOk(t *testing.T) { logger.Level("info") // nolint: errcheck defer mockCtrl.Finish() + mockTime := mock_clock.NewMockClock(mockCtrl) + + testTime := time.Date(2022, time.June, 6, 10, 0, 0, 0, time.UTC).Unix() + var retention int64 = 10 var warnValue float64 = 10 var errValue float64 = 20 @@ -1048,13 +1069,14 @@ func TestIgnoreNodataToOk(t *testing.T) { Metrics: make(map[string]moira.MetricState), State: moira.StateNODATA, Timestamp: 66, + Clock: mockTime, } triggerChecker := TriggerChecker{ triggerID: "SuperId", logger: logger, config: &Config{}, - from: 3617, - until: 3667, + from: testTime - ttl, + until: testTime, ttl: ttl, ttlState: moira.TTLStateNODATA, trigger: &moira.Trigger{ @@ -1073,14 +1095,16 @@ func TestIgnoreNodataToOk(t *testing.T) { checkData := newCheckData(&lastCheck, triggerChecker.until) Convey("First Event, NODATA - OK is ignored", t, func() { + mockTime.EXPECT().NowUnix().Return(testTime).Times(2) + triggerChecker.trigger.MuteNewMetrics = true newCheckData, err := triggerChecker.check(metricsToCheck, aloneMetrics, checkData, logger) So(err, ShouldBeNil) So(newCheckData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { - Timestamp: time.Now().Unix(), - EventTimestamp: time.Now().Unix(), + Timestamp: testTime, + EventTimestamp: testTime - checkPointGap, State: moira.StateOK, Value: nil, Values: nil, @@ -1090,6 +1114,7 @@ func TestIgnoreNodataToOk(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateNODATA, Score: 0, + Clock: mockTime, }) }) } @@ -1097,28 +1122,34 @@ func TestIgnoreNodataToOk(t *testing.T) { func TestHandleTrigger(t *testing.T) { mockCtrl := gomock.NewController(t) dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) + mockTime := mock_clock.NewMockClock(mockCtrl) logger, _ := logging.GetLogger("Test") logger.Level("info") // nolint: errcheck defer mockCtrl.Finish() + var metricsTTL int64 = 3600 var retention int64 = 10 var warnValue float64 = 10 var errValue float64 = 20 pattern := "super.puper.pattern" metric := "super.puper.metric" var ttl int64 = 600 + testTime := time.Date(2022, time.June, 6, 10, 0, 0, 0, time.UTC).Unix() + lastCheck := moira.CheckData{ Metrics: make(map[string]moira.MetricState), State: moira.StateNODATA, - Timestamp: 66, + Timestamp: testTime - metricsTTL, + Clock: mockTime, } + triggerChecker := TriggerChecker{ triggerID: "SuperId", database: dataBase, logger: logger, config: &Config{}, - from: 3617, - until: 3667, + from: testTime - 5*retention, + until: testTime, ttl: ttl, ttlState: moira.TTLStateNODATA, trigger: &moira.Trigger{ @@ -1137,10 +1168,12 @@ func TestHandleTrigger(t *testing.T) { lastCheck.MetricsToTargetRelation = conversion.GetRelations(aloneMetrics, triggerChecker.trigger.AloneMetrics) checkData := newCheckData(&lastCheck, triggerChecker.until) metricsToCheck := map[string]map[string]metricSource.MetricData{} + + mockTime.EXPECT().NowUnix().Return(testTime).Times(2) dataBase.EXPECT().PushNotificationEvent( &moira.NotificationEvent{ TriggerID: triggerChecker.triggerID, - Timestamp: 3617, + Timestamp: testTime - 5*retention, State: moira.StateOK, OldState: moira.StateNODATA, Metric: metric, @@ -1153,8 +1186,8 @@ func TestHandleTrigger(t *testing.T) { So(checkData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { - Timestamp: 3657, - EventTimestamp: 3617, + Timestamp: testTime - retention, + EventTimestamp: testTime - 5*retention, State: moira.StateOK, Value: nil, Values: map[string]float64{"t1": 4}, @@ -1164,20 +1197,22 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateNODATA, Score: 0, + Clock: mockTime, }) }) lastCheck = moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { - Timestamp: 3647, - EventTimestamp: 3607, + Timestamp: testTime - 2*retention, + EventTimestamp: testTime - 6*retention, State: moira.StateOK, Values: map[string]float64{"t1": 3}, }, }, State: moira.StateOK, - Timestamp: 3655, + Timestamp: testTime - retention - 2, + Clock: mockTime, } Convey("Last check is not empty", func() { @@ -1191,8 +1226,8 @@ func TestHandleTrigger(t *testing.T) { So(checkData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ metric: { - Timestamp: 3657, - EventTimestamp: 3607, + Timestamp: testTime - retention, + EventTimestamp: testTime - 6*retention, State: moira.StateOK, Value: nil, Values: map[string]float64{"t1": 4}, @@ -1202,13 +1237,15 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateOK, Score: 0, + Clock: mockTime, }) }) Convey("No data too long", func() { - triggerChecker.from = 4217 - triggerChecker.until = 4267 - lastCheck.Timestamp = 4267 + triggerChecker.from = testTime + ttl - 5*retention + triggerChecker.until = testTime + ttl + lastCheck.Timestamp = testTime + ttl + dataBase.EXPECT().PushNotificationEvent(&moira.NotificationEvent{ TriggerID: triggerChecker.triggerID, Timestamp: lastCheck.Timestamp, @@ -1238,14 +1275,15 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateOK, Score: 0, + Clock: mockTime, }) }) Convey("No data too long and ttlState is delete, the metric is not on Maintenance, so it will be removed", func() { - triggerChecker.from = 4217 - triggerChecker.until = 4267 + triggerChecker.from = testTime + ttl - 5*retention + triggerChecker.until = testTime + ttl triggerChecker.ttlState = moira.TTLStateDEL - lastCheck.Timestamp = 4267 + lastCheck.Timestamp = testTime + ttl dataBase.EXPECT().RemovePatternsMetrics(triggerChecker.trigger.Patterns).Return(nil) @@ -1263,18 +1301,19 @@ func TestHandleTrigger(t *testing.T) { Score: 0, LastSuccessfulCheckTimestamp: 0, MetricsToTargetRelation: map[string]string{}, + Clock: mockTime, }) }) metricState := lastCheck.Metrics[metric] - metricState.Maintenance = 5000 + metricState.Maintenance = testTime + ttl lastCheck.Metrics[metric] = metricState Convey("No data too long and ttlState is delete, but the metric is on maintenance and DeletedButKept is false, so it won't be deleted", func() { - triggerChecker.from = 4217 - triggerChecker.until = 4267 + triggerChecker.from = testTime + ttl - 5*retention + triggerChecker.until = testTime + ttl triggerChecker.ttlState = moira.TTLStateDEL - lastCheck.Timestamp = 4267 + lastCheck.Timestamp = testTime + ttl aloneMetrics := map[string]metricSource.MetricData{"t1": *metricSource.MakeMetricData(metric, []float64{}, retention, triggerChecker.from)} lastCheck.MetricsToTargetRelation = conversion.GetRelations(aloneMetrics, triggerChecker.trigger.AloneMetrics) @@ -1299,6 +1338,7 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateOK, Score: 0, + Clock: mockTime, }) }) @@ -1307,10 +1347,10 @@ func TestHandleTrigger(t *testing.T) { lastCheck.Metrics[metric] = metricState Convey("Metric on maintenance, DeletedButKept is true, ttlState is delete, but a new metric comes in and DeletedButKept becomes false", func() { - triggerChecker.from = 4217 - triggerChecker.until = 4267 + triggerChecker.from = testTime + ttl - 5*retention + triggerChecker.until = testTime + ttl triggerChecker.ttlState = moira.TTLStateDEL - lastCheck.Timestamp = 4227 + lastCheck.Timestamp = testTime + ttl + retention aloneMetrics := map[string]metricSource.MetricData{"t1": *metricSource.MakeMetricData(metric, []float64{5}, retention, triggerChecker.from)} lastCheck.MetricsToTargetRelation = conversion.GetRelations(aloneMetrics, triggerChecker.trigger.AloneMetrics) @@ -1335,18 +1375,19 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateOK, Score: 0, + Clock: mockTime, }) }) metricState = lastCheck.Metrics[metric] - metricState.Maintenance = 4000 + metricState.Maintenance = testTime + ttl - 10*retention lastCheck.Metrics[metric] = metricState Convey("No data too long and ttlState is delete, the time for Maintenance of metric is over, so it will be deleted", func() { - triggerChecker.from = 4217 - triggerChecker.until = 4267 + triggerChecker.from = testTime + ttl - 5*retention + triggerChecker.until = testTime + ttl triggerChecker.ttlState = moira.TTLStateDEL - lastCheck.Timestamp = 4267 + lastCheck.Timestamp = testTime + ttl dataBase.EXPECT().RemovePatternsMetrics(triggerChecker.trigger.Patterns).Return(nil) @@ -1364,6 +1405,7 @@ func TestHandleTrigger(t *testing.T) { Score: 0, LastSuccessfulCheckTimestamp: 0, MetricsToTargetRelation: map[string]string{}, + Clock: mockTime, }) }) }) @@ -1381,7 +1423,8 @@ func TestHandleTrigger(t *testing.T) { triggerChecker.lastCheck = &moira.CheckData{ Metrics: make(map[string]moira.MetricState), State: moira.StateNODATA, - Timestamp: 66, + Timestamp: testTime - metricsTTL, + Clock: mockTime, } Convey("Without any metrics", func() { @@ -1397,6 +1440,7 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateNODATA, Score: 0, + Clock: mockTime, }) }) @@ -1413,6 +1457,7 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateNODATA, Score: 0, + Clock: mockTime, }) }) @@ -1424,10 +1469,12 @@ func TestHandleTrigger(t *testing.T) { "t2": *metricSource.MakeMetricData(metric, []float64{5}, retention, triggerChecker.from), }, } + + mockTime.EXPECT().NowUnix().Return(testTime).Times(2) dataBase.EXPECT().PushNotificationEvent( &moira.NotificationEvent{ TriggerID: triggerChecker.triggerID, - Timestamp: 4217, + Timestamp: testTime + ttl - 5*retention, State: moira.StateERROR, OldState: moira.StateNODATA, Metric: "test2", @@ -1440,9 +1487,9 @@ func TestHandleTrigger(t *testing.T) { So(checkData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ "test2": { - EventTimestamp: 4217, + EventTimestamp: testTime + ttl - 5*retention, State: moira.StateERROR, - Timestamp: 4217, + Timestamp: testTime + ttl - 5*retention, Values: map[string]float64{"t1": 5, "t2": 5}, }, }, @@ -1450,6 +1497,7 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateNODATA, Score: 0, + Clock: mockTime, }) }) @@ -1462,10 +1510,12 @@ func TestHandleTrigger(t *testing.T) { "t2": *metricSource.MakeMetricData(metric, []float64{5}, retention, triggerChecker.from), }, } + + mockTime.EXPECT().NowUnix().Return(testTime).Times(2) dataBase.EXPECT().PushNotificationEvent( &moira.NotificationEvent{ TriggerID: triggerChecker.triggerID, - Timestamp: 4217, + Timestamp: testTime + ttl - 5*retention, State: moira.StateOK, OldState: moira.StateNODATA, Metric: "test1", @@ -1478,9 +1528,9 @@ func TestHandleTrigger(t *testing.T) { So(checkData, ShouldResemble, moira.CheckData{ Metrics: map[string]moira.MetricState{ "test1": { - EventTimestamp: 4217, + EventTimestamp: testTime + ttl - 5*retention, State: moira.StateOK, - Timestamp: 4217, + Timestamp: testTime + ttl - 5*retention, Values: map[string]float64{"t1": 10, "t2": 5}, }, }, @@ -1488,6 +1538,7 @@ func TestHandleTrigger(t *testing.T) { Timestamp: triggerChecker.until, State: moira.StateNODATA, Score: 0, + Clock: mockTime, }) }) }) diff --git a/checker/trigger_checker.go b/checker/trigger_checker.go index dfd3e94a5..655a47f42 100644 --- a/checker/trigger_checker.go +++ b/checker/trigger_checker.go @@ -5,11 +5,14 @@ import ( "time" "github.com/moira-alert/moira" + "github.com/moira-alert/moira/clock" "github.com/moira-alert/moira/database" metricSource "github.com/moira-alert/moira/metric_source" "github.com/moira-alert/moira/metrics" ) +var tenMinInSec = int64((10 * time.Minute).Seconds()) + // TriggerChecker represents data, used for handling new trigger state. type TriggerChecker struct { database moira.Database @@ -90,6 +93,7 @@ func MakeTriggerChecker( ttl: trigger.TTL, ttlState: getTTLState(trigger.TTLState), } + return triggerChecker, nil } @@ -111,6 +115,10 @@ func getLastCheck(dataBase moira.Database, triggerID string, emptyLastCheckTimes lastCheck.Timestamp = emptyLastCheckTimestamp } + if lastCheck.Clock == nil { + lastCheck.Clock = clock.NewSystemClock() + } + return &lastCheck, nil } @@ -118,6 +126,7 @@ func getTTLState(triggerTTLState *moira.TTLState) moira.TTLState { if triggerTTLState != nil { return *triggerTTLState } + return moira.TTLStateNODATA } @@ -125,5 +134,6 @@ func calculateFrom(lastCheckTimestamp, triggerTTL int64) int64 { if triggerTTL != 0 { return lastCheckTimestamp - triggerTTL } - return lastCheckTimestamp - 600 + + return lastCheckTimestamp - tenMinInSec } diff --git a/checker/trigger_checker_test.go b/checker/trigger_checker_test.go index 234ea9da0..164fb1385 100644 --- a/checker/trigger_checker_test.go +++ b/checker/trigger_checker_test.go @@ -3,8 +3,10 @@ package checker import ( "fmt" "testing" + "time" "github.com/moira-alert/moira" + "github.com/moira-alert/moira/clock" "github.com/moira-alert/moira/database" logging "github.com/moira-alert/moira/logging/zerolog_adapter" metricSource "github.com/moira-alert/moira/metric_source" @@ -15,6 +17,8 @@ import ( "go.uber.org/mock/gomock" ) +var hourInSec = int64(time.Hour.Seconds()) + func TestInitTriggerChecker(t *testing.T) { mockCtrl := gomock.NewController(t) logger, _ := logging.GetLogger("Test") @@ -120,6 +124,8 @@ func TestInitTriggerChecker(t *testing.T) { actual, err := MakeTriggerChecker(triggerID, dataBase, logger, config, metricSource.CreateTestMetricSourceProvider(localSource, nil, nil), checkerMetrics) So(err, ShouldBeNil) + expectedLastCheck := lastCheck + expectedLastCheck.Clock = clock.NewSystemClock() expected := TriggerChecker{ triggerID: triggerID, database: dataBase, @@ -129,7 +135,7 @@ func TestInitTriggerChecker(t *testing.T) { trigger: &trigger, ttl: trigger.TTL, ttlState: *trigger.TTLState, - lastCheck: &lastCheck, + lastCheck: &expectedLastCheck, from: lastCheck.Timestamp - ttl, until: actual.until, metrics: metrics, @@ -155,12 +161,14 @@ func TestInitTriggerChecker(t *testing.T) { lastCheck: &moira.CheckData{ Metrics: make(map[string]moira.MetricState), State: moira.StateOK, - Timestamp: actual.until - 3600, + Timestamp: actual.until - hourInSec, + Clock: clock.NewSystemClock(), }, - from: actual.until - 3600 - ttl, + from: actual.until - hourInSec - ttl, until: actual.until, metrics: metrics, } + So(*actual, ShouldResemble, expected) }) @@ -185,9 +193,10 @@ func TestInitTriggerChecker(t *testing.T) { lastCheck: &moira.CheckData{ Metrics: make(map[string]moira.MetricState), State: moira.StateOK, - Timestamp: actual.until - 3600, + Timestamp: actual.until - hourInSec, + Clock: clock.NewSystemClock(), }, - from: actual.until - 3600 - 600, + from: actual.until - hourInSec - tenMinInSec, until: actual.until, metrics: metrics, } @@ -201,6 +210,8 @@ func TestInitTriggerChecker(t *testing.T) { So(err, ShouldBeNil) + expectedLastCheck := lastCheck + expectedLastCheck.Clock = clock.NewSystemClock() expected := TriggerChecker{ triggerID: triggerID, database: dataBase, @@ -210,8 +221,8 @@ func TestInitTriggerChecker(t *testing.T) { trigger: &trigger, ttl: 0, ttlState: moira.TTLStateNODATA, - lastCheck: &lastCheck, - from: lastCheck.Timestamp - 600, + lastCheck: &expectedLastCheck, + from: lastCheck.Timestamp - tenMinInSec, until: actual.until, metrics: metrics, } diff --git a/clock/clock.go b/clock/clock.go index b5e253805..f78ed070c 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -10,7 +10,12 @@ func NewSystemClock() *SystemClock { return &SystemClock{} } -// Now returns time.Time. -func (t *SystemClock) Now() time.Time { +// Now returns now time.Time with UTC location. +func (t *SystemClock) NowUTC() time.Time { return time.Now().UTC() } + +// Now returns now time.Time as a Unix time. +func (t *SystemClock) NowUnix() int64 { + return time.Now().Unix() +} diff --git a/database/redis/last_check_test.go b/database/redis/last_check_test.go index 92dd43c32..e3105952c 100644 --- a/database/redis/last_check_test.go +++ b/database/redis/last_check_test.go @@ -5,6 +5,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/moira-alert/moira/clock" logging "github.com/moira-alert/moira/logging/zerolog_adapter" . "github.com/smartystreets/goconvey/convey" @@ -303,6 +304,7 @@ func TestLastCheck(t *testing.T) { }, }, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }) }) }) @@ -514,14 +516,17 @@ func TestGetTriggersLastCheck(t *testing.T) { { Timestamp: 1, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, { Timestamp: 2, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, { Timestamp: 3, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, }) }) @@ -540,11 +545,13 @@ func TestGetTriggersLastCheck(t *testing.T) { { Timestamp: 1, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, nil, { Timestamp: 3, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, }) }) @@ -556,10 +563,12 @@ func TestGetTriggersLastCheck(t *testing.T) { { Timestamp: 1, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, { Timestamp: 2, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, nil, }) @@ -573,10 +582,12 @@ func TestGetTriggersLastCheck(t *testing.T) { { Timestamp: 2, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, { Timestamp: 3, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, }) }) @@ -726,6 +737,7 @@ var lastCheckTest = moira.CheckData{ }, }, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), } var lastCheckWithNoMetrics = moira.CheckData{ @@ -734,6 +746,7 @@ var lastCheckWithNoMetrics = moira.CheckData{ Timestamp: 1504509981, Metrics: make(map[string]moira.MetricState), MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), } var lastCheckWithNoMetricsWithMaintenance = moira.CheckData{ @@ -743,4 +756,5 @@ var lastCheckWithNoMetricsWithMaintenance = moira.CheckData{ Maintenance: 1000, Metrics: make(map[string]moira.MetricState), MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), } diff --git a/database/redis/metric.go b/database/redis/metric.go index 0e12806a6..f695ae44f 100644 --- a/database/redis/metric.go +++ b/database/redis/metric.go @@ -429,7 +429,7 @@ func (connector *DbConnector) CleanUpFutureMetrics(duration time.Duration) error return ErrCleanUpDurationLessThanZero } - fromTs := connector.clock.Now().Add(duration).Unix() + fromTs := connector.clock.NowUTC().Add(duration).Unix() from := strconv.FormatInt(fromTs, 10) to := "+inf" diff --git a/database/redis/metric_test.go b/database/redis/metric_test.go index 47f392284..74e6f17d5 100644 --- a/database/redis/metric_test.go +++ b/database/redis/metric_test.go @@ -973,7 +973,7 @@ func TestCleanupFutureMetrics(t *testing.T) { }) So(err, ShouldBeNil) - mockClock.EXPECT().Now().Return(testTime).Times(1) + mockClock.EXPECT().NowUTC().Return(testTime).Times(1) err = dataBase.CleanUpFutureMetrics(time.Hour) So(err, ShouldBeNil) @@ -1033,7 +1033,7 @@ func TestCleanupFutureMetrics(t *testing.T) { }) So(err, ShouldBeNil) - mockClock.EXPECT().Now().Return(testTime).Times(1) + mockClock.EXPECT().NowUTC().Return(testTime).Times(1) err = dataBase.CleanUpFutureMetrics(5 * time.Second) So(err, ShouldBeNil) diff --git a/database/redis/notification_test.go b/database/redis/notification_test.go index 06048b813..d0a25ce56 100644 --- a/database/redis/notification_test.go +++ b/database/redis/notification_test.go @@ -8,6 +8,7 @@ import ( "github.com/go-redis/redis/v8" "github.com/moira-alert/moira" + "github.com/moira-alert/moira/clock" logging "github.com/moira-alert/moira/logging/zerolog_adapter" "github.com/moira-alert/moira/notifier" "github.com/stretchr/testify/assert" @@ -1257,14 +1258,17 @@ func TestGetNotificationsTriggerChecks(t *testing.T) { { Timestamp: 1, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, { Timestamp: 1, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, { Timestamp: 2, MetricsToTargetRelation: map[string]string{}, + Clock: clock.NewSystemClock(), }, }) @@ -1283,7 +1287,7 @@ func TestGetNotificationsTriggerChecks(t *testing.T) { notifications := []*moira.ScheduledNotification{notification1, notification2, notification3} triggerChecks, err := database.getNotificationsTriggerChecks(notifications) So(err, ShouldBeNil) - So(triggerChecks, ShouldResemble, []*moira.CheckData{nil, nil, {Timestamp: 2, MetricsToTargetRelation: map[string]string{}}}) + So(triggerChecks, ShouldResemble, []*moira.CheckData{nil, nil, {Timestamp: 2, MetricsToTargetRelation: map[string]string{}, Clock: clock.NewSystemClock()}}) err = database.RemoveAllNotifications() So(err, ShouldBeNil) diff --git a/database/redis/reply/check.go b/database/redis/reply/check.go index f3f19ce78..25968fba3 100644 --- a/database/redis/reply/check.go +++ b/database/redis/reply/check.go @@ -7,6 +7,7 @@ import ( "github.com/go-redis/redis/v8" "github.com/moira-alert/moira" + "github.com/moira-alert/moira/clock" "github.com/moira-alert/moira/database" ) @@ -85,6 +86,7 @@ func (d checkDataStorageElement) toCheckData() moira.CheckData { Suppressed: d.Suppressed, SuppressedState: d.SuppressedState, Message: d.Message, + Clock: clock.NewSystemClock(), } } diff --git a/database/redis/trigger.go b/database/redis/trigger.go index 86ca752fd..71486e91b 100644 --- a/database/redis/trigger.go +++ b/database/redis/trigger.go @@ -254,7 +254,7 @@ func (connector *DbConnector) preSaveTrigger(newTrigger *moira.Trigger, oldTrigg newTrigger.Patterns = make([]string, 0) } - now := connector.clock.Now().Unix() + now := connector.clock.NowUnix() newTrigger.UpdatedAt = &now if oldTrigger != nil { newTrigger.CreatedAt = oldTrigger.CreatedAt diff --git a/database/redis/trigger_test.go b/database/redis/trigger_test.go index 957b2c2ca..d3ff59cdf 100644 --- a/database/redis/trigger_test.go +++ b/database/redis/trigger_test.go @@ -557,7 +557,7 @@ func TestRemoteTrigger(t *testing.T) { Convey("Saving remote trigger", t, func() { Convey("Trigger should be saved correctly", func() { - systemClock.EXPECT().Now().Return(time.Date(2022, time.June, 7, 10, 0, 0, 0, time.UTC)) + systemClock.EXPECT().NowUnix().Return(time.Date(2022, time.June, 7, 10, 0, 0, 0, time.UTC).Unix()) err := dataBase.SaveTrigger(trigger.ID, trigger) So(err, ShouldBeNil) @@ -603,7 +603,7 @@ func TestRemoteTrigger(t *testing.T) { trigger.TriggerSource = moira.GraphiteLocal trigger.Patterns = []string{pattern} Convey("Trigger should be saved correctly", func() { - systemClock.EXPECT().Now().Return(time.Date(2022, time.June, 7, 10, 0, 0, 0, time.UTC)) + systemClock.EXPECT().NowUnix().Return(time.Date(2022, time.June, 7, 10, 0, 0, 0, time.UTC).Unix()) err := dataBase.SaveTrigger(trigger.ID, trigger) So(err, ShouldBeNil) @@ -645,7 +645,7 @@ func TestRemoteTrigger(t *testing.T) { trigger.TriggerSource = moira.GraphiteRemote Convey("Update this trigger as remote", func() { - systemClock.EXPECT().Now().Return(time.Date(2022, time.June, 7, 10, 0, 0, 0, time.UTC)) + systemClock.EXPECT().NowUnix().Return(time.Date(2022, time.June, 7, 10, 0, 0, 0, time.UTC).Unix()) err := dataBase.SaveTrigger(trigger.ID, trigger) So(err, ShouldBeNil) @@ -729,7 +729,7 @@ func TestDbConnector_preSaveTrigger(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() systemClock := mock_clock.NewMockClock(mockCtrl) - systemClock.EXPECT().Now().Return(testTime).Times(6) + systemClock.EXPECT().NowUnix().Return(testTime.Unix()).Times(6) connector := &DbConnector{clock: systemClock} patterns := []string{"pattern-1", "pattern-2"} diff --git a/datatypes.go b/datatypes.go index 9796479ef..00fcd77d2 100644 --- a/datatypes.go +++ b/datatypes.go @@ -334,11 +334,11 @@ func (notification *ScheduledNotification) GetState(triggerCheck *CheckData) sch return RemovedNotification } - if !triggerCheck.IsMetricOnMaintenance(notification.Event.Metric) && !triggerCheck.IsTriggerOnMaintenance() { - return ValidNotification + if triggerCheck.IsMetricOnMaintenance(notification.Event.Metric) || triggerCheck.IsTriggerOnMaintenance() { + return ResavedNotification } - return ResavedNotification + return ValidNotification } // MatchedMetric represents parsed and matched metric data. @@ -535,6 +535,7 @@ type CheckData struct { Suppressed bool `json:"suppressed,omitempty" example:"true"` SuppressedState State `json:"suppressed_state,omitempty"` Message string `json:"msg,omitempty"` + Clock Clock `json:"-"` } // Need to not show the user metrics that should have been deleted due to ttlState = Del, @@ -559,7 +560,7 @@ func (checkData *CheckData) RemoveMetricsToTargetRelation() { // IsTriggerOnMaintenance checks if the trigger is on Maintenance. func (checkData *CheckData) IsTriggerOnMaintenance() bool { - return time.Now().Unix() <= checkData.Maintenance + return checkData.Clock.NowUnix() <= checkData.Maintenance } // IsMetricOnMaintenance checks if the metric of the given trigger is on Maintenance. @@ -573,7 +574,7 @@ func (checkData *CheckData) IsMetricOnMaintenance(metric string) bool { return false } - return time.Now().Unix() <= metricState.Maintenance + return checkData.Clock.NowUnix() <= metricState.Maintenance } // MetricState represents metric state data for given timestamp. @@ -780,11 +781,11 @@ func (event NotificationEvent) FormatTimestamp(location *time.Location, timeForm } // GetOrCreateMetricState gets metric state from check data or create new if CheckData has no state for given metric. -func (checkData *CheckData) GetOrCreateMetricState(metric string, emptyTimestampValue int64, muteNewMetric bool) MetricState { - _, ok := checkData.Metrics[metric] - if !ok { - checkData.Metrics[metric] = createEmptyMetricState(emptyTimestampValue, !muteNewMetric) +func (checkData *CheckData) GetOrCreateMetricState(metric string, muteFirstMetric bool, checkPointGap int64) MetricState { + if _, ok := checkData.Metrics[metric]; !ok { + checkData.Metrics[metric] = createEmptyMetricState(muteFirstMetric, checkPointGap, checkData.Clock) } + return checkData.Metrics[metric] } @@ -799,21 +800,19 @@ func (checkData *CheckData) GetMaintenance() (MaintenanceInfo, int64) { return checkData.MaintenanceInfo, checkData.Maintenance } -func createEmptyMetricState(defaultTimestampValue int64, firstStateIsNodata bool) MetricState { - if firstStateIsNodata { - return MetricState{ - State: StateNODATA, - Timestamp: defaultTimestampValue, - } +func createEmptyMetricState(muteFirstMetric bool, checkPointGap int64, clock Clock) MetricState { + metric := MetricState{ + Timestamp: clock.NowUnix(), + EventTimestamp: clock.NowUnix() - checkPointGap, } - unixNow := time.Now().Unix() - - return MetricState{ - State: StateOK, - Timestamp: unixNow, - EventTimestamp: unixNow, + if muteFirstMetric { + metric.State = StateOK + } else { + metric.State = StateNODATA } + + return metric } // GetCheckPoint gets check point for given MetricState. diff --git a/datatypes_test.go b/datatypes_test.go index 6f03438c7..757283c2f 100644 --- a/datatypes_test.go +++ b/datatypes_test.go @@ -5,7 +5,14 @@ import ( "testing" "time" + mock_clock "github.com/moira-alert/moira/mock/clock" . "github.com/smartystreets/goconvey/convey" + "go.uber.org/mock/gomock" +) + +const ( + checkPointGap = 120 + defaultMaintenance = 600 ) func TestIsScheduleAllows(t *testing.T) { @@ -298,6 +305,11 @@ func TestScheduledNotification_GetKey(t *testing.T) { } func TestScheduledNotification_GetState(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + testTime := time.Date(2022, time.June, 6, 10, 0, 0, 0, time.UTC).Unix() + Convey("Test get state of scheduled notifications", t, func() { notification := ScheduledNotification{ Event: NotificationEvent{ @@ -311,52 +323,93 @@ func TestScheduledNotification_GetState(t *testing.T) { }) Convey("Get Resaved state with metric on maintenance", func() { + mockTime := mock_clock.NewMockClock(mockCtrl) + + mockTime.EXPECT().NowUnix().Return(testTime).Times(1) + state := notification.GetState(&CheckData{ Metrics: map[string]MetricState{ "test": { - Maintenance: time.Now().Add(time.Hour).Unix(), + Maintenance: testTime + defaultMaintenance, }, }, + Clock: mockTime, }) + So(state, ShouldEqual, ResavedNotification) }) Convey("Get Resaved state with trigger on maintenance", func() { + mockTime := mock_clock.NewMockClock(mockCtrl) + + mockTime.EXPECT().NowUnix().Return(testTime).Times(1) + state := notification.GetState(&CheckData{ - Maintenance: time.Now().Add(time.Hour).Unix(), + Maintenance: testTime + defaultMaintenance, + Clock: mockTime, }) + So(state, ShouldEqual, ResavedNotification) }) Convey("Get Valid state with trigger without metrics", func() { - state := notification.GetState(&CheckData{}) + mockTime := mock_clock.NewMockClock(mockCtrl) + + mockTime.EXPECT().NowUnix().Return(testTime).Times(1) + + state := notification.GetState(&CheckData{ + Clock: mockTime, + }) + So(state, ShouldEqual, ValidNotification) }) Convey("Get Valid state with trigger with test metric", func() { + mockTime := mock_clock.NewMockClock(mockCtrl) + + mockTime.EXPECT().NowUnix().Return(testTime).Times(2) + state := notification.GetState(&CheckData{ Metrics: map[string]MetricState{ "test": {}, }, + Clock: mockTime, }) + So(state, ShouldEqual, ValidNotification) }) }) } func TestCheckData_GetOrCreateMetricState(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + testTime := time.Date(2022, time.June, 6, 10, 0, 0, 0, time.UTC).Unix() + Convey("Test no metric", t, func() { + mockClock := mock_clock.NewMockClock(mockCtrl) checkData := CheckData{ Metrics: make(map[string]MetricState), + Clock: mockClock, } - So(checkData.GetOrCreateMetricState("my.metric", 12343, false), ShouldResemble, MetricState{State: StateNODATA, Timestamp: 12343}) + + mockClock.EXPECT().NowUnix().Return(testTime).Times(2) + So(checkData.GetOrCreateMetricState("my.metric", false, checkPointGap), ShouldResemble, MetricState{State: StateNODATA, Timestamp: testTime, EventTimestamp: testTime - checkPointGap}) }) - Convey("Test no metric, notifyAboutNew = false", t, func() { + + Convey("Test no metric, mute new metric = true", t, func() { + mockClock := mock_clock.NewMockClock(mockCtrl) checkData := CheckData{ Metrics: make(map[string]MetricState), + Clock: mockClock, } - So(checkData.GetOrCreateMetricState("my.metric", 12343, true), ShouldResemble, MetricState{State: StateOK, Timestamp: time.Now().Unix(), EventTimestamp: time.Now().Unix()}) + + mockClock.EXPECT().NowUnix().Return(testTime).Times(2) + + So(checkData.GetOrCreateMetricState("my.metric", true, checkPointGap), ShouldResemble, MetricState{State: StateOK, Timestamp: testTime, EventTimestamp: testTime - checkPointGap}) }) + Convey("Test has metric", t, func() { metricState := MetricState{Timestamp: 11211} checkData := CheckData{ @@ -364,7 +417,8 @@ func TestCheckData_GetOrCreateMetricState(t *testing.T) { "my.metric": metricState, }, } - So(checkData.GetOrCreateMetricState("my.metric", 12343, false), ShouldResemble, metricState) + + So(checkData.GetOrCreateMetricState("my.metric", true, checkPointGap), ShouldResemble, metricState) }) } @@ -492,21 +546,33 @@ func TestTrigger_IsSimple(t *testing.T) { } func TestCheckData_IsTriggerOnMaintenance(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + testTime := time.Date(2022, time.June, 6, 10, 0, 0, 0, time.UTC).Unix() + Convey("IsTriggerOnMaintenance manipulations", t, func() { - checkData := &CheckData{ - Maintenance: time.Now().Add(time.Hour).Unix(), - } + Convey("Test with trigger check Maintenance equal with current time", func() { + mockTime := mock_clock.NewMockClock(mockCtrl) + checkData := &CheckData{ + Maintenance: testTime, + Clock: mockTime, + } + + mockTime.EXPECT().NowUnix().Return(testTime).Times(1) - Convey("Test with trigger check Maintenance more than time now", func() { actual := checkData.IsTriggerOnMaintenance() So(actual, ShouldBeTrue) }) Convey("Test with trigger check Maintenance less than time now", func() { - checkData.Maintenance = time.Now().Add(-time.Hour).Unix() - defer func() { - checkData.Maintenance = time.Now().Add(time.Hour).Unix() - }() + mockTime := mock_clock.NewMockClock(mockCtrl) + checkData := &CheckData{ + Maintenance: testTime - defaultMaintenance, + Clock: mockTime, + } + + mockTime.EXPECT().NowUnix().Return(testTime).Times(1) actual := checkData.IsTriggerOnMaintenance() So(actual, ShouldBeFalse) @@ -515,55 +581,82 @@ func TestCheckData_IsTriggerOnMaintenance(t *testing.T) { } func TestCheckData_IsMetricOnMaintenance(t *testing.T) { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + testTime := time.Date(2022, time.June, 6, 10, 0, 0, 0, time.UTC).Unix() + Convey("isMetricOnMaintenance manipulations", t, func() { - checkData := &CheckData{ - Metrics: map[string]MetricState{ - "test1": { - Maintenance: time.Now().Add(time.Hour).Unix(), + Convey("Test with a metric that is not in the trigger", func() { + mockTime := mock_clock.NewMockClock(mockCtrl) + checkData := &CheckData{ + Metrics: map[string]MetricState{ + "test1": {}, + "test2": {}, }, - "test2": {}, - }, - } + Clock: mockTime, + } - Convey("Test with a metric that is not in the trigger", func() { actual := checkData.IsMetricOnMaintenance("") So(actual, ShouldBeFalse) }) Convey("Test with metrics that are in the trigger but not on maintenance", func() { + mockTime := mock_clock.NewMockClock(mockCtrl) + checkData := &CheckData{ + Metrics: map[string]MetricState{ + "test1": {}, + "test2": {}, + }, + Clock: mockTime, + } + + mockTime.EXPECT().NowUnix().Return(testTime).Times(1) + actual := checkData.IsMetricOnMaintenance("test2") So(actual, ShouldBeFalse) }) Convey("Test with metrics that are in the trigger and on maintenance", func() { + mockTime := mock_clock.NewMockClock(mockCtrl) + checkData := &CheckData{ + Metrics: map[string]MetricState{ + "test1": { + Maintenance: testTime + defaultMaintenance, + }, + "test2": {}, + }, + Clock: mockTime, + } + + mockTime.EXPECT().NowUnix().Return(testTime).Times(1) + actual := checkData.IsMetricOnMaintenance("test1") So(actual, ShouldBeTrue) }) Convey("Test with the metric that is in the trigger, but the time now is more than Maintenance", func() { - metric := checkData.Metrics["test1"] - metric.Maintenance = time.Now().Add(-time.Hour).Unix() - checkData.Metrics["test1"] = metric - defer func() { - metric := checkData.Metrics["test1"] - metric.Maintenance = time.Now().Add(time.Hour).Unix() - checkData.Metrics["test1"] = metric - }() + mockTime := mock_clock.NewMockClock(mockCtrl) + checkData := &CheckData{ + Metrics: map[string]MetricState{ + "test1": { + Maintenance: testTime - defaultMaintenance, + }, + "test2": {}, + }, + Clock: mockTime, + } + + mockTime.EXPECT().NowUnix().Return(testTime).Times(1) actual := checkData.IsMetricOnMaintenance("test1") So(actual, ShouldBeFalse) }) Convey("Test with trigger without metrics", func() { - checkData.Metrics = make(map[string]MetricState) - defer func() { - checkData.Metrics = map[string]MetricState{ - "test1": { - Maintenance: time.Now().Add(time.Hour).Unix(), - }, - "test2": {}, - } - }() + checkData := &CheckData{ + Metrics: make(map[string]MetricState), + } actual := checkData.IsMetricOnMaintenance("test1") So(actual, ShouldBeFalse) diff --git a/filter/patterns_storage.go b/filter/patterns_storage.go index 2cfcd9dd8..950102b7b 100644 --- a/filter/patterns_storage.go +++ b/filter/patterns_storage.go @@ -114,7 +114,7 @@ func (storage *PatternStorage) ProcessIncomingMetric(lineBytes []byte, maxTTL ti return nil } - if parsedMetric.IsExpired(maxTTL, storage.clock.Now()) { + if parsedMetric.IsExpired(maxTTL, storage.clock.NowUTC()) { storage.logger.Debug(). String(moira.LogFieldNameMetricName, parsedMetric.Name). String(moira.LogFieldNameMetricTimestamp, fmt.Sprint(parsedMetric.Timestamp)). diff --git a/filter/patterns_storage_test.go b/filter/patterns_storage_test.go index 7caef651f..8c3bd76da 100644 --- a/filter/patterns_storage_test.go +++ b/filter/patterns_storage_test.go @@ -48,7 +48,7 @@ func TestProcessIncomingMetric(t *testing.T) { ) systemClock := mock_clock.NewMockClock(mockCtrl) - systemClock.EXPECT().Now().Return(time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC)).AnyTimes() + systemClock.EXPECT().NowUTC().Return(time.Date(2009, 2, 13, 23, 31, 30, 0, time.UTC)).AnyTimes() patternsStorage.clock = systemClock Convey("Create new pattern storage, should no error", t, func() { diff --git a/go.mod b/go.mod index ab02cbfde..91857a6eb 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( require github.com/prometheus/common v0.37.0 require ( + github.com/golang/mock v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/mattermost/mattermost/server/public v0.1.1 github.com/mitchellh/mapstructure v1.5.0 diff --git a/go.sum b/go.sum index 29ce7ac2f..d95d23b39 100644 --- a/go.sum +++ b/go.sum @@ -680,6 +680,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= diff --git a/interfaces.go b/interfaces.go index f3f1559ef..b6e4bcd18 100644 --- a/interfaces.go +++ b/interfaces.go @@ -228,5 +228,6 @@ type PlotTheme interface { // Clock is an interface to work with Time. type Clock interface { - Now() time.Time + NowUTC() time.Time + NowUnix() int64 } diff --git a/mock/clock/clock.go b/mock/clock/clock.go index 4aab4f192..8ed030da3 100644 --- a/mock/clock/clock.go +++ b/mock/clock/clock.go @@ -39,16 +39,30 @@ func (m *MockClock) EXPECT() *MockClockMockRecorder { return m.recorder } -// Now mocks base method. -func (m *MockClock) Now() time.Time { +// NowUTC mocks base method. +func (m *MockClock) NowUTC() time.Time { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Now") + ret := m.ctrl.Call(m, "NowUTC") ret0, _ := ret[0].(time.Time) return ret0 } -// Now indicates an expected call of Now. -func (mr *MockClockMockRecorder) Now() *gomock.Call { +// NowUTC indicates an expected call of NowUTC. +func (mr *MockClockMockRecorder) NowUTC() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockClock)(nil).Now)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NowUTC", reflect.TypeOf((*MockClock)(nil).NowUTC)) +} + +// NowUnix mocks base method. +func (m *MockClock) NowUnix() int64 { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NowUnix") + ret0, _ := ret[0].(int64) + return ret0 +} + +// NowUnix indicates an expected call of NowUnix. +func (mr *MockClockMockRecorder) NowUnix() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NowUnix", reflect.TypeOf((*MockClock)(nil).NowUnix)) } diff --git a/notifier/events/event_test.go b/notifier/events/event_test.go index 219f03f60..4b79f4cbd 100644 --- a/notifier/events/event_test.go +++ b/notifier/events/event_test.go @@ -27,7 +27,7 @@ func TestEvent(t *testing.T) { scheduler := mock_scheduler.NewMockScheduler(mockCtrl) logger, _ := logging.GetLogger("Events") systemClock := mock_clock.NewMockClock(mockCtrl) - systemClock.EXPECT().Now().Return(time.Now()).AnyTimes() + systemClock.EXPECT().NowUTC().Return(time.Now()).AnyTimes() Convey("When event is TEST and subscription is disabled, should add new notification", t, func() { worker := FetchEventsWorker{ @@ -59,8 +59,8 @@ func TestEvent(t *testing.T) { SubscriptionID: event.SubscriptionID, }, SendFail: 0, - Timestamp: systemClock.Now().Unix(), - CreatedAt: systemClock.Now().Unix(), + Timestamp: systemClock.NowUTC().Unix(), + CreatedAt: systemClock.NowUTC().Unix(), Throttled: false, Contact: contact, } @@ -88,7 +88,7 @@ func TestEvent(t *testing.T) { } dataBase.EXPECT().GetContact(event.ContactID).Times(1).Return(contact, nil) dataBase.EXPECT().GetContact(contact.ID).Times(1).Return(contact, nil) - now := systemClock.Now() + now := systemClock.NowUTC() notification := moira.ScheduledNotification{ Event: moira.NotificationEvent{ TriggerID: "", diff --git a/notifier/scheduler.go b/notifier/scheduler.go index 8b69f0a35..d0348bc75 100644 --- a/notifier/scheduler.go +++ b/notifier/scheduler.go @@ -50,7 +50,7 @@ func (scheduler *StandardScheduler) ScheduleNotification(params moira.SchedulerP next time.Time throttled bool ) - now := scheduler.clock.Now() + now := scheduler.clock.NowUTC() if params.SendFail > 0 { next = now.Add(scheduler.config.ReschedulingDelay) throttled = params.ThrottledOld diff --git a/notifier/scheduler_test.go b/notifier/scheduler_test.go index cf1e75942..e148503bb 100644 --- a/notifier/scheduler_test.go +++ b/notifier/scheduler_test.go @@ -83,7 +83,7 @@ func TestThrottling(t *testing.T) { expected2 := expected expected2.SendFail = 1 expected2.Timestamp = now.Add(time.Minute).Unix() - systemClock.EXPECT().Now().Return(now).Times(1) + systemClock.EXPECT().NowUTC().Return(now).Times(1) notification := scheduler.ScheduleNotification(params2, logger) So(notification, ShouldResemble, &expected2) @@ -98,7 +98,7 @@ func TestThrottling(t *testing.T) { expected2.SendFail = 3 expected2.Timestamp = now.Add(time.Minute).Unix() expected2.Throttled = true - systemClock.EXPECT().Now().Return(now).Times(1) + systemClock.EXPECT().NowUTC().Return(now).Times(1) notification := scheduler.ScheduleNotification(params2, logger) So(notification, ShouldResemble, &expected2) @@ -119,7 +119,7 @@ func TestThrottling(t *testing.T) { expected3 := expected expected3.Event = testEvent - systemClock.EXPECT().Now().Return(now).Times(1) + systemClock.EXPECT().NowUTC().Return(now).Times(1) notification := scheduler.ScheduleNotification(params2, logger) So(notification, ShouldResemble, &expected3) @@ -130,7 +130,7 @@ func TestThrottling(t *testing.T) { dataBase.EXPECT().GetSubscription(*event.SubscriptionID).Times(1).Return(moira.SubscriptionData{}, fmt.Errorf("Error while read subscription")) params2 := params - systemClock.EXPECT().Now().Return(now).Times(1) + systemClock.EXPECT().NowUTC().Return(now).Times(1) notification := scheduler.ScheduleNotification(params2, logger) So(notification, ShouldResemble, &expected) From a8c5b002055a4983c1dcab618d23c700abf0fcde Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Thu, 5 Sep 2024 18:30:59 +0700 Subject: [PATCH 11/36] feat: trigger description to telegram alerts (#1075) --- senders/mattermost/sender.go | 1 + senders/msgformat/highlighter.go | 37 +-- senders/msgformat/highlighter_test.go | 3 +- senders/msgformat/msgformat.go | 11 +- senders/slack/slack.go | 1 + senders/slack/slack_test.go | 4 +- senders/telegram/init.go | 38 +-- senders/telegram/message_formatter.go | 256 +++++++++++++++++++++ senders/telegram/message_formatter_test.go | 199 ++++++++++++++++ senders/telegram/send.go | 79 ++++++- senders/telegram/send_test.go | 55 +++++ 11 files changed, 627 insertions(+), 57 deletions(-) create mode 100644 senders/telegram/message_formatter.go create mode 100644 senders/telegram/message_formatter_test.go diff --git a/senders/mattermost/sender.go b/senders/mattermost/sender.go index d83dbf6c7..36cbabb37 100644 --- a/senders/mattermost/sender.go +++ b/senders/mattermost/sender.go @@ -90,6 +90,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca location, uriFormatter, descriptionFormatter, + msgformat.DefaultDescriptionCutter, boldFormatter, eventStringFormatter, codeBlockStart, diff --git a/senders/msgformat/highlighter.go b/senders/msgformat/highlighter.go index 156609957..9a78403e4 100644 --- a/senders/msgformat/highlighter.go +++ b/senders/msgformat/highlighter.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "time" + "unicode/utf8" "github.com/moira-alert/moira" "github.com/moira-alert/moira/senders" @@ -23,8 +24,11 @@ type BoldFormatter func(str string) string // EventStringFormatter formats single event string. type EventStringFormatter func(event moira.NotificationEvent, location *time.Location) string -// HighlightSyntaxFormatter formats message by using functions, emojis and some other highlight patterns. -type HighlightSyntaxFormatter struct { +// DescriptionCutter cuts the given description to fit max size. +type DescriptionCutter func(desc string, maxSize int) string + +// highlightSyntaxFormatter formats message by using functions, emojis and some other highlight patterns. +type highlightSyntaxFormatter struct { // emojiGetter used in titles for better description. emojiGetter emoji_provider.StateEmojiGetter frontURI string @@ -32,13 +36,14 @@ type HighlightSyntaxFormatter struct { useEmoji bool uriFormatter UriFormatter descriptionFormatter DescriptionFormatter + descriptionCutter DescriptionCutter boldFormatter BoldFormatter eventsStringFormatter EventStringFormatter codeBlockStart string codeBlockEnd string } -// NewHighlightSyntaxFormatter creates new HighlightSyntaxFormatter with given arguments. +// NewHighlightSyntaxFormatter creates new highlightSyntaxFormatter with given arguments. func NewHighlightSyntaxFormatter( emojiGetter emoji_provider.StateEmojiGetter, useEmoji bool, @@ -46,18 +51,20 @@ func NewHighlightSyntaxFormatter( location *time.Location, uriFormatter UriFormatter, descriptionFormatter DescriptionFormatter, + descriptionCutter DescriptionCutter, boldFormatter BoldFormatter, eventsStringFormatter EventStringFormatter, codeBlockStart string, codeBlockEnd string, ) MessageFormatter { - return &HighlightSyntaxFormatter{ + return &highlightSyntaxFormatter{ emojiGetter: emojiGetter, frontURI: frontURI, location: location, useEmoji: useEmoji, uriFormatter: uriFormatter, descriptionFormatter: descriptionFormatter, + descriptionCutter: descriptionCutter, boldFormatter: boldFormatter, eventsStringFormatter: eventsStringFormatter, codeBlockStart: codeBlockStart, @@ -66,25 +73,25 @@ func NewHighlightSyntaxFormatter( } // Format formats message using given params and formatter functions. -func (formatter *HighlightSyntaxFormatter) Format(params MessageFormatterParams) string { +func (formatter *highlightSyntaxFormatter) Format(params MessageFormatterParams) string { var message strings.Builder state := params.Events.GetCurrentState(params.Throttled) emoji := formatter.emojiGetter.GetStateEmoji(state) title := formatter.buildTitle(params.Events, params.Trigger, emoji, params.Throttled) - titleLen := len([]rune(title)) + titleLen := utf8.RuneCountInString(title) desc := formatter.descriptionFormatter(params.Trigger) - descLen := len([]rune(desc)) + descLen := utf8.RuneCountInString(desc) eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) - eventsStringLen := len([]rune(eventsString)) + eventsStringLen := utf8.RuneCountInString(eventsString) charsLeftAfterTitle := params.MessageMaxChars - titleLen descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(charsLeftAfterTitle, descLen, eventsStringLen) if descLen != descNewLen { - desc = desc[:descNewLen] + "...\n" + desc = formatter.descriptionCutter(desc, descNewLen) } if eventsNewLen != eventsStringLen { eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) @@ -96,7 +103,7 @@ func (formatter *HighlightSyntaxFormatter) Format(params MessageFormatterParams) return message.String() } -func (formatter *HighlightSyntaxFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { +func (formatter *highlightSyntaxFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { state := events.GetCurrentState(throttled) title := "" if formatter.useEmoji { @@ -122,11 +129,11 @@ func (formatter *HighlightSyntaxFormatter) buildTitle(events moira.NotificationE // buildEventsString builds the string from moira events and limits it to charsForEvents. // if charsForEvents is negative buildEventsString does not limit the events string. -func (formatter *HighlightSyntaxFormatter) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { +func (formatter *highlightSyntaxFormatter) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { charsForThrottleMsg := 0 - throttleMsg := fmt.Sprintf("\nPlease, %s to generate less events.", formatter.boldFormatter(changeRecommendation)) + throttleMsg := fmt.Sprintf("\nPlease, %s to generate less events.", formatter.boldFormatter(ChangeTriggerRecommendation)) if throttled { - charsForThrottleMsg = len([]rune(throttleMsg)) + charsForThrottleMsg = utf8.RuneCountInString(throttleMsg) } charsLeftForEvents := charsForEvents - charsForThrottleMsg @@ -143,8 +150,8 @@ func (formatter *HighlightSyntaxFormatter) buildEventsString(events moira.Notifi } tailString = fmt.Sprintf("\n...and %d more events.", len(events)-eventsPrinted) - tailStringLen := len([]rune(formatter.codeBlockEnd)) + len("\n") + len([]rune(tailString)) - if !(charsForEvents < 0) && (len([]rune(eventsString))+len([]rune(line)) > charsLeftForEvents-tailStringLen) { + tailStringLen := utf8.RuneCountInString(formatter.codeBlockEnd) + len("\n") + utf8.RuneCountInString(tailString) + if !(charsForEvents < 0) && (utf8.RuneCountInString(eventsString)+utf8.RuneCountInString(line) > charsLeftForEvents-tailStringLen) { eventsLenLimitReached = true break } diff --git a/senders/msgformat/highlighter_test.go b/senders/msgformat/highlighter_test.go index 79bfaf85b..470db230e 100644 --- a/senders/msgformat/highlighter_test.go +++ b/senders/msgformat/highlighter_test.go @@ -30,6 +30,7 @@ func TestFormat(t *testing.T) { location, testUriFormatter, testDescriptionFormatter, + DefaultDescriptionCutter, testBoldFormatter, testEventStringFormatter, "```", @@ -135,7 +136,7 @@ func TestFormat(t *testing.T) { Convey("Long description and many events. both desc and events > msgLimit/2", func() { actual := formatter.Format(getParams(longEvents, moira.TriggerData{Desc: longDesc}, false)) expected := "**NODATA**\n" + - strings.Repeat("a", 1984) + "...\n" + + strings.Repeat("a", 1980) + "...\n" + "```\n" + strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 40) + "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n" + diff --git a/senders/msgformat/msgformat.go b/senders/msgformat/msgformat.go index 5d00724b2..72a6cdb37 100644 --- a/senders/msgformat/msgformat.go +++ b/senders/msgformat/msgformat.go @@ -1,12 +1,12 @@ // Package msgformat provides MessageFormatter interface which may be used for formatting messages. -// Also, it contains some realizations such as HighlightSyntaxFormatter. +// Also, it contains some realizations such as highlightSyntaxFormatter. package msgformat import ( "github.com/moira-alert/moira" ) -const changeRecommendation = "fix your system or tune this trigger" +const ChangeTriggerRecommendation = "fix your system or tune this trigger" // MessageFormatter is used for formatting messages to send via telegram, mattermost, etc. type MessageFormatter interface { @@ -21,3 +21,10 @@ type MessageFormatterParams struct { MessageMaxChars int Throttled bool } + +// DefaultDescriptionCutter cuts description, so len(newDesc) <= maxSize. Ensure that len(desc) >= maxSize and +// maxSize >= len("...\n"). +func DefaultDescriptionCutter(desc string, maxSize int) string { + suffix := "...\n" + return desc[:maxSize-len(suffix)] + suffix +} diff --git a/senders/slack/slack.go b/senders/slack/slack.go index 54f960c90..28976c6c6 100644 --- a/senders/slack/slack.go +++ b/senders/slack/slack.go @@ -70,6 +70,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca location, uriFormatter, descriptionFormatter, + msgformat.DefaultDescriptionCutter, boldFormatter, eventStringFormatter, codeBlockStart, diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index ac58d2f68..43471896f 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -161,7 +161,7 @@ some other text italic text eventsString += eventLine } actual := sender.buildMessage(events, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" So(actual, ShouldResemble, expected) }) @@ -174,7 +174,7 @@ some other text italic text 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 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n...and 5 more events." + expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n...and 5 more events." So(actual, ShouldResemble, expected) }) }) diff --git a/senders/telegram/init.go b/senders/telegram/init.go index 7815437e8..4659c1f69 100644 --- a/senders/telegram/init.go +++ b/senders/telegram/init.go @@ -3,7 +3,6 @@ package telegram import ( "errors" "fmt" - "html" "strings" "time" @@ -23,11 +22,6 @@ const ( hidden = "[DATA DELETED]" ) -var ( - codeBlockStart = "
" - codeBlockEnd = "
" -) - var pollerTimeout = 10 * time.Second // Structure that represents the Telegram configuration in the YAML file. @@ -78,17 +72,11 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca sender.apiToken = cfg.APIToken emojiProvider := telegramEmojiProvider{} - sender.formatter = msgformat.NewHighlightSyntaxFormatter( + sender.formatter = NewTelegramMessageFormatter( emojiProvider, true, cfg.FrontURI, - location, - urlFormatter, - emptyDescriptionFormatter, - boldFormatter, - eventStringFormatter, - codeBlockStart, - codeBlockEnd) + location) sender.logger = logger sender.bot, err = telebot.NewBot(telebot.Settings{ @@ -135,25 +123,3 @@ func (sender *Sender) runTelebot(contactType string) { func telegramLockKey(contactType string) string { return telegramLockPrefix + contactType } - -func urlFormatter(triggerURI, triggerName string) string { - return fmt.Sprintf("%s", triggerURI, html.EscapeString(triggerName)) -} - -func emptyDescriptionFormatter(trigger moira.TriggerData) string { - return "" -} - -func boldFormatter(str string) string { - return fmt.Sprintf("%s", html.EscapeString(str)) -} - -func eventStringFormatter(event moira.NotificationEvent, loc *time.Location) string { - return fmt.Sprintf( - "%s: %s = %s (%s to %s)", - event.FormatTimestamp(loc, moira.DefaultTimeFormat), - html.EscapeString(event.Metric), - html.EscapeString(event.GetMetricsValues(moira.DefaultNotificationSettings)), - event.OldState, - event.State) -} diff --git a/senders/telegram/message_formatter.go b/senders/telegram/message_formatter.go new file mode 100644 index 000000000..8059a2231 --- /dev/null +++ b/senders/telegram/message_formatter.go @@ -0,0 +1,256 @@ +package telegram + +import ( + "fmt" + "html" + "regexp" + "strings" + "time" + "unicode/utf8" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/senders" + "github.com/moira-alert/moira/senders/emoji_provider" + "github.com/moira-alert/moira/senders/msgformat" + "github.com/russross/blackfriday/v2" +) + +const ( + eventsBlockStart = "
" + eventsBlockEnd = "
" +) + +type messageFormatter struct { + // emojiGetter used in titles for better description. + emojiGetter emoji_provider.StateEmojiGetter + frontURI string + location *time.Location + useEmoji bool +} + +// NewTelegramMessageFormatter returns message formatter which is used in telegram sender. +// The message will be formatted with html tags supported by telegram. +func NewTelegramMessageFormatter( + emojiGetter emoji_provider.StateEmojiGetter, + useEmoji bool, + frontURI string, + location *time.Location, +) msgformat.MessageFormatter { + return &messageFormatter{ + emojiGetter: emojiGetter, + frontURI: frontURI, + location: location, + useEmoji: useEmoji, + } +} + +// Format formats message using given params and formatter functions. +func (formatter *messageFormatter) Format(params msgformat.MessageFormatterParams) string { + state := params.Events.GetCurrentState(params.Throttled) + emoji := formatter.emojiGetter.GetStateEmoji(state) + + title := formatter.buildTitle(params.Events, params.Trigger, emoji, params.Throttled) + titleLen := calcRunesCountWithoutHTML([]rune(title)) + + desc := descriptionFormatter(params.Trigger) + descLen := calcRunesCountWithoutHTML([]rune(desc)) + + eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) + eventsStringLen := calcRunesCountWithoutHTML([]rune(eventsString)) + + descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(params.MessageMaxChars-titleLen, descLen, eventsStringLen) + if descLen != descNewLen { + desc = descriptionCutter(desc, descNewLen) + } + if eventsStringLen != eventsNewLen { + eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + } + + return title + desc + eventsString +} + +// calcRunesCountWithoutHTML is used for calculating symbols in text without html tags. Special symbols +// like `>`, `<` etc. are counted not as one symbol, for example, len([]rune(">")). +// This precision is enough for us to evaluate size of message. +func calcRunesCountWithoutHTML(htmlText []rune) int { + textLen := 0 + isTag := false + + for _, r := range htmlText { + if r == '<' { + isTag = true + continue + } + + if !isTag { + textLen += 1 + } + + if r == '>' { + isTag = false + } + } + + return textLen +} + +func (formatter *messageFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { + state := events.GetCurrentState(throttled) + title := "" + if formatter.useEmoji { + title += emoji + " " + } + + title += boldFormatter(string(state)) + triggerURI := trigger.GetTriggerURI(formatter.frontURI) + if triggerURI != "" { + title += fmt.Sprintf(" %s", uriFormatter(triggerURI, trigger.Name)) + } else if trigger.Name != "" { + title += " " + trigger.Name + } + + tags := trigger.GetTags() + if tags != "" { + title += " " + tags + } + + title += "\n" + return title +} + +var throttleMsg = fmt.Sprintf("\nPlease, %s to generate less events.", boldFormatter(msgformat.ChangeTriggerRecommendation)) + +// buildEventsString builds the string from moira events and limits it to charsForEvents. +// if charsForEvents is negative buildEventsString does not limit the events string. +func (formatter *messageFormatter) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { + charsForThrottleMsg := 0 + if throttled { + charsForThrottleMsg = calcRunesCountWithoutHTML([]rune(throttleMsg)) + } + charsLeftForEvents := charsForEvents - charsForThrottleMsg + + var eventsString string + eventsString += eventsBlockStart + var tailString string + + eventsLenLimitReached := false + eventsPrinted := 0 + eventsStringLen := 0 + for _, event := range events { + line := fmt.Sprintf("\n%s", eventStringFormatter(event, formatter.location)) + if msg := event.CreateMessage(formatter.location); len(msg) > 0 { + line += fmt.Sprintf(". %s", msg) + } + + tailString = fmt.Sprintf("\n...and %d more events.", len(events)-eventsPrinted) + tailStringLen := len("\n") + utf8.RuneCountInString(tailString) + lineLen := calcRunesCountWithoutHTML([]rune(line)) + + if charsForEvents >= 0 && eventsStringLen+lineLen > charsLeftForEvents-tailStringLen { + eventsLenLimitReached = true + break + } + + eventsString += line + eventsStringLen += lineLen + eventsPrinted++ + } + eventsString += "\n" + eventsString += eventsBlockEnd + + if eventsLenLimitReached { + eventsString += tailString + } + + if throttled { + eventsString += throttleMsg + } + + return eventsString +} + +func uriFormatter(triggerURI, triggerName string) string { + return fmt.Sprintf("%s", triggerURI, html.EscapeString(triggerName)) +} + +var ( + startHeaderRegexp = regexp.MustCompile("") + endHeaderRegexp = regexp.MustCompile("") +) + +func descriptionFormatter(trigger moira.TriggerData) string { + if trigger.Desc == "" { + return "" + } + + desc := trigger.Desc + "\n" + + // Sometimes in trigger description may be text constructions like . + // blackfriday may recognise it as tag, so it won't be escaped. + // Then it is sent to telegram we will get error: 'Bad request', because telegram doesn't support such tag. + // So escaping them before blackfriday.Run. + replacer := strings.NewReplacer( + "<", "<", + ">", ">", + ) + mdWithNoTags := replacer.Replace(desc) + + htmlDescStr := string(blackfriday.Run([]byte(mdWithNoTags), + blackfriday.WithExtensions( + blackfriday.CommonExtensions & + ^blackfriday.DefinitionLists & + ^blackfriday.Tables), + blackfriday.WithRenderer( + blackfriday.NewHTMLRenderer( + blackfriday.HTMLRendererParameters{ + Flags: blackfriday.UseXHTML, + })))) + + // html headers are not supported by telegram html, so make them bold instead. + htmlDescStr = startHeaderRegexp.ReplaceAllString(htmlDescStr, "") + replacedHeaders := endHeaderRegexp.ReplaceAllString(htmlDescStr, "") + + // some tags are not supported, so replace them. + tagReplacer := strings.NewReplacer( + "

", "", + "

", "", + "
    ", "", + "
", "", + "
  • ", "- ", + "
  • ", "", + "
      ", "", + "
    ", "", + "
    ", "", + "
    ", "", + "
    ", "\n", + "
    ", "\n") + + return tagReplacer.Replace(replacedHeaders) +} + +const ( + tooLongDescMessage = "\nDescription is too long for telegram sender.\n" + badFormatMessage = "\nBad trigger description for telegram sender. Please check trigger.\n" +) + +func descriptionCutter(_ string, maxSize int) string { + if utf8.RuneCountInString(tooLongDescMessage) <= maxSize { + return tooLongDescMessage + } + + return "" +} + +func boldFormatter(str string) string { + return fmt.Sprintf("%s", html.EscapeString(str)) +} + +func eventStringFormatter(event moira.NotificationEvent, loc *time.Location) string { + return fmt.Sprintf( + "%s: %s = %s (%s to %s)", + event.FormatTimestamp(loc, moira.DefaultTimeFormat), + html.EscapeString(event.Metric), + html.EscapeString(event.GetMetricsValues(moira.DefaultNotificationSettings)), + event.OldState, + event.State) +} diff --git a/senders/telegram/message_formatter_test.go b/senders/telegram/message_formatter_test.go new file mode 100644 index 000000000..f6ed08cb2 --- /dev/null +++ b/senders/telegram/message_formatter_test.go @@ -0,0 +1,199 @@ +package telegram + +import ( + "fmt" + "strings" + "testing" + "time" + "unicode/utf8" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/senders/msgformat" + + . "github.com/smartystreets/goconvey/convey" +) + +const testFrontURI = "https://moira.uri" + +func TestMessageFormatter_Format(t *testing.T) { + location, _ := time.LoadLocation("UTC") + emojiProvider := telegramEmojiProvider{} + + formatter := NewTelegramMessageFormatter( + emojiProvider, + true, + testFrontURI, + location) + + event := moira.NotificationEvent{ + TriggerID: "TriggerID", + Values: map[string]float64{"t1": 123}, + Timestamp: 150000000, + Metric: "Metric", + OldState: moira.StateOK, + State: moira.StateNODATA, + } + + const shortDesc = `My description` + trigger := moira.TriggerData{ + Tags: []string{"tag1", "tag2"}, + Name: "Name", + ID: "TriggerID", + Desc: shortDesc, + } + + expectedFirstLine := "💣 NODATA Name [tag1][tag2]\n" + lenFirstLine := utf8.RuneCountInString(expectedFirstLine) - + utf8.RuneCountInString("") + + eventStr := "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n" + lenEventStr := utf8.RuneCountInString(eventStr) - utf8.RuneCountInString("") // 60 - 13 = 47 + + Convey("TelegramMessageFormatter", t, func() { + Convey("message with one event", func() { + events, throttled := moira.NotificationEvents{event}, false + expected := expectedFirstLine + + shortDesc + "\n" + + eventsBlockStart + "\n" + + eventStr + + eventsBlockEnd + + msg := formatter.Format(getParams(events, trigger, throttled)) + + So(msg, ShouldEqual, expected) + }) + + Convey("message with one event and throttled", func() { + events, throttled := moira.NotificationEvents{event}, true + msg := formatter.Format(getParams(events, trigger, throttled)) + + expected := expectedFirstLine + + shortDesc + "\n" + + eventsBlockStart + "\n" + + eventStr + + eventsBlockEnd + + throttleMsg + So(msg, ShouldEqual, expected) + }) + + Convey("message with 3 events", func() { + events, throttled := moira.NotificationEvents{event, event, event}, false + expected := expectedFirstLine + + shortDesc + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, 3) + + eventsBlockEnd + + msg := formatter.Format(getParams(events, trigger, throttled)) + + So(msg, ShouldEqual, expected) + }) + + Convey("message with complex description", func() { + trigger.Desc = "# Моё описание\n\nсписок:\n- **жирный**\n- *курсив*\n- `код`\n- подчёркнутый\n- ~~зачёркнутый~~\n" + + "\n------\nif a > b do smth\nif c < d do another thing\ntrue && false = false\ntrue || false = true\n" + + "\"Hello everybody!\", 'another quots'\nif I use something like nothing happens, also if i use allowed tag" + events, throttled := moira.NotificationEvents{event}, false + + expected := expectedFirstLine + + "Моё описание\n\nсписок:\n- жирный\n- курсив\n- код\n- <u>подчёркнутый</u>\n- зачёркнутый\n" + + "\n\n\nif a > b do smth\nif c < d do another thing\ntrue && false = false\ntrue || false = true\n" + + ""Hello everybody!", 'another quots'\nif I use something like <custom_tag> nothing happens, also if i use allowed <b> tag\n" + + eventsBlockStart + "\n" + + eventStr + + eventsBlockEnd + + msg := formatter.Format(getParams(events, trigger, throttled)) + + So(msg, ShouldEqual, expected) + }) + + Convey("with long messages", func() { + msgLimit := albumCaptionMaxCharacters - lenFirstLine + halfMsgLimit := msgLimit / 2 + greaterThanHalf := halfMsgLimit + 100 + lessThanHalf := halfMsgLimit - 100 + + Convey("text size of description > msgLimit / 2", func() { + var events moira.NotificationEvents + throttled := false + + eventsCount := lessThanHalf / lenEventStr + for i := 0; i < eventsCount; i++ { + events = append(events, event) + } + + trigger.Desc = strings.Repeat("**ё**ж", greaterThanHalf/2) + + expected := expectedFirstLine + + strings.Repeat("ёж", greaterThanHalf/2) + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, eventsCount) + + eventsBlockEnd + + msg := formatter.Format(getParams(events, trigger, throttled)) + + So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + So(msg, ShouldEqual, expected) + }) + + Convey("text size of events block > msgLimit / 2", func() { + var events moira.NotificationEvents + throttled := false + + eventsCount := greaterThanHalf / lenEventStr + for i := 0; i < eventsCount; i++ { + events = append(events, event) + } + + trigger.Desc = strings.Repeat("**ё**ж", lessThanHalf/2) + + expected := expectedFirstLine + + strings.Repeat("ёж", lessThanHalf/2) + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, eventsCount) + + eventsBlockEnd + + msg := formatter.Format(getParams(events, trigger, throttled)) + + So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + So(msg, ShouldEqual, expected) + }) + + Convey("both description and events block have text size > msgLimit/2", func() { + var events moira.NotificationEvents + throttled := false + + eventsCount := greaterThanHalf / lenEventStr + for i := 0; i < eventsCount; i++ { + events = append(events, event) + } + + trigger.Desc = strings.Repeat("**ё**ж", greaterThanHalf/2) + + eventsShouldBe := halfMsgLimit / lenEventStr + + expected := expectedFirstLine + + tooLongDescMessage + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, eventsShouldBe) + + eventsBlockEnd + + fmt.Sprintf("\n...and %d more events.", len(events)-eventsShouldBe) + + msg := formatter.Format(getParams(events, trigger, throttled)) + + So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + So(msg, ShouldEqual, expected) + }) + }) + }) +} + +func getParams(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) msgformat.MessageFormatterParams { + return msgformat.MessageFormatterParams{ + Events: events, + Trigger: trigger, + MessageMaxChars: albumCaptionMaxCharacters, + Throttled: throttled, + } +} diff --git a/senders/telegram/send.go b/senders/telegram/send.go index 59ba82ba7..6935dff6a 100644 --- a/senders/telegram/send.go +++ b/senders/telegram/send.go @@ -77,7 +77,9 @@ func (sender *Sender) SendEvents(events moira.NotificationEvents, contact moira. } if err := sender.talk(chat, message, plots, msgType); err != nil { - return checkBrokenContactError(sender.logger, err) + err = checkBrokenContactError(sender.logger, err) + + return sender.retryIfBadMessageError(err, events, contact, trigger, plots, throttled, chat, msgType) } return nil @@ -300,3 +302,78 @@ func getMessageType(plots [][]byte) messageType { return Message } + +func (sender *Sender) retryIfBadMessageError( + err error, + events []moira.NotificationEvent, + contact moira.ContactData, + trigger moira.TriggerData, + plots [][]byte, + throttled bool, + chat *Chat, + msgType messageType, +) error { + var e moira.SenderBrokenContactError + if isBrokenContactErr := errors.As(err, &e); !isBrokenContactErr { + if _, isBadMessage := checkBadMessageError(err); isBadMessage { + // There are some problems with message formatting. + // For example, it is too long, or have unsupported tags and so on. + // Events should not be lost, so retry to send it without description. + + sender.logger.Warning(). + String(moira.LogFieldNameContactID, contact.ID). + String(moira.LogFieldNameContactType, contact.Type). + String(moira.LogFieldNameContactValue, contact.Value). + String(moira.LogFieldNameTriggerID, trigger.ID). + String(moira.LogFieldNameTriggerName, trigger.Name). + Error(err). + Msg("Failed to send alert because of bad description. Retrying now.") + + trigger.Desc = badFormatMessage + message := sender.buildMessage(events, trigger, throttled, characterLimits[msgType]) + + err = sender.talk(chat, message, plots, msgType) + return checkBrokenContactError(sender.logger, err) + } + } + + return err +} + +var badMessageFormatErrors = map[*telebot.Error]struct{}{ + telebot.ErrTooLarge: {}, + telebot.ErrTooLongMessage: {}, +} + +const ( + errMsgPrefixCannotParseInputMedia = "telegram: Bad Request: can't parse InputMedia: Can't parse entities: Unsupported start tag" + errMsgPrefixCaptionTooLong = "telegram: Bad Request: message caption is too long (400)" + errMsgPrefixCannotParseEntities = "telegram: Bad Request: can't parse entities: Unsupported start tag" +) + +func checkBadMessageError(err error) (error, bool) { + if err == nil { + return nil, false + } + + var telebotErr *telebot.Error + if ok := errors.As(err, &telebotErr); ok { + if isBadMessageFormatError(telebotErr) { + return telebotErr, true + } + } + + errMsg := err.Error() + if strings.HasPrefix(errMsg, errMsgPrefixCannotParseInputMedia) || + strings.HasPrefix(errMsg, errMsgPrefixCaptionTooLong) || + strings.HasPrefix(errMsg, errMsgPrefixCannotParseEntities) { + return err, true + } + + return err, false +} + +func isBadMessageFormatError(e *telebot.Error) bool { + _, exists := badMessageFormatErrors[e] + return exists +} diff --git a/senders/telegram/send_test.go b/senders/telegram/send_test.go index e7413021b..489784744 100644 --- a/senders/telegram/send_test.go +++ b/senders/telegram/send_test.go @@ -227,3 +227,58 @@ func TestCheckBrokenContactError(t *testing.T) { }) }) } + +func TestCheckBadMessageError(t *testing.T) { + Convey("Check bad message error", t, func() { + Convey("nil error is nil", func() { + err, ok := checkBadMessageError(nil) + + So(err, ShouldBeNil) + So(ok, ShouldBeFalse) + }) + + Convey("proper telebot errors is recognised", func() { + for givenErr := range badMessageFormatErrors { + err, ok := checkBadMessageError(givenErr) + + So(err, ShouldEqual, givenErr) + So(ok, ShouldBeTrue) + } + }) + + Convey("other telebot errors are not recognised", func() { + otherErrors := []*telebot.Error{ + telebot.ErrInternal, + telebot.ErrEmptyMessage, + telebot.ErrWrongFileID, + telebot.ErrNoRightsToDelete, + telebot.ErrCantRemoveOwner, + telebot.ErrUnauthorized, + telebot.ErrNoRightsToSendPhoto, + telebot.ErrChatNotFound, + } + + for _, otherError := range otherErrors { + err, ok := checkBadMessageError(otherError) + + So(err, ShouldEqual, otherError) + So(ok, ShouldBeFalse) + } + }) + + Convey("errors with proper message is recognised", func() { + givenErrors := []error{ + fmt.Errorf("telegram: Bad Request: can't parse InputMedia: Can't parse entities: Unsupported start tag \"sup\" at byte offset 396 (400)"), + fmt.Errorf("telegram: Bad Request: message caption is too long (400)"), + fmt.Errorf("telegram: Bad Request: can't parse entities: Unsupported start tag \"container_name\" at byte offset 729 (400)"), + } + + for _, givenErr := range givenErrors { + err, ok := checkBadMessageError(givenErr) + + So(err, ShouldEqual, givenErr) + So(ok, ShouldBeTrue) + } + }) + }) +} From 0dab451267525ab2e86bc654fa5a511a50c6a2af Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Tue, 17 Sep 2024 10:33:48 +0700 Subject: [PATCH 12/36] fix: counting of trigger events (#1076) --- api/controller/events.go | 72 +++++++------- api/controller/events_test.go | 170 +++++++++++++++++++++++++++------- api/handler/trigger_test.go | 3 - interfaces.go | 2 +- 4 files changed, 176 insertions(+), 71 deletions(-) diff --git a/api/controller/events.go b/api/controller/events.go index c38fd9152..f982273c5 100644 --- a/api/controller/events.go +++ b/api/controller/events.go @@ -8,7 +8,12 @@ import ( "github.com/moira-alert/moira/api/dto" ) -// GetTriggerEvents gets trigger event from current page and all trigger event count. Events list is filtered by time range +const ( + zeroPage int64 = 0 + allEventsSize int64 = -1 +) + +// GetTriggerEvents gets trigger events from current page and total count of filtered trigger events. Events list is filtered by time range // with `from` and `to` params (`from` and `to` should be "+inf", "-inf" or int64 converted to string), // by metric (regular expression) and by states. If `states` map is empty or nil then all states are accepted. func GetTriggerEvents( @@ -19,11 +24,36 @@ func GetTriggerEvents( metricRegexp *regexp.Regexp, states map[string]struct{}, ) (*dto.EventsList, *api.ErrorResponse) { - events, err := getFilteredNotificationEvents(database, triggerID, page, size, from, to, metricRegexp, states) + events, err := getFilteredNotificationEvents(database, triggerID, from, to, metricRegexp, states) if err != nil { return nil, api.ErrorInternalServer(err) } - eventCount := database.GetNotificationEventCount(triggerID, -1) + + eventCount := int64(len(events)) + + if page < 0 || (page > 0 && size < 0) { + return &dto.EventsList{ + Size: size, + Page: page, + Total: eventCount, + List: []moira.NotificationEvent{}, + }, nil + } + + if page >= 0 && size >= 0 { + start := page * size + end := start + size + + if start >= eventCount { + events = []*moira.NotificationEvent{} + } else { + if end > eventCount { + end = eventCount + } + + events = events[start:end] + } + } eventsList := &dto.EventsList{ Size: size, @@ -42,44 +72,16 @@ func GetTriggerEvents( func getFilteredNotificationEvents( database moira.Database, triggerID string, - page, size int64, from, to string, metricRegexp *regexp.Regexp, states map[string]struct{}, ) ([]*moira.NotificationEvent, error) { - // fetch all events - if size < 0 { - events, err := database.GetNotificationEvents(triggerID, page, size, from, to) - if err != nil { - return nil, err - } - - return filterNotificationEvents(events, metricRegexp, states), nil - } - - // fetch at most `size` events - filtered := make([]*moira.NotificationEvent, 0, size) - var count int64 - - for int64(len(filtered)) < size { - eventsData, err := database.GetNotificationEvents(triggerID, page+count, size, from, to) - if err != nil { - return nil, err - } - - if len(eventsData) == 0 { - break - } - - filtered = append(filtered, filterNotificationEvents(eventsData, metricRegexp, states)...) - count += 1 - - if int64(len(eventsData)) < size { - break - } + events, err := database.GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to) + if err != nil { + return nil, err } - return filtered, nil + return filterNotificationEvents(events, metricRegexp, states), nil } func filterNotificationEvents( diff --git a/api/controller/events_test.go b/api/controller/events_test.go index 4c84f7fc2..bd1a0c18a 100644 --- a/api/controller/events_test.go +++ b/api/controller/events_test.go @@ -25,31 +25,41 @@ func TestGetEvents(t *testing.T) { defer mockCtrl.Finish() triggerID := uuid.Must(uuid.NewV4()).String() - var page int64 = 10 - var size int64 = 100 + var page int64 = 1 + var size int64 = 2 from := "-inf" to := "+inf" Convey("Test has events", t, func() { - var total int64 = 6000000 - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to). - Return([]*moira.NotificationEvent{ - { - State: moira.StateNODATA, - OldState: moira.StateOK, - }, - { - State: moira.StateOK, - OldState: moira.StateNODATA, - }, - }, nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + events := []*moira.NotificationEvent{ + { + State: moira.StateNODATA, + OldState: moira.StateOK, + }, + { + State: moira.StateOK, + OldState: moira.StateNODATA, + }, + { + State: moira.StateWARN, + OldState: moira.StateOK, + }, + { + State: moira.StateERROR, + OldState: moira.StateWARN, + }, + } + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to). + Return(events, nil) list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ - List: []moira.NotificationEvent{{State: moira.StateNODATA, OldState: moira.StateOK}, {State: moira.StateOK, OldState: moira.StateNODATA}}, - Total: total, + List: []moira.NotificationEvent{ + *events[2], + *events[3], + }, + Total: int64(len(events)), Size: size, Page: page, }) @@ -57,8 +67,7 @@ func TestGetEvents(t *testing.T) { Convey("Test no events", t, func() { var total int64 - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(make([]*moira.NotificationEvent, 0), nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(make([]*moira.NotificationEvent, 0), nil) list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) So(list, ShouldResemble, &dto.EventsList{ @@ -71,7 +80,7 @@ func TestGetEvents(t *testing.T) { Convey("Test error", t, func() { expected := fmt.Errorf("oooops! Can not get all contacts") - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(nil, expected) + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(nil, expected) list, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldResemble, api.ErrorInternalServer(expected)) So(list, ShouldBeNil) @@ -85,19 +94,23 @@ func TestGetEvents(t *testing.T) { filtered := []*moira.NotificationEvent{ {Metric: "metric.test.event1"}, {Metric: "a.metric.test.event2"}, + {Metric: "metric.test.event.other"}, } notFiltered := []*moira.NotificationEvent{ {Metric: "another.mEtric.test.event"}, {Metric: "metric.test"}, } - firstPortion := append(make([]*moira.NotificationEvent, 0), notFiltered[0], filtered[0]) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(firstPortion, nil) - secondPortion := append(make([]*moira.NotificationEvent, 0), filtered[1], notFiltered[1]) - dataBase.EXPECT().GetNotificationEvents(triggerID, page+1, size, from, to).Return(secondPortion, nil) + events := []*moira.NotificationEvent{ + notFiltered[0], + filtered[0], + notFiltered[1], + filtered[1], + filtered[2], + } + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(events, nil) - total := int64(len(firstPortion) + len(secondPortion)) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + total := int64(len(filtered)) actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, regexp.MustCompile(`metric\.test\.event`), allStates) So(err, ShouldBeNil) @@ -105,7 +118,7 @@ func TestGetEvents(t *testing.T) { Page: page, Size: size, Total: total, - List: toDTOList(filtered), + List: toDTOList(filtered[:size]), }) }) }) @@ -125,8 +138,7 @@ func TestGetEvents(t *testing.T) { } Convey("with empty map all allowed", func() { total := int64(len(filtered) + len(notFiltered)) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(append(filtered, notFiltered...), nil) actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, allStates) So(err, ShouldBeNil) @@ -139,9 +151,8 @@ func TestGetEvents(t *testing.T) { }) Convey("with given states", func() { - total := int64(len(filtered) + len(notFiltered)) - dataBase.EXPECT().GetNotificationEvents(triggerID, page, size, from, to).Return(append(filtered, notFiltered...), nil) - dataBase.EXPECT().GetNotificationEventCount(triggerID, int64(-1)).Return(total) + total := int64(len(filtered)) + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(append(filtered, notFiltered...), nil) actual, err := GetTriggerEvents(dataBase, triggerID, page, size, from, to, allMetrics, map[string]struct{}{ string(moira.StateOK): {}, @@ -158,6 +169,101 @@ func TestGetEvents(t *testing.T) { }) }) }) + + Convey("test paginating", t, func() { + events := []*moira.NotificationEvent{ + { + State: moira.StateNODATA, + OldState: moira.StateOK, + }, + { + State: moira.StateOK, + OldState: moira.StateNODATA, + }, + { + State: moira.StateWARN, + OldState: moira.StateOK, + }, + { + State: moira.StateERROR, + OldState: moira.StateWARN, + }, + } + total := int64(len(events)) + + type testcase struct { + description string + expectedEvents []moira.NotificationEvent + givenPage int64 + givenSize int64 + } + + testcases := []testcase{ + { + description: "with page > 0 and size > 0", + givenPage: 1, + givenSize: 1, + expectedEvents: []moira.NotificationEvent{ + *events[1], + }, + }, + { + description: "with page == 0 and size > 0", + givenPage: 0, + givenSize: 1, + expectedEvents: []moira.NotificationEvent{ + *events[0], + }, + }, + { + description: "with page > 0, size > 0, page * size + size > events count", + givenPage: 1, + givenSize: 3, + expectedEvents: []moira.NotificationEvent{ + *events[3], + }, + }, + { + description: "with page = 0, size < 0 fetch all events", + givenPage: 0, + givenSize: -10, + expectedEvents: toDTOList(events), + }, + { + description: "with page > 0, size < 0 return no events", + givenPage: 1, + givenSize: -1, + expectedEvents: []moira.NotificationEvent{}, + }, + { + description: "with page < 0 return no events", + givenPage: -1, + givenSize: 1, + expectedEvents: []moira.NotificationEvent{}, + }, + { + description: "with page * size >= len(events)", + givenPage: 1, + givenSize: int64(len(events)), + expectedEvents: []moira.NotificationEvent{}, + }, + } + + for i := range testcases { + Convey(fmt.Sprintf("test case %d: %s", i+1, testcases[i].description), func() { + dataBase.EXPECT().GetNotificationEvents(triggerID, zeroPage, allEventsSize, from, to).Return(events, nil) + + actual, err := GetTriggerEvents(dataBase, triggerID, testcases[i].givenPage, testcases[i].givenSize, from, to, allMetrics, allStates) + So(err, ShouldBeNil) + So(actual, ShouldResemble, &dto.EventsList{ + Page: testcases[i].givenPage, + Size: testcases[i].givenSize, + Total: total, + List: testcases[i].expectedEvents, + }) + }) + } + }) } func toDTOList(eventPtrs []*moira.NotificationEvent) []moira.NotificationEvent { diff --git a/api/handler/trigger_test.go b/api/handler/trigger_test.go index f9d96ec3c..d5d2a8460 100644 --- a/api/handler/trigger_test.go +++ b/api/handler/trigger_test.go @@ -421,7 +421,6 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) - db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() getTrigger(responseWriter, request) @@ -465,7 +464,6 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) - db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() getTrigger(responseWriter, request) @@ -509,7 +507,6 @@ func TestGetTriggerWithTriggerSource(t *testing.T) { db.EXPECT().GetTrigger(triggerId).Return(trigger, nil) db.EXPECT().GetTriggerThrottling(triggerId) db.EXPECT().GetNotificationEvents(triggerId, gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(make([]*moira.NotificationEvent, 0), nil) - db.EXPECT().GetNotificationEventCount(triggerId, gomock.Any()).Return(int64(0)) responseWriter := httptest.NewRecorder() getTrigger(responseWriter, request) diff --git a/interfaces.go b/interfaces.go index b6e4bcd18..94fc776bd 100644 --- a/interfaces.go +++ b/interfaces.go @@ -60,7 +60,7 @@ type Database interface { DeleteTriggerThrottling(triggerID string) error // NotificationEvent storing - GetNotificationEvents(triggerID string, start, size int64, from, to string) ([]*NotificationEvent, error) + GetNotificationEvents(triggerID string, page, size int64, from, to string) ([]*NotificationEvent, error) PushNotificationEvent(event *NotificationEvent, ui bool) error GetNotificationEventCount(triggerID string, from int64) int64 FetchNotificationEvent() (NotificationEvent, error) From 75239e8a74d17c33798cab3a4dc974f828bead95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:05:15 +0500 Subject: [PATCH 13/36] build(deps): bump actions/download-artifact in /.github/workflows (#1079) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.1.7. --- .github/workflows/swagger-publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/swagger-publish.yml b/.github/workflows/swagger-publish.yml index 03e623c36..58cb95d4b 100644 --- a/.github/workflows/swagger-publish.yml +++ b/.github/workflows/swagger-publish.yml @@ -48,7 +48,7 @@ jobs: - uses: actions/checkout@v3 - name: Download spec file artifact - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.7 with: name: specfile path: docs From 9e9068832b757030aec49d21e9bd4cbed616a873 Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Fri, 20 Sep 2024 13:20:47 +0300 Subject: [PATCH 14/36] fix(config): redis sentinel (#1090) --- cmd/config.go | 26 +++++++++++--------- cmd/config_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 12 deletions(-) create mode 100644 cmd/config_test.go diff --git a/cmd/config.go b/cmd/config.go index bb83ea439..0d033cf76 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -60,18 +60,20 @@ type RedisConfig struct { // GetSettings returns redis config parsed from moira config files. func (config *RedisConfig) GetSettings() redis.DatabaseConfig { return redis.DatabaseConfig{ - MasterName: config.MasterName, - Addrs: strings.Split(config.Addrs, ","), - Username: config.Username, - Password: config.Password, - MaxRetries: config.MaxRetries, - MetricsTTL: to.Duration(config.MetricsTTL), - DialTimeout: to.Duration(config.DialTimeout), - ReadTimeout: to.Duration(config.ReadTimeout), - WriteTimeout: to.Duration(config.WriteTimeout), - ReadOnly: config.ReadOnly, - RouteByLatency: config.RouteByLatency, - RouteRandomly: config.RouteRandomly, + MasterName: config.MasterName, + Addrs: strings.Split(config.Addrs, ","), + Username: config.Username, + Password: config.Password, + SentinelUsername: config.SentinelUsername, + SentinelPassword: config.SentinelPassword, + MaxRetries: config.MaxRetries, + MetricsTTL: to.Duration(config.MetricsTTL), + DialTimeout: to.Duration(config.DialTimeout), + ReadTimeout: to.Duration(config.ReadTimeout), + WriteTimeout: to.Duration(config.WriteTimeout), + ReadOnly: config.ReadOnly, + RouteByLatency: config.RouteByLatency, + RouteRandomly: config.RouteRandomly, } } diff --git a/cmd/config_test.go b/cmd/config_test.go new file mode 100644 index 000000000..f41583d00 --- /dev/null +++ b/cmd/config_test.go @@ -0,0 +1,61 @@ +package cmd + +import ( + "testing" + "time" + + "github.com/moira-alert/moira/database/redis" + . "github.com/smartystreets/goconvey/convey" +) + +func TestRedisConfig(t *testing.T) { + Convey("Test RedisConfig.GetSettings", t, func() { + Convey("With empty config", func() { + redisCfg := RedisConfig{} + + expected := redis.DatabaseConfig{ + Addrs: []string{""}, + } + databaseCfg := redisCfg.GetSettings() + So(databaseCfg, ShouldResemble, expected) + }) + + Convey("With filled config", func() { + redisCfg := RedisConfig{ + MasterName: "test-master", + Addrs: "redis1:6379", + SentinelUsername: "sentinel-user", + SentinelPassword: "sentinel-pass", + Username: "user", + Password: "pass", + MetricsTTL: "1m", + DialTimeout: "1m", + ReadTimeout: "1m", + WriteTimeout: "1m", + MaxRetries: 3, + ReadOnly: true, + RouteByLatency: true, + RouteRandomly: true, + } + + expected := redis.DatabaseConfig{ + MasterName: "test-master", + Addrs: []string{"redis1:6379"}, + SentinelUsername: "sentinel-user", + SentinelPassword: "sentinel-pass", + Username: "user", + Password: "pass", + MetricsTTL: time.Minute, + DialTimeout: time.Minute, + ReadTimeout: time.Minute, + WriteTimeout: time.Minute, + MaxRetries: 3, + ReadOnly: true, + RouteByLatency: true, + RouteRandomly: true, + } + databaseCfg := redisCfg.GetSettings() + So(databaseCfg, ShouldResemble, expected) + }) + }) +} From 64557be9bd0e5d18e43a4aa5b7064ee54f20afb1 Mon Sep 17 00:00:00 2001 From: Xenia N Date: Mon, 23 Sep 2024 12:45:35 +0500 Subject: [PATCH 15/36] hotfix(build) upgrade node-version (#1088) * hotfix(build) upgrade actions --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/docker-feature.yml | 4 ++-- .github/workflows/lint.yml | 4 ++-- .github/workflows/publish-packages.yml | 2 +- .github/workflows/swagger-delete.yml | 6 +++--- .github/workflows/swagger-publish.yml | 8 ++++---- .github/workflows/swagger-validate.yml | 6 +++--- .github/workflows/test.yml | 2 +- Makefile | 2 +- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 3352f4110..d9e3e7f90 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -22,14 +22,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: go - run: make build - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docker-feature.yml b/.github/workflows/docker-feature.yml index addc281f5..2f81186cb 100644 --- a/.github/workflows/docker-feature.yml +++ b/.github/workflows/docker-feature.yml @@ -16,7 +16,7 @@ jobs: if: ${{github.event.issue.pull_request != null && startsWith(github.event.comment.body, '/build') && github.event.comment.author_association == 'MEMBER'}} steps: - name: Get PR branch - uses: xt0rted/pull-request-comment-branch@v1 + uses: xt0rted/pull-request-comment-branch@v2 id: comment-branch - uses: actions/github-script@v6 @@ -38,7 +38,7 @@ jobs: - name: Checkout PR branch - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: ${{ fromJSON(steps.get-pr.outputs.result).head.repo.full_name }} ref: ${{ fromJSON(steps.get-pr.outputs.result).head.sha }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7c60ca843..8ce1afaa0 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,14 +7,14 @@ jobs: name: lint runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: go-version-file: go.mod - name: Run linter - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v4 with: version: v1.56.2 diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index f64dfcb8c..c369b72b4 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -10,7 +10,7 @@ jobs: name: Create Release runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Ruby 3.3 uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/swagger-delete.yml b/.github/workflows/swagger-delete.yml index 63f4e5c44..cb816dfa0 100644 --- a/.github/workflows/swagger-delete.yml +++ b/.github/workflows/swagger-delete.yml @@ -13,10 +13,10 @@ jobs: name: Delete api from SwaggerHub runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: '16.16.0' + node-version: '20.17.0' - run: npm i --location=global swaggerhub-cli - run: | VERSION=`echo ${GITHUB_REF_NAME}| sed 's#[^a-zA-Z0-9_\.\-]#_#g'` diff --git a/.github/workflows/swagger-publish.yml b/.github/workflows/swagger-publish.yml index 58cb95d4b..306bca3b9 100644 --- a/.github/workflows/swagger-publish.yml +++ b/.github/workflows/swagger-publish.yml @@ -15,7 +15,7 @@ jobs: working-directory: . steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: @@ -23,9 +23,9 @@ jobs: cache-dependency-path: go.sum - run: make install-swag - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '16.16.0' + node-version: '20.17.0' - run: npm install --location=global @openapitools/openapi-generator-cli - run: make spec - run: make validate-spec @@ -45,7 +45,7 @@ jobs: working-directory: . steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Download spec file artifact uses: actions/download-artifact@v4.1.7 diff --git a/.github/workflows/swagger-validate.yml b/.github/workflows/swagger-validate.yml index e4cb03f0c..ac392ad7f 100644 --- a/.github/workflows/swagger-validate.yml +++ b/.github/workflows/swagger-validate.yml @@ -12,7 +12,7 @@ jobs: working-directory: . steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: @@ -20,9 +20,9 @@ jobs: cache-dependency-path: go.sum - run: make install-swag - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: - node-version: '20.11.0' + node-version: '20.17.0' - run: npm install --location=global @openapitools/openapi-generator-cli - run: make spec - run: make validate-spec diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6bfc03317..7fe8c6f09 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: ports: - 6379:6379 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v4 with: diff --git a/Makefile b/Makefile index a8a46d544..a21c97a18 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ mock: .PHONY: install-swag install-swag: - go install github.com/swaggo/swag/cmd/swag@v1.8.12 + go install github.com/swaggo/swag/cmd/swag@v1.16.3 .PHONY: spec spec: From a4d75f90376848be4dfa5cdd8d9bb09353483215 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:46:16 +0500 Subject: [PATCH 16/36] build(deps): bump github.com/rs/cors from 1.9.0 to 1.11.0 (#1087) Bumps [github.com/rs/cors](https://github.com/rs/cors) from 1.9.0 to 1.11.0. --- go.mod | 3 +-- go.sum | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 91857a6eb..9342c174a 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 @@ -46,7 +46,6 @@ require ( require github.com/prometheus/common v0.37.0 require ( - github.com/golang/mock v1.6.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/mattermost/mattermost/server/public v0.1.1 github.com/mitchellh/mapstructure v1.5.0 diff --git a/go.sum b/go.sum index d95d23b39..db390f19c 100644 --- a/go.sum +++ b/go.sum @@ -680,7 +680,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -1081,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= From c8c05c233e14cd02557e3746b4f52a85ba48651a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:46:30 +0500 Subject: [PATCH 17/36] build(deps): bump golang.org/x/image from 0.13.0 to 0.18.0 (#1086) Bumps [golang.org/x/image](https://github.com/golang/image) from 0.13.0 to 0.18.0. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9342c174a..c92f76c3e 100644 --- a/go.mod +++ b/go.mod @@ -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 db390f19c..089f98d54 100644 --- a/go.sum +++ b/go.sum @@ -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= From d51fffa8ccc3f1cb52ae55e9140da4af6ebfc1ff Mon Sep 17 00:00:00 2001 From: Xenia N Date: Mon, 23 Sep 2024 13:20:49 +0500 Subject: [PATCH 18/36] feat(build): Upgrade codeql (#1095) From 5a546ace8e308132a01ded1906380700c2298d51 Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Thu, 26 Sep 2024 11:40:40 +0700 Subject: [PATCH 19/36] fix: validating prometheus target (#1077) --- api/handler/triggers.go | 46 ++++++++++++-- api/handler/triggers_test.go | 115 +++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 4 deletions(-) diff --git a/api/handler/triggers.go b/api/handler/triggers.go index 090ed6607..8ee2787a0 100644 --- a/api/handler/triggers.go +++ b/api/handler/triggers.go @@ -9,6 +9,8 @@ import ( "strings" "time" + prometheus "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/go-chi/chi" "github.com/go-chi/render" "github.com/moira-alert/moira" @@ -150,10 +152,40 @@ func createTrigger(writer http.ResponseWriter, request *http.Request) { } } +func is4xxCode(statusCode int64) bool { + return statusCode >= 400 && statusCode < 500 +} + +func errorResponseOnPrometheusError(promErr *prometheus.Error) *api.ErrorResponse { + // In github.com/prometheus/client_golang/api/prometheus/v1 Error has field `Type` + // which can be used to understand "the reason" of error. There are some constants in the lib. + if promErr.Type == prometheus.ErrBadData { + return api.ErrorInvalidRequest(fmt.Errorf("invalid prometheus targets: %w", promErr)) + } + + // VictoriaMetrics also supports prometheus api, BUT puts status code into Error.Type. + // So we can't just use constants from prometheus api client lib. + statusCode, err := strconv.ParseInt(string(promErr.Type), 10, 64) + if err != nil { + return api.ErrorInternalServer(promErr) + } + + codes4xxLeadTo500 := map[int64]struct{}{ + http.StatusUnauthorized: {}, + http.StatusForbidden: {}, + } + + if _, leadTo500 := codes4xxLeadTo500[statusCode]; is4xxCode(statusCode) && !leadTo500 { + return api.ErrorInvalidRequest(promErr) + } + + return api.ErrorInternalServer(promErr) +} + func getTriggerFromRequest(request *http.Request) (*dto.Trigger, *api.ErrorResponse) { trigger := &dto.Trigger{} if err := render.Bind(request, trigger); err != nil { - switch err.(type) { // nolint:errorlint + switch typedErr := err.(type) { // nolint:errorlint case local.ErrParseExpr, local.ErrEvalExpr, local.ErrUnknownFunction: return nil, api.ErrorInvalidRequest(fmt.Errorf("invalid graphite targets: %s", err.Error())) case expression.ErrInvalidExpression: @@ -169,6 +201,8 @@ func getTriggerFromRequest(request *http.Request) (*dto.Trigger, *api.ErrorRespo return nil, response case *json.UnmarshalTypeError: return nil, api.ErrorInvalidRequest(fmt.Errorf("invalid payload: %s", err.Error())) + case *prometheus.Error: + return nil, errorResponseOnPrometheusError(typedErr) default: return nil, api.ErrorInternalServer(err) } @@ -208,10 +242,14 @@ func triggerCheck(writer http.ResponseWriter, request *http.Request) { response := dto.TriggerCheckResponse{} if err := render.Bind(request, trigger); err != nil { - switch err.(type) { // nolint:errorlint + switch typedErr := err.(type) { // nolint:errorlint case expression.ErrInvalidExpression, local.ErrParseExpr, local.ErrEvalExpr, local.ErrUnknownFunction: - // TODO write comment, why errors are ignored, it is not obvious. - // In getTriggerFromRequest these types of errors lead to 400. + // TODO: move ErrInvalidExpression to separate case + + // These errors are skipped because if there are error from local source then it will be caught in + // dto.TargetVerification and will be explained in detail. + case *prometheus.Error: + render.Render(writer, request, errorResponseOnPrometheusError(typedErr)) //nolint default: render.Render(writer, request, api.ErrorInvalidRequest(err)) //nolint return diff --git a/api/handler/triggers_test.go b/api/handler/triggers_test.go index 83d0a1bea..f2624d6ef 100644 --- a/api/handler/triggers_test.go +++ b/api/handler/triggers_test.go @@ -12,6 +12,11 @@ import ( "testing" "time" + logging "github.com/moira-alert/moira/logging/zerolog_adapter" + "github.com/moira-alert/moira/metric_source/remote" + + prometheus "github.com/prometheus/client_golang/api/prometheus/v1" + "github.com/moira-alert/moira" "github.com/moira-alert/moira/api" dataBase "github.com/moira-alert/moira/database" @@ -139,6 +144,116 @@ func TestGetTriggerFromRequest(t *testing.T) { So(err, ShouldHaveSameTypeAs, api.ErrorInvalidRequest(fmt.Errorf(""))) }) }) + + Convey("With incorrect targets errors", t, func() { + graphiteLocalSrc := mock_metric_source.NewMockMetricSource(mockCtrl) + graphiteRemoteSrc := mock_metric_source.NewMockMetricSource(mockCtrl) + prometheusSrc := mock_metric_source.NewMockMetricSource(mockCtrl) + allSourceProvider := metricSource.CreateTestMetricSourceProvider(graphiteLocalSrc, graphiteRemoteSrc, prometheusSrc) + + graphiteLocalSrc.EXPECT().GetMetricsTTLSeconds().Return(int64(3600)).AnyTimes() + graphiteRemoteSrc.EXPECT().GetMetricsTTLSeconds().Return(int64(3600)).AnyTimes() + prometheusSrc.EXPECT().GetMetricsTTLSeconds().Return(int64(3600)).AnyTimes() + + triggerWarnValue := 0.0 + triggerErrorValue := 1.0 + ttlState := moira.TTLState("NODATA") + triggerDTO := dto.Trigger{ + TriggerModel: dto.TriggerModel{ + ID: "test_id", + Name: "Test trigger", + Desc: new(string), + Targets: []string{"foo.bar"}, + WarnValue: &triggerWarnValue, + ErrorValue: &triggerErrorValue, + TriggerType: "rising", + Tags: []string{"Normal", "DevOps", "DevOpsGraphite-duty"}, + TTLState: &ttlState, + TTL: moira.DefaultTTL, + Schedule: &moira.ScheduleData{}, + Expression: "", + Patterns: []string{}, + ClusterId: moira.DefaultCluster, + MuteNewMetrics: false, + AloneMetrics: map[string]bool{}, + CreatedAt: &time.Time{}, + UpdatedAt: &time.Time{}, + CreatedBy: "", + UpdatedBy: "anonymous", + }, + } + + Convey("for graphite remote", func() { + triggerDTO.TriggerSource = moira.GraphiteRemote + body, _ := json.Marshal(triggerDTO) + + request := httptest.NewRequest(http.MethodPut, "/trigger", bytes.NewReader(body)) + request.Header.Add("content-type", "application/json") + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", allSourceProvider)) + + testLogger, _ := logging.GetLogger("Test") + + request = middleware.WithLogEntry(request, middleware.NewLogEntry(testLogger, request)) + + var returnedErr error = remote.ErrRemoteTriggerResponse{ + InternalError: fmt.Errorf(""), + } + + graphiteRemoteSrc.EXPECT().Fetch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, returnedErr) + + _, errRsp := getTriggerFromRequest(request) + So(errRsp, ShouldResemble, api.ErrorRemoteServerUnavailable(returnedErr)) + }) + + Convey("for prometheus remote", func() { + triggerDTO.TriggerSource = moira.PrometheusRemote + body, _ := json.Marshal(triggerDTO) + + Convey("with error type = bad_data got bad request", func() { + request := httptest.NewRequest(http.MethodPut, "/trigger", bytes.NewReader(body)) + request.Header.Add("content-type", "application/json") + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", allSourceProvider)) + + var returnedErr error = &prometheus.Error{ + Type: prometheus.ErrBadData, + } + + prometheusSrc.EXPECT().Fetch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, returnedErr) + + _, errRsp := getTriggerFromRequest(request) + So(errRsp, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("invalid prometheus targets: %w", returnedErr))) + }) + + Convey("with other types internal server error is returned", func() { + otherTypes := []prometheus.ErrorType{ + prometheus.ErrBadResponse, + prometheus.ErrCanceled, + prometheus.ErrClient, + prometheus.ErrExec, + prometheus.ErrTimeout, + prometheus.ErrServer, + } + + for _, errType := range otherTypes { + request := httptest.NewRequest(http.MethodPut, "/trigger", bytes.NewReader(body)) + request.Header.Add("content-type", "application/json") + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", allSourceProvider)) + + var returnedErr error = &prometheus.Error{ + Type: errType, + } + + prometheusSrc.EXPECT().Fetch(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, returnedErr) + + _, errRsp := getTriggerFromRequest(request) + So(errRsp, ShouldResemble, api.ErrorInternalServer(returnedErr)) + } + }) + }) + }) } func TestGetMetricTTLByTrigger(t *testing.T) { From b8c57f3b9cc8798841d1801b616924bb83958b7c Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Thu, 26 Sep 2024 18:31:26 +0700 Subject: [PATCH 20/36] fix: up node version and upload artifact version (#1098) --- .github/workflows/swagger-publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/swagger-publish.yml b/.github/workflows/swagger-publish.yml index 306bca3b9..88f1ecee5 100644 --- a/.github/workflows/swagger-publish.yml +++ b/.github/workflows/swagger-publish.yml @@ -31,7 +31,7 @@ jobs: - run: make validate-spec - name: Save build artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: specfile path: docs/swagger.yaml @@ -53,7 +53,7 @@ jobs: name: specfile path: docs - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 - run: npm i --location=global swaggerhub-cli - run: | VERSION=`echo ${GITHUB_REF_NAME}| sed 's#[^a-zA-Z0-9_\.\-]#_#g'` From e6ab69c25c72ce0784d58b1956f9bfbbf0027946 Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Wed, 2 Oct 2024 14:49:35 +0700 Subject: [PATCH 21/36] refactor: hide credentials in tg sender (#1107) --- senders/telegram/init.go | 15 +++++++++++++-- senders/telegram/init_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/senders/telegram/init.go b/senders/telegram/init.go index 4659c1f69..aabd2a4fd 100644 --- a/senders/telegram/init.go +++ b/senders/telegram/init.go @@ -80,8 +80,9 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca sender.logger = logger sender.bot, err = telebot.NewBot(telebot.Settings{ - Token: cfg.APIToken, - Poller: &telebot.LongPoller{Timeout: pollerTimeout}, + Token: cfg.APIToken, + Poller: &telebot.LongPoller{Timeout: pollerTimeout}, + OnError: sender.customOnErrorFunc, }) if err != nil { return sender.removeTokenFromError(err) @@ -123,3 +124,13 @@ func (sender *Sender) runTelebot(contactType string) { func telegramLockKey(contactType string) string { return telegramLockPrefix + contactType } + +const errorInsideTelebotMsg = "Error inside telebot" + +func (sender *Sender) customOnErrorFunc(err error, _ telebot.Context) { + err = sender.removeTokenFromError(err) + + sender.logger.Warning(). + Error(err). + Msg(errorInsideTelebotMsg) +} diff --git a/senders/telegram/init_test.go b/senders/telegram/init_test.go index f55cdfcd0..9179cb920 100644 --- a/senders/telegram/init_test.go +++ b/senders/telegram/init_test.go @@ -1,10 +1,15 @@ package telegram import ( + "errors" "fmt" + "strings" "testing" "time" + mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" + "go.uber.org/mock/gomock" + logging "github.com/moira-alert/moira/logging/zerolog_adapter" . "github.com/smartystreets/goconvey/convey" ) @@ -31,3 +36,26 @@ func TestInit(t *testing.T) { }) }) } + +func Test_customOnErrorFunc(t *testing.T) { + Convey("test customOnErrorFunc hides credential and logs", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + logger := mock_moira_alert.NewMockLogger(mockCtrl) + eventsBuilder := mock_moira_alert.NewMockEventBuilder(mockCtrl) + + sender := Sender{ + logger: logger, + apiToken: "1111111111:SecretTokenabc_987654321hellokonturmoira", + } + + err := fmt.Errorf("https://some.api.of.telegram/bot%s/update failed to update", sender.apiToken) + + logger.EXPECT().Warning().Return(eventsBuilder).AnyTimes() + eventsBuilder.EXPECT().Error(errors.New(strings.ReplaceAll(err.Error(), sender.apiToken, hidden))).Return(eventsBuilder) + eventsBuilder.EXPECT().Msg(errorInsideTelebotMsg) + + sender.customOnErrorFunc(err, nil) + }) +} From b4d3745d74032dc59240e054c634da71eecfa9b3 Mon Sep 17 00:00:00 2001 From: Xenia N Date: Wed, 2 Oct 2024 18:53:41 +0500 Subject: [PATCH 22/36] hotfix(codeql): Remove cronjob trigger (#1108) --- .github/workflows/codeql-analysis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d9e3e7f90..c8cfb51ea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -5,8 +5,6 @@ on: branches: [ master ] pull_request: branches: [ master ] - schedule: - - cron: '33 20 * * 2' jobs: analyze: From 1fd8c2fa8084aacc242671d7d0d4fc55b722b13a Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:53:44 +0300 Subject: [PATCH 23/36] feat(api): contact template validation (#1100) --- api/controller/contact.go | 30 +++++ api/controller/contact_test.go | 171 +++++++++++++++++++++++----- api/handler/contact.go | 23 +++- api/handler/contact_test.go | 199 ++++++++++++++++++++++++++------- api/handler/handler.go | 9 +- api/handler/team_contact.go | 10 +- api/middleware/context.go | 10 ++ api/middleware/middleware.go | 6 + cmd/api/config.go | 17 +++ cmd/api/config_test.go | 35 ++++++ cmd/api/main.go | 5 + 11 files changed, 442 insertions(+), 73 deletions(-) diff --git a/api/controller/contact.go b/api/controller/contact.go index 4874624be..e1a7573de 100644 --- a/api/controller/contact.go +++ b/api/controller/contact.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "regexp" "time" "github.com/go-graphite/carbonapi/date" @@ -53,6 +54,7 @@ func GetContactById(database moira.Database, contactID string) (*dto.Contact, *a func CreateContact( dataBase moira.Database, auth *api.Authorization, + contactsTemplate []api.WebContact, contact *dto.Contact, userLogin, teamID string, @@ -74,6 +76,7 @@ func CreateContact( Type: contact.Type, Value: contact.Value, } + if contactData.ID == "" { uuid4, err := uuid.NewV4() if err != nil { @@ -90,12 +93,18 @@ func CreateContact( } } + if err := validateContact(contactsTemplate, contactData); err != nil { + return api.ErrorInvalidRequest(err) + } + if err := dataBase.SaveContact(&contactData); err != nil { return api.ErrorInternalServer(err) } + contact.User = contactData.User contact.ID = contactData.ID contact.TeamID = contactData.Team + return nil } @@ -103,6 +112,7 @@ func CreateContact( func UpdateContact( dataBase moira.Database, auth *api.Authorization, + contactsTemplate []api.WebContact, contactDTO dto.Contact, contactData moira.ContactData, ) (dto.Contact, *api.ErrorResponse) { @@ -119,6 +129,10 @@ func UpdateContact( contactData.Team = contactDTO.TeamID } + if err := validateContact(contactsTemplate, contactData); err != nil { + return contactDTO, api.ErrorInvalidRequest(err) + } + if err := dataBase.SaveContact(&contactData); err != nil { return contactDTO, api.ErrorInternalServer(err) } @@ -265,3 +279,19 @@ func isAllowedToUseContactType(auth *api.Authorization, userLogin string, contac return isAllowedContactType || isAdmin || !isAuthEnabled } + +func validateContact(contactsTemplate []api.WebContact, contact moira.ContactData) error { + var validationPattern string + for _, contactTemplate := range contactsTemplate { + if contactTemplate.ContactType == contact.Type { + validationPattern = contactTemplate.ValidationRegex + break + } + } + + if matched, err := regexp.MatchString(validationPattern, contact.Value); !matched || err != nil { + return fmt.Errorf("contact value doesn't match regex: '%s'", validationPattern) + } + + return nil +} diff --git a/api/controller/contact_test.go b/api/controller/contact_test.go index 8c398b528..c0eb92cc8 100644 --- a/api/controller/contact_test.go +++ b/api/controller/contact_test.go @@ -127,6 +127,13 @@ func TestCreateContact(t *testing.T) { }, } + contactsTemplate := []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@mail.com", + }, + } + Convey("Create for user", t, func() { Convey("Success", func() { contact := &dto.Contact{ @@ -134,7 +141,7 @@ func TestCreateContact(t *testing.T) { Type: contactType, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -153,7 +160,7 @@ func TestCreateContact(t *testing.T) { } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, &contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, &contact, userLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) So(contact.ID, ShouldResemble, contact.ID) @@ -166,7 +173,7 @@ func TestCreateContact(t *testing.T) { Type: contactType, } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("contact with this ID already exists"))) }) @@ -178,10 +185,34 @@ func TestCreateContact(t *testing.T) { } err := fmt.Errorf("oooops! Can not write contact") dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, err) - expected := CreateContact(dataBase, auth, contact, userLogin, "") + expected := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(expected, ShouldResemble, api.ErrorInternalServer(err)) }) + contactsTemplate = []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@yandex.ru", + }, + } + + Convey("Error invalid contact value", func() { + contact := &dto.Contact{ + Value: contactValue, + Type: contactType, + } + expectedErr := api.ErrorInvalidRequest(fmt.Errorf("contact value doesn't match regex: '%s'", "@yandex.ru")) + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") + So(err, ShouldResemble, expectedErr) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@mail.com", + }, + } + Convey("Error create now allowed contact", func() { contact := &dto.Contact{ ID: uuid.Must(uuid.NewV4()).String(), @@ -189,7 +220,7 @@ func TestCreateContact(t *testing.T) { Type: notAllowedContactType, } expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldResemble, expectedErr) }) @@ -215,7 +246,7 @@ func TestCreateContact(t *testing.T) { dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldBeNil) }) @@ -226,7 +257,7 @@ func TestCreateContact(t *testing.T) { } err := fmt.Errorf("oooops! Can not write contact") dataBase.EXPECT().SaveContact(gomock.Any()).Return(err) - expected := CreateContact(dataBase, auth, contact, userLogin, "") + expected := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(expected, ShouldResemble, &api.ErrorResponse{ ErrorText: err.Error(), HTTPStatusCode: http.StatusInternalServerError, @@ -243,7 +274,7 @@ func TestCreateContact(t *testing.T) { Type: contactType, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(err, ShouldBeNil) So(contact.TeamID, ShouldResemble, teamID) }) @@ -262,7 +293,7 @@ func TestCreateContact(t *testing.T) { } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, &contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, &contact, "", teamID) So(err, ShouldBeNil) So(contact.TeamID, ShouldResemble, teamID) So(contact.ID, ShouldResemble, contact.ID) @@ -284,7 +315,7 @@ func TestCreateContact(t *testing.T) { } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, &contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, &contact, "", teamID) So(err, ShouldBeNil) So(contact.TeamID, ShouldResemble, teamID) So(contact.Name, ShouldResemble, expectedContact.Name) @@ -297,7 +328,7 @@ func TestCreateContact(t *testing.T) { Type: contactType, } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, nil) - err := CreateContact(dataBase, auth, contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("contact with this ID already exists"))) }) @@ -309,7 +340,7 @@ func TestCreateContact(t *testing.T) { } err := fmt.Errorf("oooops! Can not write contact") dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, err) - expected := CreateContact(dataBase, auth, contact, "", teamID) + expected := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(expected, ShouldResemble, api.ErrorInternalServer(err)) }) @@ -320,7 +351,7 @@ func TestCreateContact(t *testing.T) { Type: notAllowedContactType, } expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) - err := CreateContact(dataBase, auth, contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(err, ShouldResemble, expectedErr) }) @@ -346,7 +377,7 @@ func TestCreateContact(t *testing.T) { dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(err, ShouldBeNil) }) @@ -357,7 +388,7 @@ func TestCreateContact(t *testing.T) { } err := fmt.Errorf("oooops! Can not write contact") dataBase.EXPECT().SaveContact(gomock.Any()).Return(err) - expected := CreateContact(dataBase, auth, contact, "", teamID) + expected := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(expected, ShouldResemble, &api.ErrorResponse{ ErrorText: err.Error(), HTTPStatusCode: http.StatusInternalServerError, @@ -389,6 +420,8 @@ func TestAdminsCreatesContact(t *testing.T) { }, } + contactsTemplate := []api.WebContact{} + Convey("Create for user", t, func() { Convey("The same user", func() { contact := &dto.Contact{ @@ -397,7 +430,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: userLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -409,7 +442,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: adminLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, adminLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, adminLogin) }) @@ -421,7 +454,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: adminLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -433,7 +466,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: userLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, adminLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -445,7 +478,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: userLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, adminLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -472,6 +505,13 @@ func TestUpdateContact(t *testing.T) { }, } + contactsTemplate := []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@mail.com", + }, + } + Convey("User update", t, func() { Convey("Success", func() { contactDTO := dto.Contact{ @@ -488,7 +528,7 @@ func TestUpdateContact(t *testing.T) { User: userLogin, } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldBeNil) So(expectedContact.User, ShouldResemble, userLogin) So(expectedContact.ID, ShouldResemble, contactID) @@ -512,7 +552,7 @@ func TestUpdateContact(t *testing.T) { User: newUser, } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldBeNil) So(expectedContact.User, ShouldResemble, newUser) So(expectedContact.ID, ShouldResemble, contactID) @@ -526,13 +566,41 @@ func TestUpdateContact(t *testing.T) { } expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) contactID := uuid.Must(uuid.NewV4()).String() - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldResemble, expectedErr) So(expectedContact.User, ShouldResemble, contactDTO.User) So(expectedContact.ID, ShouldResemble, contactDTO.ID) So(expectedContact.Name, ShouldResemble, contactDTO.Name) }) + contactsTemplate = []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@yandex.ru", + }, + } + + Convey("Error invalid contact value", func() { + contactDTO := dto.Contact{ + Value: contactValue, + Type: contactType, + } + expectedErr := api.ErrorInvalidRequest(fmt.Errorf("contact value doesn't match regex: '%s'", "@yandex.ru")) + contactID := uuid.Must(uuid.NewV4()).String() + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + So(err, ShouldResemble, expectedErr) + So(expectedContact.User, ShouldResemble, contactDTO.User) + So(expectedContact.ID, ShouldResemble, contactDTO.ID) + So(expectedContact.Name, ShouldResemble, contactDTO.Name) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@mail.com", + }, + } + Convey("Successfully update not allowed contact with disabled auth", func() { auth.Enabled = false defer func() { @@ -554,7 +622,7 @@ func TestUpdateContact(t *testing.T) { } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldBeNil) So(expectedContact.User, ShouldResemble, userLogin) So(expectedContact.ID, ShouldResemble, contactID) @@ -575,7 +643,7 @@ func TestUpdateContact(t *testing.T) { } err := fmt.Errorf("oooops") dataBase.EXPECT().SaveContact(&contact).Return(err) - expectedContact, actual := UpdateContact(dataBase, auth, contactDTO, contact) + expectedContact, actual := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, contact) So(actual, ShouldResemble, api.ErrorInternalServer(err)) So(expectedContact.User, ShouldResemble, contactDTO.User) So(expectedContact.ID, ShouldResemble, contactDTO.ID) @@ -596,7 +664,7 @@ func TestUpdateContact(t *testing.T) { Team: teamID, } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) So(err, ShouldBeNil) So(expectedContact.TeamID, ShouldResemble, teamID) So(expectedContact.ID, ShouldResemble, contactID) @@ -617,7 +685,7 @@ func TestUpdateContact(t *testing.T) { Team: newTeam, } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) So(err, ShouldBeNil) So(expectedContact.TeamID, ShouldResemble, newTeam) So(expectedContact.ID, ShouldResemble, contactID) @@ -637,7 +705,7 @@ func TestUpdateContact(t *testing.T) { } err := fmt.Errorf("oooops") dataBase.EXPECT().SaveContact(&contact).Return(err) - expectedContact, actual := UpdateContact(dataBase, auth, contactDTO, contact) + expectedContact, actual := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, contact) So(actual, ShouldResemble, api.ErrorInternalServer(err)) So(expectedContact.TeamID, ShouldResemble, contactDTO.TeamID) So(expectedContact.ID, ShouldResemble, contactDTO.ID) @@ -965,3 +1033,50 @@ func Test_isContactExists(t *testing.T) { }) }) } + +func TestValidateContact(t *testing.T) { + const ( + contactType = "phone" + contactValue = "+79998887766" + ) + + Convey("Test validateContact", t, func() { + contact := moira.ContactData{ + Type: contactType, + Value: contactValue, + } + + Convey("With empty contactsTemplate", func() { + contactsTemplate := []api.WebContact{} + + err := validateContact(contactsTemplate, contact) + So(err, ShouldBeNil) + }) + + Convey("With not matched regex pattern", func() { + contactsTemplate := []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "^9\\d{9}$", + }, + } + + notMatchedErr := fmt.Errorf("contact value doesn't match regex: '%s'", "^9\\d{9}$") + + err := validateContact(contactsTemplate, contact) + So(err, ShouldResemble, notMatchedErr) + }) + + Convey("With matched regex pattern", func() { + contactsTemplate := []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: `^\+79\d{9}$`, + }, + } + + err := validateContact(contactsTemplate, contact) + So(err, ShouldBeNil) + }) + }) +} diff --git a/api/handler/contact.go b/api/handler/contact.go index ba31d1f2d..9d5647464 100644 --- a/api/handler/contact.go +++ b/api/handler/contact.go @@ -101,8 +101,16 @@ func createNewContact(writer http.ResponseWriter, request *http.Request) { userLogin := middleware.GetLogin(request) auth := middleware.GetAuth(request) - - if err := controller.CreateContact(database, auth, contact, userLogin, contact.TeamID); err != nil { + contactsTemplate := middleware.GetContactsTemplate(request) + + if err := controller.CreateContact( + database, + auth, + contactsTemplate, + contact, + userLogin, + contact.TeamID, + ); err != nil { render.Render(writer, request, err) //nolint return } @@ -155,8 +163,15 @@ func updateContact(writer http.ResponseWriter, request *http.Request) { contactData := request.Context().Value(contactKey).(moira.ContactData) auth := middleware.GetAuth(request) - - contactDTO, err := controller.UpdateContact(database, auth, contactDTO, contactData) + contactsTemplate := middleware.GetContactsTemplate(request) + + contactDTO, err := controller.UpdateContact( + database, + auth, + contactsTemplate, + contactDTO, + contactData, + ) if err != nil { render.Render(writer, request, err) //nolint return diff --git a/api/handler/contact_test.go b/api/handler/contact_test.go index cc2ebc028..4b64b4fde 100644 --- a/api/handler/contact_test.go +++ b/api/handler/contact_test.go @@ -20,13 +20,14 @@ import ( ) const ( - ContactIDKey = "contactID" - ContactKey = "contact" - AuthKey = "auth" - LoginKey = "login" - defaultContact = "testContact" - defaultLogin = "testLogin" - defaultTeamID = "testTeamID" + testContactIDKey = "contactID" + testContactKey = "contact" + testAuthKey = "auth" + testLoginKey = "login" + testContactsTemplateKey = "contactsTemplate" + defaultContact = "testContact" + defaultLogin = "testLogin" + defaultTeamID = "testTeamID" ) func TestGetAllContacts(t *testing.T) { @@ -136,8 +137,8 @@ func TestGetContactById(t *testing.T) { } testRequest := httptest.NewRequest(http.MethodGet, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ID: contactID})) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ID: contactID})) getContactById(responseWriter, testRequest) @@ -165,8 +166,8 @@ func TestGetContactById(t *testing.T) { } testRequest := httptest.NewRequest(http.MethodGet, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ID: contactID})) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ID: contactID})) getContactById(responseWriter, testRequest) @@ -203,6 +204,13 @@ func TestCreateNewContact(t *testing.T) { }, } + contactsTemplate := []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + newContactDto := &dto.Contact{ ID: defaultContact, Name: "Mail Alerts", @@ -228,8 +236,9 @@ func TestCreateNewContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -260,8 +269,9 @@ func TestCreateNewContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -301,8 +311,9 @@ func TestCreateNewContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -332,8 +343,9 @@ func TestCreateNewContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -351,6 +363,52 @@ func TestCreateNewContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) }) + Convey("Invalid request when trying to create a new contact with invalid value", func() { + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "contact value doesn't match regex: '@yandex.ru'", + } + jsonContact, err := json.Marshal(newContactDto) + So(err, ShouldBeNil) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@yandex.ru", + }, + } + + mockDb.EXPECT().GetContact(newContactDto.ID).Return(moira.ContactData{}, db.ErrNil).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest.Header.Add("content-type", "application/json") + + createNewContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &api.ErrorResponse{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + Convey("Trying to create a contact when both userLogin and teamID specified", func() { newContactDto.TeamID = defaultTeamID defer func() { @@ -365,8 +423,9 @@ func TestCreateNewContact(t *testing.T) { So(err, ShouldBeNil) testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -405,6 +464,13 @@ func TestUpdateContact(t *testing.T) { TeamID: "", } + contactsTemplate := []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + Convey("Successful contact updated", func() { jsonContact, err := json.Marshal(updatedContactDto) So(err, ShouldBeNil) @@ -419,15 +485,15 @@ func TestUpdateContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact)) - - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Name: updatedContactDto.Name, Type: updatedContactDto.Type, Value: updatedContactDto.Value, User: updatedContactDto.User, })) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{ AllowedContactTypes: map[string]struct{}{ updatedContactDto.Type: {}, }, @@ -460,15 +526,15 @@ func TestUpdateContact(t *testing.T) { So(err, ShouldBeNil) testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact)) - - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Name: updatedContactDto.Name, Type: updatedContactDto.Type, Value: updatedContactDto.Value, User: updatedContactDto.User, })) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{ AllowedContactTypes: map[string]struct{}{ updatedContactDto.Type: {}, }, @@ -495,6 +561,60 @@ func TestUpdateContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusBadRequest) }) + Convey("Invalid request when trying to update contact with invalid value", func() { + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "contact value doesn't match regex: '@yandex.ru'", + } + jsonContact, err := json.Marshal(updatedContactDto) + So(err, ShouldBeNil) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@yandex.ru", + }, + } + + testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ + ID: contactID, + Name: updatedContactDto.Name, + Type: updatedContactDto.Type, + Value: updatedContactDto.Value, + User: updatedContactDto.User, + })) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{ + AllowedContactTypes: map[string]struct{}{ + updatedContactDto.Type: {}, + }, + })) + + testRequest.Header.Add("content-type", "application/json") + + updateContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &api.ErrorResponse{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + Convey("Internal error when trying to update contact", func() { expected := &api.ErrorResponse{ StatusText: "Internal Server Error", @@ -513,13 +633,14 @@ func TestUpdateContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: updatedContactDto.Type, Value: updatedContactDto.Value, User: updatedContactDto.User, })) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{ AllowedContactTypes: map[string]struct{}{ updatedContactDto.Type: {}, }, @@ -561,7 +682,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -587,7 +708,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -614,7 +735,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -642,7 +763,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -679,7 +800,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -718,7 +839,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -762,7 +883,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -797,7 +918,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -837,7 +958,7 @@ func TestSendTestContactNotification(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPost, "/contact/"+contactID+"/test", nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID)) sendTestContactNotification(responseWriter, testRequest) @@ -861,7 +982,7 @@ func TestSendTestContactNotification(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPost, "/contact/"+contactID+"/test", nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID)) sendTestContactNotification(responseWriter, testRequest) diff --git a/api/handler/handler.go b/api/handler/handler.go index 27647a8c3..1ac3f953d 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -38,6 +38,11 @@ func NewHandler( ) http.Handler { database = db searchIndex = index + var contactsTemplate []api.WebContact + if webConfig != nil { + contactsTemplate = webConfig.Contacts + } + router := chi.NewRouter() router.Use(render.SetContentType(render.ContentTypeJSON)) router.Use(moiramiddle.UserContext) @@ -111,7 +116,9 @@ func NewHandler( router.Route("/subscription", subscription) router.Route("/notification", notification) router.Route("/teams", teams) - router.Route("/contact", func(router chi.Router) { + router.With(moiramiddle.ContactsTemplateContext( + contactsTemplate, + )).Route("/contact", func(router chi.Router) { contact(router) contactEvents(router) }) diff --git a/api/handler/team_contact.go b/api/handler/team_contact.go index 8d515afba..58f19aaaa 100644 --- a/api/handler/team_contact.go +++ b/api/handler/team_contact.go @@ -40,8 +40,16 @@ func createNewTeamContact(writer http.ResponseWriter, request *http.Request) { teamID := middleware.GetTeamID(request) auth := middleware.GetAuth(request) + contactsTemplate := middleware.GetContactsTemplate(request) - if err := controller.CreateContact(database, auth, contact, "", teamID); err != nil { + if err := controller.CreateContact( + database, + auth, + contactsTemplate, + contact, + "", + teamID, + ); err != nil { render.Render(writer, request, err) //nolint:errcheck return } diff --git a/api/middleware/context.go b/api/middleware/context.go index 0d9e26f31..a53a3f1b8 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -36,6 +36,16 @@ func SearchIndexContext(searcher moira.Searcher) func(next http.Handler) http.Ha } } +// ContactsTemplateContext sets to requests context contacts template. +func ContactsTemplateContext(contactsTemplate []api.WebContact) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + ctx := context.WithValue(request.Context(), contactsTemplateKey, contactsTemplate) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} + // UserContext get x-webauth-user header and sets it in request context, if header is empty sets empty string. func UserContext(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index 80d65066d..d3df8e40d 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -20,6 +20,7 @@ func (key ContextKey) String() string { var ( databaseKey ContextKey = "database" searcherKey ContextKey = "searcher" + contactsTemplateKey ContextKey = "contactsTemplate" triggerIDKey ContextKey = "triggerID" clustersMetricTTLKey ContextKey = "clustersMetricTTL" populateKey ContextKey = "populated" @@ -49,6 +50,11 @@ func GetDatabase(request *http.Request) moira.Database { return request.Context().Value(databaseKey).(moira.Database) } +// GetContactsTemplate gets contacts template from request context. +func GetContactsTemplate(request *http.Request) []api.WebContact { + return request.Context().Value(contactsTemplateKey).([]api.WebContact) +} + // GetLogin gets user login string from request context, which was sets in UserContext middleware. func GetLogin(request *http.Request) string { if request.Context() != nil && request.Context().Value(loginKey) != nil { diff --git a/cmd/api/config.go b/cmd/api/config.go index 4647bb3f1..75fa2ceea 100644 --- a/cmd/api/config.go +++ b/cmd/api/config.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "regexp" "time" "github.com/moira-alert/moira" @@ -136,6 +138,21 @@ func (auth *authorization) toApiConfig(webConfig *webConfig) api.Authorization { } } +func (config *webConfig) validate() error { + for _, contactTemplate := range config.ContactsTemplate { + validationRegex := contactTemplate.ValidationRegex + if validationRegex == "" { + continue + } + + if _, err := regexp.Compile(validationRegex); err != nil { + return fmt.Errorf("contact template regex error '%s': %w", validationRegex, err) + } + } + + return nil +} + func (config *webConfig) getSettings(isRemoteEnabled bool, remotes cmd.RemotesConfig) *api.WebConfig { webContacts := make([]api.WebContact, 0, len(config.ContactsTemplate)) diff --git a/cmd/api/config_test.go b/cmd/api/config_test.go index 4777e70ea..c55a67d68 100644 --- a/cmd/api/config_test.go +++ b/cmd/api/config_test.go @@ -248,3 +248,38 @@ func Test_webConfig_getSettings(t *testing.T) { }) }) } + +func Test_webConfig_validate(t *testing.T) { + Convey("With empty web config", t, func() { + config := webConfig{} + + err := config.validate() + So(err, ShouldBeNil) + }) + + Convey("With invalid contact template pattern", t, func() { + config := webConfig{ + ContactsTemplate: []webContact{ + { + ValidationRegex: "**", + }, + }, + } + + err := config.validate() + So(err, ShouldNotBeNil) + }) + + Convey("With valid contact template pattern", t, func() { + config := webConfig{ + ContactsTemplate: []webContact{ + { + ValidationRegex: ".*", + }, + }, + } + + err := config.validate() + So(err, ShouldBeNil) + }) +} diff --git a/cmd/api/main.go b/cmd/api/main.go index 9f87d2fd9..6a9f78e7d 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -58,6 +58,11 @@ func main() { os.Exit(1) } + if err = applicationConfig.Web.validate(); err != nil { + fmt.Fprintf(os.Stderr, "Can not configure web config: %s\n", err.Error()) + os.Exit(1) + } + apiConfig := applicationConfig.API.getSettings( applicationConfig.ClustersMetricTTL(), applicationConfig.Web.getFeatureFlags(), From 8255890929b8d50c36f391904d3615a17eb9ee61 Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:24:24 +0300 Subject: [PATCH 24/36] feat(notifier): add validation notifier config (#1099) --- go.mod | 4 + go.sum | 5 ++ helpers.go | 8 ++ helpers_test.go | 56 +++++++++++-- senders/discord/init.go | 7 +- senders/discord/init_test.go | 18 +++-- senders/mail/mail.go | 18 +++-- senders/mail/mail_test.go | 91 ++++++++++++++++++---- senders/mattermost/sender.go | 19 ++--- senders/mattermost/sender_internal_test.go | 2 +- senders/mattermost/sender_manual_test.go | 2 +- senders/mattermost/sender_test.go | 82 ++++++++++--------- senders/msteams/msteams.go | 27 ++++--- senders/msteams/msteams_test.go | 14 +++- senders/opsgenie/init.go | 11 +-- senders/opsgenie/init_test.go | 16 ++-- senders/opsgenie/send_test.go | 1 - senders/pagerduty/init.go | 1 + senders/pagerduty/init_test.go | 2 + senders/pushover/pushover.go | 10 ++- senders/pushover/pushover_test.go | 27 +++++-- senders/script/script.go | 6 +- senders/script/script_test.go | 6 +- senders/slack/slack.go | 10 ++- senders/slack/slack_test.go | 13 ++-- senders/telegram/init.go | 7 +- senders/telegram/init_test.go | 13 ++-- senders/twilio/twilio.go | 22 ++---- senders/twilio/twilio_test.go | 21 ++++- senders/victorops/init.go | 9 ++- senders/victorops/init_test.go | 10 ++- senders/webhook/webhook.go | 12 +-- senders/webhook/webhook_test.go | 8 +- 33 files changed, 381 insertions(+), 177 deletions(-) diff --git a/go.mod b/go.mod index c92f76c3e..a2d3a9c52 100644 --- a/go.mod +++ b/go.mod @@ -46,6 +46,7 @@ require ( require github.com/prometheus/common v0.37.0 require ( + github.com/go-playground/validator/v10 v10.4.1 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/mattermost/mattermost/server/public v0.1.1 github.com/mitchellh/mapstructure v1.5.0 @@ -183,12 +184,15 @@ require ( github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/spec v0.20.9 // indirect github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/leodido/go-urn v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect diff --git a/go.sum b/go.sum index 089f98d54..0eacd324a 100644 --- a/go.sum +++ b/go.sum @@ -637,9 +637,13 @@ github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= @@ -898,6 +902,7 @@ github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/helpers.go b/helpers.go index c0432ca21..eab4e915e 100644 --- a/helpers.go +++ b/helpers.go @@ -5,6 +5,8 @@ import ( "math" "strings" "time" + + "github.com/go-playground/validator/v10" ) // BytesScanner allows to scan for subslices separated by separator. @@ -250,3 +252,9 @@ func MergeToSorted[T Comparable](arr1, arr2 []T) ([]T, error) { return merged, nil } + +// ValidateStruct is a default generic function that uses a validator to validate structure fields. +func ValidateStruct(s any) error { + validator := validator.New() + return validator.Struct(s) +} diff --git a/helpers_test.go b/helpers_test.go index d9c3ca4ee..4b764d6a6 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -290,25 +290,25 @@ func TestMergeToSorted(t *testing.T) { }) Convey("Test with one nil array", func() { - merged, err := MergeToSorted[myInt](nil, []myInt{1, 2, 3}) + merged, err := MergeToSorted(nil, []myInt{1, 2, 3}) So(err, ShouldBeNil) So(merged, ShouldResemble, []myInt{1, 2, 3}) }) Convey("Test with two arrays", func() { - merged, err := MergeToSorted[myInt]([]myInt{4, 5}, []myInt{1, 2, 3}) + merged, err := MergeToSorted([]myInt{4, 5}, []myInt{1, 2, 3}) So(err, ShouldBeNil) So(merged, ShouldResemble, []myInt{1, 2, 3, 4, 5}) }) Convey("Test with empty array", func() { - merged, err := MergeToSorted[myInt]([]myInt{-4, 5}, []myInt{}) + merged, err := MergeToSorted([]myInt{-4, 5}, []myInt{}) So(err, ShouldBeNil) So(merged, ShouldResemble, []myInt{-4, 5}) }) Convey("Test with sorted values but mixed up", func() { - merged, err := MergeToSorted[myInt]([]myInt{1, 9, 10}, []myInt{4, 8, 12}) + merged, err := MergeToSorted([]myInt{1, 9, 10}, []myInt{4, 8, 12}) So(err, ShouldBeNil) So(merged, ShouldResemble, []myInt{1, 4, 8, 9, 10, 12}) }) @@ -333,9 +333,55 @@ func TestMergeToSorted(t *testing.T) { } expected := append(arr2, arr1...) - merged, err := MergeToSorted[myTest](arr1, arr2) + merged, err := MergeToSorted(arr1, arr2) So(err, ShouldBeNil) So(merged, ShouldResemble, expected) }) }) } + +func TestValidateStruct(t *testing.T) { + type ValidationStruct struct { + TestInt int `validate:"required,gt=0"` + TestURL string `validate:"required,url"` + TestBool bool + } + + const ( + validURL = "https://github.com/moira-alert/moira" + validInt = 1 + ) + + Convey("Test ValidateStruct", t, func() { + Convey("With TestInt less than zero", func() { + testStruct := ValidationStruct{ + TestInt: -1, + TestURL: validURL, + } + + err := ValidateStruct(testStruct) + So(err, ShouldNotBeNil) + }) + + Convey("With invalid TestURL format", func() { + testStruct := ValidationStruct{ + TestInt: validInt, + TestURL: "test", + TestBool: true, + } + + err := ValidateStruct(testStruct) + So(err, ShouldNotBeNil) + }) + + Convey("With valid structure", func() { + testStruct := ValidationStruct{ + TestInt: validInt, + TestURL: validURL, + } + + err := ValidateStruct(testStruct) + So(err, ShouldBeNil) + }) + }) +} diff --git a/senders/discord/init.go b/senders/discord/init.go index e991d089f..926746244 100644 --- a/senders/discord/init.go +++ b/senders/discord/init.go @@ -20,7 +20,7 @@ const ( // Structure that represents the Discord configuration in the YAML file. type config struct { ContactType string `mapstructure:"contact_type"` - Token string `mapstructure:"token"` + Token string `mapstructure:"token" validate:"required"` FrontURI string `mapstructure:"front_uri"` } @@ -42,9 +42,10 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to discord config: %w", err) } - if cfg.Token == "" { - return fmt.Errorf("cannot read the discord token from the config") + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("discord config validation error: %w", err) } + sender.session, err = discordgo.New("Bot " + cfg.Token) if err != nil { return fmt.Errorf("error creating discord session: %w", err) diff --git a/senders/discord/init_test.go b/senders/discord/init_test.go index 73c2609af..11043df99 100644 --- a/senders/discord/init_test.go +++ b/senders/discord/init_test.go @@ -1,10 +1,11 @@ package discord import ( - "fmt" + "errors" "testing" "time" + "github.com/go-playground/validator/v10" "github.com/moira-alert/moira" logging "github.com/moira-alert/moira/logging/zerolog_adapter" @@ -31,9 +32,14 @@ func TestInit(t *testing.T) { location, _ := time.LoadLocation("UTC") Convey("Init tests", t, func() { sender := Sender{DataBase: &MockDB{}} - Convey("Empty map", func() { - err := sender.Init(map[string]interface{}{}, logger, nil, "") - So(err, ShouldResemble, fmt.Errorf("cannot read the discord token from the config")) + + validatorErr := validator.ValidationErrors{} + + Convey("With empty token", func() { + senderSettings := map[string]interface{}{} + + err := sender.Init(senderSettings, logger, nil, "") + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{DataBase: &MockDB{}}) }) @@ -42,7 +48,9 @@ func TestInit(t *testing.T) { "token": "123", "front_uri": "http://moira.uri", } - sender.Init(senderSettings, logger, location, "15:04") //nolint + + err := sender.Init(senderSettings, logger, location, "15:04") //nolint + So(err, ShouldBeNil) So(sender.frontURI, ShouldResemble, "http://moira.uri") So(sender.session.Token, ShouldResemble, "Bot 123") So(sender.logger, ShouldResemble, logger) diff --git a/senders/mail/mail.go b/senders/mail/mail.go index 39ce73344..10406e043 100644 --- a/senders/mail/mail.go +++ b/senders/mail/mail.go @@ -14,10 +14,10 @@ import ( // Structure that represents the Mail configuration in the YAML file. type config struct { - MailFrom string `mapstructure:"mail_from"` + MailFrom string `mapstructure:"mail_from" validate:"required"` SMTPHello string `mapstructure:"smtp_hello"` - SMTPHost string `mapstructure:"smtp_host"` - SMTPPort int64 `mapstructure:"smtp_port"` + SMTPHost string `mapstructure:"smtp_host" validate:"required"` + SMTPPort int64 `mapstructure:"smtp_port" validate:"required"` InsecureTLS bool `mapstructure:"insecure_tls"` FrontURI string `mapstructure:"front_uri"` SMTPPass string `mapstructure:"smtp_pass"` @@ -64,6 +64,10 @@ func (sender *Sender) fillSettings(senderSettings interface{}, logger moira.Logg return fmt.Errorf("failed to decode senderSettings to mail config: %w", err) } + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("mail config validation error: %w", err) + } + sender.logger = logger sender.From = cfg.MailFrom sender.SMTPHello = cfg.SMTPHello @@ -76,12 +80,11 @@ func (sender *Sender) fillSettings(senderSettings interface{}, logger moira.Logg sender.TemplateFile = cfg.TemplateFile sender.location = location sender.dateTimeFormat = dateTimeFormat + if sender.Username == "" { sender.Username = sender.From } - if sender.From == "" { - return fmt.Errorf("mail_from can't be empty") - } + return nil } @@ -106,11 +109,13 @@ func (sender *Sender) tryDial() error { return err } defer t.Close() + if sender.SMTPHello != "" { if err := t.Hello(sender.SMTPHello); err != nil { return err } } + if sender.Password != "" { tlsConfig := &tls.Config{ InsecureSkipVerify: sender.InsecureTLS, @@ -123,5 +128,6 @@ func (sender *Sender) tryDial() error { return err } } + return nil } diff --git a/senders/mail/mail_test.go b/senders/mail/mail_test.go index 3c3036130..57cc27fb3 100644 --- a/senders/mail/mail_test.go +++ b/senders/mail/mail_test.go @@ -1,33 +1,90 @@ package mail import ( - "fmt" + "errors" "testing" + "github.com/go-playground/validator/v10" . "github.com/smartystreets/goconvey/convey" ) +const ( + defaultMailFrom = "test-mail-from" + defaultSMTPHost = "test-smtp-host" + defaultSMTPPort = 80 + defaultSMTPHello = "test-smtp-hello" + defaultInsecureTLS = true + defaultFrontURI = "test-front-uri" + defaultSMTPPass = "test-smtp-pass" + defaultSMTPUser = "test-smtp-user" + defaultTemplateFile = "test-template-file" +) + func TestFillSettings(t *testing.T) { - Convey("Empty map", t, func() { + Convey("Test fillSettings", t, func() { sender := Sender{} - err := sender.fillSettings(map[string]interface{}{}, nil, nil, "") - So(err, ShouldResemble, fmt.Errorf("mail_from can't be empty")) - So(sender, ShouldResemble, Sender{}) - }) - Convey("Has From", t, func() { - sender := Sender{} - settings := map[string]interface{}{"mail_from": "123"} - Convey("No username", func() { - err := sender.fillSettings(settings, nil, nil, "") - So(err, ShouldBeNil) - So(sender, ShouldResemble, Sender{From: "123", Username: "123"}) + validatorErr := validator.ValidationErrors{} + + Convey("With empty mail_from", func() { + senderSettings := map[string]interface{}{ + "smtp_host": defaultSMTPHost, + "smtp_port": defaultSMTPPort, + } + + err := sender.fillSettings(senderSettings, nil, nil, "") + So(errors.As(err, &validatorErr), ShouldBeTrue) + So(sender, ShouldResemble, Sender{}) }) - Convey("Has username", func() { - settings["smtp_user"] = "user" - err := sender.fillSettings(settings, nil, nil, "") + + Convey("With empty smpt_host", func() { + senderSettings := map[string]interface{}{ + "mail_from": defaultMailFrom, + "smtp_port": defaultSMTPPort, + } + + err := sender.fillSettings(senderSettings, nil, nil, "") + So(errors.As(err, &validatorErr), ShouldBeTrue) + So(sender, ShouldResemble, Sender{}) + }) + + Convey("With empty smpt_port", func() { + senderSettings := map[string]interface{}{ + "mail_from": defaultMailFrom, + "smtp_host": defaultSMTPHost, + } + + err := sender.fillSettings(senderSettings, nil, nil, "") + So(errors.As(err, &validatorErr), ShouldBeTrue) + So(sender, ShouldResemble, Sender{}) + }) + + Convey("With full settings", func() { + senderSettings := map[string]interface{}{ + "mail_from": defaultMailFrom, + "smtp_host": defaultSMTPHost, + "smtp_port": defaultSMTPPort, + "smtp_hello": defaultSMTPHello, + "insecure_tls": defaultInsecureTLS, + "front_uri": defaultFrontURI, + "smtp_user": defaultSMTPUser, + "smtp_pass": defaultSMTPPass, + "template_file": defaultTemplateFile, + } + + err := sender.fillSettings(senderSettings, nil, nil, "") So(err, ShouldBeNil) - So(sender, ShouldResemble, Sender{From: "123", Username: "user"}) + So(sender, ShouldResemble, Sender{ + From: defaultMailFrom, + SMTPHello: defaultSMTPHello, + SMTPHost: defaultSMTPHost, + SMTPPort: 80, + FrontURI: defaultFrontURI, + InsecureTLS: defaultInsecureTLS, + Username: defaultSMTPUser, + Password: defaultSMTPPass, + TemplateFile: defaultTemplateFile, + }) }) }) } diff --git a/senders/mattermost/sender.go b/senders/mattermost/sender.go index 36cbabb37..1b0f85c4a 100644 --- a/senders/mattermost/sender.go +++ b/senders/mattermost/sender.go @@ -18,10 +18,10 @@ import ( // Structure that represents the Mattermost configuration in the YAML file. type config struct { - Url string `mapstructure:"url"` + Url string `mapstructure:"url" validate:"required,url"` InsecureTLS bool `mapstructure:"insecure_tls"` - APIToken string `mapstructure:"api_token"` - FrontURI string `mapstructure:"front_uri"` + APIToken string `mapstructure:"api_token" validate:"required"` + FrontURI string `mapstructure:"front_uri" validate:"required"` UseEmoji bool `mapstructure:"use_emoji"` DefaultEmoji string `mapstructure:"default_emoji"` EmojiMap map[string]string `mapstructure:"emoji_map"` @@ -53,8 +53,8 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to mattermost config: %w", err) } - if cfg.Url == "" { - return fmt.Errorf("can not read Mattermost url from config") + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("mattermost config validation error: %w", err) } client := model.NewAPIv4Client(cfg.Url) @@ -68,20 +68,13 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca } sender.client = client - - if cfg.APIToken == "" { - return fmt.Errorf("can not read Mattermost api_token from config") - } sender.client.SetToken(cfg.APIToken) - if cfg.FrontURI == "" { - return fmt.Errorf("can not read Mattermost front_uri from config") - } - emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap) if err != nil { return fmt.Errorf("cannot initialize mattermost sender, err: %w", err) } + sender.logger = logger sender.formatter = msgformat.NewHighlightSyntaxFormatter( emojiProvider, diff --git a/senders/mattermost/sender_internal_test.go b/senders/mattermost/sender_internal_test.go index 1ae3da0a2..2b6bb7a64 100644 --- a/senders/mattermost/sender_internal_test.go +++ b/senders/mattermost/sender_internal_test.go @@ -21,7 +21,7 @@ func TestSendEvents(t *testing.T) { Convey("Given configured sender", t, func() { senderSettings := map[string]interface{}{ // redundant, but necessary config - "url": "qwerty", + "url": "https://mattermost.com/", "api_token": "qwerty", "front_uri": "qwerty", "insecure_tls": true, diff --git a/senders/mattermost/sender_manual_test.go b/senders/mattermost/sender_manual_test.go index a93a627a9..35e71665f 100644 --- a/senders/mattermost/sender_manual_test.go +++ b/senders/mattermost/sender_manual_test.go @@ -18,7 +18,7 @@ func TestSender(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) const ( - url = "http://localhost:8065" + url = "https://mattermost.com/" apiToken = "8pdo6yoiutgidgxs9qxhbo7w4h" channelID = "3y6ab8rptfdr9m1hzskghpxwsc" ) diff --git a/senders/mattermost/sender_test.go b/senders/mattermost/sender_test.go index c47baa562..333182219 100644 --- a/senders/mattermost/sender_test.go +++ b/senders/mattermost/sender_test.go @@ -1,69 +1,79 @@ package mattermost import ( + "errors" "testing" + "github.com/go-playground/validator/v10" logging "github.com/moira-alert/moira/logging/zerolog_adapter" . "github.com/smartystreets/goconvey/convey" ) +const ( + defaultURL = "https://mattermost.com/" + defaultAPIToken = "test-api-token" + defaultFrontURI = "test-front-uri" + defaultInsecureTLS = true + defaultUseEmoji = true + defaultEmoji = "test-emoji" +) + +var defaultEmojiMap = map[string]string{ + "OK": ":dance_mops:", +} + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) + Convey("Init tests", t, func() { - sender := &Sender{} + sender := Sender{} - Convey("No url", func() { + validatorErr := validator.ValidationErrors{} + + Convey("With empty url", func() { senderSettings := map[string]interface{}{ - "api_token": "qwerty", - "front_uri": "qwerty", - "insecure_tls": true, + "api_token": defaultAPIToken, + "front_uri": defaultFrontURI, } + err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldNotBeNil) + So(errors.As(err, &validatorErr), ShouldBeTrue) + So(sender, ShouldResemble, Sender{}) }) - Convey("Empty url", func() { + Convey("With empty api_token", func() { senderSettings := map[string]interface{}{ - "url": "", - "api_token": "qwerty", - "front_uri": "qwerty", - "insecure_tls": true, + "url": defaultURL, + "front_uri": defaultFrontURI, } - err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldNotBeNil) - }) - Convey("No api_token", func() { - senderSettings := map[string]interface{}{"url": "qwerty", "front_uri": "qwerty"} err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldNotBeNil) + So(errors.As(err, &validatorErr), ShouldBeTrue) + So(sender, ShouldResemble, Sender{}) }) - Convey("Empty api_token", func() { - senderSettings := map[string]interface{}{"url": "qwerty", "front_uri": "qwerty", "api_token": ""} - err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldNotBeNil) - }) - - Convey("No front_uri", func() { - senderSettings := map[string]interface{}{"url": "qwerty", "api_token": "qwerty"} - err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldNotBeNil) - }) + Convey("With empty front_uri", func() { + senderSettings := map[string]interface{}{ + "url": defaultURL, + "api_token": defaultAPIToken, + } - Convey("Empty front_uri", func() { - senderSettings := map[string]interface{}{"url": "qwerty", "api_token": "qwerty", "front_uri": ""} err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldNotBeNil) + So(errors.As(err, &validatorErr), ShouldBeTrue) + So(sender, ShouldResemble, Sender{}) }) - Convey("Full config", func() { + Convey("With full config", func() { senderSettings := map[string]interface{}{ - "url": "qwerty", - "api_token": "qwerty", - "front_uri": "qwerty", - "insecure_tls": true, + "url": defaultURL, + "api_token": defaultAPIToken, + "front_uri": defaultFrontURI, + "insecure_tls": defaultInsecureTLS, + "use_emoji": defaultUseEmoji, + "default_emoji": defaultEmoji, + "emoji_map": defaultEmojiMap, } + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldBeNil) }) diff --git a/senders/msteams/msteams.go b/senders/msteams/msteams.go index 4bfd282f8..8e7e03bcc 100644 --- a/senders/msteams/msteams.go +++ b/senders/msteams/msteams.go @@ -29,15 +29,19 @@ const ( quotes = "```" ) -var throttleWarningFact = Fact{ - Name: "Warning", - Value: "Please, *fix your system or tune this trigger* to generate less events.", -} +var ( + throttleWarningFact = Fact{ + Name: "Warning", + Value: "Please, *fix your system or tune this trigger* to generate less events.", + } -var headers = map[string]string{ - "User-Agent": "Moira", - "Content-Type": "application/json", -} + headers = map[string]string{ + "User-Agent": "Moira", + "Content-Type": "application/json", + } + + defaultClientTimeout = 30 * time.Second +) // Structure that represents the MSTeams configuration in the YAML file. type config struct { @@ -62,13 +66,18 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to msteams config: %w", err) } + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("msteams config validation error: %w", err) + } + sender.logger = logger sender.location = location sender.frontURI = cfg.FrontURI sender.maxEvents = cfg.MaxEvents sender.client = &http.Client{ - Timeout: time.Duration(30) * time.Second, //nolint + Timeout: defaultClientTimeout, } + return nil } diff --git a/senders/msteams/msteams_test.go b/senders/msteams/msteams_test.go index faf612731..3fa71726a 100644 --- a/senders/msteams/msteams_test.go +++ b/senders/msteams/msteams_test.go @@ -11,18 +11,28 @@ import ( "gopkg.in/h2non/gock.v1" ) +const ( + defaultFrontURI = "test-front-uri" + defaultMaxEvents = -1 +) + func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) + Convey("Init tests", t, func() { sender := Sender{} + senderSettings := map[string]interface{}{ - "max_events": -1, + "max_events": defaultMaxEvents, + "front_uri": defaultFrontURI, } + Convey("Minimal settings", func() { err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldResemble, nil) So(sender, ShouldNotResemble, Sender{}) - So(sender.maxEvents, ShouldResemble, -1) + So(sender.maxEvents, ShouldResemble, defaultMaxEvents) + So(sender.frontURI, ShouldResemble, defaultFrontURI) }) }) } diff --git a/senders/opsgenie/init.go b/senders/opsgenie/init.go index 83c205165..72913a760 100644 --- a/senders/opsgenie/init.go +++ b/senders/opsgenie/init.go @@ -13,8 +13,7 @@ import ( // Structure that represents the OpsGenie configuration in the YAML file. type config struct { - APIKey string `mapstructure:"api_key"` - FrontURI string `mapstructure:"front_uri"` + APIKey string `mapstructure:"api_key" validate:"required"` } // Sender implements the Sender interface for opsgenie. @@ -27,7 +26,6 @@ type Sender struct { imageStoreID string imageStore moira.ImageStore imageStoreConfigured bool - frontURI string } // Init initializes the opsgenie sender. @@ -39,11 +37,11 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to opsgenie config: %w", err) } - sender.apiKey = cfg.APIKey - if sender.apiKey == "" { - return fmt.Errorf("cannot read the api_key from the sender settings") + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("opsgenie config validation error: %w", err) } + sender.apiKey = cfg.APIKey sender.imageStoreID, sender.imageStore, sender.imageStoreConfigured = senders.ReadImageStoreConfig(senderSettings, sender.ImageStores, logger) sender.client, err = alert.NewClient(&client.Config{ @@ -53,7 +51,6 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("error while creating opsgenie client: %w", err) } - sender.frontURI = cfg.FrontURI sender.logger = logger sender.location = location return nil diff --git a/senders/opsgenie/init_test.go b/senders/opsgenie/init_test.go index e39702834..5c8c952a6 100644 --- a/senders/opsgenie/init_test.go +++ b/senders/opsgenie/init_test.go @@ -1,10 +1,11 @@ package opsgenie import ( - "fmt" + "errors" "testing" "time" + "github.com/go-playground/validator/v10" "github.com/moira-alert/moira" "go.uber.org/mock/gomock" @@ -25,9 +26,13 @@ func TestInit(t *testing.T) { "s3": imageStore, }} - Convey("Empty map", func() { - err := sender.Init(map[string]interface{}{}, logger, nil, "") - So(err, ShouldResemble, fmt.Errorf("cannot read the api_key from the sender settings")) + validatorErr := validator.ValidationErrors{} + + Convey("With empty api_key", func() { + senderSettings := map[string]interface{}{} + + err := sender.Init(senderSettings, logger, nil, "") + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{ ImageStores: map[string]moira.ImageStore{ "s3": imageStore, @@ -35,7 +40,7 @@ func TestInit(t *testing.T) { }) }) - Convey("Has settings", func() { + Convey("With full settings", func() { imageStore.EXPECT().IsEnabled().Return(true) senderSettings := map[string]interface{}{ "api_key": "testkey", @@ -44,7 +49,6 @@ func TestInit(t *testing.T) { } sender.Init(senderSettings, logger, location, "15:04") //nolint So(sender.apiKey, ShouldResemble, "testkey") - So(sender.frontURI, ShouldResemble, "http://moira.uri") So(sender.logger, ShouldResemble, logger) So(sender.location, ShouldResemble, location) }) diff --git a/senders/opsgenie/send_test.go b/senders/opsgenie/send_test.go index db81c15a2..048184e44 100644 --- a/senders/opsgenie/send_test.go +++ b/senders/opsgenie/send_test.go @@ -146,7 +146,6 @@ func TestMakeCreateAlertRequest(t *testing.T) { imageStore := mock_moira_alert.NewMockImageStore(mockCtrl) sender := Sender{ - frontURI: "https://my-moira.com", location: location, logger: logger, imageStoreConfigured: true, diff --git a/senders/pagerduty/init.go b/senders/pagerduty/init.go index 0e58afc2c..cbb36c5b6 100644 --- a/senders/pagerduty/init.go +++ b/senders/pagerduty/init.go @@ -39,5 +39,6 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca sender.logger = logger sender.location = location + return nil } diff --git a/senders/pagerduty/init_test.go b/senders/pagerduty/init_test.go index df0ecbbe7..7edd32231 100644 --- a/senders/pagerduty/init_test.go +++ b/senders/pagerduty/init_test.go @@ -37,6 +37,7 @@ func TestInit(t *testing.T) { So(sender.imageStoreConfigured, ShouldResemble, true) So(sender.imageStore, ShouldResemble, imageStore) }) + Convey("Wrong image_store name", func() { senderSettings := map[string]interface{}{ "front_uri": "http://moira.uri", @@ -46,6 +47,7 @@ func TestInit(t *testing.T) { So(sender.imageStoreConfigured, ShouldResemble, false) So(sender.imageStore, ShouldResemble, nil) }) + Convey("image store not configured", func() { imageStore.EXPECT().IsEnabled().Return(false) senderSettings := map[string]interface{}{ diff --git a/senders/pushover/pushover.go b/senders/pushover/pushover.go index d8f0c4b69..ecd88d1c4 100644 --- a/senders/pushover/pushover.go +++ b/senders/pushover/pushover.go @@ -19,7 +19,7 @@ const ( // Structure that represents the Pushover configuration in the YAML file. type config struct { - APIToken string `mapstructure:"api_token"` + APIToken string `mapstructure:"api_token" validate:"required"` FrontURI string `mapstructure:"front_uri"` } @@ -41,14 +41,16 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to pushover config: %w", err) } - sender.apiToken = cfg.APIToken - if sender.apiToken == "" { - return fmt.Errorf("can not read pushover api_token from config") + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("pushover config validation error: %w", err) } + + sender.apiToken = cfg.APIToken sender.client = pushover_client.New(sender.apiToken) sender.logger = logger sender.frontURI = cfg.FrontURI sender.location = location + return nil } diff --git a/senders/pushover/pushover_test.go b/senders/pushover/pushover_test.go index a25b6ccdd..1416501d0 100644 --- a/senders/pushover/pushover_test.go +++ b/senders/pushover/pushover_test.go @@ -2,36 +2,51 @@ package pushover import ( "bytes" - "fmt" + "errors" "testing" "time" + "github.com/go-playground/validator/v10" pushover_client "github.com/gregdel/pushover" "github.com/moira-alert/moira" logging "github.com/moira-alert/moira/logging/zerolog_adapter" . "github.com/smartystreets/goconvey/convey" ) -func TestSender_Init(t *testing.T) { +func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) + + validatorErr := validator.ValidationErrors{} + Convey("Empty map", t, func() { sender := Sender{} - err := sender.Init(map[string]interface{}{}, logger, nil, "") - So(err, ShouldResemble, fmt.Errorf("can not read pushover api_token from config")) + senderSettings := map[string]interface{}{} + + err := sender.Init(senderSettings, logger, nil, "") + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{}) }) Convey("Settings has api_token", t, func() { sender := Sender{} - err := sender.Init(map[string]interface{}{"api_token": "123"}, logger, nil, "") + senderSettings := map[string]interface{}{ + "api_token": "123", + } + + err := sender.Init(senderSettings, logger, nil, "") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{apiToken: "123", client: pushover_client.New("123"), logger: logger}) }) Convey("Settings has all data", t, func() { sender := Sender{} + senderSettings := map[string]interface{}{ + "api_token": "123", + "front_uri": "321", + } location, _ := time.LoadLocation("UTC") - err := sender.Init(map[string]interface{}{"api_token": "123", "front_uri": "321"}, logger, location, "") + + err := sender.Init(senderSettings, logger, location, "") So(err, ShouldBeNil) So(sender, ShouldResemble, Sender{apiToken: "123", client: pushover_client.New("123"), frontURI: "321", logger: logger, location: location}) }) diff --git a/senders/script/script.go b/senders/script/script.go index c78e37dde..d7b7351d1 100644 --- a/senders/script/script.go +++ b/senders/script/script.go @@ -15,7 +15,7 @@ import ( // Structure that represents the Script configuration in the YAML file. type config struct { - Exec string `mapstructure:"exec"` + Exec string `mapstructure:"exec" validate:"required"` } // Sender implements moira sender interface via script execution. @@ -40,6 +40,10 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to script config: %w", err) } + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("script config validation error: %w", err) + } + _, _, err = parseExec(cfg.Exec) if err != nil { return err diff --git a/senders/script/script_test.go b/senders/script/script_test.go index 0c2d5cb67..83d500636 100644 --- a/senders/script/script_test.go +++ b/senders/script/script_test.go @@ -1,9 +1,11 @@ package script import ( + "errors" "fmt" "testing" + "github.com/go-playground/validator/v10" "github.com/moira-alert/moira" logging "github.com/moira-alert/moira/logging/zerolog_adapter" . "github.com/smartystreets/goconvey/convey" @@ -50,9 +52,11 @@ func TestInit(t *testing.T) { sender := Sender{} settings := map[string]interface{}{} + validatorErr := validator.ValidationErrors{} + Convey("Empty exec", func() { err := sender.Init(settings, logger, nil, "") - So(err, ShouldResemble, fmt.Errorf("file not found")) + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{}) }) diff --git a/senders/slack/slack.go b/senders/slack/slack.go index 28976c6c6..d1a5f4d2d 100644 --- a/senders/slack/slack.go +++ b/senders/slack/slack.go @@ -31,7 +31,7 @@ var ( // Structure that represents the Slack configuration in the YAML file. type config struct { - APIToken string `mapstructure:"api_token"` + APIToken string `mapstructure:"api_token" validate:"required"` UseEmoji bool `mapstructure:"use_emoji"` FrontURI string `mapstructure:"front_uri"` DefaultEmoji string `mapstructure:"default_emoji"` @@ -54,13 +54,15 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to slack config: %w", err) } - if cfg.APIToken == "" { - return fmt.Errorf("can not read slack api_token from config") + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("slack config validation error: %w", err) } + emojiProvider, err := emoji_provider.NewEmojiProvider(cfg.DefaultEmoji, cfg.EmojiMap) if err != nil { return fmt.Errorf("cannot initialize slack sender, err: %w", err) } + sender.logger = logger sender.emojiProvider = emojiProvider sender.formatter = msgformat.NewHighlightSyntaxFormatter( @@ -75,7 +77,9 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca eventStringFormatter, codeBlockStart, codeBlockEnd) + sender.client = slack_client.New(cfg.APIToken) + return nil } diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index 43471896f..37cd85eda 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -1,10 +1,12 @@ package slack import ( + "errors" "strings" "testing" "time" + "github.com/go-playground/validator/v10" "github.com/moira-alert/moira" logging "github.com/moira-alert/moira/logging/zerolog_adapter" . "github.com/smartystreets/goconvey/convey" @@ -15,15 +17,12 @@ func TestInit(t *testing.T) { Convey("Init tests", t, func() { sender := Sender{} senderSettings := map[string]interface{}{} - Convey("Empty map", func() { - err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldNotBeNil) - }) - Convey("has empty api_token", func() { - senderSettings["api_token"] = "" + validatorErr := validator.ValidationErrors{} + + Convey("With empty api_token", func() { err := sender.Init(senderSettings, logger, nil, "") - So(err, ShouldNotBeNil) + So(errors.As(err, &validatorErr), ShouldBeTrue) }) Convey("has api_token", func() { diff --git a/senders/telegram/init.go b/senders/telegram/init.go index aabd2a4fd..f49332c94 100644 --- a/senders/telegram/init.go +++ b/senders/telegram/init.go @@ -27,7 +27,7 @@ var pollerTimeout = 10 * time.Second // Structure that represents the Telegram configuration in the YAML file. type config struct { ContactType string `mapstructure:"contact_type"` - APIToken string `mapstructure:"api_token"` + APIToken string `mapstructure:"api_token" validate:"required"` FrontURI string `mapstructure:"front_uri"` } @@ -66,9 +66,10 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to telegram config: %w", err) } - if cfg.APIToken == "" { - return fmt.Errorf("can not read telegram api_token from config") + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("telegram config validation error: %w", err) } + sender.apiToken = cfg.APIToken emojiProvider := telegramEmojiProvider{} diff --git a/senders/telegram/init_test.go b/senders/telegram/init_test.go index 9179cb920..b5a0bbc4e 100644 --- a/senders/telegram/init_test.go +++ b/senders/telegram/init_test.go @@ -7,11 +7,11 @@ import ( "testing" "time" - mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" - "go.uber.org/mock/gomock" - + "github.com/go-playground/validator/v10" logging "github.com/moira-alert/moira/logging/zerolog_adapter" + mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" . "github.com/smartystreets/goconvey/convey" + "go.uber.org/mock/gomock" ) func TestInit(t *testing.T) { @@ -19,9 +19,12 @@ func TestInit(t *testing.T) { location, _ := time.LoadLocation("UTC") Convey("Init tests", t, func() { sender := Sender{} - Convey("Empty map", func() { + + validatorErr := validator.ValidationErrors{} + + Convey("With empty api_token", func() { err := sender.Init(map[string]interface{}{}, logger, nil, "") - So(err, ShouldResemble, fmt.Errorf("can not read telegram api_token from config")) + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{}) }) diff --git a/senders/twilio/twilio.go b/senders/twilio/twilio.go index d8fc8a77a..7fff15cf4 100644 --- a/senders/twilio/twilio.go +++ b/senders/twilio/twilio.go @@ -11,10 +11,10 @@ import ( // Structure that represents the Twilio configuration in the YAML file. type config struct { - Type string `mapstructure:"sender_type"` - APIAsid string `mapstructure:"api_asid"` - APIAuthToken string `mapstructure:"api_authtoken"` - APIFromPhone string `mapstructure:"api_fromphone"` + Type string `mapstructure:"sender_type" validate:"required"` + APIAsid string `mapstructure:"api_asid" validate:"required"` + APIAuthToken string `mapstructure:"api_authtoken" validate:"required"` + APIFromPhone string `mapstructure:"api_fromphone" validate:"required"` VoiceURL string `mapstructure:"voiceurl"` TwimletsEcho bool `mapstructure:"twimlets_echo"` AppendMessage bool `mapstructure:"append_message"` @@ -43,19 +43,12 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca if err != nil { return fmt.Errorf("failed to decode senderSettings to twilio config: %w", err) } - apiType := cfg.Type - - if cfg.APIAsid == "" { - return fmt.Errorf("can not read [%s] api_sid param from config", apiType) - } - if cfg.APIAuthToken == "" { - return fmt.Errorf("can not read [%s] api_authtoken param from config", apiType) + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("twilio config validation error: %w", err) } - if cfg.APIFromPhone == "" { - return fmt.Errorf("can not read [%s] api_fromphone param from config", apiType) - } + apiType := cfg.Type twilioClient := twilio_client.NewClient(cfg.APIAsid, cfg.APIAuthToken) @@ -65,6 +58,7 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca logger: logger, location: location, } + switch apiType { case "twilio sms": sender.sender = &twilioSenderSms{tSender} diff --git a/senders/twilio/twilio_test.go b/senders/twilio/twilio_test.go index 11b8715e3..1cd1b7753 100644 --- a/senders/twilio/twilio_test.go +++ b/senders/twilio/twilio_test.go @@ -1,11 +1,13 @@ package twilio import ( + "errors" "fmt" "testing" "time" twilio_client "github.com/carlosdp/twiliogo" + "github.com/go-playground/validator/v10" logging "github.com/moira-alert/moira/logging/zerolog_adapter" . "github.com/smartystreets/goconvey/convey" ) @@ -16,9 +18,12 @@ func TestInit(t *testing.T) { logger, _ := logging.ConfigureLog("stdout", "debug", "test", true) location, _ := time.LoadLocation("UTC") settings := map[string]interface{}{} + + validatorErr := validator.ValidationErrors{} + Convey("no api asid", func() { err := sender.Init(settings, logger, nil, "15:04") - So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_sid param from config", "")) + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{}) }) @@ -26,7 +31,7 @@ func TestInit(t *testing.T) { Convey("no api authtoken", func() { err := sender.Init(settings, logger, nil, "15:04") - So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_authtoken param from config", "")) + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{}) }) @@ -34,7 +39,7 @@ func TestInit(t *testing.T) { Convey("no api fromphone", func() { err := sender.Init(settings, logger, nil, "15:04") - So(err, ShouldResemble, fmt.Errorf("can not read [%s] api_fromphone param from config", "")) + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{}) }) @@ -42,7 +47,15 @@ func TestInit(t *testing.T) { Convey("no api type", func() { err := sender.Init(settings, logger, nil, "15:04") - So(err, ShouldResemble, fmt.Errorf("wrong twilio type: %s", "")) + So(errors.As(err, &validatorErr), ShouldBeTrue) + So(sender, ShouldResemble, Sender{}) + }) + + settings["sender_type"] = "test" + + Convey("with unknown api type", func() { + err := sender.Init(settings, logger, nil, "15:04") + So(err, ShouldResemble, fmt.Errorf("wrong twilio type: %s", "test")) So(sender, ShouldResemble, Sender{}) }) diff --git a/senders/victorops/init.go b/senders/victorops/init.go index e47e53485..c623f8208 100644 --- a/senders/victorops/init.go +++ b/senders/victorops/init.go @@ -12,7 +12,7 @@ import ( // Structure that represents the VictorOps configuration in the YAML file. type config struct { - RoutingURL string `mapstructure:"routing_url"` + RoutingURL string `mapstructure:"routing_url" validate:"required"` ImageStore string `mapstructure:"image_store"` FrontURI string `mapstructure:"front_uri"` } @@ -40,11 +40,12 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to victorops config: %w", err) } - sender.routingURL = cfg.RoutingURL - if sender.routingURL == "" { - return fmt.Errorf("cannot read the routing url from the yaml config") + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("victorops config validation error: %w", err) } + sender.routingURL = cfg.RoutingURL + sender.imageStoreID = cfg.ImageStore if sender.imageStoreID == "" { logger.Warning().Msg("Cannot read image_store from the config, will not be able to attach plot images to events") diff --git a/senders/victorops/init_test.go b/senders/victorops/init_test.go index 05c2111a0..e34b6abfe 100644 --- a/senders/victorops/init_test.go +++ b/senders/victorops/init_test.go @@ -1,10 +1,11 @@ package victorops import ( - "fmt" + "errors" "testing" "time" + "github.com/go-playground/validator/v10" "github.com/moira-alert/moira" "github.com/moira-alert/moira/senders/victorops/api" "go.uber.org/mock/gomock" @@ -25,9 +26,12 @@ func TestInit(t *testing.T) { sender := Sender{ImageStores: map[string]moira.ImageStore{ "s3": imageStore, }} - Convey("Empty map", func() { + + validatorErr := validator.ValidationErrors{} + + Convey("With empty routing url", func() { err := sender.Init(map[string]interface{}{}, logger, nil, "") - So(err, ShouldResemble, fmt.Errorf("cannot read the routing url from the yaml config")) + So(errors.As(err, &validatorErr), ShouldBeTrue) So(sender, ShouldResemble, Sender{ ImageStores: map[string]moira.ImageStore{ "s3": imageStore, diff --git a/senders/webhook/webhook.go b/senders/webhook/webhook.go index 3812a8799..036c877cb 100644 --- a/senders/webhook/webhook.go +++ b/senders/webhook/webhook.go @@ -1,7 +1,6 @@ package webhook import ( - "errors" "fmt" "io" "net/http" @@ -11,11 +10,9 @@ import ( "github.com/moira-alert/moira" ) -var ErrMissingURL = errors.New("can not read url from config") - // Structure that represents the Webhook configuration in the YAML file. type config struct { - URL string `mapstructure:"url"` + URL string `mapstructure:"url" validate:"required"` Body string `mapstructure:"body"` Headers map[string]string `mapstructure:"headers"` User string `mapstructure:"user"` @@ -42,13 +39,12 @@ func (sender *Sender) Init(senderSettings interface{}, logger moira.Logger, loca return fmt.Errorf("failed to decode senderSettings to webhook config: %w", err) } - sender.url = cfg.URL - if sender.url == "" { - return ErrMissingURL + if err = moira.ValidateStruct(cfg); err != nil { + return fmt.Errorf("webhook config validation error: %w", err) } + sender.url = cfg.URL sender.body = cfg.Body - sender.user, sender.password = cfg.User, cfg.Password sender.headers = map[string]string{ diff --git a/senders/webhook/webhook_test.go b/senders/webhook/webhook_test.go index 07369f11c..845054881 100644 --- a/senders/webhook/webhook_test.go +++ b/senders/webhook/webhook_test.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "encoding/base64" + "errors" "fmt" "net/http" "net/http/httptest" @@ -12,6 +13,7 @@ import ( "testing" "time" + "github.com/go-playground/validator/v10" "github.com/moira-alert/moira" logging "github.com/moira-alert/moira/logging/zerolog_adapter" @@ -38,12 +40,14 @@ var ( func TestSender_Init(t *testing.T) { Convey("Test Init function", t, func() { - Convey("With empty settings", func() { + validatorErr := validator.ValidationErrors{} + + Convey("With empty url", func() { settings := map[string]interface{}{} sender := Sender{} err := sender.Init(settings, logger, location, dateTimeFormat) - So(err, ShouldResemble, ErrMissingURL) + So(errors.As(err, &validatorErr), ShouldBeTrue) }) Convey("With only url", func() { From 1e92a14aad02c5c545e63275df243b9b7c2f4622 Mon Sep 17 00:00:00 2001 From: Tetrergeru Date: Tue, 8 Oct 2024 10:18:54 +0200 Subject: [PATCH 25/36] feat: update cabonapi to v0.17.0 (#1074) --- database/redis/database.go | 11 +- database/redis/notification.go | 19 +- database/redis/notification_test.go | 31 +- go.mod | 51 ++- go.sum | 592 +++----------------------- metric_source/local/database_test.go | 347 +++++++++++++++ metric_source/local/eval.go | 258 ++++++----- metric_source/local/fetchdata.go | 14 +- metric_source/local/fetchdata_test.go | 74 ++-- metric_source/local/local.go | 6 +- metric_source/local/local_test.go | 131 ++++-- metric_source/local/timer.go | 27 +- metric_source/local/timer_test.go | 16 +- notifier/config.go | 1 + 14 files changed, 768 insertions(+), 810 deletions(-) create mode 100644 metric_source/local/database_test.go diff --git a/database/redis/database.go b/database/redis/database.go index 5e3497080..03cf0cd56 100644 --- a/database/redis/database.go +++ b/database/redis/database.go @@ -91,9 +91,11 @@ func NewDatabase(logger moira.Logger, config DatabaseConfig, nh NotificationHist // NewTestDatabase use it only for tests. func NewTestDatabase(logger moira.Logger) *DbConnector { - return NewDatabase(logger, DatabaseConfig{ - Addrs: []string{"0.0.0.0:6379"}, - }, + return NewDatabase( + logger, DatabaseConfig{ + Addrs: []string{"0.0.0.0:6379"}, + MetricsTTL: time.Hour, + }, NotificationHistoryConfig{ NotificationHistoryTTL: time.Hour * 48, }, @@ -104,7 +106,8 @@ func NewTestDatabase(logger moira.Logger) *DbConnector { TransactionHeuristicLimit: 10000, ResaveTime: 30 * time.Second, }, - testSource) + testSource, + ) } // NewTestDatabaseWithIncorrectConfig use it only for tests. diff --git a/database/redis/notification.go b/database/redis/notification.go index a8f9c3f3c..82caf03d4 100644 --- a/database/redis/notification.go +++ b/database/redis/notification.go @@ -8,14 +8,17 @@ import ( "strings" "time" - "github.com/moira-alert/moira/notifier" - "github.com/go-redis/redis/v8" "github.com/moira-alert/moira" "github.com/moira-alert/moira/database/redis/reply" ) +// Separate const to prevent cyclic dependencies. +// Original const is declared in notifier package, notifier depends on all metric source packages. +// Thus it prevents us from using database in tests for local metric source. +const notificationsLimitUnlimited = int64(-1) + type notificationTypes struct { Valid, ToRemove, ToResaveNew, ToResaveOld []*moira.ScheduledNotification } @@ -294,8 +297,8 @@ func (connector *DbConnector) FetchNotifications(to int64, limit int64) ([]*moir } // No limit - if limit == notifier.NotificationsLimitUnlimited { - return connector.fetchNotifications(to, notifier.NotificationsLimitUnlimited) + if limit == notificationsLimitUnlimited { + return connector.fetchNotifications(to, notificationsLimitUnlimited) } count, err := connector.notificationsCount(to) @@ -305,7 +308,7 @@ func (connector *DbConnector) FetchNotifications(to int64, limit int64) ([]*moir // Hope count will be not greater then limit when we call fetchNotificationsNoLimit if limit > connector.notification.TransactionHeuristicLimit && count < limit/2 { - return connector.fetchNotifications(to, notifier.NotificationsLimitUnlimited) + return connector.fetchNotifications(to, notificationsLimitUnlimited) } return connector.fetchNotifications(to, limit) @@ -354,7 +357,7 @@ func (connector *DbConnector) fetchNotifications(to int64, limit int64) ([]*moir // sorted by timestamp in one transaction with or without limit, depending on whether limit is nil. func getNotificationsInTxWithLimit(ctx context.Context, tx *redis.Tx, to int64, limit int64) ([]*moira.ScheduledNotification, error) { var rng *redis.ZRangeBy - if limit != notifier.NotificationsLimitUnlimited { + if limit != notificationsLimitUnlimited { rng = &redis.ZRangeBy{Min: "-inf", Max: strconv.FormatInt(to, 10), Offset: 0, Count: limit} } else { rng = &redis.ZRangeBy{Min: "-inf", Max: strconv.FormatInt(to, 10)} @@ -393,7 +396,7 @@ func getLimitedNotifications( limitedNotifications := notifications - if limit != notifier.NotificationsLimitUnlimited { + if limit != notificationsLimitUnlimited { limitedNotifications = limitNotifications(notifications) lastTs := limitedNotifications[len(limitedNotifications)-1].Timestamp @@ -401,7 +404,7 @@ func getLimitedNotifications( // this means that all notifications have same timestamp, // we hope that all notifications with same timestamp should fit our memory var err error - limitedNotifications, err = getNotificationsInTxWithLimit(ctx, tx, lastTs, notifier.NotificationsLimitUnlimited) + limitedNotifications, err = getNotificationsInTxWithLimit(ctx, tx, lastTs, notificationsLimitUnlimited) if err != nil { return nil, fmt.Errorf("failed to get notification without limit in transaction: %w", err) } diff --git a/database/redis/notification_test.go b/database/redis/notification_test.go index d0a25ce56..c287f78d5 100644 --- a/database/redis/notification_test.go +++ b/database/redis/notification_test.go @@ -10,7 +10,6 @@ import ( "github.com/moira-alert/moira" "github.com/moira-alert/moira/clock" logging "github.com/moira-alert/moira/logging/zerolog_adapter" - "github.com/moira-alert/moira/notifier" "github.com/stretchr/testify/assert" . "github.com/smartystreets/goconvey/convey" @@ -59,7 +58,7 @@ func TestScheduledNotification(t *testing.T) { }) Convey("Test fetch notifications", func() { - actual, err := database.FetchNotifications(now-database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err := database.FetchNotifications(now-database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld}) @@ -68,7 +67,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 2) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ification, ¬ificationNew}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ification, ¬ificationNew}) @@ -128,7 +127,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 0) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) }) @@ -167,7 +166,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 0) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) }) @@ -197,7 +196,7 @@ func TestScheduledNotificationErrorConnection(t *testing.T) { So(err, ShouldNotBeNil) So(total, ShouldEqual, 0) - actual2, err := database.FetchNotifications(0, notifier.NotificationsLimitUnlimited) + actual2, err := database.FetchNotifications(0, notificationsLimitUnlimited) So(err, ShouldNotBeNil) So(actual2, ShouldBeNil) @@ -284,7 +283,7 @@ func TestFetchNotifications(t *testing.T) { Convey("Test fetch notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) - actual, err := database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err := database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) @@ -330,7 +329,7 @@ func TestGetNotificationsInTxWithLimit(t *testing.T) { Convey("Test with zero notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{}) err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notifier.NotificationsLimitUnlimited) + actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) return nil @@ -344,7 +343,7 @@ func TestGetNotificationsInTxWithLimit(t *testing.T) { Convey("Test all notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notifier.NotificationsLimitUnlimited) + actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) return nil @@ -418,7 +417,7 @@ func TestGetLimitedNotifications(t *testing.T) { Convey("Test all notifications with different timestamps without limit", func() { notifications := []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew} err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getLimitedNotifications(ctx, tx, notifier.NotificationsLimitUnlimited, notifications) + actual, err := getLimitedNotifications(ctx, tx, notificationsLimitUnlimited, notifications) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) return nil @@ -913,7 +912,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) @@ -936,7 +935,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) @@ -947,7 +946,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Test all notification with ts and without limit in db", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld, notification4}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification4, ¬ification, ¬ificationNew}) @@ -1016,7 +1015,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew3}) @@ -1052,7 +1051,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew2}) @@ -1092,7 +1091,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew3}) diff --git a/go.mod b/go.mod index a2d3a9c52..b309808bd 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible github.com/PagerDuty/go-pagerduty v1.5.1 - github.com/ansel1/merry v1.6.2 + github.com/ansel1/merry v1.8.0 github.com/aws/aws-sdk-go v1.44.293 github.com/blevesearch/bleve/v2 v2.3.8 github.com/bwmarrin/discordgo v0.25.0 @@ -15,7 +15,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/render v1.0.1 - github.com/go-graphite/carbonapi v0.16.0 + github.com/go-graphite/carbonapi v0.17.0 github.com/go-graphite/protocol v1.0.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-redsync/redsync/v4 v4.4.4 @@ -57,13 +57,11 @@ require ( ) require ( - bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0 // indirect github.com/JaderDias/movingmedian v0.0.0-20220813210630-d8c6b6de8835 // indirect github.com/Masterminds/sprig/v3 v3.2.3 github.com/RoaringBitmap/roaring v1.3.0 // indirect github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect - github.com/ansel1/merry/v2 v2.1.1 // indirect - github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect + github.com/ansel1/merry/v2 v2.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.8.0 // indirect github.com/blend/go-sdk v2.0.0+incompatible // indirect @@ -82,21 +80,19 @@ require ( github.com/blevesearch/zapx/v13 v13.3.8 // indirect github.com/blevesearch/zapx/v14 v14.3.8 // indirect github.com/blevesearch/zapx/v15 v15.3.11 // indirect - github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/disintegration/imaging v1.6.2 // indirect github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497 // indirect github.com/francoispqt/gojay v1.2.13 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/gomodule/redigo v1.8.9 // indirect + github.com/gomodule/redigo v1.9.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 github.com/gopherjs/gopherjs v1.17.2 // indirect @@ -115,7 +111,7 @@ require ( github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80 // indirect github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/maruel/natural v1.1.0 // indirect + github.com/maruel/natural v1.1.1 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect github.com/mattermost/logr/v2 v2.0.21 // indirect @@ -125,28 +121,26 @@ require ( github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/msaf1980/go-stringutils v0.1.4 // indirect + github.com/msaf1980/go-stringutils v0.1.6 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - github.com/natefinch/atomic v1.0.1 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.1 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pkg/errors v0.9.1 - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartystreets/assertions v1.2.0 // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.16.0 // indirect - github.com/stretchr/objx v0.5.1 // indirect - github.com/stretchr/testify v1.8.4 - github.com/subosito/gotenv v1.4.2 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.9.0 + github.com/subosito/gotenv v1.6.0 // indirect github.com/tinylib/msgp v1.1.9 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -154,9 +148,8 @@ require ( github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect go.etcd.io/bbolt v1.3.7 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.24.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.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.18.0 // indirect @@ -164,7 +157,7 @@ require ( golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - gonum.org/v1/gonum v0.12.0 // indirect + gonum.org/v1/gonum v0.15.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect @@ -199,9 +192,13 @@ require ( github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/oklog/run v1.1.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/swag v1.8.12 // indirect + github.com/tebeka/strftime v0.1.5 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.62.0 // indirect diff --git a/go.sum b/go.sum index 0eacd324a..a013bebd9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0 h1:800dI8vRxVMgss6UcZY8gxk8PvYw7Qo1ZI3TrUkTKjc= -bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0/go.mod h1:9BKpS/J2txC7Ql3QUhesesiV3HsIsA7zl7VK6cQVg5M= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -32,368 +30,31 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= @@ -435,14 +96,10 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGn github.com/alicebob/miniredis/v2 v2.22.0 h1:lIHHiSkEyS1MkKHCHzN+0mWrA4YdbGdimE5iZ2sHSzo= github.com/alicebob/miniredis/v2 v2.22.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/ansel1/merry v1.5.0/go.mod h1:wUy/yW0JX0ix9GYvUbciq+bi3jW/vlKPlbpI7qdZpOw= -github.com/ansel1/merry v1.5.1/go.mod h1:wUy/yW0JX0ix9GYvUbciq+bi3jW/vlKPlbpI7qdZpOw= -github.com/ansel1/merry v1.6.2 h1:0xr40haRrfVzmOH/JVOu7KOKGEI1c/7q5EmgTEbn+Ng= -github.com/ansel1/merry v1.6.2/go.mod h1:pAcMW+2uxIgpzEON021vMtFsrymREY6faJWiiz1QGVQ= -github.com/ansel1/merry/v2 v2.0.1/go.mod h1:dD5OhpiPrVkvgseRYd+xgYlx7s6ytU3v9BTTJlDA7FM= -github.com/ansel1/merry/v2 v2.1.1 h1:Ax0gQh7Z/GfimoVg2EDBAU6CJIieWwVvhtBKJdkCE1M= -github.com/ansel1/merry/v2 v2.1.1/go.mod h1:4p/FFyQbCgqlDbseWOVQaL5USpgkE9sr5xh4V6Ry0JU= -github.com/ansel1/vespucci/v4 v4.1.1/go.mod h1:zzdrO4IgBfgcGMbGTk/qNGL8JPslmW3nPpcBHKReFYY= +github.com/ansel1/merry v1.8.0 h1:3RddCV1ubXegKphsodbkmZ4QuROep/ZaPCuwlKuCfFg= +github.com/ansel1/merry v1.8.0/go.mod h1:wJVu1mHEtEUWq5zTTX9RiWjcE+xL8y7BGYl2VTYdP7M= +github.com/ansel1/merry/v2 v2.2.1 h1:PJpynLFvIpJkn8ZGgNHLq332zIyBc/wTqp3o42ZpWdU= +github.com/ansel1/merry/v2 v2.2.1/go.mod h1:K9lCkM6tJ8s7LQVQ0ZmZ0WrB3BCyr+ZDzoqotzzoxpI= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -451,10 +108,6 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.44.293 h1:oBPrQqsyMYe61Sl/xKVvQFflXjPwYH11aKi8QR3Nhts= github.com/aws/aws-sdk-go v1.44.293/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= -github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -500,8 +153,6 @@ github.com/blevesearch/zapx/v14 v14.3.8/go.mod h1:vS6exLagv0vXmgpUbNRZC6UuEV0xwT github.com/blevesearch/zapx/v15 v15.3.11 h1:dstyZki9s10FNLsW4LpEvPQ+fmM3nX15h4wKfcBwnEg= github.com/blevesearch/zapx/v15 v15.3.11/go.mod h1:hiYbBDf5/Ud/Eji0faUmMTOyeOjcl8q1vWGgRe7+bIQ= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 h1:hjXJeBcAMS1WGENGqDpzvmgS43oECTx8UXq31UBu0Jw= -github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= @@ -510,8 +161,6 @@ github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0 github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1 h1:hXakhQtPnXH839q1pBl/GqfTSchqE+R5Fqn98Iu7UQM= github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1/go.mod h1:pAxCBpjl/0JxYZlWGP/Dyi8f/LQSCQD2WAsG/iNzqQ8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -527,14 +176,11 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -543,10 +189,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb h1:X9MwMz6mVZEWcbhsri5TwaCm/Q4USFdAAmy1T7RCGjw= -github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb/go.mod h1:pD/+9DfmmQ+xvOI1fxUltHV69BxC1aeTILPQg9Kw1hE= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768 h1:Xzl7CSuSnGsyU+9xmSU2h8w3d7Tnis66xeoNN207tLo= github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768/go.mod h1:alfmlCqcg4uw9jaoIU1nOp9RFdJLMuu8P07BCEgpgoo= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -571,10 +216,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497 h1:DIQ8EvZ8OjuPNfcV4NgsyBeZho7WsTD0JEkDM5napMI= github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497/go.mod h1:YXKUYPSqs+jDG8mvexHN2uTik4PKwg2B0WK9itQ0VrE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -589,13 +231,13 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= @@ -610,8 +252,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-graphite/carbonapi v0.16.0 h1:HvPjAKYChiwdHtNpFu33hLRpPCYA7gyyeFWpPR2XvXs= -github.com/go-graphite/carbonapi v0.16.0/go.mod h1:RQpis4h2a1kxn1s/R5LQXbumFj+kR2bRz+BebPN1Z1Q= +github.com/go-graphite/carbonapi v0.17.0 h1:6JowndAU0qsxUoBftCBy23Rgt8dCxf+1wHxfGZ3GA2E= +github.com/go-graphite/carbonapi v0.17.0/go.mod h1:EwQ1MJBzP8BBozLkgwTZdePM/5SqPSxhtY0uDdER2LQ= github.com/go-graphite/protocol v1.0.0 h1:Fqb0mkVVtfMrn6vw6Ntm3raf3gVVZCOVdZu4JosW5qE= github.com/go-graphite/protocol v1.0.0/go.mod h1:eonkg/0UGhJUYu+PshOg1NzWSUcXskr/yHeQXJHJr8Y= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -670,7 +312,6 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -702,14 +343,14 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= -github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= -github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= +github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -726,7 +367,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -757,14 +397,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -774,10 +408,6 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= @@ -795,8 +425,6 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= @@ -848,7 +476,6 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= @@ -882,8 +509,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= @@ -911,8 +536,6 @@ github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80/go.mod h1:T7SQVaLtK7m github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463 h1:SN/0TEkyYpp8tit79JPUnecebCGZsXiYYPxN8i3I6Rk= github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463/go.mod h1:rWIJAUD2hPOAyOzc3jBShAhN4CAZeLAyzUA/n8tE8ak= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -922,8 +545,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ= -github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= @@ -941,7 +564,6 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= @@ -984,15 +606,13 @@ github.com/moira-alert/blackfriday-slack v0.1.2 h1:W6VbDlHDBxoB7X+OJ+3xZZuzMcQ0q github.com/moira-alert/blackfriday-slack v0.1.2/go.mod h1:tYMK3laTzU1wgxeOpUPdw36KHD3eTyQNDfxtg1nXLWI= github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19 h1:dV1yczr6ndr5fCnBvj2SjBJxJNtnBtfZye0gDwTrPLs= github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19/go.mod h1:ktrkvZGboMQfYyBXAV05imlVxGIvVdeCn5vz91Fw1vE= -github.com/msaf1980/go-stringutils v0.1.4 h1:UwsIT0hplHVucqbknk3CoNqKkmIuSHhsbBldXxyld5U= -github.com/msaf1980/go-stringutils v0.1.4/go.mod h1:AxmV/6JuQUAtZJg5XmYATB5ZwCWgtpruVHY03dswRf8= +github.com/msaf1980/go-stringutils v0.1.6 h1:qri8o+4XLJCJYemHcvJY6xJhrGTmllUoPwayKEj4NSg= +github.com/msaf1980/go-stringutils v0.1.6/go.mod h1:xpicaTIpLAVzL0gUQkciB1zjypDGKsOCI25cKQbRQYA= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= @@ -1028,8 +648,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -1037,10 +657,10 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= @@ -1096,6 +716,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -1135,31 +759,29 @@ github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1171,15 +793,14 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= @@ -1187,6 +808,8 @@ github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4 github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg= +github.com/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig= github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU= github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -1230,24 +853,20 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk= go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1256,7 +875,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1265,7 +883,6 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= @@ -1310,9 +927,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1364,7 +979,6 @@ golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1372,17 +986,9 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= @@ -1408,14 +1014,6 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= @@ -1431,10 +1029,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1509,7 +1104,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1526,17 +1120,11 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= @@ -1545,7 +1133,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1558,8 +1145,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= @@ -1567,8 +1152,6 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1629,20 +1212,16 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= -gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= +gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -1683,24 +1262,8 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1757,7 +1320,6 @@ google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -1783,59 +1345,18 @@ google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220208230804-65c12eb4c068/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -1867,18 +1388,10 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -1896,7 +1409,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff --git a/metric_source/local/database_test.go b/metric_source/local/database_test.go new file mode 100644 index 000000000..2314bc366 --- /dev/null +++ b/metric_source/local/database_test.go @@ -0,0 +1,347 @@ +package local + +import ( + "fmt" + "math" + "testing" + "time" + + "github.com/moira-alert/moira" + + "github.com/moira-alert/moira/database/redis" + + logging "github.com/moira-alert/moira/logging/zerolog_adapter" + + . "github.com/smartystreets/goconvey/convey" +) + +type metricMock struct { + values []float64 + patterns []string +} + +type testCase struct { + metrics map[string]metricMock + from int64 + retention int64 + target string + expected map[string][]float64 +} + +func saveMetrics(database moira.Database, metrics map[string]metricMock, now, retention int64) error { + maxValues := 0 + for _, m := range metrics { + if len(m.values) > maxValues { + maxValues = len(m.values) + } + } + + timeStart := now - retention*int64(maxValues-1) + for i := range maxValues { + time := timeStart + int64(i)*retention + + metricsMap := make(map[string]*moira.MatchedMetric, len(metrics)) + for name, metric := range metrics { + if len(metric.values) < i { + continue + } + + metricsMap[name] = &moira.MatchedMetric{ + Metric: name, + Patterns: metric.patterns, + Value: metric.values[i], + Timestamp: time, + RetentionTimestamp: time, + Retention: int(retention), + } + } + + err := database.SaveMetrics(metricsMap) + if err != nil { + return err + } + } + return nil +} + +func TestLocalSourceWithDatabase(t *testing.T) { + logger, _ := logging.ConfigureLog("stdout", "info", "test", true) // nolint: govet + database := redis.NewTestDatabase(logger) + localSource := Create(database) + + defer database.Flush() + + retention := int64(60) + now := floorToMultiplier(time.Now().Unix(), retention) + nan := math.NaN() + + testCases := []testCase{ + { + metrics: map[string]metricMock{ + "metric1": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"pattern"}, + }, + "metric2": { + values: []float64{5.0, 4.0, 3.0, 2.0, 1.0}, + patterns: []string{"pattern"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "pattern", + expected: map[string][]float64{ + "metric1": {1.0, 2.0, 3.0, 4.0, 5.0}, + "metric2": {5.0, 4.0, 3.0, 2.0, 1.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric1": { + values: []float64{1.0, 3.0, 1.0, 3.0, 1.0, 3.0}, + patterns: []string{"pattern"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "movingAverage(pattern, 2)", + expected: map[string][]float64{ + "movingAverage(metric1,2)": {2.0, 2.0, 2.0, 2.0, 2.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric1": { + values: []float64{1.0, nan, 2.0, nan, nan}, + patterns: []string{"pattern"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "keepLastValue(pattern, 1)", + expected: map[string][]float64{ + "keepLastValue(metric1,1)": {1.0, 1.0, 2.0, 2.0, nan}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.1.foo": { + values: []float64{1.0, 2.0, 1.0, 2.0, 1.0}, + patterns: []string{"metric.*.*", "metric.1.foo"}, + }, + "metric.1.bar": { + values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, + patterns: []string{"metric.*.*", "metric.1.bar"}, + }, + "metric.2.foo": { + values: []float64{3.0, 2.0, 3.0, 2.0, 3.0}, + patterns: []string{"metric.*.*", "metric.2.foo"}, + }, + "metric.2.bar": { + values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, + patterns: []string{"metric.*.*", "metric.2.bar"}, + }, + }, + from: now - retention*4, + retention: retention, + target: `applyByNode(metric.*.*, 1, "movingMax(%.foo, '2m')")`, + expected: map[string][]float64{ + "movingMax(metric.1.foo,'2m')": {1.0, 2.0, 2.0, 2.0, 2.0}, + "movingMax(metric.2.foo,'2m')": {3.0, 3.0, 3.0, 3.0, 3.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "aliasByNode(metric.*, 1)", + expected: map[string][]float64{ + "foo": {1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "aliasByNode(metric.*, 2)", + expected: map[string][]float64{ + "": {1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "consolidateBy(metric.*, 'max')", + expected: map[string][]float64{ + `consolidateBy(metric.foo,"max")`: {1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.1": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + "metric.2": { + values: []float64{-1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + "metric.3": { + values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "minimumBelow(metric.*, 0)", + expected: map[string][]float64{ + "metric.2": {-1.0, 2.0, 3.0, 4.0, 5.0}, + "metric.3": {-1.0, -2.0, -3.0, -4.0, -5.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo.1": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*.*"}, + }, + "metric.foo.2": { + values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, + patterns: []string{"metric.*.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "groupByNode(metric.*.*, 1, 'sumSeries')", + expected: map[string][]float64{ + "foo": {2.5, 4.5, 6.5, 8.5, 10.5}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo.1": { + values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, + patterns: []string{"metric.*.*"}, + }, + "metric.foo.2": { + values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, + patterns: []string{"metric.*.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "groupByNode(metric.*.*, 1, 'unique')", + expected: map[string][]float64{ + "foo": {1.5, 2.5, 3.5, 4.5, 5.5}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{0, 1, 2, 3, 4, 5}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "hitcount(metric.*, '2m')", + expected: map[string][]float64{ + "hitcount(metric.foo,'2m')": {60, 300, 540}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1, nan, 3, nan, nan, 6}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "interpolate(metric.*, 1)", + expected: map[string][]float64{ + "interpolate(metric.foo)": {1, 2, 3, nan, nan, 6}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1, nan, 3, nan, nan, 6}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "interpolate(metric.*)", + expected: map[string][]float64{ + "interpolate(metric.foo)": {1, 2, 3, 4, 5, 6}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1, 2, 3, 4, 5, 6}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "smartSummarize(metric.*, '2m', 'average')", + expected: map[string][]float64{ + "smartSummarize(metric.foo,'2m','average')": {1.5, 3.5, 5.5}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1.5, 2, 3, 4, 5, 6.5}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "smartSummarize(metric.*, '3m', 'median')", + expected: map[string][]float64{ + "smartSummarize(metric.foo,'3m','median')": {2, 5}, + }, + }, + } + + Convey("Run test cases", t, func() { + for _, testCase := range testCases { + Convey(fmt.Sprintf("Target '%s'", testCase.target), func() { + database.Flush() + + err := saveMetrics(database, testCase.metrics, now, testCase.retention) + So(err, ShouldBeNil) + + result, err := localSource.Fetch(testCase.target, testCase.from, now, true) + So(err, ShouldBeNil) + + resultData := result.GetMetricsData() + resultMap := map[string][]float64{} + for _, data := range resultData { + resultMap[data.Name] = data.Values + } + + So(resultMap, shouldEqualIfNaNsEqual, testCase.expected) + }) + } + }) +} diff --git a/metric_source/local/eval.go b/metric_source/local/eval.go index 3d36c9e2a..e8f22e59b 100644 --- a/metric_source/local/eval.go +++ b/metric_source/local/eval.go @@ -3,9 +3,9 @@ package local import ( "context" "errors" - "fmt" "runtime/debug" + "github.com/ansel1/merry" "github.com/go-graphite/carbonapi/expr" "github.com/go-graphite/carbonapi/expr/helper" "github.com/go-graphite/carbonapi/expr/types" @@ -14,75 +14,139 @@ import ( metricSource "github.com/moira-alert/moira/metric_source" ) -type evalCtx struct { - from int64 - until int64 +type evaluator struct { + database moira.Database + metrics []string } -func (ctx *evalCtx) fetchAndEval(database moira.Database, target string, result *FetchResult) error { - exp, err := ctx.parse(target) - if err != nil { - return err - } +func (eval *evaluator) fetchAndEval(target string, from, until int64, result *FetchResult) (err error) { + defer func() { + if r := recover(); r != nil { + err = ErrEvaluateTargetFailedWithPanic{ + target: target, + recoverMessage: r, + stackRecord: debug.Stack(), + } + } + }() - fetchedMetrics, err := ctx.getMetricsData(database, exp) + exp, err := eval.parse(target) if err != nil { return err } - commonStep := fetchedMetrics.calculateCommonStep() - ctx.scaleToCommonStep(commonStep, fetchedMetrics) + values := make(map[parser.MetricRequest][]*types.MetricData) - rewritten, newTargets, err := ctx.rewriteExpr(exp, fetchedMetrics) + fetchedMetrics, err := expr.FetchAndEvalExp(context.Background(), eval, exp, from, until, values) if err != nil { - return err + return merry.Unwrap(err) } - if rewritten { - for _, newTarget := range newTargets { - err = ctx.fetchAndEvalNoRewrite(database, newTarget, result) - if err != nil { - return err - } + eval.writeResult(exp, fetchedMetrics, result) + + return nil +} + +// Fetch is an implementation of Evaluator interface from carbonapi. +// It returns a map the metrics requested in the current invocation, scaled to a common step. +func (eval *evaluator) Fetch( + ctx context.Context, + exprs []parser.Expr, + from, until int64, + values map[parser.MetricRequest][]*types.MetricData, +) (map[parser.MetricRequest][]*types.MetricData, error) { + fetch := newFetchCtx(0, 0) + + for _, exp := range exprs { + ms := exp.Metrics(from, until) + if err := fetch.getMetricsData(eval.database, ms); err != nil { + return nil, err } - return nil } - metricsData, err := ctx.eval(target, exp, fetchedMetrics) - if err != nil { - return err - } + fetch.scaleToCommonStep() - ctx.writeResult(exp, fetchedMetrics, metricsData, result) + eval.metrics = append(eval.metrics, fetch.fetchedMetrics.metrics...) - return nil + return fetch.fetchedMetrics.metricsMap, nil } -func (ctx *evalCtx) fetchAndEvalNoRewrite(database moira.Database, target string, result *FetchResult) error { - exp, err := ctx.parse(target) +// Eval is an implementation of Evaluator interface from carbonapi. +// It uses the raw data within the values map being passed into it to in order to evaluate the input expression. +func (eval *evaluator) Eval( + ctx context.Context, + exp parser.Expr, + from, until int64, + values map[parser.MetricRequest][]*types.MetricData, +) (results []*types.MetricData, err error) { + rewritten, newTargets, err := expr.RewriteExpr(ctx, eval, exp, from, until, values) if err != nil { - return err + return nil, err } - fetchedMetrics, err := ctx.getMetricsData(database, exp) + if rewritten { + return eval.evalRewritten(ctx, newTargets, from, until, values) + } + + results, err = expr.EvalExpr(ctx, eval, exp, from, until, values) if err != nil { - return err + if errors.Is(err, parser.ErrMissingTimeseries) { + err = nil + } else if isErrUnknownFunction(err) { + err = ErrorUnknownFunction(err) + } else { + err = ErrEvalExpr{ + target: exp.ToString(), + internalError: err, + } + } } - commonStep := fetchedMetrics.calculateCommonStep() - ctx.scaleToCommonStep(commonStep, fetchedMetrics) + return results, err +} - metricsData, err := ctx.eval(target, exp, fetchedMetrics) - if err != nil { - return err +func (eval *evaluator) evalRewritten( + ctx context.Context, + newTargets []string, + from, until int64, + values map[parser.MetricRequest][]*types.MetricData, +) (results []*types.MetricData, err error) { + for _, target := range newTargets { + exp, _, err := parser.ParseExpr(target) + if err != nil { + return nil, err + } + + var targetValues map[parser.MetricRequest][]*types.MetricData + targetValues, err = eval.Fetch(ctx, []parser.Expr{exp}, from, until, values) + if err != nil { + return nil, err + } + + result, err := eval.Eval(ctx, exp, from, until, targetValues) + if err != nil { + return nil, err + } + + results = append(results, result...) } - ctx.writeResult(exp, fetchedMetrics, metricsData, result) + return results, nil +} - return nil +func (eval *evaluator) writeResult(exp parser.Expr, metricsData []*types.MetricData, result *FetchResult) { + result.Metrics = append(result.Metrics, eval.metrics...) + for _, mr := range exp.Metrics(0, 0) { + result.Patterns = append(result.Patterns, mr.Metric) + } + + for _, metricData := range metricsData { + md := newMetricDataFromGraphite(metricData, len(result.Metrics) != len(result.Patterns)) + result.MetricsData = append(result.MetricsData, md) + } } -func (ctx *evalCtx) parse(target string) (parser.Expr, error) { +func (eval *evaluator) parse(target string) (parser.Expr, error) { parsedExpr, _, err := parser.ParseExpr(target) if err != nil { return nil, ErrParseExpr{ @@ -93,109 +157,71 @@ func (ctx *evalCtx) parse(target string) (parser.Expr, error) { return parsedExpr, nil } -func (ctx *evalCtx) getMetricsData(database moira.Database, parsedExpr parser.Expr) (*fetchedMetrics, error) { - metricRequests := parsedExpr.Metrics() +type fetchCtx struct { + from int64 + until int64 + fetchedMetrics *fetchedMetrics +} - metrics := make([]string, 0) - metricsMap := make(map[parser.MetricRequest][]*types.MetricData) +func newFetchCtx(from, until int64) *fetchCtx { + return &fetchCtx{ + from, + until, + &fetchedMetrics{ + metricsMap: make(map[parser.MetricRequest][]*types.MetricData), + metrics: make([]string, 0), + }, + } +} +func (ctx *fetchCtx) getMetricsData(database moira.Database, metricRequests []parser.MetricRequest) error { fetchData := fetchData{database} for _, mr := range metricRequests { + // Other fields are used in carbon for database side consolidations + request := parser.MetricRequest{ + Metric: mr.Metric, + From: mr.From, + Until: mr.Until, + } + from := mr.From + ctx.from until := mr.Until + ctx.until metricNames, err := fetchData.fetchMetricNames(mr.Metric) if err != nil { - return nil, err + return err } - timer := NewTimerRoundingTimestamps(from, until, metricNames.retention) + timer := newTimerRoundingTimestamps(from, until, metricNames.retention) metricsData, err := fetchData.fetchMetricValues(mr.Metric, metricNames, timer) if err != nil { - return nil, err + return err } - metricsMap[mr] = metricsData - metrics = append(metrics, metricNames.metrics...) + ctx.fetchedMetrics.metricsMap[request] = metricsData + ctx.fetchedMetrics.metrics = append(ctx.fetchedMetrics.metrics, metricNames.metrics...) } - return &fetchedMetrics{metricsMap, metrics}, nil + return nil } -func (ctx *evalCtx) scaleToCommonStep(retention int64, fetchedMetrics *fetchedMetrics) { - from, until := RoundTimestamps(ctx.from, ctx.until, retention) - ctx.from, ctx.until = from, until +func (ctx *fetchCtx) scaleToCommonStep() { + retention := ctx.fetchedMetrics.calculateCommonStep() metricMap := make(map[parser.MetricRequest][]*types.MetricData) - for metricRequest, metricData := range fetchedMetrics.metricsMap { - metricRequest.From += from - metricRequest.Until += until + for metricRequest, metricData := range ctx.fetchedMetrics.metricsMap { + metricRequest.From += ctx.from + metricRequest.Until += ctx.until metricData = helper.ScaleToCommonStep(metricData, retention) metricMap[metricRequest] = metricData } - fetchedMetrics.metricsMap = metricMap + ctx.fetchedMetrics.metricsMap = metricMap } -func (ctx *evalCtx) rewriteExpr(parsedExpr parser.Expr, metrics *fetchedMetrics) (bool, []string, error) { - rewritten, newTargets, err := expr.RewriteExpr( - context.Background(), - parsedExpr, - ctx.from, - ctx.until, - metrics.metricsMap, - ) - - if err != nil && !errors.Is(err, parser.ErrMissingTimeseries) { - return false, nil, fmt.Errorf("failed RewriteExpr: %s", err.Error()) - } - return rewritten, newTargets, nil -} - -func (ctx *evalCtx) eval(target string, parsedExpr parser.Expr, metrics *fetchedMetrics) (result []*types.MetricData, err error) { - defer func() { - if r := recover(); r != nil { - result = nil - err = ErrEvaluateTargetFailedWithPanic{ - target: target, - recoverMessage: r, - stackRecord: debug.Stack(), - } - } - }() - - result, err = expr.EvalExpr(context.Background(), parsedExpr, ctx.from, ctx.until, metrics.metricsMap) - if err != nil { - if errors.Is(err, parser.ErrMissingTimeseries) { - err = nil - } else if isErrUnknownFunction(err) { - err = ErrorUnknownFunction(err) - } else { - err = ErrEvalExpr{ - target: target, - internalError: err, - } - } - } - - return result, err -} - -func (ctx *evalCtx) writeResult(exp parser.Expr, metrics *fetchedMetrics, metricsData []*types.MetricData, result *FetchResult) { - for _, metricData := range metricsData { - md := newMetricDataFromGraphit(metricData, metrics.hasWildcard()) - result.MetricsData = append(result.MetricsData, md) - } - - result.Metrics = append(result.Metrics, metrics.metrics...) - for _, mr := range exp.Metrics() { - result.Patterns = append(result.Patterns, mr.Metric) - } -} - -func newMetricDataFromGraphit(md *types.MetricData, wildcard bool) metricSource.MetricData { +func newMetricDataFromGraphite(md *types.MetricData, wildcard bool) metricSource.MetricData { return metricSource.MetricData{ Name: md.Name, StartTime: md.StartTime, @@ -211,10 +237,6 @@ type fetchedMetrics struct { metrics []string } -func (m *fetchedMetrics) hasWildcard() bool { - return len(m.metrics) == 0 -} - func (m *fetchedMetrics) calculateCommonStep() int64 { commonStep := int64(1) for _, metricsData := range m.metricsMap { diff --git a/metric_source/local/fetchdata.go b/metric_source/local/fetchdata.go index e2a7a4a6d..880e46432 100644 --- a/metric_source/local/fetchdata.go +++ b/metric_source/local/fetchdata.go @@ -38,7 +38,7 @@ func (fd *fetchData) fetchMetricNames(pattern string) (*metricsWithRetention, er return &metricsWithRetention{retention, metrics}, nil } -func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithRetention, timer Timer) ([]*types.MetricData, error) { +func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithRetention, timer timer) ([]*types.MetricData, error) { if len(metrics.metrics) == 0 { return fetchDataNoMetrics(timer, pattern), nil } @@ -58,7 +58,7 @@ func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithReten return metricsData, nil } -func fetchDataNoMetrics(timer Timer, pattern string) []*types.MetricData { +func fetchDataNoMetrics(timer timer, pattern string) []*types.MetricData { dataList := map[string][]*moira.MetricValue{pattern: make([]*moira.MetricValue, 0)} valuesMap := unpackMetricsValues(dataList, timer) metricsData := createMetricData(pattern, timer, valuesMap[pattern]) @@ -66,7 +66,7 @@ func fetchDataNoMetrics(timer Timer, pattern string) []*types.MetricData { return []*types.MetricData{metricsData} } -func createMetricData(metric string, timer Timer, values []float64) *types.MetricData { +func createMetricData(metric string, timer timer, values []float64) *types.MetricData { fetchResponse := pb.FetchResponse{ Name: metric, StartTime: timer.startTime, @@ -77,7 +77,7 @@ func createMetricData(metric string, timer Timer, values []float64) *types.Metri return &types.MetricData{FetchResponse: fetchResponse, Tags: tags.ExtractTags(metric)} } -func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer Timer) map[string][]float64 { +func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer timer) map[string][]float64 { valuesMap := make(map[string][]float64, len(metricsData)) for metric, metricData := range metricsData { valuesMap[metric] = unpackMetricValues(metricData, timer) @@ -85,13 +85,13 @@ func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer Time return valuesMap } -func unpackMetricValues(metricData []*moira.MetricValue, timer Timer) []float64 { +func unpackMetricValues(metricData []*moira.MetricValue, timer timer) []float64 { points := make(map[int]*moira.MetricValue, len(metricData)) for _, metricValue := range metricData { - points[timer.GetTimeSlot(metricValue.RetentionTimestamp)] = metricValue + points[timer.getTimeSlot(metricValue.RetentionTimestamp)] = metricValue } - numberOfTimeSlots := timer.NumberOfTimeSlots() + numberOfTimeSlots := timer.numberOfTimeSlots() values := make([]float64, 0, numberOfTimeSlots) diff --git a/metric_source/local/fetchdata_test.go b/metric_source/local/fetchdata_test.go index c55142ab3..c5b4a0aae 100644 --- a/metric_source/local/fetchdata_test.go +++ b/metric_source/local/fetchdata_test.go @@ -18,7 +18,7 @@ func BenchmarkUnpackMetricsValues(b *testing.B) { var until int64 = 1317 var retention int64 = 10 - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) metricsCount := 7300 @@ -47,7 +47,7 @@ func BenchmarkUnpackMetricValues(b *testing.B) { var until int64 = 317 var retention int64 = 10 - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) metricsValues := make([]*moira.MetricValue, 0) @@ -75,7 +75,7 @@ func TestFetchDataErrors(t *testing.T) { pattern := "super-puper-pattern" metric := "super-puper-metric" - timer := NewTimerRoundingTimestamps(17, 67, 10) + timer := newTimerRoundingTimestamps(17, 67, 10) retentionErr := fmt.Errorf("Ooops, retention error") patternErr := fmt.Errorf("Ooops, pattern error") @@ -145,7 +145,7 @@ func TestFetchData(t *testing.T) { var from int64 = 17 var until int64 = 67 var retention int64 = 10 - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) Convey("Test no metrics", t, func() { dataBase.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) @@ -153,7 +153,7 @@ func TestFetchData(t *testing.T) { metrics, err := fetchedData.fetchMetricNames(pattern) So(err, ShouldBeNil) - timerNoMetrics := NewTimerRoundingTimestamps(from, until, metrics.retention) + timerNoMetrics := newTimerRoundingTimestamps(from, until, metrics.retention) metricValues, err := fetchedData.fetchMetricValues(pattern, metrics, timerNoMetrics) So(metricValues[0], shouldEqualIfNaNsEqual, &types.MetricData{ @@ -235,28 +235,28 @@ func TestUnpackMetricValuesNoData(t *testing.T) { metricData := map[string][]*moira.MetricValue{"metric": make([]*moira.MetricValue, 0)} Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 0 until 10", t, func() { - timer := NewTimerRoundingTimestamps(0, 10, retention) + timer := newTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN(), math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 1 until 11", t, func() { - timer := NewTimerRoundingTimestamps(1, 11, retention) + timer := newTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) @@ -273,49 +273,49 @@ func TestUnpackMetricValues(t *testing.T) { }} Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{}) }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.0}) }) Convey("From 1 until 11", t, func() { - timer := NewTimerRoundingTimestamps(1, 11, retention) + timer := newTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.0}) }) Convey("From 0 until 10", t, func() { - timer := NewTimerRoundingTimestamps(0, 10, retention) + timer := newTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.0}) }) Convey("From 0 until 11", t, func() { - timer := NewTimerRoundingTimestamps(0, 11, retention) + timer := newTimerRoundingTimestamps(0, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 0 until 19", t, func() { - timer := NewTimerRoundingTimestamps(0, 19, retention) + timer := newTimerRoundingTimestamps(0, 19, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 1 until 30", t, func() { - timer := NewTimerRoundingTimestamps(1, 30, retention) + timer := newTimerRoundingTimestamps(1, 30, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) @@ -330,7 +330,7 @@ func TestMultipleSeriesNoData(t *testing.T) { } Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -338,7 +338,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN()}) @@ -346,7 +346,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 1 until 5", t, func() { - timer := NewTimerRoundingTimestamps(1, 5, retention) + timer := newTimerRoundingTimestamps(1, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -354,7 +354,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 0 until 5", t, func() { - timer := NewTimerRoundingTimestamps(0, 5, retention) + timer := newTimerRoundingTimestamps(0, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN()}) @@ -362,7 +362,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 5 until 30", t, func() { - timer := NewTimerRoundingTimestamps(5, 30, retention) + timer := newTimerRoundingTimestamps(5, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN(), math.NaN(), math.NaN()}) @@ -387,7 +387,7 @@ func TestMultipleSeries(t *testing.T) { } Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -395,7 +395,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{100.0}) @@ -403,7 +403,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 1 until 5", t, func() { - timer := NewTimerRoundingTimestamps(1, 5, retention) + timer := newTimerRoundingTimestamps(1, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -411,7 +411,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 5", t, func() { - timer := NewTimerRoundingTimestamps(0, 5, retention) + timer := newTimerRoundingTimestamps(0, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.0}) @@ -419,7 +419,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 9", t, func() { - timer := NewTimerRoundingTimestamps(0, 9, retention) + timer := newTimerRoundingTimestamps(0, 9, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00}) @@ -427,7 +427,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 10", t, func() { - timer := NewTimerRoundingTimestamps(0, 10, retention) + timer := newTimerRoundingTimestamps(0, 10, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) @@ -435,7 +435,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 1 until 11", t, func() { - timer := NewTimerRoundingTimestamps(1, 11, retention) + timer := newTimerRoundingTimestamps(1, 11, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{200.00}) @@ -443,7 +443,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 30", t, func() { - timer := NewTimerRoundingTimestamps(0, 30, retention) + timer := newTimerRoundingTimestamps(0, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00, 300.00, math.NaN()}) @@ -451,7 +451,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 5 until 30", t, func() { - timer := NewTimerRoundingTimestamps(5, 30, retention) + timer := newTimerRoundingTimestamps(5, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) @@ -468,49 +468,49 @@ func TestShiftedSeries(t *testing.T) { }} Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{}) }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.0}) }) Convey("From 1 until 11", t, func() { - timer := NewTimerRoundingTimestamps(1, 11, retention) + timer := newTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.0}) }) Convey("From 0 until 10", t, func() { - timer := NewTimerRoundingTimestamps(0, 10, retention) + timer := newTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.0}) }) Convey("From 0 until 11", t, func() { - timer := NewTimerRoundingTimestamps(0, 11, retention) + timer := newTimerRoundingTimestamps(0, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 0 until 19", t, func() { - timer := NewTimerRoundingTimestamps(0, 19, retention) + timer := newTimerRoundingTimestamps(0, 19, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 1 until 30", t, func() { - timer := NewTimerRoundingTimestamps(1, 30, retention) + timer := newTimerRoundingTimestamps(1, 30, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) diff --git a/metric_source/local/local.go b/metric_source/local/local.go index 6183200bf..f844f79aa 100644 --- a/metric_source/local/local.go +++ b/metric_source/local/local.go @@ -33,7 +33,7 @@ func (local *Local) IsConfigured() (bool, error) { return true, nil } -// IsConfigured always returns true. It easy to configure local source =). +// IsAvailable always returns true. It easy to configure local source =). func (local *Local) IsAvailable() (bool, error) { return true, nil } @@ -45,9 +45,9 @@ func (local *Local) Fetch(target string, from int64, until int64, allowRealTimeA from = moira.MaxInt64(from, until-local.database.GetMetricsTTLSeconds()) result := CreateEmptyFetchResult() - ctx := evalCtx{from, until} + eval := evaluator{local.database, make([]string, 0)} - err := ctx.fetchAndEval(local.database, target, result) + err := eval.fetchAndEval(target, from, until, result) if err != nil { return nil, err } diff --git a/metric_source/local/local_test.go b/metric_source/local/local_test.go index 6f0feb8a9..716f0040e 100644 --- a/metric_source/local/local_test.go +++ b/metric_source/local/local_test.go @@ -12,6 +12,7 @@ import ( "github.com/go-graphite/carbonapi/pkg/parser" "github.com/google/go-cmp/cmp" "github.com/moira-alert/moira" + metricSource "github.com/moira-alert/moira/metric_source" mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" . "github.com/smartystreets/goconvey/convey" @@ -88,14 +89,17 @@ func TestLocalSourceFetchErrors(t *testing.T) { }) Convey("Panic while evaluate target", t, func() { - database.EXPECT().GetPatternMetrics(pattern1).Return([]string{metric1}, nil) - database.EXPECT().GetMetricRetention(metric1).Return(retention, nil) + // moving* functions with an integer second parameter require two metric fetches + database.EXPECT().GetPatternMetrics(pattern1).Return([]string{metric1}, nil).Times(2) + database.EXPECT().GetMetricRetention(metric1).Return(retention, nil).Times(2) database.EXPECT().GetMetricsValues([]string{metric1}, retentionFrom, retentionUntil-1).Return(dataList, nil) + database.EXPECT().GetMetricsValues([]string{metric1}, int64(30), retentionUntil-1).Return(dataList, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("movingAverage(super.puper.pattern, -1)", from, until, true) expectedErrSubstring := strings.Split(ErrEvaluateTargetFailedWithPanic{target: "movingAverage(super.puper.pattern, -1)"}.Error(), ":")[0] + So(err, ShouldNotBeNil) So(err.Error(), ShouldStartWith, expectedErrSubstring) So(result, ShouldBeNil) }) @@ -108,7 +112,6 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { defer mockCtrl.Finish() pattern := pattern1 - pattern2 := pattern2 var metricsTTL int64 = 3600 @@ -116,12 +119,12 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) - result, err := localSource.Fetch("aliasByNode(super.puper.pattern, 2)", 17, 17, false) + result, err := localSource.Fetch("super.puper.pattern", 17, 17, false) So(err, ShouldBeNil) So(result, shouldEqualIfNaNsEqual, &FetchResult{ MetricsData: []metricSource.MetricData{{ - Name: "pattern", + Name: "super.puper.pattern", StartTime: 60, StopTime: 60, StepTime: 60, @@ -155,7 +158,6 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { Convey("Single pattern, from 7 until 57", t, func() { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) - database.EXPECT().GetPatternMetrics(pattern2).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("aliasByNode(super.puper.pattern, 2)", 7, 57, true) @@ -176,6 +178,7 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { Convey("Two patterns, from 17 until 67", t, func() { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) + database.EXPECT().GetPatternMetrics(pattern2).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("alias(sum(super.puper.pattern, super.duper.pattern), 'pattern')", 17, 67, true) @@ -254,6 +257,7 @@ func TestLocalSourceFetchMultipleMetrics(t *testing.T) { StopTime: retentionUntil, StepTime: retention, Values: []float64{2, 2, 2, 2, 2}, + Wildcard: true, }, }, Metrics: metrics, @@ -262,6 +266,79 @@ func TestLocalSourceFetchMultipleMetrics(t *testing.T) { }) } +func TestLocalSourceApplyByNode(t *testing.T) { + mockCtrl := gomock.NewController(t) + database := mock_moira_alert.NewMockDatabase(mockCtrl) + localSource := Create(database) + defer mockCtrl.Finish() + + var from int64 = 17 + var until int64 = 67 + var retentionFrom int64 = 20 + var retentionUntil int64 = 70 + var retention int64 = 10 + var metricsTTL int64 = 3600 + + Convey("Test success evaluate multiple metrics with pow function", t, func() { + metrics := []string{ + "my.pattern.foo", + } + + metricList := make(map[string][]*moira.MetricValue) + metricList["my.pattern.foo"] = []*moira.MetricValue{ + {RetentionTimestamp: 20, Timestamp: 23, Value: 0.5}, + {RetentionTimestamp: 30, Timestamp: 33, Value: 0.4}, + {RetentionTimestamp: 40, Timestamp: 43, Value: 0.5}, + {RetentionTimestamp: 50, Timestamp: 53, Value: 0.5}, + {RetentionTimestamp: 60, Timestamp: 63, Value: 0.5}, + } + + metrics2 := []string{ + "your.my.pattern.foo", + } + + metricList2 := make(map[string][]*moira.MetricValue) + metricList2["your.my.pattern.foo"] = []*moira.MetricValue{ + {RetentionTimestamp: 20, Timestamp: 23, Value: 1}, + {RetentionTimestamp: 30, Timestamp: 33, Value: 2}, + {RetentionTimestamp: 40, Timestamp: 43, Value: 3}, + {RetentionTimestamp: 50, Timestamp: 53, Value: 4}, + {RetentionTimestamp: 60, Timestamp: 63, Value: 5}, + } + + database.EXPECT().GetPatternMetrics("my.pattern.*").Return(metrics, nil) + database.EXPECT().GetMetricRetention(metrics[0]).Return(retention, nil) + database.EXPECT().GetMetricsValues(metrics, retentionFrom, retentionUntil-1).Return(metricList, nil) + + database.EXPECT().GetPatternMetrics("your.my.pattern.foo").Return(metrics2, nil).AnyTimes() + database.EXPECT().GetMetricRetention(metrics2[0]).Return(retention, nil).AnyTimes() + database.EXPECT().GetMetricsValues(metrics2, retentionFrom, retentionUntil-1).Return(metricList2, nil) + + database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) + + result, err := localSource.Fetch(`alias(applyByNode(my.pattern.*, 2, "your.%"), 'min')`, from, until, true) + + So(err, ShouldBeNil) + So(result, shouldEqualIfNaNsEqual, &FetchResult{ + MetricsData: []metricSource.MetricData{ + { + Name: "min", + StartTime: retentionFrom, + StopTime: retentionUntil, + StepTime: retention, + Values: []float64{1, 2, 3, 4, 5}, + Wildcard: true, + }, + }, + Metrics: []string{ + "my.pattern.foo", + "your.my.pattern.foo", + }, + Patterns: []string{"my.pattern.*"}, + }) + }) +} + func TestLocalSourceFetch(t *testing.T) { mockCtrl := gomock.NewController(t) database := mock_moira_alert.NewMockDatabase(mockCtrl) @@ -495,49 +572,49 @@ func TestLocalMetricsTTL(t *testing.T) { } func TestLocal_evalExpr(t *testing.T) { + mockCtrl := gomock.NewController(t) + Convey("When everything is correct, we don't return any error", t, func() { - ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `seriesByTag('name=k8s.dev-cl1.kube_pod_status_ready', 'condition!=true', 'namespace=default', 'pod=~*')` - expression, err := ctx.parse(target) + _, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) So(err, ShouldBeNil) - res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) - So(err, ShouldBeNil) - So(res, ShouldBeNil) }) Convey("When get panic, it should return error", t, func() { - ctx := evalCtx{from: 0, until: 0} - - expression, _ := ctx.parse(`;fg`) - res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) - So(err.Error(), ShouldContainSubstring, "panic while evaluate target target: message: 'runtime error: invalid memory address or nil pointer dereference") - So(res, ShouldBeNil) + res, err := evalWithNoMetricsHelper(mockCtrl, `;fg`, 0, 0) + So(err.Error(), ShouldContainSubstring, "failed to parse target") + So(res.Metrics, ShouldBeEmpty) }) Convey("When no metrics, should not return error", t, func() { - ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `alias( divideSeries( alias( sumSeries( exclude( groupByNode( OFD.Production.{ofd-api,ofd-front}.*.fns-service-client.v120.*.GetCashboxRegistrationInformationAsync.ResponseCode.*.Meter.Rate-15-min-Requests-per-s, 9, "sum" ), "Ok" ) ), "bad" ), alias( sumSeries( OFD.Production.{ofd-api,ofd-front}.*.fns-service-client.v120.*.GetCashboxRegistrationInformationAsync.ResponseCode.*.Meter.Rate-15-min-Requests-per-s ), "total" ) ), "Result" )` - expression, err := ctx.parse(target) + res, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) So(err, ShouldBeNil) - res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: make(map[parser.MetricRequest][]*types.MetricData)}) - So(err, ShouldBeNil) - So(res, ShouldBeEmpty) + So(res.Metrics, ShouldBeEmpty) }) Convey("When got unknown func, should return error", t, func() { - ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `vf('name=k8s.dev-cl1.kube_pod_status_ready', 'condition!=true', 'namespace=default', 'pod=~*')` - expression, _ := ctx.parse(target) - res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) - So(err, ShouldBeError) + res, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) So(err.Error(), ShouldResemble, `Unknown graphite function: "vf"`) - So(res, ShouldBeNil) + So(res.Metrics, ShouldBeEmpty) }) } +func evalWithNoMetricsHelper(mockCtrl *gomock.Controller, target string, from, until int64) (*FetchResult, error) { + database := mock_moira_alert.NewMockDatabase(mockCtrl) + database.EXPECT().GetPatternMetrics(gomock.Any()).Return([]string{}, nil).AnyTimes() + eval := evaluator{database, make([]string, 0)} + + result := CreateEmptyFetchResult() + err := eval.fetchAndEval(target, from, until, result) + + return result, err +} + func shouldEqualIfNaNsEqual(actual interface{}, expected ...interface{}) string { allowUnexportedOption := cmp.AllowUnexported(types.MetricData{}) diff --git a/metric_source/local/timer.go b/metric_source/local/timer.go index 224977bc7..98f57bce4 100644 --- a/metric_source/local/timer.go +++ b/metric_source/local/timer.go @@ -1,35 +1,32 @@ package local -// Timer is responsible for managing time ranges and metrics' timeslots. -type Timer struct { +type timer struct { startTime int64 stopTime int64 stepTime int64 } -// Rounds start and stop time in a specific manner requered by carbonapi. -func RoundTimestamps(startTime, stopTime, retention int64) (roundedStart, roundedStop int64) { - return ceilToMultiplier(startTime, retention), floorToMultiplier(stopTime, retention) + retention +func roundTimestamps(startTime, stopTime, retention int64) (roundedStart, roundedStop int64) { + until := floorToMultiplier(stopTime, retention) + retention + from := ceilToMultiplier(startTime, retention) + + return from, until } -// Creates new timer rounding start and stop time in a specific manner requered by carbonapi. -// Timers should be created only with this function. -func NewTimerRoundingTimestamps(startTime int64, stopTime int64, retention int64) Timer { - startTime, stopTime = RoundTimestamps(startTime, stopTime, retention) - return Timer{ +func newTimerRoundingTimestamps(startTime int64, stopTime int64, retention int64) timer { + startTime, stopTime = roundTimestamps(startTime, stopTime, retention) + return timer{ startTime: startTime, stopTime: stopTime, stepTime: retention, } } -// Returns the number of timeslots from this timer's startTime until its stopTime with it's retention. -func (t Timer) NumberOfTimeSlots() int { - return t.GetTimeSlot(t.stopTime) +func (t timer) numberOfTimeSlots() int { + return t.getTimeSlot(t.stopTime) } -// Returns the index of given timestamp (rounded by timestamp) in this timer's time range. -func (t Timer) GetTimeSlot(timestamp int64) int { +func (t timer) getTimeSlot(timestamp int64) int { timeSlot := floorToMultiplier(timestamp-t.startTime, t.stepTime) / t.stepTime return int(timeSlot) } diff --git a/metric_source/local/timer_test.go b/metric_source/local/timer_test.go index cf25214e6..29563ca4a 100644 --- a/metric_source/local/timer_test.go +++ b/metric_source/local/timer_test.go @@ -14,26 +14,26 @@ func TestTimerNumberOfTimeSlots(t *testing.T) { Convey("Given `from` is divisible by retention", t, func() { for _, from := range []int64{0, retention} { until := from + retention*steps - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) - So(timer.NumberOfTimeSlots(), ShouldEqual, steps+1) + So(timer.numberOfTimeSlots(), ShouldEqual, steps+1) } }) Convey("Given `from` is divisible by retention", t, func() { from := int64(0) until := int64(0) - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) - So(timer.NumberOfTimeSlots(), ShouldEqual, 1) + So(timer.numberOfTimeSlots(), ShouldEqual, 1) }) Convey("Given `from` is not divisible by retention", t, func() { for from := int64(1); from < retention; from++ { until := from + retention*steps - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) - So(timer.NumberOfTimeSlots(), ShouldEqual, steps) + So(timer.numberOfTimeSlots(), ShouldEqual, steps) } }) } @@ -43,7 +43,7 @@ func TestTimerGetTimeSlot(t *testing.T) { retention := int64(10) from := int64(10) until := int64(60) - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) testCases := []struct { timestamp int64 @@ -61,7 +61,7 @@ func TestTimerGetTimeSlot(t *testing.T) { } for _, testCase := range testCases { - actual := timer.GetTimeSlot(testCase.timestamp) + actual := timer.getTimeSlot(testCase.timestamp) So(actual, ShouldEqual, testCase.timeSlot) } }) diff --git a/notifier/config.go b/notifier/config.go index a89b4b7ec..34c6e53d4 100644 --- a/notifier/config.go +++ b/notifier/config.go @@ -4,6 +4,7 @@ import ( "time" ) +// There is a duplicate of this constant in database package to prevent cyclic dependencies. const NotificationsLimitUnlimited = int64(-1) // Config is sending settings including log settings. From 4a66a39e2832048e298c5ab010b391c9eca03b99 Mon Sep 17 00:00:00 2001 From: Tetrergeru Date: Tue, 8 Oct 2024 14:13:10 +0200 Subject: [PATCH 26/36] Revert "feat: update cabonapi to v0.17.0 (#1074)" (#1111) This reverts commit 1e92a14aad02c5c545e63275df243b9b7c2f4622. --- database/redis/database.go | 11 +- database/redis/notification.go | 19 +- database/redis/notification_test.go | 31 +- go.mod | 51 +-- go.sum | 592 +++++++++++++++++++++++--- metric_source/local/database_test.go | 347 --------------- metric_source/local/eval.go | 258 +++++------ metric_source/local/fetchdata.go | 14 +- metric_source/local/fetchdata_test.go | 74 ++-- metric_source/local/local.go | 6 +- metric_source/local/local_test.go | 131 ++---- metric_source/local/timer.go | 27 +- metric_source/local/timer_test.go | 16 +- notifier/config.go | 1 - 14 files changed, 810 insertions(+), 768 deletions(-) delete mode 100644 metric_source/local/database_test.go diff --git a/database/redis/database.go b/database/redis/database.go index 03cf0cd56..5e3497080 100644 --- a/database/redis/database.go +++ b/database/redis/database.go @@ -91,11 +91,9 @@ func NewDatabase(logger moira.Logger, config DatabaseConfig, nh NotificationHist // NewTestDatabase use it only for tests. func NewTestDatabase(logger moira.Logger) *DbConnector { - return NewDatabase( - logger, DatabaseConfig{ - Addrs: []string{"0.0.0.0:6379"}, - MetricsTTL: time.Hour, - }, + return NewDatabase(logger, DatabaseConfig{ + Addrs: []string{"0.0.0.0:6379"}, + }, NotificationHistoryConfig{ NotificationHistoryTTL: time.Hour * 48, }, @@ -106,8 +104,7 @@ func NewTestDatabase(logger moira.Logger) *DbConnector { TransactionHeuristicLimit: 10000, ResaveTime: 30 * time.Second, }, - testSource, - ) + testSource) } // NewTestDatabaseWithIncorrectConfig use it only for tests. diff --git a/database/redis/notification.go b/database/redis/notification.go index 82caf03d4..a8f9c3f3c 100644 --- a/database/redis/notification.go +++ b/database/redis/notification.go @@ -8,17 +8,14 @@ import ( "strings" "time" + "github.com/moira-alert/moira/notifier" + "github.com/go-redis/redis/v8" "github.com/moira-alert/moira" "github.com/moira-alert/moira/database/redis/reply" ) -// Separate const to prevent cyclic dependencies. -// Original const is declared in notifier package, notifier depends on all metric source packages. -// Thus it prevents us from using database in tests for local metric source. -const notificationsLimitUnlimited = int64(-1) - type notificationTypes struct { Valid, ToRemove, ToResaveNew, ToResaveOld []*moira.ScheduledNotification } @@ -297,8 +294,8 @@ func (connector *DbConnector) FetchNotifications(to int64, limit int64) ([]*moir } // No limit - if limit == notificationsLimitUnlimited { - return connector.fetchNotifications(to, notificationsLimitUnlimited) + if limit == notifier.NotificationsLimitUnlimited { + return connector.fetchNotifications(to, notifier.NotificationsLimitUnlimited) } count, err := connector.notificationsCount(to) @@ -308,7 +305,7 @@ func (connector *DbConnector) FetchNotifications(to int64, limit int64) ([]*moir // Hope count will be not greater then limit when we call fetchNotificationsNoLimit if limit > connector.notification.TransactionHeuristicLimit && count < limit/2 { - return connector.fetchNotifications(to, notificationsLimitUnlimited) + return connector.fetchNotifications(to, notifier.NotificationsLimitUnlimited) } return connector.fetchNotifications(to, limit) @@ -357,7 +354,7 @@ func (connector *DbConnector) fetchNotifications(to int64, limit int64) ([]*moir // sorted by timestamp in one transaction with or without limit, depending on whether limit is nil. func getNotificationsInTxWithLimit(ctx context.Context, tx *redis.Tx, to int64, limit int64) ([]*moira.ScheduledNotification, error) { var rng *redis.ZRangeBy - if limit != notificationsLimitUnlimited { + if limit != notifier.NotificationsLimitUnlimited { rng = &redis.ZRangeBy{Min: "-inf", Max: strconv.FormatInt(to, 10), Offset: 0, Count: limit} } else { rng = &redis.ZRangeBy{Min: "-inf", Max: strconv.FormatInt(to, 10)} @@ -396,7 +393,7 @@ func getLimitedNotifications( limitedNotifications := notifications - if limit != notificationsLimitUnlimited { + if limit != notifier.NotificationsLimitUnlimited { limitedNotifications = limitNotifications(notifications) lastTs := limitedNotifications[len(limitedNotifications)-1].Timestamp @@ -404,7 +401,7 @@ func getLimitedNotifications( // this means that all notifications have same timestamp, // we hope that all notifications with same timestamp should fit our memory var err error - limitedNotifications, err = getNotificationsInTxWithLimit(ctx, tx, lastTs, notificationsLimitUnlimited) + limitedNotifications, err = getNotificationsInTxWithLimit(ctx, tx, lastTs, notifier.NotificationsLimitUnlimited) if err != nil { return nil, fmt.Errorf("failed to get notification without limit in transaction: %w", err) } diff --git a/database/redis/notification_test.go b/database/redis/notification_test.go index c287f78d5..d0a25ce56 100644 --- a/database/redis/notification_test.go +++ b/database/redis/notification_test.go @@ -10,6 +10,7 @@ import ( "github.com/moira-alert/moira" "github.com/moira-alert/moira/clock" logging "github.com/moira-alert/moira/logging/zerolog_adapter" + "github.com/moira-alert/moira/notifier" "github.com/stretchr/testify/assert" . "github.com/smartystreets/goconvey/convey" @@ -58,7 +59,7 @@ func TestScheduledNotification(t *testing.T) { }) Convey("Test fetch notifications", func() { - actual, err := database.FetchNotifications(now-database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint + actual, err := database.FetchNotifications(now-database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld}) @@ -67,7 +68,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 2) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ification, ¬ificationNew}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ification, ¬ificationNew}) @@ -127,7 +128,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 0) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) }) @@ -166,7 +167,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 0) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) }) @@ -196,7 +197,7 @@ func TestScheduledNotificationErrorConnection(t *testing.T) { So(err, ShouldNotBeNil) So(total, ShouldEqual, 0) - actual2, err := database.FetchNotifications(0, notificationsLimitUnlimited) + actual2, err := database.FetchNotifications(0, notifier.NotificationsLimitUnlimited) So(err, ShouldNotBeNil) So(actual2, ShouldBeNil) @@ -283,7 +284,7 @@ func TestFetchNotifications(t *testing.T) { Convey("Test fetch notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) - actual, err := database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint + actual, err := database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) @@ -329,7 +330,7 @@ func TestGetNotificationsInTxWithLimit(t *testing.T) { Convey("Test with zero notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{}) err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notificationsLimitUnlimited) + actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notifier.NotificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) return nil @@ -343,7 +344,7 @@ func TestGetNotificationsInTxWithLimit(t *testing.T) { Convey("Test all notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notificationsLimitUnlimited) + actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notifier.NotificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) return nil @@ -417,7 +418,7 @@ func TestGetLimitedNotifications(t *testing.T) { Convey("Test all notifications with different timestamps without limit", func() { notifications := []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew} err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getLimitedNotifications(ctx, tx, notificationsLimitUnlimited, notifications) + actual, err := getLimitedNotifications(ctx, tx, notifier.NotificationsLimitUnlimited, notifications) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) return nil @@ -912,7 +913,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) @@ -935,7 +936,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) @@ -946,7 +947,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Test all notification with ts and without limit in db", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld, notification4}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification4, ¬ification, ¬ificationNew}) @@ -1015,7 +1016,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew3}) @@ -1051,7 +1052,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew2}) @@ -1091,7 +1092,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew3}) diff --git a/go.mod b/go.mod index b309808bd..a2d3a9c52 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible github.com/PagerDuty/go-pagerduty v1.5.1 - github.com/ansel1/merry v1.8.0 + github.com/ansel1/merry v1.6.2 github.com/aws/aws-sdk-go v1.44.293 github.com/blevesearch/bleve/v2 v2.3.8 github.com/bwmarrin/discordgo v0.25.0 @@ -15,7 +15,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/render v1.0.1 - github.com/go-graphite/carbonapi v0.17.0 + github.com/go-graphite/carbonapi v0.16.0 github.com/go-graphite/protocol v1.0.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-redsync/redsync/v4 v4.4.4 @@ -57,11 +57,13 @@ require ( ) require ( + bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0 // indirect github.com/JaderDias/movingmedian v0.0.0-20220813210630-d8c6b6de8835 // indirect github.com/Masterminds/sprig/v3 v3.2.3 github.com/RoaringBitmap/roaring v1.3.0 // indirect github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect - github.com/ansel1/merry/v2 v2.2.1 // indirect + github.com/ansel1/merry/v2 v2.1.1 // indirect + github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.8.0 // indirect github.com/blend/go-sdk v2.0.0+incompatible // indirect @@ -80,19 +82,21 @@ require ( github.com/blevesearch/zapx/v13 v13.3.8 // indirect github.com/blevesearch/zapx/v14 v14.3.8 // indirect github.com/blevesearch/zapx/v15 v15.3.11 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb // indirect github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/disintegration/imaging v1.6.2 // indirect github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497 // indirect github.com/francoispqt/gojay v1.2.13 // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect - github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/gomodule/redigo v1.9.2 // indirect + github.com/gomodule/redigo v1.8.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 github.com/gopherjs/gopherjs v1.17.2 // indirect @@ -111,7 +115,7 @@ require ( github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80 // indirect github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/maruel/natural v1.1.1 // indirect + github.com/maruel/natural v1.1.0 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect github.com/mattermost/logr/v2 v2.0.21 // indirect @@ -121,26 +125,28 @@ require ( github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/msaf1980/go-stringutils v0.1.6 // indirect + github.com/msaf1980/go-stringutils v0.1.4 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/natefinch/atomic v1.0.1 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.1 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pkg/errors v0.9.1 - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartystreets/assertions v1.2.0 // indirect - github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.18.2 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.9.0 - github.com/subosito/gotenv v1.6.0 // indirect + github.com/spf13/viper v1.16.0 // indirect + github.com/stretchr/objx v0.5.1 // indirect + github.com/stretchr/testify v1.8.4 + github.com/subosito/gotenv v1.4.2 // indirect github.com/tinylib/msgp v1.1.9 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -148,8 +154,9 @@ require ( github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect go.etcd.io/bbolt v1.3.7 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + 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.18.0 // indirect @@ -157,7 +164,7 @@ require ( golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - gonum.org/v1/gonum v0.15.0 // indirect + gonum.org/v1/gonum v0.12.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect @@ -192,13 +199,9 @@ require ( github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/oklog/run v1.1.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/swag v1.8.12 // indirect - github.com/tebeka/strftime v0.1.5 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.62.0 // indirect diff --git a/go.sum b/go.sum index a013bebd9..0eacd324a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0 h1:800dI8vRxVMgss6UcZY8gxk8PvYw7Qo1ZI3TrUkTKjc= +bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0/go.mod h1:9BKpS/J2txC7Ql3QUhesesiV3HsIsA7zl7VK6cQVg5M= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -30,31 +32,368 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= @@ -96,10 +435,14 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGn github.com/alicebob/miniredis/v2 v2.22.0 h1:lIHHiSkEyS1MkKHCHzN+0mWrA4YdbGdimE5iZ2sHSzo= github.com/alicebob/miniredis/v2 v2.22.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/ansel1/merry v1.8.0 h1:3RddCV1ubXegKphsodbkmZ4QuROep/ZaPCuwlKuCfFg= -github.com/ansel1/merry v1.8.0/go.mod h1:wJVu1mHEtEUWq5zTTX9RiWjcE+xL8y7BGYl2VTYdP7M= -github.com/ansel1/merry/v2 v2.2.1 h1:PJpynLFvIpJkn8ZGgNHLq332zIyBc/wTqp3o42ZpWdU= -github.com/ansel1/merry/v2 v2.2.1/go.mod h1:K9lCkM6tJ8s7LQVQ0ZmZ0WrB3BCyr+ZDzoqotzzoxpI= +github.com/ansel1/merry v1.5.0/go.mod h1:wUy/yW0JX0ix9GYvUbciq+bi3jW/vlKPlbpI7qdZpOw= +github.com/ansel1/merry v1.5.1/go.mod h1:wUy/yW0JX0ix9GYvUbciq+bi3jW/vlKPlbpI7qdZpOw= +github.com/ansel1/merry v1.6.2 h1:0xr40haRrfVzmOH/JVOu7KOKGEI1c/7q5EmgTEbn+Ng= +github.com/ansel1/merry v1.6.2/go.mod h1:pAcMW+2uxIgpzEON021vMtFsrymREY6faJWiiz1QGVQ= +github.com/ansel1/merry/v2 v2.0.1/go.mod h1:dD5OhpiPrVkvgseRYd+xgYlx7s6ytU3v9BTTJlDA7FM= +github.com/ansel1/merry/v2 v2.1.1 h1:Ax0gQh7Z/GfimoVg2EDBAU6CJIieWwVvhtBKJdkCE1M= +github.com/ansel1/merry/v2 v2.1.1/go.mod h1:4p/FFyQbCgqlDbseWOVQaL5USpgkE9sr5xh4V6Ry0JU= +github.com/ansel1/vespucci/v4 v4.1.1/go.mod h1:zzdrO4IgBfgcGMbGTk/qNGL8JPslmW3nPpcBHKReFYY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -108,6 +451,10 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.44.293 h1:oBPrQqsyMYe61Sl/xKVvQFflXjPwYH11aKi8QR3Nhts= github.com/aws/aws-sdk-go v1.44.293/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= +github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -153,6 +500,8 @@ github.com/blevesearch/zapx/v14 v14.3.8/go.mod h1:vS6exLagv0vXmgpUbNRZC6UuEV0xwT github.com/blevesearch/zapx/v15 v15.3.11 h1:dstyZki9s10FNLsW4LpEvPQ+fmM3nX15h4wKfcBwnEg= github.com/blevesearch/zapx/v15 v15.3.11/go.mod h1:hiYbBDf5/Ud/Eji0faUmMTOyeOjcl8q1vWGgRe7+bIQ= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 h1:hjXJeBcAMS1WGENGqDpzvmgS43oECTx8UXq31UBu0Jw= +github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= @@ -161,6 +510,8 @@ github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0 github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1 h1:hXakhQtPnXH839q1pBl/GqfTSchqE+R5Fqn98Iu7UQM= github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1/go.mod h1:pAxCBpjl/0JxYZlWGP/Dyi8f/LQSCQD2WAsG/iNzqQ8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -176,11 +527,14 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -189,9 +543,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb h1:X9MwMz6mVZEWcbhsri5TwaCm/Q4USFdAAmy1T7RCGjw= +github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb/go.mod h1:pD/+9DfmmQ+xvOI1fxUltHV69BxC1aeTILPQg9Kw1hE= github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768 h1:Xzl7CSuSnGsyU+9xmSU2h8w3d7Tnis66xeoNN207tLo= github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768/go.mod h1:alfmlCqcg4uw9jaoIU1nOp9RFdJLMuu8P07BCEgpgoo= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -216,7 +571,10 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497 h1:DIQ8EvZ8OjuPNfcV4NgsyBeZho7WsTD0JEkDM5napMI= github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497/go.mod h1:YXKUYPSqs+jDG8mvexHN2uTik4PKwg2B0WK9itQ0VrE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -231,13 +589,13 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= @@ -252,8 +610,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-graphite/carbonapi v0.17.0 h1:6JowndAU0qsxUoBftCBy23Rgt8dCxf+1wHxfGZ3GA2E= -github.com/go-graphite/carbonapi v0.17.0/go.mod h1:EwQ1MJBzP8BBozLkgwTZdePM/5SqPSxhtY0uDdER2LQ= +github.com/go-graphite/carbonapi v0.16.0 h1:HvPjAKYChiwdHtNpFu33hLRpPCYA7gyyeFWpPR2XvXs= +github.com/go-graphite/carbonapi v0.16.0/go.mod h1:RQpis4h2a1kxn1s/R5LQXbumFj+kR2bRz+BebPN1Z1Q= github.com/go-graphite/protocol v1.0.0 h1:Fqb0mkVVtfMrn6vw6Ntm3raf3gVVZCOVdZu4JosW5qE= github.com/go-graphite/protocol v1.0.0/go.mod h1:eonkg/0UGhJUYu+PshOg1NzWSUcXskr/yHeQXJHJr8Y= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -312,6 +670,7 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -343,14 +702,14 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= -github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= -github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= +github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= +github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -367,6 +726,7 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -397,8 +757,14 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -408,6 +774,10 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= @@ -425,6 +795,8 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= @@ -476,6 +848,7 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= @@ -509,6 +882,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= @@ -536,6 +911,8 @@ github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80/go.mod h1:T7SQVaLtK7m github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463 h1:SN/0TEkyYpp8tit79JPUnecebCGZsXiYYPxN8i3I6Rk= github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463/go.mod h1:rWIJAUD2hPOAyOzc3jBShAhN4CAZeLAyzUA/n8tE8ak= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -545,8 +922,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= -github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ= +github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= @@ -564,6 +941,7 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= @@ -606,13 +984,15 @@ github.com/moira-alert/blackfriday-slack v0.1.2 h1:W6VbDlHDBxoB7X+OJ+3xZZuzMcQ0q github.com/moira-alert/blackfriday-slack v0.1.2/go.mod h1:tYMK3laTzU1wgxeOpUPdw36KHD3eTyQNDfxtg1nXLWI= github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19 h1:dV1yczr6ndr5fCnBvj2SjBJxJNtnBtfZye0gDwTrPLs= github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19/go.mod h1:ktrkvZGboMQfYyBXAV05imlVxGIvVdeCn5vz91Fw1vE= -github.com/msaf1980/go-stringutils v0.1.6 h1:qri8o+4XLJCJYemHcvJY6xJhrGTmllUoPwayKEj4NSg= -github.com/msaf1980/go-stringutils v0.1.6/go.mod h1:xpicaTIpLAVzL0gUQkciB1zjypDGKsOCI25cKQbRQYA= +github.com/msaf1980/go-stringutils v0.1.4 h1:UwsIT0hplHVucqbknk3CoNqKkmIuSHhsbBldXxyld5U= +github.com/msaf1980/go-stringutils v0.1.4/go.mod h1:AxmV/6JuQUAtZJg5XmYATB5ZwCWgtpruVHY03dswRf8= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= @@ -648,8 +1028,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= -github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -657,10 +1037,10 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= @@ -716,10 +1096,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -759,29 +1135,31 @@ github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= -github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= +github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -793,14 +1171,15 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= @@ -808,8 +1187,6 @@ github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4 github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg= -github.com/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig= github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU= github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -853,20 +1230,24 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk= go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -875,6 +1256,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -883,6 +1265,7 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= @@ -927,7 +1310,9 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -979,6 +1364,7 @@ golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -986,9 +1372,17 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= @@ -1014,6 +1408,14 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= @@ -1029,7 +1431,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1104,6 +1509,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1120,11 +1526,17 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= @@ -1133,6 +1545,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1145,6 +1558,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= @@ -1152,6 +1567,8 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1212,16 +1629,20 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= -gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= +gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -1262,8 +1683,24 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1320,6 +1757,7 @@ google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -1345,18 +1783,59 @@ google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220208230804-65c12eb4c068/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -1388,10 +1867,18 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -1409,6 +1896,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff --git a/metric_source/local/database_test.go b/metric_source/local/database_test.go deleted file mode 100644 index 2314bc366..000000000 --- a/metric_source/local/database_test.go +++ /dev/null @@ -1,347 +0,0 @@ -package local - -import ( - "fmt" - "math" - "testing" - "time" - - "github.com/moira-alert/moira" - - "github.com/moira-alert/moira/database/redis" - - logging "github.com/moira-alert/moira/logging/zerolog_adapter" - - . "github.com/smartystreets/goconvey/convey" -) - -type metricMock struct { - values []float64 - patterns []string -} - -type testCase struct { - metrics map[string]metricMock - from int64 - retention int64 - target string - expected map[string][]float64 -} - -func saveMetrics(database moira.Database, metrics map[string]metricMock, now, retention int64) error { - maxValues := 0 - for _, m := range metrics { - if len(m.values) > maxValues { - maxValues = len(m.values) - } - } - - timeStart := now - retention*int64(maxValues-1) - for i := range maxValues { - time := timeStart + int64(i)*retention - - metricsMap := make(map[string]*moira.MatchedMetric, len(metrics)) - for name, metric := range metrics { - if len(metric.values) < i { - continue - } - - metricsMap[name] = &moira.MatchedMetric{ - Metric: name, - Patterns: metric.patterns, - Value: metric.values[i], - Timestamp: time, - RetentionTimestamp: time, - Retention: int(retention), - } - } - - err := database.SaveMetrics(metricsMap) - if err != nil { - return err - } - } - return nil -} - -func TestLocalSourceWithDatabase(t *testing.T) { - logger, _ := logging.ConfigureLog("stdout", "info", "test", true) // nolint: govet - database := redis.NewTestDatabase(logger) - localSource := Create(database) - - defer database.Flush() - - retention := int64(60) - now := floorToMultiplier(time.Now().Unix(), retention) - nan := math.NaN() - - testCases := []testCase{ - { - metrics: map[string]metricMock{ - "metric1": { - values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - patterns: []string{"pattern"}, - }, - "metric2": { - values: []float64{5.0, 4.0, 3.0, 2.0, 1.0}, - patterns: []string{"pattern"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "pattern", - expected: map[string][]float64{ - "metric1": {1.0, 2.0, 3.0, 4.0, 5.0}, - "metric2": {5.0, 4.0, 3.0, 2.0, 1.0}, - }, - }, - { - metrics: map[string]metricMock{ - "metric1": { - values: []float64{1.0, 3.0, 1.0, 3.0, 1.0, 3.0}, - patterns: []string{"pattern"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "movingAverage(pattern, 2)", - expected: map[string][]float64{ - "movingAverage(metric1,2)": {2.0, 2.0, 2.0, 2.0, 2.0}, - }, - }, - { - metrics: map[string]metricMock{ - "metric1": { - values: []float64{1.0, nan, 2.0, nan, nan}, - patterns: []string{"pattern"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "keepLastValue(pattern, 1)", - expected: map[string][]float64{ - "keepLastValue(metric1,1)": {1.0, 1.0, 2.0, 2.0, nan}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.1.foo": { - values: []float64{1.0, 2.0, 1.0, 2.0, 1.0}, - patterns: []string{"metric.*.*", "metric.1.foo"}, - }, - "metric.1.bar": { - values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, - patterns: []string{"metric.*.*", "metric.1.bar"}, - }, - "metric.2.foo": { - values: []float64{3.0, 2.0, 3.0, 2.0, 3.0}, - patterns: []string{"metric.*.*", "metric.2.foo"}, - }, - "metric.2.bar": { - values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, - patterns: []string{"metric.*.*", "metric.2.bar"}, - }, - }, - from: now - retention*4, - retention: retention, - target: `applyByNode(metric.*.*, 1, "movingMax(%.foo, '2m')")`, - expected: map[string][]float64{ - "movingMax(metric.1.foo,'2m')": {1.0, 2.0, 2.0, 2.0, 2.0}, - "movingMax(metric.2.foo,'2m')": {3.0, 3.0, 3.0, 3.0, 3.0}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo": { - values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "aliasByNode(metric.*, 1)", - expected: map[string][]float64{ - "foo": {1.0, 2.0, 3.0, 4.0, 5.0}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo": { - values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "aliasByNode(metric.*, 2)", - expected: map[string][]float64{ - "": {1.0, 2.0, 3.0, 4.0, 5.0}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo": { - values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "consolidateBy(metric.*, 'max')", - expected: map[string][]float64{ - `consolidateBy(metric.foo,"max")`: {1.0, 2.0, 3.0, 4.0, 5.0}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.1": { - values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - patterns: []string{"metric.*"}, - }, - "metric.2": { - values: []float64{-1.0, 2.0, 3.0, 4.0, 5.0}, - patterns: []string{"metric.*"}, - }, - "metric.3": { - values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "minimumBelow(metric.*, 0)", - expected: map[string][]float64{ - "metric.2": {-1.0, 2.0, 3.0, 4.0, 5.0}, - "metric.3": {-1.0, -2.0, -3.0, -4.0, -5.0}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo.1": { - values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, - patterns: []string{"metric.*.*"}, - }, - "metric.foo.2": { - values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, - patterns: []string{"metric.*.*"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "groupByNode(metric.*.*, 1, 'sumSeries')", - expected: map[string][]float64{ - "foo": {2.5, 4.5, 6.5, 8.5, 10.5}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo.1": { - values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, - patterns: []string{"metric.*.*"}, - }, - "metric.foo.2": { - values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, - patterns: []string{"metric.*.*"}, - }, - }, - from: now - retention*4, - retention: retention, - target: "groupByNode(metric.*.*, 1, 'unique')", - expected: map[string][]float64{ - "foo": {1.5, 2.5, 3.5, 4.5, 5.5}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo": { - values: []float64{0, 1, 2, 3, 4, 5}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*5, - retention: retention, - target: "hitcount(metric.*, '2m')", - expected: map[string][]float64{ - "hitcount(metric.foo,'2m')": {60, 300, 540}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo": { - values: []float64{1, nan, 3, nan, nan, 6}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*5, - retention: retention, - target: "interpolate(metric.*, 1)", - expected: map[string][]float64{ - "interpolate(metric.foo)": {1, 2, 3, nan, nan, 6}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo": { - values: []float64{1, nan, 3, nan, nan, 6}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*5, - retention: retention, - target: "interpolate(metric.*)", - expected: map[string][]float64{ - "interpolate(metric.foo)": {1, 2, 3, 4, 5, 6}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo": { - values: []float64{1, 2, 3, 4, 5, 6}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*5, - retention: retention, - target: "smartSummarize(metric.*, '2m', 'average')", - expected: map[string][]float64{ - "smartSummarize(metric.foo,'2m','average')": {1.5, 3.5, 5.5}, - }, - }, - { - metrics: map[string]metricMock{ - "metric.foo": { - values: []float64{1.5, 2, 3, 4, 5, 6.5}, - patterns: []string{"metric.*"}, - }, - }, - from: now - retention*5, - retention: retention, - target: "smartSummarize(metric.*, '3m', 'median')", - expected: map[string][]float64{ - "smartSummarize(metric.foo,'3m','median')": {2, 5}, - }, - }, - } - - Convey("Run test cases", t, func() { - for _, testCase := range testCases { - Convey(fmt.Sprintf("Target '%s'", testCase.target), func() { - database.Flush() - - err := saveMetrics(database, testCase.metrics, now, testCase.retention) - So(err, ShouldBeNil) - - result, err := localSource.Fetch(testCase.target, testCase.from, now, true) - So(err, ShouldBeNil) - - resultData := result.GetMetricsData() - resultMap := map[string][]float64{} - for _, data := range resultData { - resultMap[data.Name] = data.Values - } - - So(resultMap, shouldEqualIfNaNsEqual, testCase.expected) - }) - } - }) -} diff --git a/metric_source/local/eval.go b/metric_source/local/eval.go index e8f22e59b..3d36c9e2a 100644 --- a/metric_source/local/eval.go +++ b/metric_source/local/eval.go @@ -3,9 +3,9 @@ package local import ( "context" "errors" + "fmt" "runtime/debug" - "github.com/ansel1/merry" "github.com/go-graphite/carbonapi/expr" "github.com/go-graphite/carbonapi/expr/helper" "github.com/go-graphite/carbonapi/expr/types" @@ -14,139 +14,75 @@ import ( metricSource "github.com/moira-alert/moira/metric_source" ) -type evaluator struct { - database moira.Database - metrics []string +type evalCtx struct { + from int64 + until int64 } -func (eval *evaluator) fetchAndEval(target string, from, until int64, result *FetchResult) (err error) { - defer func() { - if r := recover(); r != nil { - err = ErrEvaluateTargetFailedWithPanic{ - target: target, - recoverMessage: r, - stackRecord: debug.Stack(), - } - } - }() - - exp, err := eval.parse(target) +func (ctx *evalCtx) fetchAndEval(database moira.Database, target string, result *FetchResult) error { + exp, err := ctx.parse(target) if err != nil { return err } - values := make(map[parser.MetricRequest][]*types.MetricData) - - fetchedMetrics, err := expr.FetchAndEvalExp(context.Background(), eval, exp, from, until, values) + fetchedMetrics, err := ctx.getMetricsData(database, exp) if err != nil { - return merry.Unwrap(err) + return err } - eval.writeResult(exp, fetchedMetrics, result) + commonStep := fetchedMetrics.calculateCommonStep() + ctx.scaleToCommonStep(commonStep, fetchedMetrics) - return nil -} + rewritten, newTargets, err := ctx.rewriteExpr(exp, fetchedMetrics) + if err != nil { + return err + } -// Fetch is an implementation of Evaluator interface from carbonapi. -// It returns a map the metrics requested in the current invocation, scaled to a common step. -func (eval *evaluator) Fetch( - ctx context.Context, - exprs []parser.Expr, - from, until int64, - values map[parser.MetricRequest][]*types.MetricData, -) (map[parser.MetricRequest][]*types.MetricData, error) { - fetch := newFetchCtx(0, 0) - - for _, exp := range exprs { - ms := exp.Metrics(from, until) - if err := fetch.getMetricsData(eval.database, ms); err != nil { - return nil, err + if rewritten { + for _, newTarget := range newTargets { + err = ctx.fetchAndEvalNoRewrite(database, newTarget, result) + if err != nil { + return err + } } + return nil } - fetch.scaleToCommonStep() + metricsData, err := ctx.eval(target, exp, fetchedMetrics) + if err != nil { + return err + } - eval.metrics = append(eval.metrics, fetch.fetchedMetrics.metrics...) + ctx.writeResult(exp, fetchedMetrics, metricsData, result) - return fetch.fetchedMetrics.metricsMap, nil + return nil } -// Eval is an implementation of Evaluator interface from carbonapi. -// It uses the raw data within the values map being passed into it to in order to evaluate the input expression. -func (eval *evaluator) Eval( - ctx context.Context, - exp parser.Expr, - from, until int64, - values map[parser.MetricRequest][]*types.MetricData, -) (results []*types.MetricData, err error) { - rewritten, newTargets, err := expr.RewriteExpr(ctx, eval, exp, from, until, values) +func (ctx *evalCtx) fetchAndEvalNoRewrite(database moira.Database, target string, result *FetchResult) error { + exp, err := ctx.parse(target) if err != nil { - return nil, err - } - - if rewritten { - return eval.evalRewritten(ctx, newTargets, from, until, values) + return err } - results, err = expr.EvalExpr(ctx, eval, exp, from, until, values) + fetchedMetrics, err := ctx.getMetricsData(database, exp) if err != nil { - if errors.Is(err, parser.ErrMissingTimeseries) { - err = nil - } else if isErrUnknownFunction(err) { - err = ErrorUnknownFunction(err) - } else { - err = ErrEvalExpr{ - target: exp.ToString(), - internalError: err, - } - } + return err } - return results, err -} - -func (eval *evaluator) evalRewritten( - ctx context.Context, - newTargets []string, - from, until int64, - values map[parser.MetricRequest][]*types.MetricData, -) (results []*types.MetricData, err error) { - for _, target := range newTargets { - exp, _, err := parser.ParseExpr(target) - if err != nil { - return nil, err - } - - var targetValues map[parser.MetricRequest][]*types.MetricData - targetValues, err = eval.Fetch(ctx, []parser.Expr{exp}, from, until, values) - if err != nil { - return nil, err - } + commonStep := fetchedMetrics.calculateCommonStep() + ctx.scaleToCommonStep(commonStep, fetchedMetrics) - result, err := eval.Eval(ctx, exp, from, until, targetValues) - if err != nil { - return nil, err - } - - results = append(results, result...) + metricsData, err := ctx.eval(target, exp, fetchedMetrics) + if err != nil { + return err } - return results, nil -} - -func (eval *evaluator) writeResult(exp parser.Expr, metricsData []*types.MetricData, result *FetchResult) { - result.Metrics = append(result.Metrics, eval.metrics...) - for _, mr := range exp.Metrics(0, 0) { - result.Patterns = append(result.Patterns, mr.Metric) - } + ctx.writeResult(exp, fetchedMetrics, metricsData, result) - for _, metricData := range metricsData { - md := newMetricDataFromGraphite(metricData, len(result.Metrics) != len(result.Patterns)) - result.MetricsData = append(result.MetricsData, md) - } + return nil } -func (eval *evaluator) parse(target string) (parser.Expr, error) { +func (ctx *evalCtx) parse(target string) (parser.Expr, error) { parsedExpr, _, err := parser.ParseExpr(target) if err != nil { return nil, ErrParseExpr{ @@ -157,71 +93,109 @@ func (eval *evaluator) parse(target string) (parser.Expr, error) { return parsedExpr, nil } -type fetchCtx struct { - from int64 - until int64 - fetchedMetrics *fetchedMetrics -} +func (ctx *evalCtx) getMetricsData(database moira.Database, parsedExpr parser.Expr) (*fetchedMetrics, error) { + metricRequests := parsedExpr.Metrics() -func newFetchCtx(from, until int64) *fetchCtx { - return &fetchCtx{ - from, - until, - &fetchedMetrics{ - metricsMap: make(map[parser.MetricRequest][]*types.MetricData), - metrics: make([]string, 0), - }, - } -} + metrics := make([]string, 0) + metricsMap := make(map[parser.MetricRequest][]*types.MetricData) -func (ctx *fetchCtx) getMetricsData(database moira.Database, metricRequests []parser.MetricRequest) error { fetchData := fetchData{database} for _, mr := range metricRequests { - // Other fields are used in carbon for database side consolidations - request := parser.MetricRequest{ - Metric: mr.Metric, - From: mr.From, - Until: mr.Until, - } - from := mr.From + ctx.from until := mr.Until + ctx.until metricNames, err := fetchData.fetchMetricNames(mr.Metric) if err != nil { - return err + return nil, err } - timer := newTimerRoundingTimestamps(from, until, metricNames.retention) + timer := NewTimerRoundingTimestamps(from, until, metricNames.retention) metricsData, err := fetchData.fetchMetricValues(mr.Metric, metricNames, timer) if err != nil { - return err + return nil, err } - ctx.fetchedMetrics.metricsMap[request] = metricsData - ctx.fetchedMetrics.metrics = append(ctx.fetchedMetrics.metrics, metricNames.metrics...) + metricsMap[mr] = metricsData + metrics = append(metrics, metricNames.metrics...) } - return nil + return &fetchedMetrics{metricsMap, metrics}, nil } -func (ctx *fetchCtx) scaleToCommonStep() { - retention := ctx.fetchedMetrics.calculateCommonStep() +func (ctx *evalCtx) scaleToCommonStep(retention int64, fetchedMetrics *fetchedMetrics) { + from, until := RoundTimestamps(ctx.from, ctx.until, retention) + ctx.from, ctx.until = from, until metricMap := make(map[parser.MetricRequest][]*types.MetricData) - for metricRequest, metricData := range ctx.fetchedMetrics.metricsMap { - metricRequest.From += ctx.from - metricRequest.Until += ctx.until + for metricRequest, metricData := range fetchedMetrics.metricsMap { + metricRequest.From += from + metricRequest.Until += until metricData = helper.ScaleToCommonStep(metricData, retention) metricMap[metricRequest] = metricData } - ctx.fetchedMetrics.metricsMap = metricMap + fetchedMetrics.metricsMap = metricMap } -func newMetricDataFromGraphite(md *types.MetricData, wildcard bool) metricSource.MetricData { +func (ctx *evalCtx) rewriteExpr(parsedExpr parser.Expr, metrics *fetchedMetrics) (bool, []string, error) { + rewritten, newTargets, err := expr.RewriteExpr( + context.Background(), + parsedExpr, + ctx.from, + ctx.until, + metrics.metricsMap, + ) + + if err != nil && !errors.Is(err, parser.ErrMissingTimeseries) { + return false, nil, fmt.Errorf("failed RewriteExpr: %s", err.Error()) + } + return rewritten, newTargets, nil +} + +func (ctx *evalCtx) eval(target string, parsedExpr parser.Expr, metrics *fetchedMetrics) (result []*types.MetricData, err error) { + defer func() { + if r := recover(); r != nil { + result = nil + err = ErrEvaluateTargetFailedWithPanic{ + target: target, + recoverMessage: r, + stackRecord: debug.Stack(), + } + } + }() + + result, err = expr.EvalExpr(context.Background(), parsedExpr, ctx.from, ctx.until, metrics.metricsMap) + if err != nil { + if errors.Is(err, parser.ErrMissingTimeseries) { + err = nil + } else if isErrUnknownFunction(err) { + err = ErrorUnknownFunction(err) + } else { + err = ErrEvalExpr{ + target: target, + internalError: err, + } + } + } + + return result, err +} + +func (ctx *evalCtx) writeResult(exp parser.Expr, metrics *fetchedMetrics, metricsData []*types.MetricData, result *FetchResult) { + for _, metricData := range metricsData { + md := newMetricDataFromGraphit(metricData, metrics.hasWildcard()) + result.MetricsData = append(result.MetricsData, md) + } + + result.Metrics = append(result.Metrics, metrics.metrics...) + for _, mr := range exp.Metrics() { + result.Patterns = append(result.Patterns, mr.Metric) + } +} + +func newMetricDataFromGraphit(md *types.MetricData, wildcard bool) metricSource.MetricData { return metricSource.MetricData{ Name: md.Name, StartTime: md.StartTime, @@ -237,6 +211,10 @@ type fetchedMetrics struct { metrics []string } +func (m *fetchedMetrics) hasWildcard() bool { + return len(m.metrics) == 0 +} + func (m *fetchedMetrics) calculateCommonStep() int64 { commonStep := int64(1) for _, metricsData := range m.metricsMap { diff --git a/metric_source/local/fetchdata.go b/metric_source/local/fetchdata.go index 880e46432..e2a7a4a6d 100644 --- a/metric_source/local/fetchdata.go +++ b/metric_source/local/fetchdata.go @@ -38,7 +38,7 @@ func (fd *fetchData) fetchMetricNames(pattern string) (*metricsWithRetention, er return &metricsWithRetention{retention, metrics}, nil } -func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithRetention, timer timer) ([]*types.MetricData, error) { +func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithRetention, timer Timer) ([]*types.MetricData, error) { if len(metrics.metrics) == 0 { return fetchDataNoMetrics(timer, pattern), nil } @@ -58,7 +58,7 @@ func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithReten return metricsData, nil } -func fetchDataNoMetrics(timer timer, pattern string) []*types.MetricData { +func fetchDataNoMetrics(timer Timer, pattern string) []*types.MetricData { dataList := map[string][]*moira.MetricValue{pattern: make([]*moira.MetricValue, 0)} valuesMap := unpackMetricsValues(dataList, timer) metricsData := createMetricData(pattern, timer, valuesMap[pattern]) @@ -66,7 +66,7 @@ func fetchDataNoMetrics(timer timer, pattern string) []*types.MetricData { return []*types.MetricData{metricsData} } -func createMetricData(metric string, timer timer, values []float64) *types.MetricData { +func createMetricData(metric string, timer Timer, values []float64) *types.MetricData { fetchResponse := pb.FetchResponse{ Name: metric, StartTime: timer.startTime, @@ -77,7 +77,7 @@ func createMetricData(metric string, timer timer, values []float64) *types.Metri return &types.MetricData{FetchResponse: fetchResponse, Tags: tags.ExtractTags(metric)} } -func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer timer) map[string][]float64 { +func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer Timer) map[string][]float64 { valuesMap := make(map[string][]float64, len(metricsData)) for metric, metricData := range metricsData { valuesMap[metric] = unpackMetricValues(metricData, timer) @@ -85,13 +85,13 @@ func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer time return valuesMap } -func unpackMetricValues(metricData []*moira.MetricValue, timer timer) []float64 { +func unpackMetricValues(metricData []*moira.MetricValue, timer Timer) []float64 { points := make(map[int]*moira.MetricValue, len(metricData)) for _, metricValue := range metricData { - points[timer.getTimeSlot(metricValue.RetentionTimestamp)] = metricValue + points[timer.GetTimeSlot(metricValue.RetentionTimestamp)] = metricValue } - numberOfTimeSlots := timer.numberOfTimeSlots() + numberOfTimeSlots := timer.NumberOfTimeSlots() values := make([]float64, 0, numberOfTimeSlots) diff --git a/metric_source/local/fetchdata_test.go b/metric_source/local/fetchdata_test.go index c5b4a0aae..c55142ab3 100644 --- a/metric_source/local/fetchdata_test.go +++ b/metric_source/local/fetchdata_test.go @@ -18,7 +18,7 @@ func BenchmarkUnpackMetricsValues(b *testing.B) { var until int64 = 1317 var retention int64 = 10 - timer := newTimerRoundingTimestamps(from, until, retention) + timer := NewTimerRoundingTimestamps(from, until, retention) metricsCount := 7300 @@ -47,7 +47,7 @@ func BenchmarkUnpackMetricValues(b *testing.B) { var until int64 = 317 var retention int64 = 10 - timer := newTimerRoundingTimestamps(from, until, retention) + timer := NewTimerRoundingTimestamps(from, until, retention) metricsValues := make([]*moira.MetricValue, 0) @@ -75,7 +75,7 @@ func TestFetchDataErrors(t *testing.T) { pattern := "super-puper-pattern" metric := "super-puper-metric" - timer := newTimerRoundingTimestamps(17, 67, 10) + timer := NewTimerRoundingTimestamps(17, 67, 10) retentionErr := fmt.Errorf("Ooops, retention error") patternErr := fmt.Errorf("Ooops, pattern error") @@ -145,7 +145,7 @@ func TestFetchData(t *testing.T) { var from int64 = 17 var until int64 = 67 var retention int64 = 10 - timer := newTimerRoundingTimestamps(from, until, retention) + timer := NewTimerRoundingTimestamps(from, until, retention) Convey("Test no metrics", t, func() { dataBase.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) @@ -153,7 +153,7 @@ func TestFetchData(t *testing.T) { metrics, err := fetchedData.fetchMetricNames(pattern) So(err, ShouldBeNil) - timerNoMetrics := newTimerRoundingTimestamps(from, until, metrics.retention) + timerNoMetrics := NewTimerRoundingTimestamps(from, until, metrics.retention) metricValues, err := fetchedData.fetchMetricValues(pattern, metrics, timerNoMetrics) So(metricValues[0], shouldEqualIfNaNsEqual, &types.MetricData{ @@ -235,28 +235,28 @@ func TestUnpackMetricValuesNoData(t *testing.T) { metricData := map[string][]*moira.MetricValue{"metric": make([]*moira.MetricValue, 0)} Convey("From 1 until 1", t, func() { - timer := newTimerRoundingTimestamps(1, 1, retention) + timer := NewTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 0 until 0", t, func() { - timer := newTimerRoundingTimestamps(0, 0, retention) + timer := NewTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 0 until 10", t, func() { - timer := newTimerRoundingTimestamps(0, 10, retention) + timer := NewTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN(), math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 1 until 11", t, func() { - timer := newTimerRoundingTimestamps(1, 11, retention) + timer := NewTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) @@ -273,49 +273,49 @@ func TestUnpackMetricValues(t *testing.T) { }} Convey("From 1 until 1", t, func() { - timer := newTimerRoundingTimestamps(1, 1, retention) + timer := NewTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{}) }) Convey("From 0 until 0", t, func() { - timer := newTimerRoundingTimestamps(0, 0, retention) + timer := NewTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.0}) }) Convey("From 1 until 11", t, func() { - timer := newTimerRoundingTimestamps(1, 11, retention) + timer := NewTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.0}) }) Convey("From 0 until 10", t, func() { - timer := newTimerRoundingTimestamps(0, 10, retention) + timer := NewTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.0}) }) Convey("From 0 until 11", t, func() { - timer := newTimerRoundingTimestamps(0, 11, retention) + timer := NewTimerRoundingTimestamps(0, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 0 until 19", t, func() { - timer := newTimerRoundingTimestamps(0, 19, retention) + timer := NewTimerRoundingTimestamps(0, 19, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 1 until 30", t, func() { - timer := newTimerRoundingTimestamps(1, 30, retention) + timer := NewTimerRoundingTimestamps(1, 30, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) @@ -330,7 +330,7 @@ func TestMultipleSeriesNoData(t *testing.T) { } Convey("From 1 until 1", t, func() { - timer := newTimerRoundingTimestamps(1, 1, retention) + timer := NewTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -338,7 +338,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 0 until 0", t, func() { - timer := newTimerRoundingTimestamps(0, 0, retention) + timer := NewTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN()}) @@ -346,7 +346,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 1 until 5", t, func() { - timer := newTimerRoundingTimestamps(1, 5, retention) + timer := NewTimerRoundingTimestamps(1, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -354,7 +354,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 0 until 5", t, func() { - timer := newTimerRoundingTimestamps(0, 5, retention) + timer := NewTimerRoundingTimestamps(0, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN()}) @@ -362,7 +362,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 5 until 30", t, func() { - timer := newTimerRoundingTimestamps(5, 30, retention) + timer := NewTimerRoundingTimestamps(5, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN(), math.NaN(), math.NaN()}) @@ -387,7 +387,7 @@ func TestMultipleSeries(t *testing.T) { } Convey("From 1 until 1", t, func() { - timer := newTimerRoundingTimestamps(1, 1, retention) + timer := NewTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -395,7 +395,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 0", t, func() { - timer := newTimerRoundingTimestamps(0, 0, retention) + timer := NewTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{100.0}) @@ -403,7 +403,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 1 until 5", t, func() { - timer := newTimerRoundingTimestamps(1, 5, retention) + timer := NewTimerRoundingTimestamps(1, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -411,7 +411,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 5", t, func() { - timer := newTimerRoundingTimestamps(0, 5, retention) + timer := NewTimerRoundingTimestamps(0, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.0}) @@ -419,7 +419,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 9", t, func() { - timer := newTimerRoundingTimestamps(0, 9, retention) + timer := NewTimerRoundingTimestamps(0, 9, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00}) @@ -427,7 +427,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 10", t, func() { - timer := newTimerRoundingTimestamps(0, 10, retention) + timer := NewTimerRoundingTimestamps(0, 10, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) @@ -435,7 +435,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 1 until 11", t, func() { - timer := newTimerRoundingTimestamps(1, 11, retention) + timer := NewTimerRoundingTimestamps(1, 11, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{200.00}) @@ -443,7 +443,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 30", t, func() { - timer := newTimerRoundingTimestamps(0, 30, retention) + timer := NewTimerRoundingTimestamps(0, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00, 300.00, math.NaN()}) @@ -451,7 +451,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 5 until 30", t, func() { - timer := newTimerRoundingTimestamps(5, 30, retention) + timer := NewTimerRoundingTimestamps(5, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) @@ -468,49 +468,49 @@ func TestShiftedSeries(t *testing.T) { }} Convey("From 1 until 1", t, func() { - timer := newTimerRoundingTimestamps(1, 1, retention) + timer := NewTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{}) }) Convey("From 0 until 0", t, func() { - timer := newTimerRoundingTimestamps(0, 0, retention) + timer := NewTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.0}) }) Convey("From 1 until 11", t, func() { - timer := newTimerRoundingTimestamps(1, 11, retention) + timer := NewTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.0}) }) Convey("From 0 until 10", t, func() { - timer := newTimerRoundingTimestamps(0, 10, retention) + timer := NewTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.0}) }) Convey("From 0 until 11", t, func() { - timer := newTimerRoundingTimestamps(0, 11, retention) + timer := NewTimerRoundingTimestamps(0, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 0 until 19", t, func() { - timer := newTimerRoundingTimestamps(0, 19, retention) + timer := NewTimerRoundingTimestamps(0, 19, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 1 until 30", t, func() { - timer := newTimerRoundingTimestamps(1, 30, retention) + timer := NewTimerRoundingTimestamps(1, 30, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) diff --git a/metric_source/local/local.go b/metric_source/local/local.go index f844f79aa..6183200bf 100644 --- a/metric_source/local/local.go +++ b/metric_source/local/local.go @@ -33,7 +33,7 @@ func (local *Local) IsConfigured() (bool, error) { return true, nil } -// IsAvailable always returns true. It easy to configure local source =). +// IsConfigured always returns true. It easy to configure local source =). func (local *Local) IsAvailable() (bool, error) { return true, nil } @@ -45,9 +45,9 @@ func (local *Local) Fetch(target string, from int64, until int64, allowRealTimeA from = moira.MaxInt64(from, until-local.database.GetMetricsTTLSeconds()) result := CreateEmptyFetchResult() - eval := evaluator{local.database, make([]string, 0)} + ctx := evalCtx{from, until} - err := eval.fetchAndEval(target, from, until, result) + err := ctx.fetchAndEval(local.database, target, result) if err != nil { return nil, err } diff --git a/metric_source/local/local_test.go b/metric_source/local/local_test.go index 716f0040e..6f0feb8a9 100644 --- a/metric_source/local/local_test.go +++ b/metric_source/local/local_test.go @@ -12,7 +12,6 @@ import ( "github.com/go-graphite/carbonapi/pkg/parser" "github.com/google/go-cmp/cmp" "github.com/moira-alert/moira" - metricSource "github.com/moira-alert/moira/metric_source" mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" . "github.com/smartystreets/goconvey/convey" @@ -89,17 +88,14 @@ func TestLocalSourceFetchErrors(t *testing.T) { }) Convey("Panic while evaluate target", t, func() { - // moving* functions with an integer second parameter require two metric fetches - database.EXPECT().GetPatternMetrics(pattern1).Return([]string{metric1}, nil).Times(2) - database.EXPECT().GetMetricRetention(metric1).Return(retention, nil).Times(2) + database.EXPECT().GetPatternMetrics(pattern1).Return([]string{metric1}, nil) + database.EXPECT().GetMetricRetention(metric1).Return(retention, nil) database.EXPECT().GetMetricsValues([]string{metric1}, retentionFrom, retentionUntil-1).Return(dataList, nil) - database.EXPECT().GetMetricsValues([]string{metric1}, int64(30), retentionUntil-1).Return(dataList, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("movingAverage(super.puper.pattern, -1)", from, until, true) expectedErrSubstring := strings.Split(ErrEvaluateTargetFailedWithPanic{target: "movingAverage(super.puper.pattern, -1)"}.Error(), ":")[0] - So(err, ShouldNotBeNil) So(err.Error(), ShouldStartWith, expectedErrSubstring) So(result, ShouldBeNil) }) @@ -112,6 +108,7 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { defer mockCtrl.Finish() pattern := pattern1 + pattern2 := pattern2 var metricsTTL int64 = 3600 @@ -119,12 +116,12 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) - result, err := localSource.Fetch("super.puper.pattern", 17, 17, false) + result, err := localSource.Fetch("aliasByNode(super.puper.pattern, 2)", 17, 17, false) So(err, ShouldBeNil) So(result, shouldEqualIfNaNsEqual, &FetchResult{ MetricsData: []metricSource.MetricData{{ - Name: "super.puper.pattern", + Name: "pattern", StartTime: 60, StopTime: 60, StepTime: 60, @@ -158,6 +155,7 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { Convey("Single pattern, from 7 until 57", t, func() { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) + database.EXPECT().GetPatternMetrics(pattern2).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("aliasByNode(super.puper.pattern, 2)", 7, 57, true) @@ -178,7 +176,6 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { Convey("Two patterns, from 17 until 67", t, func() { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) - database.EXPECT().GetPatternMetrics(pattern2).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("alias(sum(super.puper.pattern, super.duper.pattern), 'pattern')", 17, 67, true) @@ -257,7 +254,6 @@ func TestLocalSourceFetchMultipleMetrics(t *testing.T) { StopTime: retentionUntil, StepTime: retention, Values: []float64{2, 2, 2, 2, 2}, - Wildcard: true, }, }, Metrics: metrics, @@ -266,79 +262,6 @@ func TestLocalSourceFetchMultipleMetrics(t *testing.T) { }) } -func TestLocalSourceApplyByNode(t *testing.T) { - mockCtrl := gomock.NewController(t) - database := mock_moira_alert.NewMockDatabase(mockCtrl) - localSource := Create(database) - defer mockCtrl.Finish() - - var from int64 = 17 - var until int64 = 67 - var retentionFrom int64 = 20 - var retentionUntil int64 = 70 - var retention int64 = 10 - var metricsTTL int64 = 3600 - - Convey("Test success evaluate multiple metrics with pow function", t, func() { - metrics := []string{ - "my.pattern.foo", - } - - metricList := make(map[string][]*moira.MetricValue) - metricList["my.pattern.foo"] = []*moira.MetricValue{ - {RetentionTimestamp: 20, Timestamp: 23, Value: 0.5}, - {RetentionTimestamp: 30, Timestamp: 33, Value: 0.4}, - {RetentionTimestamp: 40, Timestamp: 43, Value: 0.5}, - {RetentionTimestamp: 50, Timestamp: 53, Value: 0.5}, - {RetentionTimestamp: 60, Timestamp: 63, Value: 0.5}, - } - - metrics2 := []string{ - "your.my.pattern.foo", - } - - metricList2 := make(map[string][]*moira.MetricValue) - metricList2["your.my.pattern.foo"] = []*moira.MetricValue{ - {RetentionTimestamp: 20, Timestamp: 23, Value: 1}, - {RetentionTimestamp: 30, Timestamp: 33, Value: 2}, - {RetentionTimestamp: 40, Timestamp: 43, Value: 3}, - {RetentionTimestamp: 50, Timestamp: 53, Value: 4}, - {RetentionTimestamp: 60, Timestamp: 63, Value: 5}, - } - - database.EXPECT().GetPatternMetrics("my.pattern.*").Return(metrics, nil) - database.EXPECT().GetMetricRetention(metrics[0]).Return(retention, nil) - database.EXPECT().GetMetricsValues(metrics, retentionFrom, retentionUntil-1).Return(metricList, nil) - - database.EXPECT().GetPatternMetrics("your.my.pattern.foo").Return(metrics2, nil).AnyTimes() - database.EXPECT().GetMetricRetention(metrics2[0]).Return(retention, nil).AnyTimes() - database.EXPECT().GetMetricsValues(metrics2, retentionFrom, retentionUntil-1).Return(metricList2, nil) - - database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) - - result, err := localSource.Fetch(`alias(applyByNode(my.pattern.*, 2, "your.%"), 'min')`, from, until, true) - - So(err, ShouldBeNil) - So(result, shouldEqualIfNaNsEqual, &FetchResult{ - MetricsData: []metricSource.MetricData{ - { - Name: "min", - StartTime: retentionFrom, - StopTime: retentionUntil, - StepTime: retention, - Values: []float64{1, 2, 3, 4, 5}, - Wildcard: true, - }, - }, - Metrics: []string{ - "my.pattern.foo", - "your.my.pattern.foo", - }, - Patterns: []string{"my.pattern.*"}, - }) - }) -} - func TestLocalSourceFetch(t *testing.T) { mockCtrl := gomock.NewController(t) database := mock_moira_alert.NewMockDatabase(mockCtrl) @@ -572,49 +495,49 @@ func TestLocalMetricsTTL(t *testing.T) { } func TestLocal_evalExpr(t *testing.T) { - mockCtrl := gomock.NewController(t) - Convey("When everything is correct, we don't return any error", t, func() { + ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `seriesByTag('name=k8s.dev-cl1.kube_pod_status_ready', 'condition!=true', 'namespace=default', 'pod=~*')` - _, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) + expression, err := ctx.parse(target) So(err, ShouldBeNil) + res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) + So(err, ShouldBeNil) + So(res, ShouldBeNil) }) Convey("When get panic, it should return error", t, func() { - res, err := evalWithNoMetricsHelper(mockCtrl, `;fg`, 0, 0) - So(err.Error(), ShouldContainSubstring, "failed to parse target") - So(res.Metrics, ShouldBeEmpty) + ctx := evalCtx{from: 0, until: 0} + + expression, _ := ctx.parse(`;fg`) + res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) + So(err.Error(), ShouldContainSubstring, "panic while evaluate target target: message: 'runtime error: invalid memory address or nil pointer dereference") + So(res, ShouldBeNil) }) Convey("When no metrics, should not return error", t, func() { + ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `alias( divideSeries( alias( sumSeries( exclude( groupByNode( OFD.Production.{ofd-api,ofd-front}.*.fns-service-client.v120.*.GetCashboxRegistrationInformationAsync.ResponseCode.*.Meter.Rate-15-min-Requests-per-s, 9, "sum" ), "Ok" ) ), "bad" ), alias( sumSeries( OFD.Production.{ofd-api,ofd-front}.*.fns-service-client.v120.*.GetCashboxRegistrationInformationAsync.ResponseCode.*.Meter.Rate-15-min-Requests-per-s ), "total" ) ), "Result" )` - res, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) + expression, err := ctx.parse(target) So(err, ShouldBeNil) - So(res.Metrics, ShouldBeEmpty) + res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: make(map[parser.MetricRequest][]*types.MetricData)}) + So(err, ShouldBeNil) + So(res, ShouldBeEmpty) }) Convey("When got unknown func, should return error", t, func() { + ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `vf('name=k8s.dev-cl1.kube_pod_status_ready', 'condition!=true', 'namespace=default', 'pod=~*')` - res, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) + expression, _ := ctx.parse(target) + res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) + So(err, ShouldBeError) So(err.Error(), ShouldResemble, `Unknown graphite function: "vf"`) - So(res.Metrics, ShouldBeEmpty) + So(res, ShouldBeNil) }) } -func evalWithNoMetricsHelper(mockCtrl *gomock.Controller, target string, from, until int64) (*FetchResult, error) { - database := mock_moira_alert.NewMockDatabase(mockCtrl) - database.EXPECT().GetPatternMetrics(gomock.Any()).Return([]string{}, nil).AnyTimes() - eval := evaluator{database, make([]string, 0)} - - result := CreateEmptyFetchResult() - err := eval.fetchAndEval(target, from, until, result) - - return result, err -} - func shouldEqualIfNaNsEqual(actual interface{}, expected ...interface{}) string { allowUnexportedOption := cmp.AllowUnexported(types.MetricData{}) diff --git a/metric_source/local/timer.go b/metric_source/local/timer.go index 98f57bce4..224977bc7 100644 --- a/metric_source/local/timer.go +++ b/metric_source/local/timer.go @@ -1,32 +1,35 @@ package local -type timer struct { +// Timer is responsible for managing time ranges and metrics' timeslots. +type Timer struct { startTime int64 stopTime int64 stepTime int64 } -func roundTimestamps(startTime, stopTime, retention int64) (roundedStart, roundedStop int64) { - until := floorToMultiplier(stopTime, retention) + retention - from := ceilToMultiplier(startTime, retention) - - return from, until +// Rounds start and stop time in a specific manner requered by carbonapi. +func RoundTimestamps(startTime, stopTime, retention int64) (roundedStart, roundedStop int64) { + return ceilToMultiplier(startTime, retention), floorToMultiplier(stopTime, retention) + retention } -func newTimerRoundingTimestamps(startTime int64, stopTime int64, retention int64) timer { - startTime, stopTime = roundTimestamps(startTime, stopTime, retention) - return timer{ +// Creates new timer rounding start and stop time in a specific manner requered by carbonapi. +// Timers should be created only with this function. +func NewTimerRoundingTimestamps(startTime int64, stopTime int64, retention int64) Timer { + startTime, stopTime = RoundTimestamps(startTime, stopTime, retention) + return Timer{ startTime: startTime, stopTime: stopTime, stepTime: retention, } } -func (t timer) numberOfTimeSlots() int { - return t.getTimeSlot(t.stopTime) +// Returns the number of timeslots from this timer's startTime until its stopTime with it's retention. +func (t Timer) NumberOfTimeSlots() int { + return t.GetTimeSlot(t.stopTime) } -func (t timer) getTimeSlot(timestamp int64) int { +// Returns the index of given timestamp (rounded by timestamp) in this timer's time range. +func (t Timer) GetTimeSlot(timestamp int64) int { timeSlot := floorToMultiplier(timestamp-t.startTime, t.stepTime) / t.stepTime return int(timeSlot) } diff --git a/metric_source/local/timer_test.go b/metric_source/local/timer_test.go index 29563ca4a..cf25214e6 100644 --- a/metric_source/local/timer_test.go +++ b/metric_source/local/timer_test.go @@ -14,26 +14,26 @@ func TestTimerNumberOfTimeSlots(t *testing.T) { Convey("Given `from` is divisible by retention", t, func() { for _, from := range []int64{0, retention} { until := from + retention*steps - timer := newTimerRoundingTimestamps(from, until, retention) + timer := NewTimerRoundingTimestamps(from, until, retention) - So(timer.numberOfTimeSlots(), ShouldEqual, steps+1) + So(timer.NumberOfTimeSlots(), ShouldEqual, steps+1) } }) Convey("Given `from` is divisible by retention", t, func() { from := int64(0) until := int64(0) - timer := newTimerRoundingTimestamps(from, until, retention) + timer := NewTimerRoundingTimestamps(from, until, retention) - So(timer.numberOfTimeSlots(), ShouldEqual, 1) + So(timer.NumberOfTimeSlots(), ShouldEqual, 1) }) Convey("Given `from` is not divisible by retention", t, func() { for from := int64(1); from < retention; from++ { until := from + retention*steps - timer := newTimerRoundingTimestamps(from, until, retention) + timer := NewTimerRoundingTimestamps(from, until, retention) - So(timer.numberOfTimeSlots(), ShouldEqual, steps) + So(timer.NumberOfTimeSlots(), ShouldEqual, steps) } }) } @@ -43,7 +43,7 @@ func TestTimerGetTimeSlot(t *testing.T) { retention := int64(10) from := int64(10) until := int64(60) - timer := newTimerRoundingTimestamps(from, until, retention) + timer := NewTimerRoundingTimestamps(from, until, retention) testCases := []struct { timestamp int64 @@ -61,7 +61,7 @@ func TestTimerGetTimeSlot(t *testing.T) { } for _, testCase := range testCases { - actual := timer.getTimeSlot(testCase.timestamp) + actual := timer.GetTimeSlot(testCase.timestamp) So(actual, ShouldEqual, testCase.timeSlot) } }) diff --git a/notifier/config.go b/notifier/config.go index 34c6e53d4..a89b4b7ec 100644 --- a/notifier/config.go +++ b/notifier/config.go @@ -4,7 +4,6 @@ import ( "time" ) -// There is a duplicate of this constant in database package to prevent cyclic dependencies. const NotificationsLimitUnlimited = int64(-1) // Config is sending settings including log settings. From b8b58a8c6585cede7889c59e3c1ac68ff97ce3b4 Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Tue, 8 Oct 2024 19:21:47 +0700 Subject: [PATCH 27/36] hotfix(api): adding contacts to team (#1110) --- api/handler/handler.go | 16 +- api/handler/team_contact_test.go | 291 +++++++++++++++++++++++++++++++ 2 files changed, 300 insertions(+), 7 deletions(-) create mode 100644 api/handler/team_contact_test.go diff --git a/api/handler/handler.go b/api/handler/handler.go index 1ac3f953d..dd120172d 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -43,6 +43,8 @@ func NewHandler( contactsTemplate = webConfig.Contacts } + contactsTemplateMiddleware := moiramiddle.ContactsTemplateContext(contactsTemplate) + router := chi.NewRouter() router.Use(render.SetContentType(render.ContentTypeJSON)) router.Use(moiramiddle.UserContext) @@ -115,13 +117,13 @@ func NewHandler( router.Route("/event", event) router.Route("/subscription", subscription) router.Route("/notification", notification) - router.Route("/teams", teams) - router.With(moiramiddle.ContactsTemplateContext( - contactsTemplate, - )).Route("/contact", func(router chi.Router) { - contact(router) - contactEvents(router) - }) + router.With(contactsTemplateMiddleware). + Route("/teams", teams) + router.With(contactsTemplateMiddleware). + Route("/contact", func(router chi.Router) { + contact(router) + contactEvents(router) + }) router.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL("/api/swagger/doc.json"), )) diff --git a/api/handler/team_contact_test.go b/api/handler/team_contact_test.go new file mode 100644 index 000000000..912e6a187 --- /dev/null +++ b/api/handler/team_contact_test.go @@ -0,0 +1,291 @@ +package handler + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/moira-alert/moira" + "github.com/moira-alert/moira/api" + "github.com/moira-alert/moira/api/dto" + "github.com/moira-alert/moira/api/middleware" + db "github.com/moira-alert/moira/database" + mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" + "go.uber.org/mock/gomock" + + . "github.com/smartystreets/goconvey/convey" +) + +const testTeamIDKey = "teamID" + +func TestCreateNewTeamContact(t *testing.T) { + Convey("Test create new team contact", t, func() { + mockCtrl := gomock.NewController(t) + defer mockCtrl.Finish() + + responseWriter := httptest.NewRecorder() + mockDb := mock_moira_alert.NewMockDatabase(mockCtrl) + + team := defaultTeamID + targetRoute := fmt.Sprintf("/api/teams/%s/contacts", team) + testErr := errors.New("test error") + + auth := &api.Authorization{ + Enabled: false, + AllowedContactTypes: map[string]struct{}{ + "mail": {}, + }, + } + + contactsTemplate := []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + + newContactDto := &dto.Contact{ + ID: defaultContact, + Name: "Mail Alerts", + Type: "mail", + Value: "moira@skbkontur.ru", + User: "", + TeamID: team, + } + + Convey("Correctly create new team contact with the given id", func() { + jsonContact, err := json.Marshal(newContactDto) + So(err, ShouldBeNil) + + mockDb.EXPECT().GetContact(defaultContact).Return(moira.ContactData{}, db.ErrNil).Times(1) + mockDb.EXPECT().SaveContact(&moira.ContactData{ + ID: newContactDto.ID, + Name: newContactDto.Name, + Type: newContactDto.Type, + Value: newContactDto.Value, + User: "", + Team: newContactDto.TeamID, + }).Return(nil).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPost, targetRoute, bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testTeamIDKey, team)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest.Header.Add("content-type", "application/json") + + createNewTeamContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + + actual := &dto.Contact{} + err = json.Unmarshal(contentBytes, actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, newContactDto) + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Correctly create team contact without given id", func() { + newContactDto.ID = "" + defer func() { + newContactDto.ID = defaultContact + }() + + jsonContact, err := json.Marshal(newContactDto) + So(err, ShouldBeNil) + + mockDb.EXPECT().SaveContact(gomock.Any()).Return(nil).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPost, targetRoute, bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testTeamIDKey, team)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest.Header.Add("content-type", "application/json") + + createNewTeamContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + + actual := &dto.Contact{} + err = json.Unmarshal(contentBytes, actual) + So(err, ShouldBeNil) + + So(actual.TeamID, ShouldEqual, newContactDto.TeamID) + So(actual.Type, ShouldEqual, newContactDto.Type) + So(actual.User, ShouldEqual, newContactDto.User) + So(actual.Value, ShouldEqual, newContactDto.Value) + So(actual.Name, ShouldEqual, newContactDto.Name) + So(response.StatusCode, ShouldEqual, http.StatusOK) + }) + + Convey("Trying to create a new team contact with the id of an existing contact", func() { + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "contact with this ID already exists", + } + jsonContact, err := json.Marshal(newContactDto) + So(err, ShouldBeNil) + + mockDb.EXPECT().GetContact(newContactDto.ID).Return(moira.ContactData{ + ID: newContactDto.ID, + Type: newContactDto.Type, + Name: newContactDto.Name, + Value: newContactDto.Value, + User: newContactDto.User, + Team: newContactDto.TeamID, + }, nil).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPost, targetRoute, bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testTeamIDKey, team)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest.Header.Add("content-type", "application/json") + + createNewTeamContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + + actual := &api.ErrorResponse{} + err = json.Unmarshal(contentBytes, actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + Convey("Internal error when trying to create a new team contact with id", func() { + expected := &api.ErrorResponse{ + StatusText: "Internal Server Error", + ErrorText: testErr.Error(), + } + jsonContact, err := json.Marshal(newContactDto) + So(err, ShouldBeNil) + + mockDb.EXPECT().GetContact(newContactDto.ID).Return(moira.ContactData{}, testErr).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPost, targetRoute, bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testTeamIDKey, team)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest.Header.Add("content-type", "application/json") + + createNewTeamContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + + actual := &api.ErrorResponse{} + err = json.Unmarshal(contentBytes, actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) + }) + + Convey("Invalid request when trying to create a new team contact with invalid value", func() { + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "contact value doesn't match regex: '@yandex.ru'", + } + jsonContact, err := json.Marshal(newContactDto) + So(err, ShouldBeNil) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@yandex.ru", + }, + } + + mockDb.EXPECT().GetContact(newContactDto.ID).Return(moira.ContactData{}, db.ErrNil).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPost, targetRoute, bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testTeamIDKey, team)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest.Header.Add("content-type", "application/json") + + createNewTeamContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + + actual := &api.ErrorResponse{} + err = json.Unmarshal(contentBytes, actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + + Convey("Trying to create a team contact when both userLogin and teamID specified", func() { + newContactDto.User = defaultLogin + defer func() { + newContactDto.User = "" + }() + + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "contact cannot have both the user field and the team_id field filled in", + } + jsonContact, err := json.Marshal(newContactDto) + So(err, ShouldBeNil) + + testRequest := httptest.NewRequest(http.MethodPost, targetRoute, bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testTeamIDKey, team)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest.Header.Add("content-type", "application/json") + + createNewTeamContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + + actual := &api.ErrorResponse{} + err = json.Unmarshal(contentBytes, actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + }) +} From 993d37f4a64241aa765aafce6d9523a6a1d24dbc Mon Sep 17 00:00:00 2001 From: Tetrergeru Date: Thu, 10 Oct 2024 12:04:48 +0200 Subject: [PATCH 28/36] feat: update cabonapi to v0.17.0 (#1112) --- database/redis/database.go | 11 +- database/redis/notification.go | 19 +- database/redis/notification_test.go | 31 +- go.mod | 51 ++- go.sum | 592 +++----------------------- metric_source/local/database_test.go | 431 +++++++++++++++++++ metric_source/local/eval.go | 258 ++++++----- metric_source/local/fetchdata.go | 14 +- metric_source/local/fetchdata_test.go | 74 ++-- metric_source/local/local.go | 6 +- metric_source/local/local_test.go | 131 ++++-- metric_source/local/timer.go | 27 +- metric_source/local/timer_test.go | 16 +- notifier/config.go | 1 + 14 files changed, 852 insertions(+), 810 deletions(-) create mode 100644 metric_source/local/database_test.go diff --git a/database/redis/database.go b/database/redis/database.go index 5e3497080..03cf0cd56 100644 --- a/database/redis/database.go +++ b/database/redis/database.go @@ -91,9 +91,11 @@ func NewDatabase(logger moira.Logger, config DatabaseConfig, nh NotificationHist // NewTestDatabase use it only for tests. func NewTestDatabase(logger moira.Logger) *DbConnector { - return NewDatabase(logger, DatabaseConfig{ - Addrs: []string{"0.0.0.0:6379"}, - }, + return NewDatabase( + logger, DatabaseConfig{ + Addrs: []string{"0.0.0.0:6379"}, + MetricsTTL: time.Hour, + }, NotificationHistoryConfig{ NotificationHistoryTTL: time.Hour * 48, }, @@ -104,7 +106,8 @@ func NewTestDatabase(logger moira.Logger) *DbConnector { TransactionHeuristicLimit: 10000, ResaveTime: 30 * time.Second, }, - testSource) + testSource, + ) } // NewTestDatabaseWithIncorrectConfig use it only for tests. diff --git a/database/redis/notification.go b/database/redis/notification.go index a8f9c3f3c..82caf03d4 100644 --- a/database/redis/notification.go +++ b/database/redis/notification.go @@ -8,14 +8,17 @@ import ( "strings" "time" - "github.com/moira-alert/moira/notifier" - "github.com/go-redis/redis/v8" "github.com/moira-alert/moira" "github.com/moira-alert/moira/database/redis/reply" ) +// Separate const to prevent cyclic dependencies. +// Original const is declared in notifier package, notifier depends on all metric source packages. +// Thus it prevents us from using database in tests for local metric source. +const notificationsLimitUnlimited = int64(-1) + type notificationTypes struct { Valid, ToRemove, ToResaveNew, ToResaveOld []*moira.ScheduledNotification } @@ -294,8 +297,8 @@ func (connector *DbConnector) FetchNotifications(to int64, limit int64) ([]*moir } // No limit - if limit == notifier.NotificationsLimitUnlimited { - return connector.fetchNotifications(to, notifier.NotificationsLimitUnlimited) + if limit == notificationsLimitUnlimited { + return connector.fetchNotifications(to, notificationsLimitUnlimited) } count, err := connector.notificationsCount(to) @@ -305,7 +308,7 @@ func (connector *DbConnector) FetchNotifications(to int64, limit int64) ([]*moir // Hope count will be not greater then limit when we call fetchNotificationsNoLimit if limit > connector.notification.TransactionHeuristicLimit && count < limit/2 { - return connector.fetchNotifications(to, notifier.NotificationsLimitUnlimited) + return connector.fetchNotifications(to, notificationsLimitUnlimited) } return connector.fetchNotifications(to, limit) @@ -354,7 +357,7 @@ func (connector *DbConnector) fetchNotifications(to int64, limit int64) ([]*moir // sorted by timestamp in one transaction with or without limit, depending on whether limit is nil. func getNotificationsInTxWithLimit(ctx context.Context, tx *redis.Tx, to int64, limit int64) ([]*moira.ScheduledNotification, error) { var rng *redis.ZRangeBy - if limit != notifier.NotificationsLimitUnlimited { + if limit != notificationsLimitUnlimited { rng = &redis.ZRangeBy{Min: "-inf", Max: strconv.FormatInt(to, 10), Offset: 0, Count: limit} } else { rng = &redis.ZRangeBy{Min: "-inf", Max: strconv.FormatInt(to, 10)} @@ -393,7 +396,7 @@ func getLimitedNotifications( limitedNotifications := notifications - if limit != notifier.NotificationsLimitUnlimited { + if limit != notificationsLimitUnlimited { limitedNotifications = limitNotifications(notifications) lastTs := limitedNotifications[len(limitedNotifications)-1].Timestamp @@ -401,7 +404,7 @@ func getLimitedNotifications( // this means that all notifications have same timestamp, // we hope that all notifications with same timestamp should fit our memory var err error - limitedNotifications, err = getNotificationsInTxWithLimit(ctx, tx, lastTs, notifier.NotificationsLimitUnlimited) + limitedNotifications, err = getNotificationsInTxWithLimit(ctx, tx, lastTs, notificationsLimitUnlimited) if err != nil { return nil, fmt.Errorf("failed to get notification without limit in transaction: %w", err) } diff --git a/database/redis/notification_test.go b/database/redis/notification_test.go index d0a25ce56..c287f78d5 100644 --- a/database/redis/notification_test.go +++ b/database/redis/notification_test.go @@ -10,7 +10,6 @@ import ( "github.com/moira-alert/moira" "github.com/moira-alert/moira/clock" logging "github.com/moira-alert/moira/logging/zerolog_adapter" - "github.com/moira-alert/moira/notifier" "github.com/stretchr/testify/assert" . "github.com/smartystreets/goconvey/convey" @@ -59,7 +58,7 @@ func TestScheduledNotification(t *testing.T) { }) Convey("Test fetch notifications", func() { - actual, err := database.FetchNotifications(now-database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err := database.FetchNotifications(now-database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld}) @@ -68,7 +67,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 2) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ification, ¬ificationNew}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ification, ¬ificationNew}) @@ -128,7 +127,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 0) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) }) @@ -167,7 +166,7 @@ func TestScheduledNotification(t *testing.T) { So(total, ShouldEqual, 0) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) - actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err = database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) }) @@ -197,7 +196,7 @@ func TestScheduledNotificationErrorConnection(t *testing.T) { So(err, ShouldNotBeNil) So(total, ShouldEqual, 0) - actual2, err := database.FetchNotifications(0, notifier.NotificationsLimitUnlimited) + actual2, err := database.FetchNotifications(0, notificationsLimitUnlimited) So(err, ShouldNotBeNil) So(actual2, ShouldBeNil) @@ -284,7 +283,7 @@ func TestFetchNotifications(t *testing.T) { Convey("Test fetch notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) - actual, err := database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) //nolint + actual, err := database.FetchNotifications(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) //nolint So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) @@ -330,7 +329,7 @@ func TestGetNotificationsInTxWithLimit(t *testing.T) { Convey("Test with zero notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{}) err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notifier.NotificationsLimitUnlimited) + actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) return nil @@ -344,7 +343,7 @@ func TestGetNotificationsInTxWithLimit(t *testing.T) { Convey("Test all notifications without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notifier.NotificationsLimitUnlimited) + actual, err := getNotificationsInTxWithLimit(ctx, tx, now+database.getDelayedTimeInSeconds()*2, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) return nil @@ -418,7 +417,7 @@ func TestGetLimitedNotifications(t *testing.T) { Convey("Test all notifications with different timestamps without limit", func() { notifications := []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew} err := client.Watch(ctx, func(tx *redis.Tx) error { - actual, err := getLimitedNotifications(ctx, tx, notifier.NotificationsLimitUnlimited, notifications) + actual, err := getLimitedNotifications(ctx, tx, notificationsLimitUnlimited, notifications) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) return nil @@ -913,7 +912,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification, ¬ificationNew}) @@ -936,7 +935,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{}) @@ -947,7 +946,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Test all notification with ts and without limit in db", func() { addNotifications(database, []moira.ScheduledNotification{notification, notificationNew, notificationOld, notification4}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds(), notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ification4, ¬ification, ¬ificationNew}) @@ -1016,7 +1015,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew3}) @@ -1052,7 +1051,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("Without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew2}) @@ -1092,7 +1091,7 @@ func TestFetchNotificationsDo(t *testing.T) { Convey("without limit", func() { addNotifications(database, []moira.ScheduledNotification{notificationOld, notificationOld2, notification, notificationNew, notificationNew2, notificationNew3}) - actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notifier.NotificationsLimitUnlimited) + actual, err := database.fetchNotificationsDo(now+database.getDelayedTimeInSeconds()+3, notificationsLimitUnlimited) So(err, ShouldBeNil) So(actual, ShouldResemble, []*moira.ScheduledNotification{¬ificationOld, ¬ificationOld2, ¬ification, ¬ificationNew, ¬ificationNew3}) diff --git a/go.mod b/go.mod index a2d3a9c52..b309808bd 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 require ( github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible github.com/PagerDuty/go-pagerduty v1.5.1 - github.com/ansel1/merry v1.6.2 + github.com/ansel1/merry v1.8.0 github.com/aws/aws-sdk-go v1.44.293 github.com/blevesearch/bleve/v2 v2.3.8 github.com/bwmarrin/discordgo v0.25.0 @@ -15,7 +15,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/render v1.0.1 - github.com/go-graphite/carbonapi v0.16.0 + github.com/go-graphite/carbonapi v0.17.0 github.com/go-graphite/protocol v1.0.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-redsync/redsync/v4 v4.4.4 @@ -57,13 +57,11 @@ require ( ) require ( - bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0 // indirect github.com/JaderDias/movingmedian v0.0.0-20220813210630-d8c6b6de8835 // indirect github.com/Masterminds/sprig/v3 v3.2.3 github.com/RoaringBitmap/roaring v1.3.0 // indirect github.com/aclements/go-moremath v0.0.0-20210112150236-f10218a38794 // indirect - github.com/ansel1/merry/v2 v2.1.1 // indirect - github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect + github.com/ansel1/merry/v2 v2.2.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.8.0 // indirect github.com/blend/go-sdk v2.0.0+incompatible // indirect @@ -82,21 +80,19 @@ require ( github.com/blevesearch/zapx/v13 v13.3.8 // indirect github.com/blevesearch/zapx/v14 v14.3.8 // indirect github.com/blevesearch/zapx/v15 v15.3.11 // indirect - github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/disintegration/imaging v1.6.2 // indirect github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497 // indirect github.com/francoispqt/gojay v1.2.13 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/gomodule/redigo v1.8.9 // indirect + github.com/gomodule/redigo v1.9.2 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.6.0 github.com/gopherjs/gopherjs v1.17.2 // indirect @@ -115,7 +111,7 @@ require ( github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80 // indirect github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/maruel/natural v1.1.0 // indirect + github.com/maruel/natural v1.1.1 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect github.com/mattermost/logr/v2 v2.0.21 // indirect @@ -125,28 +121,26 @@ require ( github.com/mjibson/go-dsp v0.0.0-20180508042940-11479a337f12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/msaf1980/go-stringutils v0.1.4 // indirect + github.com/msaf1980/go-stringutils v0.1.6 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect - github.com/natefinch/atomic v1.0.1 // indirect github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.1 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/pkg/errors v0.9.1 - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartystreets/assertions v1.2.0 // indirect - github.com/spf13/afero v1.9.5 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.16.0 // indirect - github.com/stretchr/objx v0.5.1 // indirect - github.com/stretchr/testify v1.8.4 - github.com/subosito/gotenv v1.4.2 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.9.0 + github.com/subosito/gotenv v1.6.0 // indirect github.com/tinylib/msgp v1.1.9 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect @@ -154,9 +148,8 @@ require ( github.com/wiggin77/merror v1.0.5 // indirect github.com/wiggin77/srslog v1.0.1 // indirect go.etcd.io/bbolt v1.3.7 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/multierr v1.8.0 // indirect - go.uber.org/zap v1.24.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.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.18.0 // indirect @@ -164,7 +157,7 @@ require ( golang.org/x/oauth2 v0.17.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect - gonum.org/v1/gonum v0.12.0 // indirect + gonum.org/v1/gonum v0.15.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect @@ -199,9 +192,13 @@ require ( github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/oklog/run v1.1.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect github.com/swaggo/files v1.0.1 // indirect github.com/swaggo/swag v1.8.12 // indirect + github.com/tebeka/strftime v0.1.5 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.62.0 // indirect diff --git a/go.sum b/go.sum index 0eacd324a..a013bebd9 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0 h1:800dI8vRxVMgss6UcZY8gxk8PvYw7Qo1ZI3TrUkTKjc= -bitbucket.org/tebeka/strftime v0.0.0-20140926081919-2194253a23c0/go.mod h1:9BKpS/J2txC7Ql3QUhesesiV3HsIsA7zl7VK6cQVg5M= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -32,368 +30,31 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= @@ -435,14 +96,10 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGn github.com/alicebob/miniredis/v2 v2.22.0 h1:lIHHiSkEyS1MkKHCHzN+0mWrA4YdbGdimE5iZ2sHSzo= github.com/alicebob/miniredis/v2 v2.22.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/ansel1/merry v1.5.0/go.mod h1:wUy/yW0JX0ix9GYvUbciq+bi3jW/vlKPlbpI7qdZpOw= -github.com/ansel1/merry v1.5.1/go.mod h1:wUy/yW0JX0ix9GYvUbciq+bi3jW/vlKPlbpI7qdZpOw= -github.com/ansel1/merry v1.6.2 h1:0xr40haRrfVzmOH/JVOu7KOKGEI1c/7q5EmgTEbn+Ng= -github.com/ansel1/merry v1.6.2/go.mod h1:pAcMW+2uxIgpzEON021vMtFsrymREY6faJWiiz1QGVQ= -github.com/ansel1/merry/v2 v2.0.1/go.mod h1:dD5OhpiPrVkvgseRYd+xgYlx7s6ytU3v9BTTJlDA7FM= -github.com/ansel1/merry/v2 v2.1.1 h1:Ax0gQh7Z/GfimoVg2EDBAU6CJIieWwVvhtBKJdkCE1M= -github.com/ansel1/merry/v2 v2.1.1/go.mod h1:4p/FFyQbCgqlDbseWOVQaL5USpgkE9sr5xh4V6Ry0JU= -github.com/ansel1/vespucci/v4 v4.1.1/go.mod h1:zzdrO4IgBfgcGMbGTk/qNGL8JPslmW3nPpcBHKReFYY= +github.com/ansel1/merry v1.8.0 h1:3RddCV1ubXegKphsodbkmZ4QuROep/ZaPCuwlKuCfFg= +github.com/ansel1/merry v1.8.0/go.mod h1:wJVu1mHEtEUWq5zTTX9RiWjcE+xL8y7BGYl2VTYdP7M= +github.com/ansel1/merry/v2 v2.2.1 h1:PJpynLFvIpJkn8ZGgNHLq332zIyBc/wTqp3o42ZpWdU= +github.com/ansel1/merry/v2 v2.2.1/go.mod h1:K9lCkM6tJ8s7LQVQ0ZmZ0WrB3BCyr+ZDzoqotzzoxpI= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= @@ -451,10 +108,6 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aws/aws-sdk-go v1.44.293 h1:oBPrQqsyMYe61Sl/xKVvQFflXjPwYH11aKi8QR3Nhts= github.com/aws/aws-sdk-go v1.44.293/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df h1:GSoSVRLoBaFpOOds6QyY1L8AX7uoY+Ln3BHc22W40X0= -github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df/go.mod h1:hiVxq5OP2bUGBRNS3Z/bt/reCLFNbdcST6gISi1fiOM= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -500,8 +153,6 @@ github.com/blevesearch/zapx/v14 v14.3.8/go.mod h1:vS6exLagv0vXmgpUbNRZC6UuEV0xwT github.com/blevesearch/zapx/v15 v15.3.11 h1:dstyZki9s10FNLsW4LpEvPQ+fmM3nX15h4wKfcBwnEg= github.com/blevesearch/zapx/v15 v15.3.11/go.mod h1:hiYbBDf5/Ud/Eji0faUmMTOyeOjcl8q1vWGgRe7+bIQ= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= -github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 h1:hjXJeBcAMS1WGENGqDpzvmgS43oECTx8UXq31UBu0Jw= -github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= @@ -510,8 +161,6 @@ github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0 github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1 h1:hXakhQtPnXH839q1pBl/GqfTSchqE+R5Fqn98Iu7UQM= github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1/go.mod h1:pAxCBpjl/0JxYZlWGP/Dyi8f/LQSCQD2WAsG/iNzqQ8= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -527,14 +176,11 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -543,10 +189,9 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 h1:M5QgkYacWj0Xs8MhpIK/5uwU02icXpEoSo9sM2aRCps= github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432/go.mod h1:xwIwAxMvYnVrGJPe2FKx5prTrnAjGOD8zvDOnxnrrkM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb h1:X9MwMz6mVZEWcbhsri5TwaCm/Q4USFdAAmy1T7RCGjw= -github.com/dgryski/go-expirecache v0.0.0-20170314133854-743ef98b2adb/go.mod h1:pD/+9DfmmQ+xvOI1fxUltHV69BxC1aeTILPQg9Kw1hE= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768 h1:Xzl7CSuSnGsyU+9xmSU2h8w3d7Tnis66xeoNN207tLo= github.com/dgryski/go-onlinestats v0.0.0-20170612111826-1c7d19468768/go.mod h1:alfmlCqcg4uw9jaoIU1nOp9RFdJLMuu8P07BCEgpgoo= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= @@ -571,10 +216,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497 h1:DIQ8EvZ8OjuPNfcV4NgsyBeZho7WsTD0JEkDM5napMI= github.com/evmar/gocairo v0.0.0-20160222165215-ddd30f837497/go.mod h1:YXKUYPSqs+jDG8mvexHN2uTik4PKwg2B0WK9itQ0VrE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -589,13 +231,13 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= @@ -610,8 +252,8 @@ github.com/go-errors/errors v1.1.1/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWE github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-graphite/carbonapi v0.16.0 h1:HvPjAKYChiwdHtNpFu33hLRpPCYA7gyyeFWpPR2XvXs= -github.com/go-graphite/carbonapi v0.16.0/go.mod h1:RQpis4h2a1kxn1s/R5LQXbumFj+kR2bRz+BebPN1Z1Q= +github.com/go-graphite/carbonapi v0.17.0 h1:6JowndAU0qsxUoBftCBy23Rgt8dCxf+1wHxfGZ3GA2E= +github.com/go-graphite/carbonapi v0.17.0/go.mod h1:EwQ1MJBzP8BBozLkgwTZdePM/5SqPSxhtY0uDdER2LQ= github.com/go-graphite/protocol v1.0.0 h1:Fqb0mkVVtfMrn6vw6Ntm3raf3gVVZCOVdZu4JosW5qE= github.com/go-graphite/protocol v1.0.0/go.mod h1:eonkg/0UGhJUYu+PshOg1NzWSUcXskr/yHeQXJHJr8Y= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -670,7 +312,6 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGw github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -702,14 +343,14 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= -github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= -github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= +github.com/gomodule/redigo v1.9.2 h1:HrutZBLhSIU8abiSfW8pj8mPhOyMYjZT/wcA4/L9L9s= +github.com/gomodule/redigo v1.9.2/go.mod h1:KsU3hiK/Ay8U42qpaJk+kuNa3C+spxapWpM+ywhcgtw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -726,7 +367,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= @@ -757,14 +397,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -774,10 +408,6 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= @@ -795,8 +425,6 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= @@ -848,7 +476,6 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= @@ -882,8 +509,6 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= @@ -911,8 +536,6 @@ github.com/lomik/og-rek v0.0.0-20170411191824-628eefeb8d80/go.mod h1:T7SQVaLtK7m github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463 h1:SN/0TEkyYpp8tit79JPUnecebCGZsXiYYPxN8i3I6Rk= github.com/lomik/zapwriter v0.0.0-20210624082824-c1161d1eb463/go.mod h1:rWIJAUD2hPOAyOzc3jBShAhN4CAZeLAyzUA/n8tE8ak= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -922,8 +545,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/maruel/natural v1.1.0 h1:2z1NgP/Vae+gYrtC0VuvrTJ6U35OuyUqDdfluLqMWuQ= -github.com/maruel/natural v1.1.0/go.mod h1:eFVhYCcUOfZFxXoDZam8Ktya72wa79fNC3lc/leA0DQ= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 h1:Khvh6waxG1cHc4Cz5ef9n3XVCxRWpAKUtqg9PJl5+y8= github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404/go.mod h1:RyS7FDNQlzF1PsjbJWHRI35exqaKGSO9qD4iv8QjE34= github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 h1:Y1Tu/swM31pVwwb2BTCsOdamENjjWCI6qmfHLbk6OZI= @@ -941,7 +564,6 @@ github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= @@ -984,15 +606,13 @@ github.com/moira-alert/blackfriday-slack v0.1.2 h1:W6VbDlHDBxoB7X+OJ+3xZZuzMcQ0q github.com/moira-alert/blackfriday-slack v0.1.2/go.mod h1:tYMK3laTzU1wgxeOpUPdw36KHD3eTyQNDfxtg1nXLWI= github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19 h1:dV1yczr6ndr5fCnBvj2SjBJxJNtnBtfZye0gDwTrPLs= github.com/moira-alert/go-chart v0.0.0-20240813052818-d1336bdacc19/go.mod h1:ktrkvZGboMQfYyBXAV05imlVxGIvVdeCn5vz91Fw1vE= -github.com/msaf1980/go-stringutils v0.1.4 h1:UwsIT0hplHVucqbknk3CoNqKkmIuSHhsbBldXxyld5U= -github.com/msaf1980/go-stringutils v0.1.4/go.mod h1:AxmV/6JuQUAtZJg5XmYATB5ZwCWgtpruVHY03dswRf8= +github.com/msaf1980/go-stringutils v0.1.6 h1:qri8o+4XLJCJYemHcvJY6xJhrGTmllUoPwayKEj4NSg= +github.com/msaf1980/go-stringutils v0.1.6/go.mod h1:xpicaTIpLAVzL0gUQkciB1zjypDGKsOCI25cKQbRQYA= github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= -github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= @@ -1028,8 +648,8 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= +github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -1037,10 +657,10 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= @@ -1096,6 +716,10 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -1135,31 +759,29 @@ github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYl github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= -github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= -github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= -github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1171,15 +793,14 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203/go.mod h1:oqN97ltKNihBbwlX8dLpwxCl3+HnXKV/R0e+sRLd9C8= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= -github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= @@ -1187,6 +808,8 @@ github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4 github.com/swaggo/swag v1.8.12 h1:pctzkNPu0AlQP2royqX3apjKCQonAnf7KGoxeO4y64w= github.com/swaggo/swag v1.8.12/go.mod h1:lNfm6Gg+oAq3zRJQNEMBE66LIJKM44mxFqhEEgy2its= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tebeka/strftime v0.1.5 h1:1NQKN1NiQgkqd/2moD6ySP/5CoZQsKa1d3ZhJ44Jpmg= +github.com/tebeka/strftime v0.1.5/go.mod h1:29/OidkoWHdEKZqzyDLUyC+LmgDgdHo4WAFCDT7D/Ig= github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU= github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -1230,24 +853,20 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk= go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU= -go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1256,7 +875,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -1265,7 +883,6 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= @@ -1310,9 +927,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1364,7 +979,6 @@ golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -1372,17 +986,9 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= @@ -1408,14 +1014,6 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= @@ -1431,10 +1029,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1509,7 +1104,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1526,17 +1120,11 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= @@ -1545,7 +1133,6 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1558,8 +1145,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= @@ -1567,8 +1152,6 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1629,20 +1212,16 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= -gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +gonum.org/v1/gonum v0.15.0 h1:2lYxjRbTYyxkJxlhC+LvJIx3SsANPdRybu1tGj9/OrQ= +gonum.org/v1/gonum v0.15.0/go.mod h1:xzZVBJBtS+Mz4q0Yl2LJTk+OxOg4jiXZ7qBoM0uISGo= google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= @@ -1683,24 +1262,8 @@ google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/S google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1757,7 +1320,6 @@ google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= @@ -1783,59 +1345,18 @@ google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220208230804-65c12eb4c068/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -1867,18 +1388,10 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= @@ -1896,7 +1409,6 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= diff --git a/metric_source/local/database_test.go b/metric_source/local/database_test.go new file mode 100644 index 000000000..7f3a40b65 --- /dev/null +++ b/metric_source/local/database_test.go @@ -0,0 +1,431 @@ +package local + +import ( + "fmt" + "math" + "testing" + "time" + + "github.com/moira-alert/moira" + + "github.com/moira-alert/moira/database/redis" + + logging "github.com/moira-alert/moira/logging/zerolog_adapter" + + . "github.com/smartystreets/goconvey/convey" +) + +type metricMock struct { + values []float64 + patterns []string +} + +type testCase struct { + metrics map[string]metricMock + from int64 + retention int64 + target string + expected map[string][]float64 + expectedWildcards map[string]bool +} + +func saveMetrics(database moira.Database, metrics map[string]metricMock, now, retention int64) error { + maxValues := 0 + for _, m := range metrics { + if len(m.values) > maxValues { + maxValues = len(m.values) + } + } + + timeStart := now - retention*int64(maxValues-1) + for i := range maxValues { + time := timeStart + int64(i)*retention + + metricsMap := make(map[string]*moira.MatchedMetric, len(metrics)) + for name, metric := range metrics { + if len(metric.values) <= i { + continue + } + + metricsMap[name] = &moira.MatchedMetric{ + Metric: name, + Patterns: metric.patterns, + Value: metric.values[i], + Timestamp: time, + RetentionTimestamp: time, + Retention: int(retention), + } + } + + err := database.SaveMetrics(metricsMap) + if err != nil { + return err + } + } + return nil +} + +func TestLocalSourceWithDatabaseWildcards(t *testing.T) { + logger, _ := logging.ConfigureLog("stdout", "info", "test", true) // nolint: govet + database := redis.NewTestDatabase(logger) + localSource := Create(database) + + defer database.Flush() + + retention := int64(60) + now := floorToMultiplier(time.Now().Unix(), retention) - 2 + + testCases := []testCase{ + { + metrics: map[string]metricMock{ + "metric1": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"pattern"}, + }, + "metric2": { + values: []float64{5.0, 4.0, 3.0, 2.0, 1.0}, + patterns: []string{"pattern"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "pattern", + expectedWildcards: map[string]bool{ + "metric1": false, + "metric2": false, + }, + }, + { + metrics: map[string]metricMock{ + "metric1": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"pattern1"}, + }, + "metric2": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"pattern1"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "divideSeries(pattern1, pattern2)", + expectedWildcards: map[string]bool{ + "divideSeries(metric1,pattern2)": false, + "divideSeries(metric2,pattern2)": false, + }, + }, + { + metrics: map[string]metricMock{}, + from: now - retention*4, + retention: retention, + target: "pattern", + expectedWildcards: map[string]bool{ + "pattern": true, + }, + }, + } + + Convey("Run test cases", t, func() { + for idx, testCase := range testCases { + Convey(fmt.Sprintf("suite %d, Target '%s'", idx, testCase.target), func() { + database.Flush() + + err := saveMetrics(database, testCase.metrics, now, testCase.retention) + So(err, ShouldBeNil) + + result, err := localSource.Fetch(testCase.target, testCase.from, now, true) + So(err, ShouldBeNil) + + resultData := result.GetMetricsData() + + wildcardResultMap := map[string]bool{} + for _, data := range resultData { + wildcardResultMap[data.Name] = data.Wildcard + } + So(wildcardResultMap, shouldEqualIfNaNsEqual, testCase.expectedWildcards) + }) + } + }) +} + +func TestLocalSourceWithDatabase(t *testing.T) { + logger, _ := logging.ConfigureLog("stdout", "info", "test", true) // nolint: govet + database := redis.NewTestDatabase(logger) + localSource := Create(database) + + defer database.Flush() + + retention := int64(60) + now := floorToMultiplier(time.Now().Unix(), retention) + nan := math.NaN() + + testCases := []testCase{ + { + metrics: map[string]metricMock{ + "metric1": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"pattern"}, + }, + "metric2": { + values: []float64{5.0, 4.0, 3.0, 2.0, 1.0}, + patterns: []string{"pattern"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "pattern", + expected: map[string][]float64{ + "metric1": {1.0, 2.0, 3.0, 4.0, 5.0}, + "metric2": {5.0, 4.0, 3.0, 2.0, 1.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric1": { + values: []float64{1.0, 3.0, 1.0, 3.0, 1.0, 3.0}, + patterns: []string{"pattern"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "movingAverage(pattern, 2)", + expected: map[string][]float64{ + "movingAverage(metric1,2)": {2.0, 2.0, 2.0, 2.0, 2.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric1": { + values: []float64{1.0, nan, 2.0, nan, nan}, + patterns: []string{"pattern"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "keepLastValue(pattern, 1)", + expected: map[string][]float64{ + "keepLastValue(metric1,1)": {1.0, 1.0, 2.0, 2.0, nan}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.1.foo": { + values: []float64{1.0, 2.0, 1.0, 2.0, 1.0}, + patterns: []string{"metric.*.*", "metric.1.foo"}, + }, + "metric.1.bar": { + values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, + patterns: []string{"metric.*.*", "metric.1.bar"}, + }, + "metric.2.foo": { + values: []float64{3.0, 2.0, 3.0, 2.0, 3.0}, + patterns: []string{"metric.*.*", "metric.2.foo"}, + }, + "metric.2.bar": { + values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, + patterns: []string{"metric.*.*", "metric.2.bar"}, + }, + }, + from: now - retention*4, + retention: retention, + target: `applyByNode(metric.*.*, 1, "movingMax(%.foo, '2m')")`, + expected: map[string][]float64{ + "movingMax(metric.1.foo,'2m')": {1.0, 2.0, 2.0, 2.0, 2.0}, + "movingMax(metric.2.foo,'2m')": {3.0, 3.0, 3.0, 3.0, 3.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "aliasByNode(metric.*, 1)", + expected: map[string][]float64{ + "foo": {1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "aliasByNode(metric.*, 2)", + expected: map[string][]float64{ + "": {1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "consolidateBy(metric.*, 'max')", + expected: map[string][]float64{ + `consolidateBy(metric.foo,"max")`: {1.0, 2.0, 3.0, 4.0, 5.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.1": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + "metric.2": { + values: []float64{-1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*"}, + }, + "metric.3": { + values: []float64{-1.0, -2.0, -3.0, -4.0, -5.0}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "minimumBelow(metric.*, 0)", + expected: map[string][]float64{ + "metric.2": {-1.0, 2.0, 3.0, 4.0, 5.0}, + "metric.3": {-1.0, -2.0, -3.0, -4.0, -5.0}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo.1": { + values: []float64{1.0, 2.0, 3.0, 4.0, 5.0}, + patterns: []string{"metric.*.*"}, + }, + "metric.foo.2": { + values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, + patterns: []string{"metric.*.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "groupByNode(metric.*.*, 1, 'sumSeries')", + expected: map[string][]float64{ + "foo": {2.5, 4.5, 6.5, 8.5, 10.5}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo.1": { + values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, + patterns: []string{"metric.*.*"}, + }, + "metric.foo.2": { + values: []float64{1.5, 2.5, 3.5, 4.5, 5.5}, + patterns: []string{"metric.*.*"}, + }, + }, + from: now - retention*4, + retention: retention, + target: "groupByNode(metric.*.*, 1, 'unique')", + expected: map[string][]float64{ + "foo": {1.5, 2.5, 3.5, 4.5, 5.5}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{0, 1, 2, 3, 4, 5}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "hitcount(metric.*, '2m')", + expected: map[string][]float64{ + "hitcount(metric.foo,'2m')": {60, 300, 540}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1, nan, 3, nan, nan, 6}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "interpolate(metric.*, 1)", + expected: map[string][]float64{ + "interpolate(metric.foo)": {1, 2, 3, nan, nan, 6}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1, nan, 3, nan, nan, 6}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "interpolate(metric.*)", + expected: map[string][]float64{ + "interpolate(metric.foo)": {1, 2, 3, 4, 5, 6}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1, 2, 3, 4, 5, 6}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "smartSummarize(metric.*, '2m', 'average')", + expected: map[string][]float64{ + "smartSummarize(metric.foo,'2m','average')": {1.5, 3.5, 5.5}, + }, + }, + { + metrics: map[string]metricMock{ + "metric.foo": { + values: []float64{1.5, 2, 3, 4, 5, 6.5}, + patterns: []string{"metric.*"}, + }, + }, + from: now - retention*5, + retention: retention, + target: "smartSummarize(metric.*, '3m', 'median')", + expected: map[string][]float64{ + "smartSummarize(metric.foo,'3m','median')": {2, 5}, + }, + }, + } + + Convey("Run test cases", t, func() { + for _, testCase := range testCases { + Convey(fmt.Sprintf("Target '%s'", testCase.target), func() { + database.Flush() + + err := saveMetrics(database, testCase.metrics, now, testCase.retention) + So(err, ShouldBeNil) + + result, err := localSource.Fetch(testCase.target, testCase.from, now, true) + So(err, ShouldBeNil) + + resultData := result.GetMetricsData() + resultMap := map[string][]float64{} + for _, data := range resultData { + resultMap[data.Name] = data.Values + } + + So(resultMap, shouldEqualIfNaNsEqual, testCase.expected) + }) + } + }) +} diff --git a/metric_source/local/eval.go b/metric_source/local/eval.go index 3d36c9e2a..4b429d189 100644 --- a/metric_source/local/eval.go +++ b/metric_source/local/eval.go @@ -3,9 +3,9 @@ package local import ( "context" "errors" - "fmt" "runtime/debug" + "github.com/ansel1/merry" "github.com/go-graphite/carbonapi/expr" "github.com/go-graphite/carbonapi/expr/helper" "github.com/go-graphite/carbonapi/expr/types" @@ -14,75 +14,139 @@ import ( metricSource "github.com/moira-alert/moira/metric_source" ) -type evalCtx struct { - from int64 - until int64 +type evaluator struct { + database moira.Database + metrics []string } -func (ctx *evalCtx) fetchAndEval(database moira.Database, target string, result *FetchResult) error { - exp, err := ctx.parse(target) - if err != nil { - return err - } +func (eval *evaluator) fetchAndEval(target string, from, until int64, result *FetchResult) (err error) { + defer func() { + if r := recover(); r != nil { + err = ErrEvaluateTargetFailedWithPanic{ + target: target, + recoverMessage: r, + stackRecord: debug.Stack(), + } + } + }() - fetchedMetrics, err := ctx.getMetricsData(database, exp) + exp, err := eval.parse(target) if err != nil { return err } - commonStep := fetchedMetrics.calculateCommonStep() - ctx.scaleToCommonStep(commonStep, fetchedMetrics) + values := make(map[parser.MetricRequest][]*types.MetricData) - rewritten, newTargets, err := ctx.rewriteExpr(exp, fetchedMetrics) + fetchedMetrics, err := expr.FetchAndEvalExp(context.Background(), eval, exp, from, until, values) if err != nil { - return err + return merry.Unwrap(err) } - if rewritten { - for _, newTarget := range newTargets { - err = ctx.fetchAndEvalNoRewrite(database, newTarget, result) - if err != nil { - return err - } + eval.writeResult(exp, fetchedMetrics, result) + + return nil +} + +// Fetch is an implementation of Evaluator interface from carbonapi. +// It returns a map the metrics requested in the current invocation, scaled to a common step. +func (eval *evaluator) Fetch( + ctx context.Context, + exprs []parser.Expr, + from, until int64, + values map[parser.MetricRequest][]*types.MetricData, +) (map[parser.MetricRequest][]*types.MetricData, error) { + fetch := newFetchCtx(0, 0) + + for _, exp := range exprs { + ms := exp.Metrics(from, until) + if err := fetch.getMetricsData(eval.database, ms); err != nil { + return nil, err } - return nil } - metricsData, err := ctx.eval(target, exp, fetchedMetrics) - if err != nil { - return err - } + fetch.scaleToCommonStep() - ctx.writeResult(exp, fetchedMetrics, metricsData, result) + eval.metrics = append(eval.metrics, fetch.fetchedMetrics.metrics...) - return nil + return fetch.fetchedMetrics.metricsMap, nil } -func (ctx *evalCtx) fetchAndEvalNoRewrite(database moira.Database, target string, result *FetchResult) error { - exp, err := ctx.parse(target) +// Eval is an implementation of Evaluator interface from carbonapi. +// It uses the raw data within the values map being passed into it to in order to evaluate the input expression. +func (eval *evaluator) Eval( + ctx context.Context, + exp parser.Expr, + from, until int64, + values map[parser.MetricRequest][]*types.MetricData, +) (results []*types.MetricData, err error) { + rewritten, newTargets, err := expr.RewriteExpr(ctx, eval, exp, from, until, values) if err != nil { - return err + return nil, err } - fetchedMetrics, err := ctx.getMetricsData(database, exp) + if rewritten { + return eval.evalRewritten(ctx, newTargets, from, until, values) + } + + results, err = expr.EvalExpr(ctx, eval, exp, from, until, values) if err != nil { - return err + if errors.Is(err, parser.ErrMissingTimeseries) { + err = nil + } else if isErrUnknownFunction(err) { + err = ErrorUnknownFunction(err) + } else { + err = ErrEvalExpr{ + target: exp.ToString(), + internalError: err, + } + } } - commonStep := fetchedMetrics.calculateCommonStep() - ctx.scaleToCommonStep(commonStep, fetchedMetrics) + return results, err +} - metricsData, err := ctx.eval(target, exp, fetchedMetrics) - if err != nil { - return err +func (eval *evaluator) evalRewritten( + ctx context.Context, + newTargets []string, + from, until int64, + values map[parser.MetricRequest][]*types.MetricData, +) (results []*types.MetricData, err error) { + for _, target := range newTargets { + exp, _, err := parser.ParseExpr(target) + if err != nil { + return nil, err + } + + var targetValues map[parser.MetricRequest][]*types.MetricData + targetValues, err = eval.Fetch(ctx, []parser.Expr{exp}, from, until, values) + if err != nil { + return nil, err + } + + result, err := eval.Eval(ctx, exp, from, until, targetValues) + if err != nil { + return nil, err + } + + results = append(results, result...) } - ctx.writeResult(exp, fetchedMetrics, metricsData, result) + return results, nil +} - return nil +func (eval *evaluator) writeResult(exp parser.Expr, metricsData []*types.MetricData, result *FetchResult) { + result.Metrics = append(result.Metrics, eval.metrics...) + for _, mr := range exp.Metrics(0, 0) { + result.Patterns = append(result.Patterns, mr.Metric) + } + + for _, metricData := range metricsData { + md := newMetricDataFromGraphite(metricData, len(result.Metrics) == 0) + result.MetricsData = append(result.MetricsData, md) + } } -func (ctx *evalCtx) parse(target string) (parser.Expr, error) { +func (eval *evaluator) parse(target string) (parser.Expr, error) { parsedExpr, _, err := parser.ParseExpr(target) if err != nil { return nil, ErrParseExpr{ @@ -93,109 +157,71 @@ func (ctx *evalCtx) parse(target string) (parser.Expr, error) { return parsedExpr, nil } -func (ctx *evalCtx) getMetricsData(database moira.Database, parsedExpr parser.Expr) (*fetchedMetrics, error) { - metricRequests := parsedExpr.Metrics() +type fetchCtx struct { + from int64 + until int64 + fetchedMetrics *fetchedMetrics +} - metrics := make([]string, 0) - metricsMap := make(map[parser.MetricRequest][]*types.MetricData) +func newFetchCtx(from, until int64) *fetchCtx { + return &fetchCtx{ + from, + until, + &fetchedMetrics{ + metricsMap: make(map[parser.MetricRequest][]*types.MetricData), + metrics: make([]string, 0), + }, + } +} +func (ctx *fetchCtx) getMetricsData(database moira.Database, metricRequests []parser.MetricRequest) error { fetchData := fetchData{database} for _, mr := range metricRequests { + // Other fields are used in carbon for database side consolidations + request := parser.MetricRequest{ + Metric: mr.Metric, + From: mr.From, + Until: mr.Until, + } + from := mr.From + ctx.from until := mr.Until + ctx.until metricNames, err := fetchData.fetchMetricNames(mr.Metric) if err != nil { - return nil, err + return err } - timer := NewTimerRoundingTimestamps(from, until, metricNames.retention) + timer := newTimerRoundingTimestamps(from, until, metricNames.retention) metricsData, err := fetchData.fetchMetricValues(mr.Metric, metricNames, timer) if err != nil { - return nil, err + return err } - metricsMap[mr] = metricsData - metrics = append(metrics, metricNames.metrics...) + ctx.fetchedMetrics.metricsMap[request] = metricsData + ctx.fetchedMetrics.metrics = append(ctx.fetchedMetrics.metrics, metricNames.metrics...) } - return &fetchedMetrics{metricsMap, metrics}, nil + return nil } -func (ctx *evalCtx) scaleToCommonStep(retention int64, fetchedMetrics *fetchedMetrics) { - from, until := RoundTimestamps(ctx.from, ctx.until, retention) - ctx.from, ctx.until = from, until +func (ctx *fetchCtx) scaleToCommonStep() { + retention := ctx.fetchedMetrics.calculateCommonStep() metricMap := make(map[parser.MetricRequest][]*types.MetricData) - for metricRequest, metricData := range fetchedMetrics.metricsMap { - metricRequest.From += from - metricRequest.Until += until + for metricRequest, metricData := range ctx.fetchedMetrics.metricsMap { + metricRequest.From += ctx.from + metricRequest.Until += ctx.until metricData = helper.ScaleToCommonStep(metricData, retention) metricMap[metricRequest] = metricData } - fetchedMetrics.metricsMap = metricMap + ctx.fetchedMetrics.metricsMap = metricMap } -func (ctx *evalCtx) rewriteExpr(parsedExpr parser.Expr, metrics *fetchedMetrics) (bool, []string, error) { - rewritten, newTargets, err := expr.RewriteExpr( - context.Background(), - parsedExpr, - ctx.from, - ctx.until, - metrics.metricsMap, - ) - - if err != nil && !errors.Is(err, parser.ErrMissingTimeseries) { - return false, nil, fmt.Errorf("failed RewriteExpr: %s", err.Error()) - } - return rewritten, newTargets, nil -} - -func (ctx *evalCtx) eval(target string, parsedExpr parser.Expr, metrics *fetchedMetrics) (result []*types.MetricData, err error) { - defer func() { - if r := recover(); r != nil { - result = nil - err = ErrEvaluateTargetFailedWithPanic{ - target: target, - recoverMessage: r, - stackRecord: debug.Stack(), - } - } - }() - - result, err = expr.EvalExpr(context.Background(), parsedExpr, ctx.from, ctx.until, metrics.metricsMap) - if err != nil { - if errors.Is(err, parser.ErrMissingTimeseries) { - err = nil - } else if isErrUnknownFunction(err) { - err = ErrorUnknownFunction(err) - } else { - err = ErrEvalExpr{ - target: target, - internalError: err, - } - } - } - - return result, err -} - -func (ctx *evalCtx) writeResult(exp parser.Expr, metrics *fetchedMetrics, metricsData []*types.MetricData, result *FetchResult) { - for _, metricData := range metricsData { - md := newMetricDataFromGraphit(metricData, metrics.hasWildcard()) - result.MetricsData = append(result.MetricsData, md) - } - - result.Metrics = append(result.Metrics, metrics.metrics...) - for _, mr := range exp.Metrics() { - result.Patterns = append(result.Patterns, mr.Metric) - } -} - -func newMetricDataFromGraphit(md *types.MetricData, wildcard bool) metricSource.MetricData { +func newMetricDataFromGraphite(md *types.MetricData, wildcard bool) metricSource.MetricData { return metricSource.MetricData{ Name: md.Name, StartTime: md.StartTime, @@ -211,10 +237,6 @@ type fetchedMetrics struct { metrics []string } -func (m *fetchedMetrics) hasWildcard() bool { - return len(m.metrics) == 0 -} - func (m *fetchedMetrics) calculateCommonStep() int64 { commonStep := int64(1) for _, metricsData := range m.metricsMap { diff --git a/metric_source/local/fetchdata.go b/metric_source/local/fetchdata.go index e2a7a4a6d..880e46432 100644 --- a/metric_source/local/fetchdata.go +++ b/metric_source/local/fetchdata.go @@ -38,7 +38,7 @@ func (fd *fetchData) fetchMetricNames(pattern string) (*metricsWithRetention, er return &metricsWithRetention{retention, metrics}, nil } -func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithRetention, timer Timer) ([]*types.MetricData, error) { +func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithRetention, timer timer) ([]*types.MetricData, error) { if len(metrics.metrics) == 0 { return fetchDataNoMetrics(timer, pattern), nil } @@ -58,7 +58,7 @@ func (fd *fetchData) fetchMetricValues(pattern string, metrics *metricsWithReten return metricsData, nil } -func fetchDataNoMetrics(timer Timer, pattern string) []*types.MetricData { +func fetchDataNoMetrics(timer timer, pattern string) []*types.MetricData { dataList := map[string][]*moira.MetricValue{pattern: make([]*moira.MetricValue, 0)} valuesMap := unpackMetricsValues(dataList, timer) metricsData := createMetricData(pattern, timer, valuesMap[pattern]) @@ -66,7 +66,7 @@ func fetchDataNoMetrics(timer Timer, pattern string) []*types.MetricData { return []*types.MetricData{metricsData} } -func createMetricData(metric string, timer Timer, values []float64) *types.MetricData { +func createMetricData(metric string, timer timer, values []float64) *types.MetricData { fetchResponse := pb.FetchResponse{ Name: metric, StartTime: timer.startTime, @@ -77,7 +77,7 @@ func createMetricData(metric string, timer Timer, values []float64) *types.Metri return &types.MetricData{FetchResponse: fetchResponse, Tags: tags.ExtractTags(metric)} } -func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer Timer) map[string][]float64 { +func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer timer) map[string][]float64 { valuesMap := make(map[string][]float64, len(metricsData)) for metric, metricData := range metricsData { valuesMap[metric] = unpackMetricValues(metricData, timer) @@ -85,13 +85,13 @@ func unpackMetricsValues(metricsData map[string][]*moira.MetricValue, timer Time return valuesMap } -func unpackMetricValues(metricData []*moira.MetricValue, timer Timer) []float64 { +func unpackMetricValues(metricData []*moira.MetricValue, timer timer) []float64 { points := make(map[int]*moira.MetricValue, len(metricData)) for _, metricValue := range metricData { - points[timer.GetTimeSlot(metricValue.RetentionTimestamp)] = metricValue + points[timer.getTimeSlot(metricValue.RetentionTimestamp)] = metricValue } - numberOfTimeSlots := timer.NumberOfTimeSlots() + numberOfTimeSlots := timer.numberOfTimeSlots() values := make([]float64, 0, numberOfTimeSlots) diff --git a/metric_source/local/fetchdata_test.go b/metric_source/local/fetchdata_test.go index c55142ab3..c5b4a0aae 100644 --- a/metric_source/local/fetchdata_test.go +++ b/metric_source/local/fetchdata_test.go @@ -18,7 +18,7 @@ func BenchmarkUnpackMetricsValues(b *testing.B) { var until int64 = 1317 var retention int64 = 10 - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) metricsCount := 7300 @@ -47,7 +47,7 @@ func BenchmarkUnpackMetricValues(b *testing.B) { var until int64 = 317 var retention int64 = 10 - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) metricsValues := make([]*moira.MetricValue, 0) @@ -75,7 +75,7 @@ func TestFetchDataErrors(t *testing.T) { pattern := "super-puper-pattern" metric := "super-puper-metric" - timer := NewTimerRoundingTimestamps(17, 67, 10) + timer := newTimerRoundingTimestamps(17, 67, 10) retentionErr := fmt.Errorf("Ooops, retention error") patternErr := fmt.Errorf("Ooops, pattern error") @@ -145,7 +145,7 @@ func TestFetchData(t *testing.T) { var from int64 = 17 var until int64 = 67 var retention int64 = 10 - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) Convey("Test no metrics", t, func() { dataBase.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) @@ -153,7 +153,7 @@ func TestFetchData(t *testing.T) { metrics, err := fetchedData.fetchMetricNames(pattern) So(err, ShouldBeNil) - timerNoMetrics := NewTimerRoundingTimestamps(from, until, metrics.retention) + timerNoMetrics := newTimerRoundingTimestamps(from, until, metrics.retention) metricValues, err := fetchedData.fetchMetricValues(pattern, metrics, timerNoMetrics) So(metricValues[0], shouldEqualIfNaNsEqual, &types.MetricData{ @@ -235,28 +235,28 @@ func TestUnpackMetricValuesNoData(t *testing.T) { metricData := map[string][]*moira.MetricValue{"metric": make([]*moira.MetricValue, 0)} Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 0 until 10", t, func() { - timer := NewTimerRoundingTimestamps(0, 10, retention) + timer := newTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN(), math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) }) Convey("From 1 until 11", t, func() { - timer := NewTimerRoundingTimestamps(1, 11, retention) + timer := newTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) expected := []float64{math.NaN()} So(val["metric"], shouldEqualIfNaNsEqual, expected) @@ -273,49 +273,49 @@ func TestUnpackMetricValues(t *testing.T) { }} Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{}) }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.0}) }) Convey("From 1 until 11", t, func() { - timer := NewTimerRoundingTimestamps(1, 11, retention) + timer := newTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.0}) }) Convey("From 0 until 10", t, func() { - timer := NewTimerRoundingTimestamps(0, 10, retention) + timer := newTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.0}) }) Convey("From 0 until 11", t, func() { - timer := NewTimerRoundingTimestamps(0, 11, retention) + timer := newTimerRoundingTimestamps(0, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 0 until 19", t, func() { - timer := NewTimerRoundingTimestamps(0, 19, retention) + timer := newTimerRoundingTimestamps(0, 19, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 1 until 30", t, func() { - timer := NewTimerRoundingTimestamps(1, 30, retention) + timer := newTimerRoundingTimestamps(1, 30, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) @@ -330,7 +330,7 @@ func TestMultipleSeriesNoData(t *testing.T) { } Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -338,7 +338,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN()}) @@ -346,7 +346,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 1 until 5", t, func() { - timer := NewTimerRoundingTimestamps(1, 5, retention) + timer := newTimerRoundingTimestamps(1, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -354,7 +354,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 0 until 5", t, func() { - timer := NewTimerRoundingTimestamps(0, 5, retention) + timer := newTimerRoundingTimestamps(0, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN()}) @@ -362,7 +362,7 @@ func TestMultipleSeriesNoData(t *testing.T) { }) Convey("From 5 until 30", t, func() { - timer := NewTimerRoundingTimestamps(5, 30, retention) + timer := newTimerRoundingTimestamps(5, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{math.NaN(), math.NaN(), math.NaN()}) @@ -387,7 +387,7 @@ func TestMultipleSeries(t *testing.T) { } Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -395,7 +395,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric1"], shouldEqualIfNaNsEqual, []float64{100.0}) @@ -403,7 +403,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 1 until 5", t, func() { - timer := NewTimerRoundingTimestamps(1, 5, retention) + timer := newTimerRoundingTimestamps(1, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{}) @@ -411,7 +411,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 5", t, func() { - timer := NewTimerRoundingTimestamps(0, 5, retention) + timer := newTimerRoundingTimestamps(0, 5, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.0}) @@ -419,7 +419,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 9", t, func() { - timer := NewTimerRoundingTimestamps(0, 9, retention) + timer := newTimerRoundingTimestamps(0, 9, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00}) @@ -427,7 +427,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 10", t, func() { - timer := NewTimerRoundingTimestamps(0, 10, retention) + timer := newTimerRoundingTimestamps(0, 10, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) @@ -435,7 +435,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 1 until 11", t, func() { - timer := NewTimerRoundingTimestamps(1, 11, retention) + timer := newTimerRoundingTimestamps(1, 11, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{200.00}) @@ -443,7 +443,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 0 until 30", t, func() { - timer := NewTimerRoundingTimestamps(0, 30, retention) + timer := newTimerRoundingTimestamps(0, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00, 300.00, math.NaN()}) @@ -451,7 +451,7 @@ func TestMultipleSeries(t *testing.T) { }) Convey("From 5 until 30", t, func() { - timer := NewTimerRoundingTimestamps(5, 30, retention) + timer := newTimerRoundingTimestamps(5, 30, retention) val1 := unpackMetricsValues(metricData, timer) So(val1["metric1"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) @@ -468,49 +468,49 @@ func TestShiftedSeries(t *testing.T) { }} Convey("From 1 until 1", t, func() { - timer := NewTimerRoundingTimestamps(1, 1, retention) + timer := newTimerRoundingTimestamps(1, 1, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{}) }) Convey("From 0 until 0", t, func() { - timer := NewTimerRoundingTimestamps(0, 0, retention) + timer := newTimerRoundingTimestamps(0, 0, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.0}) }) Convey("From 1 until 11", t, func() { - timer := NewTimerRoundingTimestamps(1, 11, retention) + timer := newTimerRoundingTimestamps(1, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.0}) }) Convey("From 0 until 10", t, func() { - timer := NewTimerRoundingTimestamps(0, 10, retention) + timer := newTimerRoundingTimestamps(0, 10, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.0}) }) Convey("From 0 until 11", t, func() { - timer := NewTimerRoundingTimestamps(0, 11, retention) + timer := newTimerRoundingTimestamps(0, 11, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 0 until 19", t, func() { - timer := NewTimerRoundingTimestamps(0, 19, retention) + timer := newTimerRoundingTimestamps(0, 19, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{100.00, 200.00}) }) Convey("From 1 until 30", t, func() { - timer := NewTimerRoundingTimestamps(1, 30, retention) + timer := newTimerRoundingTimestamps(1, 30, retention) val := unpackMetricsValues(metricData, timer) So(val["metric"], shouldEqualIfNaNsEqual, []float64{200.00, 300.00, math.NaN()}) diff --git a/metric_source/local/local.go b/metric_source/local/local.go index 6183200bf..f844f79aa 100644 --- a/metric_source/local/local.go +++ b/metric_source/local/local.go @@ -33,7 +33,7 @@ func (local *Local) IsConfigured() (bool, error) { return true, nil } -// IsConfigured always returns true. It easy to configure local source =). +// IsAvailable always returns true. It easy to configure local source =). func (local *Local) IsAvailable() (bool, error) { return true, nil } @@ -45,9 +45,9 @@ func (local *Local) Fetch(target string, from int64, until int64, allowRealTimeA from = moira.MaxInt64(from, until-local.database.GetMetricsTTLSeconds()) result := CreateEmptyFetchResult() - ctx := evalCtx{from, until} + eval := evaluator{local.database, make([]string, 0)} - err := ctx.fetchAndEval(local.database, target, result) + err := eval.fetchAndEval(target, from, until, result) if err != nil { return nil, err } diff --git a/metric_source/local/local_test.go b/metric_source/local/local_test.go index 6f0feb8a9..e4bfc81a3 100644 --- a/metric_source/local/local_test.go +++ b/metric_source/local/local_test.go @@ -12,6 +12,7 @@ import ( "github.com/go-graphite/carbonapi/pkg/parser" "github.com/google/go-cmp/cmp" "github.com/moira-alert/moira" + metricSource "github.com/moira-alert/moira/metric_source" mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert" . "github.com/smartystreets/goconvey/convey" @@ -88,14 +89,17 @@ func TestLocalSourceFetchErrors(t *testing.T) { }) Convey("Panic while evaluate target", t, func() { - database.EXPECT().GetPatternMetrics(pattern1).Return([]string{metric1}, nil) - database.EXPECT().GetMetricRetention(metric1).Return(retention, nil) + // moving* functions with an integer second parameter require two metric fetches + database.EXPECT().GetPatternMetrics(pattern1).Return([]string{metric1}, nil).Times(2) + database.EXPECT().GetMetricRetention(metric1).Return(retention, nil).Times(2) database.EXPECT().GetMetricsValues([]string{metric1}, retentionFrom, retentionUntil-1).Return(dataList, nil) + database.EXPECT().GetMetricsValues([]string{metric1}, int64(30), retentionUntil-1).Return(dataList, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("movingAverage(super.puper.pattern, -1)", from, until, true) expectedErrSubstring := strings.Split(ErrEvaluateTargetFailedWithPanic{target: "movingAverage(super.puper.pattern, -1)"}.Error(), ":")[0] + So(err, ShouldNotBeNil) So(err.Error(), ShouldStartWith, expectedErrSubstring) So(result, ShouldBeNil) }) @@ -108,7 +112,6 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { defer mockCtrl.Finish() pattern := pattern1 - pattern2 := pattern2 var metricsTTL int64 = 3600 @@ -116,12 +119,12 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) - result, err := localSource.Fetch("aliasByNode(super.puper.pattern, 2)", 17, 17, false) + result, err := localSource.Fetch("super.puper.pattern", 17, 17, false) So(err, ShouldBeNil) So(result, shouldEqualIfNaNsEqual, &FetchResult{ MetricsData: []metricSource.MetricData{{ - Name: "pattern", + Name: "super.puper.pattern", StartTime: 60, StopTime: 60, StepTime: 60, @@ -155,7 +158,6 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { Convey("Single pattern, from 7 until 57", t, func() { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) - database.EXPECT().GetPatternMetrics(pattern2).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("aliasByNode(super.puper.pattern, 2)", 7, 57, true) @@ -176,6 +178,7 @@ func TestLocalSourceFetchNoMetrics(t *testing.T) { Convey("Two patterns, from 17 until 67", t, func() { database.EXPECT().GetPatternMetrics(pattern).Return([]string{}, nil) + database.EXPECT().GetPatternMetrics(pattern2).Return([]string{}, nil) database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) result, err := localSource.Fetch("alias(sum(super.puper.pattern, super.duper.pattern), 'pattern')", 17, 67, true) @@ -254,6 +257,7 @@ func TestLocalSourceFetchMultipleMetrics(t *testing.T) { StopTime: retentionUntil, StepTime: retention, Values: []float64{2, 2, 2, 2, 2}, + Wildcard: false, }, }, Metrics: metrics, @@ -262,6 +266,79 @@ func TestLocalSourceFetchMultipleMetrics(t *testing.T) { }) } +func TestLocalSourceApplyByNode(t *testing.T) { + mockCtrl := gomock.NewController(t) + database := mock_moira_alert.NewMockDatabase(mockCtrl) + localSource := Create(database) + defer mockCtrl.Finish() + + var from int64 = 17 + var until int64 = 67 + var retentionFrom int64 = 20 + var retentionUntil int64 = 70 + var retention int64 = 10 + var metricsTTL int64 = 3600 + + Convey("Test success evaluate multiple metrics with pow function", t, func() { + metrics := []string{ + "my.pattern.foo", + } + + metricList := make(map[string][]*moira.MetricValue) + metricList["my.pattern.foo"] = []*moira.MetricValue{ + {RetentionTimestamp: 20, Timestamp: 23, Value: 0.5}, + {RetentionTimestamp: 30, Timestamp: 33, Value: 0.4}, + {RetentionTimestamp: 40, Timestamp: 43, Value: 0.5}, + {RetentionTimestamp: 50, Timestamp: 53, Value: 0.5}, + {RetentionTimestamp: 60, Timestamp: 63, Value: 0.5}, + } + + metrics2 := []string{ + "your.my.pattern.foo", + } + + metricList2 := make(map[string][]*moira.MetricValue) + metricList2["your.my.pattern.foo"] = []*moira.MetricValue{ + {RetentionTimestamp: 20, Timestamp: 23, Value: 1}, + {RetentionTimestamp: 30, Timestamp: 33, Value: 2}, + {RetentionTimestamp: 40, Timestamp: 43, Value: 3}, + {RetentionTimestamp: 50, Timestamp: 53, Value: 4}, + {RetentionTimestamp: 60, Timestamp: 63, Value: 5}, + } + + database.EXPECT().GetPatternMetrics("my.pattern.*").Return(metrics, nil) + database.EXPECT().GetMetricRetention(metrics[0]).Return(retention, nil) + database.EXPECT().GetMetricsValues(metrics, retentionFrom, retentionUntil-1).Return(metricList, nil) + + database.EXPECT().GetPatternMetrics("your.my.pattern.foo").Return(metrics2, nil).AnyTimes() + database.EXPECT().GetMetricRetention(metrics2[0]).Return(retention, nil).AnyTimes() + database.EXPECT().GetMetricsValues(metrics2, retentionFrom, retentionUntil-1).Return(metricList2, nil) + + database.EXPECT().GetMetricsTTLSeconds().Return(metricsTTL) + + result, err := localSource.Fetch(`alias(applyByNode(my.pattern.*, 2, "your.%"), 'min')`, from, until, true) + + So(err, ShouldBeNil) + So(result, shouldEqualIfNaNsEqual, &FetchResult{ + MetricsData: []metricSource.MetricData{ + { + Name: "min", + StartTime: retentionFrom, + StopTime: retentionUntil, + StepTime: retention, + Values: []float64{1, 2, 3, 4, 5}, + Wildcard: false, + }, + }, + Metrics: []string{ + "my.pattern.foo", + "your.my.pattern.foo", + }, + Patterns: []string{"my.pattern.*"}, + }) + }) +} + func TestLocalSourceFetch(t *testing.T) { mockCtrl := gomock.NewController(t) database := mock_moira_alert.NewMockDatabase(mockCtrl) @@ -495,49 +572,49 @@ func TestLocalMetricsTTL(t *testing.T) { } func TestLocal_evalExpr(t *testing.T) { + mockCtrl := gomock.NewController(t) + Convey("When everything is correct, we don't return any error", t, func() { - ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `seriesByTag('name=k8s.dev-cl1.kube_pod_status_ready', 'condition!=true', 'namespace=default', 'pod=~*')` - expression, err := ctx.parse(target) + _, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) So(err, ShouldBeNil) - res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) - So(err, ShouldBeNil) - So(res, ShouldBeNil) }) Convey("When get panic, it should return error", t, func() { - ctx := evalCtx{from: 0, until: 0} - - expression, _ := ctx.parse(`;fg`) - res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) - So(err.Error(), ShouldContainSubstring, "panic while evaluate target target: message: 'runtime error: invalid memory address or nil pointer dereference") - So(res, ShouldBeNil) + res, err := evalWithNoMetricsHelper(mockCtrl, `;fg`, 0, 0) + So(err.Error(), ShouldContainSubstring, "failed to parse target") + So(res.Metrics, ShouldBeEmpty) }) Convey("When no metrics, should not return error", t, func() { - ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `alias( divideSeries( alias( sumSeries( exclude( groupByNode( OFD.Production.{ofd-api,ofd-front}.*.fns-service-client.v120.*.GetCashboxRegistrationInformationAsync.ResponseCode.*.Meter.Rate-15-min-Requests-per-s, 9, "sum" ), "Ok" ) ), "bad" ), alias( sumSeries( OFD.Production.{ofd-api,ofd-front}.*.fns-service-client.v120.*.GetCashboxRegistrationInformationAsync.ResponseCode.*.Meter.Rate-15-min-Requests-per-s ), "total" ) ), "Result" )` - expression, err := ctx.parse(target) + res, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) So(err, ShouldBeNil) - res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: make(map[parser.MetricRequest][]*types.MetricData)}) - So(err, ShouldBeNil) - So(res, ShouldBeEmpty) + So(res.Metrics, ShouldBeEmpty) }) Convey("When got unknown func, should return error", t, func() { - ctx := evalCtx{from: time.Now().Add(-1 * time.Hour).Unix(), until: time.Now().Unix()} target := `vf('name=k8s.dev-cl1.kube_pod_status_ready', 'condition!=true', 'namespace=default', 'pod=~*')` - expression, _ := ctx.parse(target) - res, err := ctx.eval("target", expression, &fetchedMetrics{metricsMap: nil}) - So(err, ShouldBeError) + res, err := evalWithNoMetricsHelper(mockCtrl, target, time.Now().Add(-1*time.Hour).Unix(), time.Now().Unix()) So(err.Error(), ShouldResemble, `Unknown graphite function: "vf"`) - So(res, ShouldBeNil) + So(res.Metrics, ShouldBeEmpty) }) } +func evalWithNoMetricsHelper(mockCtrl *gomock.Controller, target string, from, until int64) (*FetchResult, error) { + database := mock_moira_alert.NewMockDatabase(mockCtrl) + database.EXPECT().GetPatternMetrics(gomock.Any()).Return([]string{}, nil).AnyTimes() + eval := evaluator{database, make([]string, 0)} + + result := CreateEmptyFetchResult() + err := eval.fetchAndEval(target, from, until, result) + + return result, err +} + func shouldEqualIfNaNsEqual(actual interface{}, expected ...interface{}) string { allowUnexportedOption := cmp.AllowUnexported(types.MetricData{}) diff --git a/metric_source/local/timer.go b/metric_source/local/timer.go index 224977bc7..98f57bce4 100644 --- a/metric_source/local/timer.go +++ b/metric_source/local/timer.go @@ -1,35 +1,32 @@ package local -// Timer is responsible for managing time ranges and metrics' timeslots. -type Timer struct { +type timer struct { startTime int64 stopTime int64 stepTime int64 } -// Rounds start and stop time in a specific manner requered by carbonapi. -func RoundTimestamps(startTime, stopTime, retention int64) (roundedStart, roundedStop int64) { - return ceilToMultiplier(startTime, retention), floorToMultiplier(stopTime, retention) + retention +func roundTimestamps(startTime, stopTime, retention int64) (roundedStart, roundedStop int64) { + until := floorToMultiplier(stopTime, retention) + retention + from := ceilToMultiplier(startTime, retention) + + return from, until } -// Creates new timer rounding start and stop time in a specific manner requered by carbonapi. -// Timers should be created only with this function. -func NewTimerRoundingTimestamps(startTime int64, stopTime int64, retention int64) Timer { - startTime, stopTime = RoundTimestamps(startTime, stopTime, retention) - return Timer{ +func newTimerRoundingTimestamps(startTime int64, stopTime int64, retention int64) timer { + startTime, stopTime = roundTimestamps(startTime, stopTime, retention) + return timer{ startTime: startTime, stopTime: stopTime, stepTime: retention, } } -// Returns the number of timeslots from this timer's startTime until its stopTime with it's retention. -func (t Timer) NumberOfTimeSlots() int { - return t.GetTimeSlot(t.stopTime) +func (t timer) numberOfTimeSlots() int { + return t.getTimeSlot(t.stopTime) } -// Returns the index of given timestamp (rounded by timestamp) in this timer's time range. -func (t Timer) GetTimeSlot(timestamp int64) int { +func (t timer) getTimeSlot(timestamp int64) int { timeSlot := floorToMultiplier(timestamp-t.startTime, t.stepTime) / t.stepTime return int(timeSlot) } diff --git a/metric_source/local/timer_test.go b/metric_source/local/timer_test.go index cf25214e6..29563ca4a 100644 --- a/metric_source/local/timer_test.go +++ b/metric_source/local/timer_test.go @@ -14,26 +14,26 @@ func TestTimerNumberOfTimeSlots(t *testing.T) { Convey("Given `from` is divisible by retention", t, func() { for _, from := range []int64{0, retention} { until := from + retention*steps - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) - So(timer.NumberOfTimeSlots(), ShouldEqual, steps+1) + So(timer.numberOfTimeSlots(), ShouldEqual, steps+1) } }) Convey("Given `from` is divisible by retention", t, func() { from := int64(0) until := int64(0) - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) - So(timer.NumberOfTimeSlots(), ShouldEqual, 1) + So(timer.numberOfTimeSlots(), ShouldEqual, 1) }) Convey("Given `from` is not divisible by retention", t, func() { for from := int64(1); from < retention; from++ { until := from + retention*steps - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) - So(timer.NumberOfTimeSlots(), ShouldEqual, steps) + So(timer.numberOfTimeSlots(), ShouldEqual, steps) } }) } @@ -43,7 +43,7 @@ func TestTimerGetTimeSlot(t *testing.T) { retention := int64(10) from := int64(10) until := int64(60) - timer := NewTimerRoundingTimestamps(from, until, retention) + timer := newTimerRoundingTimestamps(from, until, retention) testCases := []struct { timestamp int64 @@ -61,7 +61,7 @@ func TestTimerGetTimeSlot(t *testing.T) { } for _, testCase := range testCases { - actual := timer.GetTimeSlot(testCase.timestamp) + actual := timer.getTimeSlot(testCase.timestamp) So(actual, ShouldEqual, testCase.timeSlot) } }) diff --git a/notifier/config.go b/notifier/config.go index a89b4b7ec..34c6e53d4 100644 --- a/notifier/config.go +++ b/notifier/config.go @@ -4,6 +4,7 @@ import ( "time" ) +// There is a duplicate of this constant in database package to prevent cyclic dependencies. const NotificationsLimitUnlimited = int64(-1) // Config is sending settings including log settings. From 0fd0b04d40165881493add9bf2f8462ecd0bbac5 Mon Sep 17 00:00:00 2001 From: HeavyPunk <66221822+HeavyPunk@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:23:52 +0500 Subject: [PATCH 29/36] feat(api): print trigger id when it already exists (#1113) * feat(api): print trigger id when it already exists --- api/controller/triggers.go | 2 +- api/controller/triggers_test.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/api/controller/triggers.go b/api/controller/triggers.go index 4c6ec7075..de97e8d3e 100644 --- a/api/controller/triggers.go +++ b/api/controller/triggers.go @@ -35,7 +35,7 @@ func CreateTrigger(dataBase moira.Database, trigger *dto.TriggerModel, timeSerie return nil, api.ErrorInternalServer(err) } if exists { - return nil, api.ErrorInvalidRequest(fmt.Errorf("trigger with this ID already exists")) + return nil, api.ErrorInvalidRequest(fmt.Errorf("trigger with this ID (%s) already exists", trigger.ID)) } } resp, err := saveTrigger(dataBase, trigger.ToMoiraTrigger(), trigger.ID, timeSeriesNames) diff --git a/api/controller/triggers_test.go b/api/controller/triggers_test.go index 30b174d54..a70ca9d35 100644 --- a/api/controller/triggers_test.go +++ b/api/controller/triggers_test.go @@ -73,11 +73,12 @@ func TestCreateTrigger(t *testing.T) { }) Convey("Trigger already exists", t, func() { - triggerModel := dto.TriggerModel{ID: uuid.Must(uuid.NewV4()).String()} + triggerId := uuid.Must(uuid.NewV4()).String() + triggerModel := dto.TriggerModel{ID: triggerId} trigger := triggerModel.ToMoiraTrigger() dataBase.EXPECT().GetTrigger(triggerModel.ID).Return(*trigger, nil) resp, err := CreateTrigger(dataBase, &triggerModel, make(map[string]bool)) - So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("trigger with this ID already exists"))) + So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("trigger with this ID (%s) already exists", triggerId))) So(resp, ShouldBeNil) }) From 65419292efbfaaf8c2369399d860fb3f56b089f9 Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Tue, 15 Oct 2024 14:58:02 +0700 Subject: [PATCH 30/36] feat: trigger name limit (#1080) --- api/config.go | 27 +++++++++++++++++ api/dto/triggers.go | 43 +++++++++++++++++++++------ api/dto/triggers_test.go | 56 +++++++++++++++++++++++++++++++++--- api/handler/handler.go | 1 + api/handler/trigger_test.go | 9 +++++- api/handler/triggers_test.go | 13 +++++++++ api/middleware/context.go | 10 +++++++ api/middleware/middleware.go | 6 ++++ cmd/api/config.go | 29 +++++++++++++++++++ cmd/api/config_test.go | 5 ++++ local/api.yml | 3 ++ 11 files changed, 189 insertions(+), 13 deletions(-) diff --git a/api/config.go b/api/config.go index 7750b68d5..29055ae71 100644 --- a/api/config.go +++ b/api/config.go @@ -38,6 +38,7 @@ type Config struct { MetricsTTL map[moira.ClusterKey]time.Duration Flags FeatureFlags Authorization Authorization + Limits LimitsConfig } // WebConfig is container for web ui configuration parameters. @@ -60,3 +61,29 @@ type MetricSourceCluster struct { func (WebConfig) Render(w http.ResponseWriter, r *http.Request) error { return nil } + +const ( + // DefaultTriggerNameMaxSize which will be used while validating dto.Trigger. + DefaultTriggerNameMaxSize = 200 +) + +// LimitsConfig contains limits for some entities. +type LimitsConfig struct { + // Trigger contains limits for triggers. + Trigger TriggerLimits +} + +// TriggerLimits contains all limits applied for triggers. +type TriggerLimits struct { + // MaxNameSize is the amount of characters allowed in trigger name. + MaxNameSize int +} + +// GetTestLimitsConfig is used for testing. +func GetTestLimitsConfig() LimitsConfig { + return LimitsConfig{ + Trigger: TriggerLimits{ + MaxNameSize: DefaultTriggerNameMaxSize, + }, + } +} diff --git a/api/dto/triggers.go b/api/dto/triggers.go index b35ce20ef..e22e28990 100644 --- a/api/dto/triggers.go +++ b/api/dto/triggers.go @@ -2,11 +2,13 @@ package dto import ( + "errors" "fmt" "net/http" "regexp" "strconv" "time" + "unicode/utf8" "github.com/moira-alert/moira/templating" @@ -19,8 +21,26 @@ import ( var targetNameRegex = regexp.MustCompile("^t\\d+$") -// ErrBadAloneMetricName is used when any key in map TriggerModel.AloneMetric doesn't match targetNameRegex. -var ErrBadAloneMetricName = fmt.Errorf("alone metrics' target name must match the pattern: ^t\\d+$, for example: 't1'") +var ( + // errBadAloneMetricName is used when any key in map TriggerModel.AloneMetric doesn't match targetNameRegex. + errBadAloneMetricName = errors.New("alone metrics' target name must match the pattern: ^t\\d+$, for example: 't1'") + + // errTargetsRequired is returned when there is no targets in Trigger. + errTargetsRequired = errors.New("targets are required") + + // errTagsRequired is returned when there is no tags in Trigger. + errTagsRequired = errors.New("tags are required") + + // errTriggerNameRequired is returned when there is empty Name in Trigger. + errTriggerNameRequired = errors.New("trigger name is required") + + // errAloneMetricTargetIndexOutOfRange is returned when target index is out of range. Example: if we have target "t1", + // then "1" is a target index. + errAloneMetricTargetIndexOutOfRange = errors.New("alone metrics target index should be in range from 1 to length of targets") + + // errAsteriskPatternNotAllowed is returned then one of Trigger.Patterns contain only "*". + errAsteriskPatternNotAllowed = errors.New("pattern \"*\" is not allowed to use") +) // TODO(litleleprikon): Remove after https://github.com/moira-alert/moira/issues/550 will be resolved. var asteriskPattern = "*" @@ -152,15 +172,22 @@ func CreateTriggerModel(trigger *moira.Trigger) TriggerModel { func (trigger *Trigger) Bind(request *http.Request) error { trigger.Tags = normalizeTags(trigger.Tags) if len(trigger.Targets) == 0 { - return api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("targets is required")} + return api.ErrInvalidRequestContent{ValidationError: errTargetsRequired} } if len(trigger.Tags) == 0 { - return api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("tags is required")} + return api.ErrInvalidRequestContent{ValidationError: errTagsRequired} } if trigger.Name == "" { - return api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("trigger name is required")} + return api.ErrInvalidRequestContent{ValidationError: errTriggerNameRequired} + } + + limits := middleware.GetLimits(request) + if utf8.RuneCountInString(trigger.Name) > limits.Trigger.MaxNameSize { + return api.ErrInvalidRequestContent{ + ValidationError: fmt.Errorf("trigger name too long, should not be greater than %d symbols", limits.Trigger.MaxNameSize), + } } if err := checkWarnErrorExpression(trigger); err != nil { @@ -173,7 +200,7 @@ func (trigger *Trigger) Bind(request *http.Request) error { for targetName := range trigger.AloneMetrics { if !targetNameRegex.MatchString(targetName) { - return api.ErrInvalidRequestContent{ValidationError: ErrBadAloneMetricName} + return api.ErrInvalidRequestContent{ValidationError: errBadAloneMetricName} } targetIndexStr := targetName[1:] @@ -183,7 +210,7 @@ func (trigger *Trigger) Bind(request *http.Request) error { } 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")} + return api.ErrInvalidRequestContent{ValidationError: errAloneMetricTargetIndexOutOfRange} } } @@ -224,7 +251,7 @@ func (trigger *Trigger) Bind(request *http.Request) error { // TODO(litleleprikon): Remove after https://github.com/moira-alert/moira/issues/550 will be resolved for _, pattern := range trigger.Patterns { if pattern == asteriskPattern { - return api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("pattern \"*\" is not allowed to use")} + return api.ErrInvalidRequestContent{ValidationError: errAsteriskPatternNotAllowed} } } diff --git a/api/dto/triggers_test.go b/api/dto/triggers_test.go index 90b8a87d0..be944ab20 100644 --- a/api/dto/triggers_test.go +++ b/api/dto/triggers_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "strings" "testing" "time" @@ -18,6 +19,52 @@ import ( ) func TestTriggerValidation(t *testing.T) { + Convey("Test trigger name and tags", t, func() { + trigger := Trigger{ + TriggerModel: TriggerModel{}, + } + + limit := api.GetTestLimitsConfig() + + request, _ := http.NewRequest("PUT", "/api/trigger", nil) + request.Header.Set("Content-Type", "application/json") + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", limit)) + + Convey("with empty targets", func() { + err := trigger.Bind(request) + + So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: errTargetsRequired}) + }) + + trigger.Targets = []string{"foo.bar"} + + Convey("with empty tag in tag list", func() { + trigger.Tags = []string{""} + + err := trigger.Bind(request) + + So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: errTagsRequired}) + }) + + trigger.Tags = append(trigger.Tags, "tag1") + + Convey("with empty Name", func() { + err := trigger.Bind(request) + + So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: errTriggerNameRequired}) + }) + + Convey("with too long Name", func() { + trigger.Name = strings.Repeat("ё", limit.Trigger.MaxNameSize+1) + + err := trigger.Bind(request) + + So(err, ShouldResemble, api.ErrInvalidRequestContent{ + ValidationError: fmt.Errorf("trigger name too long, should not be greater than %d symbols", limit.Trigger.MaxNameSize), + }) + }) + }) + Convey("Tests targets, values and expression validation", t, func() { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -31,6 +78,7 @@ func TestTriggerValidation(t *testing.T) { request.Header.Set("Content-Type", "application/json") ctx := request.Context() ctx = context.WithValue(ctx, middleware.ContextKey("metricSourceProvider"), sourceProvider) + ctx = context.WithValue(ctx, middleware.ContextKey("limits"), api.GetTestLimitsConfig()) request = request.WithContext(ctx) desc := "Graphite ClickHouse" @@ -203,19 +251,19 @@ func TestTriggerValidation(t *testing.T) { trigger.AloneMetrics = map[string]bool{"ttt": true} tr := Trigger{trigger, throttling} err := tr.Bind(request) - So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: ErrBadAloneMetricName}) + So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: errBadAloneMetricName}) }) Convey("have more than 1 metric name but only 1 need", func() { trigger.AloneMetrics = map[string]bool{"t1 t2": true} tr := Trigger{trigger, throttling} err := tr.Bind(request) - So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: ErrBadAloneMetricName}) + So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: errBadAloneMetricName}) }) Convey("have target higher than total amount of targets", func() { trigger.AloneMetrics = map[string]bool{"t3": true} tr := Trigger{trigger, throttling} err := tr.Bind(request) - So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("alone metrics target index should be in range from 1 to length of targets")}) + So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: errAloneMetricTargetIndexOutOfRange}) }) }) @@ -237,7 +285,7 @@ func TestTriggerValidation(t *testing.T) { tr := Trigger{trigger, throttling} fetchResult.EXPECT().GetPatterns().Return([]string{"*"}, nil).AnyTimes() err := tr.Bind(request) - So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: fmt.Errorf("pattern \"*\" is not allowed to use")}) + So(err, ShouldResemble, api.ErrInvalidRequestContent{ValidationError: errAsteriskPatternNotAllowed}) }) }) }) diff --git a/api/handler/handler.go b/api/handler/handler.go index dd120172d..22d04417c 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -50,6 +50,7 @@ func NewHandler( router.Use(moiramiddle.UserContext) router.Use(moiramiddle.RequestLogger(log)) router.Use(middleware.NoCache) + router.Use(moiramiddle.LimitsContext(apiConfig.Limits)) router.NotFound(notFoundHandler) router.MethodNotAllowed(methodNotAllowedHandler) diff --git a/api/handler/trigger_test.go b/api/handler/trigger_test.go index d5d2a8460..5f099273f 100644 --- a/api/handler/trigger_test.go +++ b/api/handler/trigger_test.go @@ -10,6 +10,8 @@ import ( "testing" "time" + "github.com/moira-alert/moira/api" + "github.com/moira-alert/moira" "github.com/moira-alert/moira/api/dto" "github.com/moira-alert/moira/api/middleware" @@ -165,8 +167,8 @@ func TestUpdateTrigger(t *testing.T) { testRequest.Header.Add("content-type", "application/json") testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "metricSourceProvider", sourceProvider)) testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "clustersMetricTTL", MakeTestTTLs())) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), triggerIDKey, triggerID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() updateTrigger(responseWriter, testRequest) @@ -208,6 +210,7 @@ func TestUpdateTrigger(t *testing.T) { request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), triggerIDKey, triggerID)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() updateTrigger(responseWriter, request) @@ -247,6 +250,7 @@ func TestUpdateTrigger(t *testing.T) { request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), triggerIDKey, triggerID)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() updateTrigger(responseWriter, request) @@ -272,6 +276,7 @@ func TestUpdateTrigger(t *testing.T) { request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), triggerIDKey, triggerID)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() updateTrigger(responseWriter, request) @@ -335,6 +340,7 @@ func TestUpdateTrigger(t *testing.T) { request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), triggerIDKey, triggerID)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() updateTrigger(responseWriter, request) @@ -353,6 +359,7 @@ func TestUpdateTrigger(t *testing.T) { request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), triggerIDKey, triggerID)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() updateTrigger(responseWriter, request) diff --git a/api/handler/triggers_test.go b/api/handler/triggers_test.go index f2624d6ef..01c3420d3 100644 --- a/api/handler/triggers_test.go +++ b/api/handler/triggers_test.go @@ -100,6 +100,7 @@ func TestGetTriggerFromRequest(t *testing.T) { request := httptest.NewRequest(http.MethodPut, "/trigger", bytes.NewReader(body)) request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) Convey("It should be parsed successfully", func() { triggerDTO.TTL = moira.DefaultTTL @@ -138,6 +139,7 @@ func TestGetTriggerFromRequest(t *testing.T) { request := httptest.NewRequest(http.MethodPut, "/trigger", strings.NewReader(body)) request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) Convey("Parser should return en error", func() { _, err := getTriggerFromRequest(request) @@ -190,6 +192,7 @@ func TestGetTriggerFromRequest(t *testing.T) { request := httptest.NewRequest(http.MethodPut, "/trigger", bytes.NewReader(body)) request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", allSourceProvider)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) testLogger, _ := logging.GetLogger("Test") @@ -214,6 +217,7 @@ func TestGetTriggerFromRequest(t *testing.T) { request := httptest.NewRequest(http.MethodPut, "/trigger", bytes.NewReader(body)) request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", allSourceProvider)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) var returnedErr error = &prometheus.Error{ Type: prometheus.ErrBadData, @@ -240,6 +244,7 @@ func TestGetTriggerFromRequest(t *testing.T) { request := httptest.NewRequest(http.MethodPut, "/trigger", bytes.NewReader(body)) request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", allSourceProvider)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) var returnedErr error = &prometheus.Error{ Type: errType, @@ -366,6 +371,7 @@ func TestTriggerCheckHandler(t *testing.T) { testRequest.Header.Add("content-type", "application/json") testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "metricSourceProvider", sourceProvider)) testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "clustersMetricTTL", MakeTestTTLs())) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "limits", api.GetTestLimitsConfig())) triggerCheck(responseWriter, testRequest) @@ -430,6 +436,7 @@ func TestCreateTriggerHandler(t *testing.T) { testRequest.Header.Add("content-type", "application/json") testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "metricSourceProvider", sourceProvider)) testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "clustersMetricTTL", MakeTestTTLs())) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() createTrigger(responseWriter, testRequest) @@ -467,6 +474,7 @@ func TestCreateTriggerHandler(t *testing.T) { request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() createTrigger(responseWriter, request) @@ -505,6 +513,7 @@ func TestCreateTriggerHandler(t *testing.T) { request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() createTrigger(responseWriter, request) @@ -528,6 +537,7 @@ func TestCreateTriggerHandler(t *testing.T) { request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() createTrigger(responseWriter, request) @@ -590,6 +600,7 @@ func TestCreateTriggerHandler(t *testing.T) { request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() createTrigger(responseWriter, request) @@ -607,6 +618,7 @@ func TestCreateTriggerHandler(t *testing.T) { request.Header.Add("content-type", "application/json") request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) responseWriter := httptest.NewRecorder() createTrigger(responseWriter, request) @@ -826,6 +838,7 @@ func newTriggerCreateRequest( request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "clustersMetricTTL", MakeTestTTLs())) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), triggerIDKey, triggerId)) + request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) return request } diff --git a/api/middleware/context.go b/api/middleware/context.go index a53a3f1b8..67d6d7c1e 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -341,3 +341,13 @@ func StatesContext() func(next http.Handler) http.Handler { }) } } + +// LimitsContext places api.LimitsConfig to request context. +func LimitsContext(limit api.LimitsConfig) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + ctx := context.WithValue(request.Context(), limitsContextKey, limit) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index d3df8e40d..14b8e8ca8 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -42,6 +42,7 @@ var ( authKey ContextKey = "auth" metricContextKey ContextKey = "metric" statesContextKey ContextKey = "states" + limitsContextKey ContextKey = "limits" anonymousUser = "anonymous" ) @@ -180,3 +181,8 @@ func GetMetric(request *http.Request) string { func GetStates(request *http.Request) map[string]struct{} { return request.Context().Value(statesContextKey).(map[string]struct{}) } + +// GetLimits returns configured limits. +func GetLimits(request *http.Request) api.LimitsConfig { + return request.Context().Value(limitsContextKey).(api.LimitsConfig) +} diff --git a/cmd/api/config.go b/cmd/api/config.go index 75fa2ceea..b63770873 100644 --- a/cmd/api/config.go +++ b/cmd/api/config.go @@ -48,6 +48,29 @@ type apiConfig struct { EnableCORS bool `yaml:"enable_cors"` // Authorization contains authorization configuration. Authorization authorization `yaml:"authorization"` + // Limits contains limits applied to entities and so on. + Limits LimitsConfig `yaml:"limits"` +} + +// LimitsConfig contains configurable moira limits. +type LimitsConfig struct { + // Trigger contains the limits applied to triggers. + Trigger TriggerLimitsConfig `yaml:"trigger"` +} + +// TriggerLimitsConfig represents the limits which will be applied to all triggers. +type TriggerLimitsConfig struct { + // MaxNameSize is the max amount of characters allowed in trigger name. + MaxNameSize int `yaml:"max_name_size"` +} + +// ToLimits converts LimitsConfig to api.LimitsConfig. +func (conf LimitsConfig) ToLimits() api.LimitsConfig { + return api.LimitsConfig{ + Trigger: api.TriggerLimits{ + MaxNameSize: conf.Trigger.MaxNameSize, + }, + } } type authorization struct { @@ -116,6 +139,7 @@ func (config *apiConfig) getSettings( MetricsTTL: metricsTTL, Flags: flags, Authorization: config.Authorization.toApiConfig(webConfig), + Limits: config.Limits.ToLimits(), } } @@ -231,6 +255,11 @@ func getDefault() config { API: apiConfig{ Listen: ":8081", EnableCORS: false, + Limits: LimitsConfig{ + Trigger: TriggerLimitsConfig{ + MaxNameSize: api.DefaultTriggerNameMaxSize, + }, + }, }, Web: webConfig{ RemoteAllowed: false, diff --git a/cmd/api/config_test.go b/cmd/api/config_test.go index c55a67d68..4d7f86a2b 100644 --- a/cmd/api/config_test.go +++ b/cmd/api/config_test.go @@ -88,6 +88,11 @@ func Test_webConfig_getDefault(t *testing.T) { API: apiConfig{ Listen: ":8081", EnableCORS: false, + Limits: LimitsConfig{ + Trigger: TriggerLimitsConfig{ + MaxNameSize: api.DefaultTriggerNameMaxSize, + }, + }, }, Web: webConfig{ RemoteAllowed: false, diff --git a/local/api.yml b/local/api.yml index a48bddd83..631240d4e 100644 --- a/local/api.yml +++ b/local/api.yml @@ -37,6 +37,9 @@ prometheus_remote: api: listen: ":8081" enable_cors: false + limits: + trigger: + max_name_size: 200 web: contacts_template: - type: mail From ae27338d8f6606f21c648ed82c107d2194493f3a Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Wed, 23 Oct 2024 19:45:26 +0700 Subject: [PATCH 31/36] refactor: change alert parts calculations (#1084) --- senders/calc_message_parts.go | 76 +++++++- senders/calc_message_parts_test.go | 177 +++++++++++++++++ senders/msgformat/defaults.go | 42 +++++ senders/msgformat/defaults_test.go | 43 +++++ senders/msgformat/highlighter.go | 40 ++-- senders/msgformat/highlighter_test.go | 209 +++++++++++++++++---- senders/msgformat/msgformat.go | 7 - senders/slack/slack_test.go | 34 +++- senders/telegram/message_formatter.go | 50 +++-- senders/telegram/message_formatter_test.go | 197 ++++++++++++++----- 10 files changed, 738 insertions(+), 137 deletions(-) create mode 100644 senders/msgformat/defaults.go create mode 100644 senders/msgformat/defaults_test.go diff --git a/senders/calc_message_parts.go b/senders/calc_message_parts.go index bebb2c928..ee4aeaf2a 100644 --- a/senders/calc_message_parts.go +++ b/senders/calc_message_parts.go @@ -6,11 +6,81 @@ func CalculateMessagePartsLength(maxChars, descLen, eventsLen int) (descNewLen i if descLen+eventsLen <= maxChars { return descLen, eventsLen } - if descLen > maxChars/2 && eventsLen <= maxChars/2 { + + halfOfMaxChars := maxChars / partsCountForMessageWithDescAndEvents + + if descLen > halfOfMaxChars && eventsLen <= halfOfMaxChars { return maxChars - eventsLen - 10, eventsLen } - if eventsLen > maxChars/2 && descLen <= maxChars/2 { + + if eventsLen > halfOfMaxChars && descLen <= halfOfMaxChars { return descLen, maxChars - descLen } - return maxChars/2 - 10, maxChars / 2 + + return halfOfMaxChars - 10, halfOfMaxChars +} + +const ( + // partsCountForMessageWithDescAndEvents is used then you need to split given maxChars fairly by half + // between description and events. + partsCountForMessageWithDescAndEvents = 2 + // partsCountForMessageWithTagsDescAndEvents is used then you need to split given maxChars fairly by three parts + // between tags, description and events. + partsCountForMessageWithTagsDescAndEvents = 3 +) + +// CalculateMessagePartsBetweenTagsDescEvents calculates and returns the length of tags, description and events string +// in order to fit the max chars limit. +func CalculateMessagePartsBetweenTagsDescEvents(maxChars, tagsLen, descLen, eventsLen int) (tagsNewLen int, descNewLen int, eventsNewLen int) { // nolint + if maxChars <= 0 { + return 0, 0, 0 + } + + if tagsLen+descLen+eventsLen <= maxChars { + return tagsLen, descLen, eventsLen + } + + fairMaxLen := maxChars / partsCountForMessageWithTagsDescAndEvents + + switch { + case tagsLen > fairMaxLen && descLen <= fairMaxLen && eventsLen <= fairMaxLen: + // give free space to tags + tagsNewLen = maxChars - descLen - eventsLen + + return min(tagsNewLen, tagsLen), descLen, eventsLen + case tagsLen <= fairMaxLen && descLen > fairMaxLen && eventsLen <= fairMaxLen: + // give free space to description + descNewLen = maxChars - tagsLen - eventsLen + + return tagsLen, min(descNewLen, descLen), eventsLen + case tagsLen <= fairMaxLen && descLen <= fairMaxLen && eventsLen > fairMaxLen: + // give free space to events + eventsNewLen = maxChars - tagsLen - descLen + + return tagsLen, descLen, min(eventsNewLen, eventsLen) + case tagsLen > fairMaxLen && descLen > fairMaxLen && eventsLen <= fairMaxLen: + // description is more important than tags + tagsNewLen = fairMaxLen + descNewLen = maxChars - tagsNewLen - eventsLen + + return tagsNewLen, min(descNewLen, descLen), eventsLen + case tagsLen > fairMaxLen && descLen <= fairMaxLen && eventsLen > fairMaxLen: + // events are more important than tags + tagsNewLen = fairMaxLen + eventsNewLen = maxChars - tagsNewLen - descLen + + return tagsNewLen, descLen, min(eventsNewLen, eventsLen) + case tagsLen <= fairMaxLen && descLen > fairMaxLen && eventsLen > fairMaxLen: + // split free space from tags fairly between description and events + spaceFromTags := fairMaxLen - tagsLen + halfOfSpaceFromTags := spaceFromTags / partsCountForMessageWithDescAndEvents + + descNewLen = fairMaxLen + halfOfSpaceFromTags + eventsNewLen = fairMaxLen + halfOfSpaceFromTags + + return tagsLen, min(descNewLen, descLen), min(eventsNewLen, eventsLen) + default: + // all 3 blocks have length greater than maxChars/3, so split space fairly + return fairMaxLen, fairMaxLen, fairMaxLen + } } diff --git a/senders/calc_message_parts_test.go b/senders/calc_message_parts_test.go index 19882a9bd..82a17d60d 100644 --- a/senders/calc_message_parts_test.go +++ b/senders/calc_message_parts_test.go @@ -1,6 +1,7 @@ package senders import ( + "fmt" "testing" . "github.com/smartystreets/goconvey/convey" @@ -33,3 +34,179 @@ func TestCalculateMessagePartsLength(t *testing.T) { }) }) } + +func TestCalculateMessagePartsBetweenTagsDescEvents(t *testing.T) { + Convey("Message parts calculating test (for tags, desc, events)", t, func() { + type given struct { + maxChars int + tagsLen int + descLen int + eventsLen int + } + + type expected struct { + tagsLen int + descLen int + eventsLen int + } + + type testcase struct { + given given + expected expected + description string + } + + cases := []testcase{ + { + description: "with maxChars < 0", + given: given{ + maxChars: -1, + tagsLen: 10, + descLen: 10, + eventsLen: 10, + }, + expected: expected{ + tagsLen: 0, + descLen: 0, + eventsLen: 0, + }, + }, + { + description: "with tagsLen + descLen + eventsLen <= maxChars", + given: given{ + maxChars: 100, + tagsLen: 20, + descLen: 50, + eventsLen: 30, + }, + expected: expected{ + tagsLen: 20, + descLen: 50, + eventsLen: 30, + }, + }, + { + description: "with tagsLen > maxChars/3, descLen and eventsLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 50, + descLen: 30, + eventsLen: 30, + }, + expected: expected{ + tagsLen: 40, + descLen: 30, + eventsLen: 30, + }, + }, + { + description: "with descLen > maxChars/3, tagsLen and eventsLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 30, + descLen: 50, + eventsLen: 31, + }, + expected: expected{ + tagsLen: 30, + descLen: 39, + eventsLen: 31, + }, + }, + { + description: "with eventsLen > maxChars/3, tagsLen and descLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 33, + descLen: 33, + eventsLen: 61, + }, + expected: expected{ + tagsLen: 33, + descLen: 33, + eventsLen: 34, + }, + }, + { + description: "with tagsLen and descLen > maxChars/3, eventsLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 55, + descLen: 46, + eventsLen: 31, + }, + expected: expected{ + tagsLen: 33, + descLen: 36, + eventsLen: 31, + }, + }, + { + description: "with tagsLen and eventsLen > maxChars/3, descLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 55, + descLen: 33, + eventsLen: 100, + }, + expected: expected{ + tagsLen: 33, + descLen: 33, + eventsLen: 34, + }, + }, + { + description: "with descLen and eventsLen > maxChars/3, tagsLen <= maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 29, + descLen: 56, + eventsLen: 100, + }, + expected: expected{ + tagsLen: 29, + descLen: 35, + eventsLen: 35, + }, + }, + { + description: "with tagsLen, descLen and eventsLen > maxChars/3", + given: given{ + maxChars: 100, + tagsLen: 55, + descLen: 40, + eventsLen: 100, + }, + expected: expected{ + tagsLen: 33, + descLen: 33, + eventsLen: 33, + }, + }, + { + description: "with tagsLen, descLen > maxChars/3, eventsLen <= maxChars/3 and maxChars - maxChars/3 - eventsLen > descLen", + given: given{ + maxChars: 100, + tagsLen: 100, + descLen: 34, + eventsLen: 20, + }, + expected: expected{ + tagsLen: 33, + descLen: 34, + eventsLen: 20, + }, + }, + } + + for i, c := range cases { + Convey(fmt.Sprintf("case %d: %s", i+1, c.description), func() { + tagsNewLen, descNewLen, eventsNewLen := CalculateMessagePartsBetweenTagsDescEvents(c.given.maxChars, c.given.tagsLen, c.given.descLen, c.given.eventsLen) + + So(tagsNewLen, ShouldResemble, c.expected.tagsLen) + So(descNewLen, ShouldResemble, c.expected.descLen) + So(eventsNewLen, ShouldResemble, c.expected.eventsLen) + }) + } + }) +} diff --git a/senders/msgformat/defaults.go b/senders/msgformat/defaults.go new file mode 100644 index 000000000..ad485a348 --- /dev/null +++ b/senders/msgformat/defaults.go @@ -0,0 +1,42 @@ +package msgformat + +import "unicode/utf8" + +// DefaultDescriptionCutter cuts description, so len(newDesc) <= maxSize. Ensure that len(desc) >= maxSize and +// maxSize >= len("...\n"). +func DefaultDescriptionCutter(desc string, maxSize int) string { + suffix := "...\n" + return desc[:maxSize-len(suffix)] + suffix +} + +var bracketsLen = utf8.RuneCountInString("[]") + +// DefaultTagsLimiter cuts and formats tags to fit maxSize. There will be no tag parts, for example: +// +// if we have +// +// tags = []string{"tag1", "tag2} +// maxSize = 8 +// +// so call DefaultTagsLimiter(tags, maxSize) will return " [tag1]". +func DefaultTagsLimiter(tags []string, maxSize int) string { + tagsStr := " " + lenTagsStr := utf8.RuneCountInString(tagsStr) + + for i := range tags { + lenTag := utf8.RuneCountInString(tags[i]) + bracketsLen + + if lenTagsStr+lenTag > maxSize { + break + } + + tagsStr += "[" + tags[i] + "]" + lenTagsStr += lenTag + } + + if tagsStr == " " { + return "" + } + + return tagsStr +} diff --git a/senders/msgformat/defaults_test.go b/senders/msgformat/defaults_test.go new file mode 100644 index 000000000..2704e905f --- /dev/null +++ b/senders/msgformat/defaults_test.go @@ -0,0 +1,43 @@ +package msgformat + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestDefaultTagsLimiter(t *testing.T) { + Convey("Test default tags limiter", t, func() { + tags := []string{"tag1", "tag2"} + + Convey("with maxSize < 0", func() { + tagsStr := DefaultTagsLimiter(tags, -1) + + So(tagsStr, ShouldResemble, "") + }) + + Convey("with maxSize > total characters in tags string", func() { + tagsStr := DefaultTagsLimiter(tags, 30) + + So(tagsStr, ShouldResemble, " [tag1][tag2]") + }) + + Convey("with maxSize not enough for all tags", func() { + tagsStr := DefaultTagsLimiter(tags, 8) + + So(tagsStr, ShouldResemble, " [tag1]") + }) + + Convey("with one long tag > maxSize", func() { + tagsStr := DefaultTagsLimiter([]string{"long_tag"}, 4) + + So(tagsStr, ShouldResemble, "") + }) + + Convey("with no tags", func() { + tagsStr := DefaultTagsLimiter([]string{}, 0) + + So(tagsStr, ShouldResemble, "") + }) + }) +} diff --git a/senders/msgformat/highlighter.go b/senders/msgformat/highlighter.go index 9a78403e4..2e21387fe 100644 --- a/senders/msgformat/highlighter.go +++ b/senders/msgformat/highlighter.go @@ -2,7 +2,6 @@ package msgformat import ( "fmt" - "strings" "time" "unicode/utf8" @@ -27,6 +26,10 @@ type EventStringFormatter func(event moira.NotificationEvent, location *time.Loc // DescriptionCutter cuts the given description to fit max size. type DescriptionCutter func(desc string, maxSize int) string +// TagsLimiter should prepare tags string in format like " [tag1][tag2][tag3]", +// but characters count should be less than or equal to maxSize. +type TagsLimiter func(tags []string, maxSize int) string + // highlightSyntaxFormatter formats message by using functions, emojis and some other highlight patterns. type highlightSyntaxFormatter struct { // emojiGetter used in titles for better description. @@ -74,35 +77,44 @@ func NewHighlightSyntaxFormatter( // Format formats message using given params and formatter functions. func (formatter *highlightSyntaxFormatter) Format(params MessageFormatterParams) string { - var message strings.Builder state := params.Events.GetCurrentState(params.Throttled) emoji := formatter.emojiGetter.GetStateEmoji(state) title := formatter.buildTitle(params.Events, params.Trigger, emoji, params.Throttled) - titleLen := utf8.RuneCountInString(title) + titleLen := utf8.RuneCountInString(title) + len("\n") + + var tags string + var tagsLen int + + triggerTags := params.Trigger.GetTags() + if len(triggerTags) != 0 { + tags = " " + triggerTags + tagsLen = utf8.RuneCountInString(tags) + } desc := formatter.descriptionFormatter(params.Trigger) descLen := utf8.RuneCountInString(desc) - eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) - eventsStringLen := utf8.RuneCountInString(eventsString) + events := formatter.buildEventsString(params.Events, -1, params.Throttled) + eventsStringLen := utf8.RuneCountInString(events) charsLeftAfterTitle := params.MessageMaxChars - titleLen - descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(charsLeftAfterTitle, descLen, eventsStringLen) + tagsNewLen, descNewLen, eventsNewLen := senders.CalculateMessagePartsBetweenTagsDescEvents(charsLeftAfterTitle, tagsLen, descLen, eventsStringLen) + if tagsNewLen != tagsLen { + tags = DefaultTagsLimiter(params.Trigger.Tags, tagsNewLen) + } if descLen != descNewLen { desc = formatter.descriptionCutter(desc, descNewLen) } if eventsNewLen != eventsStringLen { - eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + events = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) } - message.WriteString(title) - message.WriteString(desc) - message.WriteString(eventsString) - return message.String() + return title + tags + "\n" + desc + events } +// buildTitle builds title string for alert (emoji, trigger state, trigger name with link). func (formatter *highlightSyntaxFormatter) buildTitle(events moira.NotificationEvents, trigger moira.TriggerData, emoji string, throttled bool) string { state := events.GetCurrentState(throttled) title := "" @@ -118,12 +130,6 @@ func (formatter *highlightSyntaxFormatter) buildTitle(events moira.NotificationE title += " " + trigger.Name } - tags := trigger.GetTags() - if tags != "" { - title += " " + tags - } - - title += "\n" return title } diff --git a/senders/msgformat/highlighter_test.go b/senders/msgformat/highlighter_test.go index 470db230e..0390e0802 100644 --- a/senders/msgformat/highlighter_test.go +++ b/senders/msgformat/highlighter_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" "time" + "unicode/utf8" "github.com/moira-alert/moira" "github.com/moira-alert/moira/senders/emoji_provider" @@ -89,64 +90,196 @@ func TestFormat(t *testing.T) { }) Convey("Long message parts", func() { + trigger.Desc = "" + trigger.Tags = []string{} + const ( - msgLimit = 4_000 - halfLimit = msgLimit / 2 - greaterThanHalf = halfLimit + 100 - lessThanHalf = halfLimit - 100 + titleLine = "**NODATA** [Name](http://moira.url/trigger/TriggerID)" + eventLine = "\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)" + endSuffix = "...\n" + lenEndSuffix = 4 ) - const eventLine = "\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)" - oneEventLineLen := len([]rune(eventLine)) - - longDesc := strings.Repeat("a", greaterThanHalf) + lenTitle := utf8.RuneCountInString(titleLine) + len("\n") // 54 symbols + oneEventLineLen := utf8.RuneCountInString(eventLine) // 47 symbols - // Events list with chars greater than half of the message limit - var longEvents moira.NotificationEvents - for i := 0; i < greaterThanHalf/oneEventLineLen; i++ { - longEvents = append(longEvents, event) - } + var ( + msgLimit = testMaxChars - lenTitle // 3947 + thirdOfLimit = msgLimit / 3 // 1315 + greaterThanThird = thirdOfLimit + 100 // 1415 + lessThanThird = thirdOfLimit - 100 // 1215 + ) - Convey("Long description. desc > msgLimit/2", func() { - var events moira.NotificationEvents - for i := 0; i < lessThanHalf/oneEventLineLen; i++ { - events = append(events, event) + Convey("with long tags (tagsLen >= msgLimit), desc and events < msgLimit/3", func() { + trigger.Tags = []string{ + strings.Repeat("a", 1000), + strings.Repeat("b", 1000), + strings.Repeat("c", 1000), + strings.Repeat("d", 1000), } + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, oneEventLineLen, lessThanThird) + + expected := titleLine + + DefaultTagsLimiter(trigger.Tags, + msgLimit-utf8.RuneCountInString(trigger.Desc)-len("```\n```")-oneEventLineLen*len(events), + ) + "\n" + + strings.Repeat("a", lessThanThird) + "\n" + + "```" + + strings.Repeat(eventLine, len(events)) + + "\n```" + + actual := formatter.Format(getParams(events, trigger, false)) - actual := formatter.Format(getParams(events, moira.TriggerData{Desc: longDesc}, false)) - expected := "**NODATA**\n" + - strings.Repeat("a", 2100) + "\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 39) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) }) - Convey("Many events. eventString > msgLimit/2", func() { - desc := strings.Repeat("a", lessThanHalf) - actual := formatter.Format(getParams(longEvents, moira.TriggerData{Desc: desc}, false)) - expected := "**NODATA**\n" + - desc + "\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 43) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + Convey("with description > msgLimit/3, tags and events < msgLimit/3, and sum of lengths is greater than msgLimit", func() { + longDescLen := greaterThanThird + 200 + + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(longDescLen) + events := genEventsByLimit(event, oneEventLineLen, lessThanThird) + + tagsStr := " " + trigger.GetTags() + + expected := titleLine + tagsStr + "\n" + + strings.Repeat("a", + msgLimit-utf8.RuneCountInString(tagsStr)-len("```\n```")-oneEventLineLen*len(events)-lenEndSuffix, + ) + endSuffix + + "```" + + strings.Repeat(eventLine, len(events)) + + "\n```" + + actual := formatter.Format(getParams(events, trigger, false)) + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) }) - Convey("Long description and many events. both desc and events > msgLimit/2", func() { - actual := formatter.Format(getParams(longEvents, moira.TriggerData{Desc: longDesc}, false)) - expected := "**NODATA**\n" + - strings.Repeat("a", 1980) + "...\n" + - "```\n" + - strings.Repeat("02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n", 40) + - "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n" + + Convey("with long events string (> msgLimit/3), desc and tags < msgLimit/3", func() { + longEventsLen := greaterThanThird + 200 + + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, oneEventLineLen, longEventsLen) + + tagsStr := " " + trigger.GetTags() + + expected := titleLine + tagsStr + "\n" + + strings.Repeat("a", lessThanThird) + "\n" + + "```" + + strings.Repeat(eventLine, 31) + + "\n```\n" + "...and 3 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) + }) + + Convey("with tags and desc > msgLimit/3, events <= msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, oneEventLineLen, lessThanThird) + + expected := titleLine + DefaultTagsLimiter(trigger.Tags, thirdOfLimit) + "\n" + + strings.Repeat("a", greaterThanThird) + "\n" + + "```" + + strings.Repeat(eventLine, len(events)) + + "\n```" + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) + }) + + Convey("with tags and events > msgLimit/3, desc <= msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, oneEventLineLen, greaterThanThird) + + expected := titleLine + DefaultTagsLimiter(trigger.Tags, thirdOfLimit) + "\n" + + strings.Repeat("a", lessThanThird) + "\n" + + "```" + + strings.Repeat(eventLine, 29) + + "\n```\n" + "...and 1 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) + }) + + Convey("with desc and events > msgLimit/3, tags <= msgLimit/3", func() { + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, oneEventLineLen, greaterThanThird) + + tagsStr := DefaultTagsLimiter(trigger.Tags, lessThanThird) + + expected := titleLine + tagsStr + "\n" + + strings.Repeat("a", thirdOfLimit+(thirdOfLimit-utf8.RuneCountInString(tagsStr))/2-lenEndSuffix) + endSuffix + + "```" + + strings.Repeat(eventLine, 28) + + "\n```\n" + "...and 2 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) + }) + + Convey("tags, description and events all have len > msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, oneEventLineLen, greaterThanThird) + + expected := titleLine + DefaultTagsLimiter(trigger.Tags, thirdOfLimit) + "\n" + + strings.Repeat("a", thirdOfLimit-lenEndSuffix) + endSuffix + + "```" + + strings.Repeat(eventLine, thirdOfLimit/oneEventLineLen) + + "\n```\n" + + "...and 3 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, testMaxChars) }) }) }) } +func genTagsByLimit(limit int) []string { + tagName := "tag1" + + tagsCount := (limit - 1) / (len(tagName) + 2) + + tags := make([]string, 0, tagsCount) + + for i := 0; i < tagsCount; i++ { + tags = append(tags, tagName) + } + + return tags +} + +func genDescByLimit(limit int) string { + return strings.Repeat("a", limit) +} + +func genEventsByLimit(event moira.NotificationEvent, oneEventLineLen int, limit int) moira.NotificationEvents { + var events moira.NotificationEvents + for i := 0; i < limit/oneEventLineLen; i++ { + events = append(events, event) + } + return events +} + func testBoldFormatter(str string) string { return fmt.Sprintf("**%s**", str) } diff --git a/senders/msgformat/msgformat.go b/senders/msgformat/msgformat.go index 72a6cdb37..42b4aaf59 100644 --- a/senders/msgformat/msgformat.go +++ b/senders/msgformat/msgformat.go @@ -21,10 +21,3 @@ type MessageFormatterParams struct { MessageMaxChars int Throttled bool } - -// DefaultDescriptionCutter cuts description, so len(newDesc) <= maxSize. Ensure that len(desc) >= maxSize and -// maxSize >= len("...\n"). -func DefaultDescriptionCutter(desc string, maxSize int) string { - suffix := "...\n" - return desc[:maxSize-len(suffix)] + suffix -} diff --git a/senders/slack/slack_test.go b/senders/slack/slack_test.go index 37cd85eda..b4049562d 100644 --- a/senders/slack/slack_test.go +++ b/senders/slack/slack_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" "time" + "unicode/utf8" "github.com/go-playground/validator/v10" "github.com/moira-alert/moira" @@ -129,7 +130,8 @@ some other text italic text }) eventLine := "\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)" - oneEventLineLen := len([]rune(eventLine)) + oneEventLineLen := utf8.RuneCountInString(eventLine) + // Events list with chars less than half the message limit var shortEvents moira.NotificationEvents var shortEventsString string @@ -137,6 +139,7 @@ some other text italic text shortEvents = append(shortEvents, event) shortEventsString += eventLine } + // Events list with chars greater than half the message limit var longEvents moira.NotificationEvents var longEventsString string @@ -144,6 +147,7 @@ some other text italic text longEvents = append(longEvents, event) longEventsString += eventLine } + longDesc := strings.Repeat("a", messageMaxCharacters/2+100) Convey("Print moira message with desc + events < msgLimit", func() { @@ -159,22 +163,44 @@ some other text italic text events = append(events, event) eventsString += eventLine } + + expected := "*NODATA*\n" + + strings.Repeat("a", 1991) + "...\n" + + "```" + + eventsString + "\n```" + actual := sender.buildMessage(events, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```" + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, messageMaxCharacters) }) Convey("Print moira message events string > msgLimit/2", func() { desc := strings.Repeat("a", messageMaxCharacters/2-100) + + expected := "*NODATA*\n" + + desc + "\n" + + "```" + + strings.Repeat(eventLine, 41) + "\n```" + + "\n...and 5 more events." + actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: desc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n...and 3 more events." + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, messageMaxCharacters) }) Convey("Print moira message with both desc and events > msgLimit/2", func() { + expected := "*NODATA*\n" + + strings.Repeat("a", 1991) + "...\n" + + "```" + + strings.Repeat(eventLine, 41) + "\n```" + + "\n...and 5 more events." + actual := sender.buildMessage(longEvents, moira.TriggerData{Desc: longDesc}, false) - expected := "*NODATA*\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...\n```\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n```\n...and 5 more events." + So(actual, ShouldResemble, expected) + So(utf8.RuneCountInString(actual), ShouldBeLessThanOrEqualTo, messageMaxCharacters) }) }) } diff --git a/senders/telegram/message_formatter.go b/senders/telegram/message_formatter.go index 8059a2231..a453ceaa8 100644 --- a/senders/telegram/message_formatter.go +++ b/senders/telegram/message_formatter.go @@ -46,33 +46,57 @@ func NewTelegramMessageFormatter( // Format formats message using given params and formatter functions. func (formatter *messageFormatter) Format(params msgformat.MessageFormatterParams) string { + params.Trigger.Tags = htmlEscapeTags(params.Trigger.Tags) + state := params.Events.GetCurrentState(params.Throttled) emoji := formatter.emojiGetter.GetStateEmoji(state) title := formatter.buildTitle(params.Events, params.Trigger, emoji, params.Throttled) - titleLen := calcRunesCountWithoutHTML([]rune(title)) + titleLen := calcRunesCountWithoutHTML(title) + len("\n") + + var tags string + var tagsLen int + + triggerTags := params.Trigger.GetTags() + if len(triggerTags) != 0 { + tags = " " + triggerTags + tagsLen = calcRunesCountWithoutHTML(tags) + } desc := descriptionFormatter(params.Trigger) - descLen := calcRunesCountWithoutHTML([]rune(desc)) + descLen := calcRunesCountWithoutHTML(desc) - eventsString := formatter.buildEventsString(params.Events, -1, params.Throttled) - eventsStringLen := calcRunesCountWithoutHTML([]rune(eventsString)) + events := formatter.buildEventsString(params.Events, -1, params.Throttled) + eventsStringLen := calcRunesCountWithoutHTML(events) - descNewLen, eventsNewLen := senders.CalculateMessagePartsLength(params.MessageMaxChars-titleLen, descLen, eventsStringLen) + tagsNewLen, descNewLen, eventsNewLen := senders.CalculateMessagePartsBetweenTagsDescEvents(params.MessageMaxChars-titleLen, tagsLen, descLen, eventsStringLen) + if tagsLen != tagsNewLen { + tags = msgformat.DefaultTagsLimiter(params.Trigger.Tags, tagsNewLen) + } if descLen != descNewLen { desc = descriptionCutter(desc, descNewLen) } if eventsStringLen != eventsNewLen { - eventsString = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + events = formatter.buildEventsString(params.Events, eventsNewLen, params.Throttled) + } + + return title + tags + "\n" + desc + events +} + +func htmlEscapeTags(tags []string) []string { + escapedTags := make([]string, 0, len(tags)) + + for _, tag := range tags { + escapedTags = append(escapedTags, html.EscapeString(tag)) } - return title + desc + eventsString + return escapedTags } // calcRunesCountWithoutHTML is used for calculating symbols in text without html tags. Special symbols // like `>`, `<` etc. are counted not as one symbol, for example, len([]rune(">")). // This precision is enough for us to evaluate size of message. -func calcRunesCountWithoutHTML(htmlText []rune) int { +func calcRunesCountWithoutHTML(htmlText string) int { textLen := 0 isTag := false @@ -109,12 +133,6 @@ func (formatter *messageFormatter) buildTitle(events moira.NotificationEvents, t title += " " + trigger.Name } - tags := trigger.GetTags() - if tags != "" { - title += " " + tags - } - - title += "\n" return title } @@ -125,7 +143,7 @@ var throttleMsg = fmt.Sprintf("\nPlease, %s to generate less events.", boldForma func (formatter *messageFormatter) buildEventsString(events moira.NotificationEvents, charsForEvents int, throttled bool) string { charsForThrottleMsg := 0 if throttled { - charsForThrottleMsg = calcRunesCountWithoutHTML([]rune(throttleMsg)) + charsForThrottleMsg = calcRunesCountWithoutHTML(throttleMsg) } charsLeftForEvents := charsForEvents - charsForThrottleMsg @@ -144,7 +162,7 @@ func (formatter *messageFormatter) buildEventsString(events moira.NotificationEv tailString = fmt.Sprintf("\n...and %d more events.", len(events)-eventsPrinted) tailStringLen := len("\n") + utf8.RuneCountInString(tailString) - lineLen := calcRunesCountWithoutHTML([]rune(line)) + lineLen := calcRunesCountWithoutHTML(line) if charsForEvents >= 0 && eventsStringLen+lineLen > charsLeftForEvents-tailStringLen { eventsLenLimitReached = true diff --git a/senders/telegram/message_formatter_test.go b/senders/telegram/message_formatter_test.go index f6ed08cb2..f28308cfc 100644 --- a/senders/telegram/message_formatter_test.go +++ b/senders/telegram/message_formatter_test.go @@ -1,7 +1,6 @@ package telegram import ( - "fmt" "strings" "testing" "time" @@ -43,8 +42,6 @@ func TestMessageFormatter_Format(t *testing.T) { } expectedFirstLine := "💣 NODATA Name [tag1][tag2]\n" - lenFirstLine := utf8.RuneCountInString(expectedFirstLine) - - utf8.RuneCountInString("") eventStr := "02:40 (GMT+00:00): Metric = 123 (OK to NODATA)\n" lenEventStr := utf8.RuneCountInString(eventStr) - utf8.RuneCountInString("") // 60 - 13 = 47 @@ -109,86 +106,182 @@ func TestMessageFormatter_Format(t *testing.T) { }) Convey("with long messages", func() { - msgLimit := albumCaptionMaxCharacters - lenFirstLine - halfMsgLimit := msgLimit / 2 - greaterThanHalf := halfMsgLimit + 100 - lessThanHalf := halfMsgLimit - 100 + const ( + titleWithoutTags = "💣 NODATA Name" + ) + + titleLen := utf8.RuneCountInString(titleWithoutTags) - + utf8.RuneCountInString("") + len("\n") // 70 - 57 + 1 = 14 + + msgLimit := albumCaptionMaxCharacters - titleLen // 1024 - 14 = 1010 + thirdOfMsgLimit := msgLimit / 3 + greaterThanThird := thirdOfMsgLimit + 150 + lessThanThird := thirdOfMsgLimit - 100 + + // see genDescByLimit + symbolAtEndOfDescription := "" + if thirdOfMsgLimit%2 != 0 { + symbolAtEndOfDescription = "i" + } + + Convey("with tags > msgLimit/3, desc and events < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird + 200) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, lenEventStr, lessThanThird) + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, + msgLimit-lessThanThird-len(events)*lenEventStr-len("\n")) + "\n" + + strings.Repeat("ёж", lessThanThird/2) + symbolAtEndOfDescription + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, len(events)) + eventsBlockEnd - Convey("text size of description > msgLimit / 2", func() { - var events moira.NotificationEvents - throttled := false + actual := formatter.Format(getParams(events, trigger, false)) - eventsCount := lessThanHalf / lenEventStr - for i := 0; i < eventsCount; i++ { - events = append(events, event) - } + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + }) - trigger.Desc = strings.Repeat("**ё**ж", greaterThanHalf/2) + Convey("with desc > msgLimit/3, tags and events < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(greaterThanThird + 200) + events := genEventsByLimit(event, lenEventStr, lessThanThird) - expected := expectedFirstLine + - strings.Repeat("ёж", greaterThanHalf/2) + "\n" + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, lessThanThird) + "\n" + + tooLongDescMessage + eventsBlockStart + "\n" + - strings.Repeat(eventStr, eventsCount) + - eventsBlockEnd + strings.Repeat(eventStr, len(events)) + eventsBlockEnd - msg := formatter.Format(getParams(events, trigger, throttled)) + actual := formatter.Format(getParams(events, trigger, false)) - So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) - So(msg, ShouldEqual, expected) + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) }) - Convey("text size of events block > msgLimit / 2", func() { - var events moira.NotificationEvents - throttled := false + Convey("with events > msgLimit/3, tags and desc < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, lenEventStr, greaterThanThird+200) + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, lessThanThird) + "\n" + + strings.Repeat("ёж", lessThanThird/2) + symbolAtEndOfDescription + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, 10) + eventsBlockEnd + + "\n...and 4 more events." + + actual := formatter.Format(getParams(events, trigger, false)) - eventsCount := greaterThanHalf / lenEventStr - for i := 0; i < eventsCount; i++ { - events = append(events, event) - } + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + }) - trigger.Desc = strings.Repeat("**ё**ж", lessThanHalf/2) + Convey("with tags and desc > msgLimit/3, events < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, lenEventStr, lessThanThird) - expected := expectedFirstLine + - strings.Repeat("ёж", lessThanHalf/2) + "\n" + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, thirdOfMsgLimit) + "\n" + + tooLongDescMessage + eventsBlockStart + "\n" + - strings.Repeat(eventStr, eventsCount) + - eventsBlockEnd + strings.Repeat(eventStr, len(events)) + eventsBlockEnd - msg := formatter.Format(getParams(events, trigger, throttled)) + actual := formatter.Format(getParams(events, trigger, false)) - So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) - So(msg, ShouldEqual, expected) + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) }) - Convey("both description and events block have text size > msgLimit/2", func() { - var events moira.NotificationEvents - throttled := false + Convey("with tags and events > msgLimit / 3, desc < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(lessThanThird) + events := genEventsByLimit(event, lenEventStr, greaterThanThird) - eventsCount := greaterThanHalf / lenEventStr - for i := 0; i < eventsCount; i++ { - events = append(events, event) - } + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, thirdOfMsgLimit) + "\n" + + strings.Repeat("ёж", lessThanThird/2) + symbolAtEndOfDescription + "\n" + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, 8) + eventsBlockEnd + + "\n...and 2 more events." + + actual := formatter.Format(getParams(events, trigger, false)) - trigger.Desc = strings.Repeat("**ё**ж", greaterThanHalf/2) + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + }) - eventsShouldBe := halfMsgLimit / lenEventStr + Convey("with desc and events > msgLimit / 3, tags < msgLimit/3", func() { + trigger.Tags = genTagsByLimit(lessThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, lenEventStr, greaterThanThird) - expected := expectedFirstLine + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, lessThanThird) + "\n" + tooLongDescMessage + eventsBlockStart + "\n" + - strings.Repeat(eventStr, eventsShouldBe) + + strings.Repeat(eventStr, 7) + eventsBlockEnd + + "\n...and 3 more events." + + actual := formatter.Format(getParams(events, trigger, false)) + + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) + }) + + Convey("with tags, desc, events > msgLimit/3", func() { + trigger.Tags = genTagsByLimit(greaterThanThird) + trigger.Desc = genDescByLimit(greaterThanThird) + events := genEventsByLimit(event, lenEventStr, greaterThanThird) + + expected := titleWithoutTags + + msgformat.DefaultTagsLimiter(trigger.Tags, thirdOfMsgLimit) + "\n" + + tooLongDescMessage + + eventsBlockStart + "\n" + + strings.Repeat(eventStr, 6) + eventsBlockEnd + - fmt.Sprintf("\n...and %d more events.", len(events)-eventsShouldBe) + "\n...and 4 more events." - msg := formatter.Format(getParams(events, trigger, throttled)) + actual := formatter.Format(getParams(events, trigger, false)) - So(calcRunesCountWithoutHTML([]rune(msg)), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) - So(msg, ShouldEqual, expected) + So(actual, ShouldResemble, expected) + So(calcRunesCountWithoutHTML(actual), ShouldBeLessThanOrEqualTo, albumCaptionMaxCharacters) }) }) }) } +func genTagsByLimit(limit int) []string { + tagName := "tag1" + + tagsCount := (limit - 1) / (len(tagName) + 2) + + tags := make([]string, 0, tagsCount) + + for i := 0; i < tagsCount; i++ { + tags = append(tags, tagName) + } + + return tags +} + +func genDescByLimit(limit int) string { + str := strings.Repeat("**ё**ж", limit/2) + if limit%2 != 0 { + str += "i" + } + return str +} + +func genEventsByLimit(event moira.NotificationEvent, oneEventLineLen int, limit int) moira.NotificationEvents { + var events moira.NotificationEvents + for i := 0; i < limit/oneEventLineLen; i++ { + events = append(events, event) + } + return events +} + func getParams(events moira.NotificationEvents, trigger moira.TriggerData, throttled bool) msgformat.MessageFormatterParams { return msgformat.MessageFormatterParams{ Events: events, From ab5833a66c74066e7dd911e36701b005df04942c Mon Sep 17 00:00:00 2001 From: Tetrergeru Date: Tue, 29 Oct 2024 10:04:00 +0100 Subject: [PATCH 32/36] perf(filter): Optimize maps read/writes in hot path (#1114) --- filter/prefix_tree.go | 11 +++---- filter/prefix_tree_test.go | 41 +++++++++++++++++++++++---- filter/series_by_tag_pattern_index.go | 4 +-- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/filter/prefix_tree.go b/filter/prefix_tree.go index 7a4ee7eab..b94421ee8 100644 --- a/filter/prefix_tree.go +++ b/filter/prefix_tree.go @@ -113,26 +113,23 @@ func (source *PrefixTree) Match(metric string) []string { } // MatchWithValue finds metric in tree and returns payloads for all matched nodes. -func (source *PrefixTree) MatchWithValue(metric string) map[string]MatchingHandler { +func (source *PrefixTree) MatchWithValue(metric string, callback func(string, MatchingHandler)) { nodes, _ := source.findNodes(metric) if nodes == nil { - return map[string]MatchingHandler{} + return } - matched := make(map[string]MatchingHandler) for _, node := range nodes { if node.Terminal { if node.Payload == nil { - matched[node.Prefix] = nil + callback(node.Prefix, nil) } for pattern, matchingHandler := range node.Payload { - matched[pattern] = matchingHandler + callback(pattern, matchingHandler) } } } - - return matched } func (source *PrefixTree) findNodes(metric string) ([]*PatternNode, int) { diff --git a/filter/prefix_tree_test.go b/filter/prefix_tree_test.go index 7420e6bf7..cdd2afce7 100644 --- a/filter/prefix_tree_test.go +++ b/filter/prefix_tree_test.go @@ -19,7 +19,12 @@ func TestPrefixTree(t *testing.T) { }) Convey("MatchWithValue should return empty map", func() { - matchedPatterns := prefixTree.MatchWithValue("any_string") + matchedPatterns := map[string]MatchingHandler{} + prefixTree.MatchWithValue("any_string", func(s string, mh MatchingHandler) { + _, ok := matchedPatterns[s] + So(ok, ShouldBeFalse) + matchedPatterns[s] = mh + }) So(matchedPatterns, ShouldResemble, map[string]MatchingHandler{}) }) }) @@ -96,14 +101,28 @@ func TestPrefixTree(t *testing.T) { "Complex.matching.pattern": nil, "Complex.*.*": nil, } - matchedPatterns := prefixTree.MatchWithValue(metric) + + matchedPatterns := map[string]MatchingHandler{} + prefixTree.MatchWithValue(metric, func(s string, mh MatchingHandler) { + _, ok := matchedPatterns[s] + So(ok, ShouldBeFalse) + matchedPatterns[s] = mh + }) + So(matchedPatterns, ShouldResemble, matchedValue) }) Convey("For metrics not from tree", func() { metric := "Simple.notmatching.pattern" matchedValue := map[string]MatchingHandler{} - matchedPatterns := prefixTree.MatchWithValue(metric) + + matchedPatterns := map[string]MatchingHandler{} + prefixTree.MatchWithValue(metric, func(s string, mh MatchingHandler) { + _, ok := matchedPatterns[s] + So(ok, ShouldBeFalse) + matchedPatterns[s] = mh + }) + So(matchedPatterns, ShouldResemble, matchedValue) }) }) @@ -143,7 +162,13 @@ func TestPrefixTree(t *testing.T) { }}, } for _, testCase := range testCases { - matchedPatterns := prefixTree.MatchWithValue(testCase.Metric) + matchedPatterns := map[string]MatchingHandler{} + prefixTree.MatchWithValue(testCase.Metric, func(s string, mh MatchingHandler) { + _, ok := matchedPatterns[s] + So(ok, ShouldBeFalse) + matchedPatterns[s] = mh + }) + So(len(matchedPatterns), ShouldEqual, len(testCase.MatchedPatterns)) for pKey, pValue := range testCase.MatchedPatterns { So(matchedPatterns, ShouldContainKey, pKey) @@ -162,7 +187,13 @@ func TestPrefixTree(t *testing.T) { "Simple.notmatching.pattern", } for _, testCase := range testCases { - matchedPatterns := prefixTree.MatchWithValue(testCase) + matchedPatterns := map[string]MatchingHandler{} + prefixTree.MatchWithValue(testCase, func(s string, mh MatchingHandler) { + _, ok := matchedPatterns[s] + So(ok, ShouldBeFalse) + matchedPatterns[s] = mh + }) + So(matchedPatterns, ShouldResemble, map[string]MatchingHandler{}) } }) diff --git a/filter/series_by_tag_pattern_index.go b/filter/series_by_tag_pattern_index.go index 23bb7ff56..69df49b8a 100644 --- a/filter/series_by_tag_pattern_index.go +++ b/filter/series_by_tag_pattern_index.go @@ -73,12 +73,12 @@ func NewSeriesByTagPatternIndex( func (index *SeriesByTagPatternIndex) MatchPatterns(metricName string, labels map[string]string) []string { matchedPatterns := make([]string, 0) - matchingHandlersWithCorrespondingNameTag := index.namesPrefixTree.MatchWithValue(metricName) - for pattern, matchingHandler := range matchingHandlersWithCorrespondingNameTag { + callback := func(pattern string, matchingHandler MatchingHandler) { if matchingHandler(metricName, labels) { matchedPatterns = append(matchedPatterns, pattern) } } + index.namesPrefixTree.MatchWithValue(metricName, callback) for pattern, matchingHandler := range index.withoutStrictNameTagPatternMatchers { if matchingHandler(metricName, labels) { From 96f13126af1bd4b941e150a56abe6cd687824db4 Mon Sep 17 00:00:00 2001 From: Tetrergeru Date: Tue, 29 Oct 2024 14:06:37 +0100 Subject: [PATCH 33/36] perf(filter): replace map with slice in hot path (#1115) --- filter/series_by_tag_pattern_index.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/filter/series_by_tag_pattern_index.go b/filter/series_by_tag_pattern_index.go index 69df49b8a..cb8c95b39 100644 --- a/filter/series_by_tag_pattern_index.go +++ b/filter/series_by_tag_pattern_index.go @@ -11,11 +11,16 @@ type SeriesByTagPatternIndex struct { // namesPrefixTree stores MatchingHandler's for patterns that have name tag in prefix tree structure namesPrefixTree *PrefixTree // withoutStrictNameTagPatternMatchers stores MatchingHandler's for patterns that have no name tag - withoutStrictNameTagPatternMatchers map[string]MatchingHandler + withoutStrictNameTagPatternMatchers []patternAndHandler // Flags for compatibility with different graphite behaviours compatibility Compatibility } +type patternAndHandler struct { + pattern string + handler MatchingHandler +} + // NewSeriesByTagPatternIndex creates new SeriesByTagPatternIndex using seriesByTag patterns and parsed specs comes from ParseSeriesByTag. func NewSeriesByTagPatternIndex( logger moira.Logger, @@ -25,7 +30,7 @@ func NewSeriesByTagPatternIndex( metrics *metrics.FilterMetrics, ) *SeriesByTagPatternIndex { namesPrefixTree := &PrefixTree{Logger: logger, Root: &PatternNode{}} - withoutStrictNameTagPatternMatchers := make(map[string]MatchingHandler) + withoutStrictNameTagPatternMatchers := make([]patternAndHandler, 0) var patternMatchingEvicted int64 @@ -54,7 +59,13 @@ func NewSeriesByTagPatternIndex( } if patternMatching.nameTagValue == "" { - withoutStrictNameTagPatternMatchers[pattern] = patternMatching.matchingHandler + withoutStrictNameTagPatternMatchers = append( + withoutStrictNameTagPatternMatchers, + patternAndHandler{ + pattern: pattern, + handler: patternMatching.matchingHandler, + }, + ) } else { namesPrefixTree.AddWithPayload(patternMatching.nameTagValue, pattern, patternMatching.matchingHandler) } @@ -80,9 +91,9 @@ func (index *SeriesByTagPatternIndex) MatchPatterns(metricName string, labels ma } index.namesPrefixTree.MatchWithValue(metricName, callback) - for pattern, matchingHandler := range index.withoutStrictNameTagPatternMatchers { - if matchingHandler(metricName, labels) { - matchedPatterns = append(matchedPatterns, pattern) + for _, patternAndHandler := range index.withoutStrictNameTagPatternMatchers { + if patternAndHandler.handler(metricName, labels) { + matchedPatterns = append(matchedPatterns, patternAndHandler.pattern) } } From 63742c9b71df45ab842f82a325c7305263595211 Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Thu, 31 Oct 2024 14:56:28 +0300 Subject: [PATCH 34/36] fix(notifier): incorrect sending alerts to subscriptions (#1109) --- notifier/scheduler.go | 5 ++- notifier/scheduler_test.go | 75 +++++++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/notifier/scheduler.go b/notifier/scheduler.go index d0348bc75..446c1d376 100644 --- a/notifier/scheduler.go +++ b/notifier/scheduler.go @@ -50,10 +50,11 @@ func (scheduler *StandardScheduler) ScheduleNotification(params moira.SchedulerP next time.Time throttled bool ) + now := scheduler.clock.NowUTC() if params.SendFail > 0 { next = now.Add(scheduler.config.ReschedulingDelay) - throttled = params.ThrottledOld + next, throttled = scheduler.calculateNextDelivery(next, ¶ms.Event, logger) } else { if params.Event.State == moira.StateTEST { next = now @@ -62,6 +63,7 @@ func (scheduler *StandardScheduler) ScheduleNotification(params moira.SchedulerP next, throttled = scheduler.calculateNextDelivery(now, ¶ms.Event, logger) } } + notification := &moira.ScheduledNotification{ Event: params.Event, Trigger: params.Trigger, @@ -78,6 +80,7 @@ func (scheduler *StandardScheduler) ScheduleNotification(params moira.SchedulerP Int64("notification_timestamp_unix", next.Unix()). Int64("notification_created_at_unix", now.Unix()). Msg("Scheduled notification") + return notification } diff --git a/notifier/scheduler_test.go b/notifier/scheduler_test.go index e148503bb..79d3b9681 100644 --- a/notifier/scheduler_test.go +++ b/notifier/scheduler_test.go @@ -1,6 +1,7 @@ package notifier import ( + "errors" "fmt" "testing" "time" @@ -45,6 +46,15 @@ func TestThrottling(t *testing.T) { SubscriptionID: &subID, } + subscription := moira.SubscriptionData{ + ID: "SubscriptionID-000000000000001", + Enabled: true, + Tags: []string{"test-tag"}, + Contacts: []string{"ContactID-000000000000001"}, + ThrottlingEnabled: true, + Schedule: schedule5, + } + mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() dataBase := mock_moira_alert.NewMockDatabase(mockCtrl) @@ -52,6 +62,7 @@ func TestThrottling(t *testing.T) { metrics2 := metrics.ConfigureNotifierMetrics(metrics.NewDummyRegistry(), "notifier") now := time.Now() + next := now.Add(10 * time.Minute) systemClock := mock_clock.NewMockClock(mockCtrl) scheduler := NewScheduler(dataBase, logger, metrics2, SchedulerConfig{ReschedulingDelay: time.Minute}, systemClock) @@ -84,6 +95,51 @@ func TestThrottling(t *testing.T) { expected2.SendFail = 1 expected2.Timestamp = now.Add(time.Minute).Unix() systemClock.EXPECT().NowUTC().Return(now).Times(1) + dataBase.EXPECT().GetTriggerThrottling(params2.Event.TriggerID).Return(now, now) + dataBase.EXPECT().GetSubscription(*params2.Event.SubscriptionID).Return(subscription, nil) + dataBase.EXPECT().GetNotificationEventCount(event.TriggerID, now.Unix()).Return(int64(0)) + dataBase.EXPECT().GetNotificationEventCount(event.TriggerID, now.Unix()).Return(int64(0)) + + notification := scheduler.ScheduleNotification(params2, logger) + So(notification, ShouldResemble, &expected2) + }) + + Convey("Test sendFail more that 0, and no throttling, but subscription doesn't exists, should send message in one minute", t, func() { + params2 := params + params2.ThrottledOld = false + params2.SendFail = 1 + testErr := errors.New("subscription doesn't exist") + + expected2 := expected + expected2.SendFail = 1 + expected2.Timestamp = now.Add(time.Minute).Unix() + systemClock.EXPECT().NowUTC().Return(now).Times(1) + dataBase.EXPECT().GetTriggerThrottling(params2.Event.TriggerID).Return(now, now) + dataBase.EXPECT().GetSubscription(*params2.Event.SubscriptionID).Return(moira.SubscriptionData{}, testErr) + + notification := scheduler.ScheduleNotification(params2, logger) + So(notification, ShouldResemble, &expected2) + }) + + Convey("Test sendFail more that 0, and no throttling, but the subscription schedule postpones the dispatch time, should send message in one minute", t, func() { + params2 := params + params2.ThrottledOld = false + params2.SendFail = 1 + + // 2015-09-02, 01:00:00 GMT+03:00 + testNow := time.Unix(1441144800, 0) + testSubscription := subscription + testSubscription.ThrottlingEnabled = false + testSubscription.Schedule = schedule3 + + expected2 := expected + expected2.SendFail = 1 + // 2015-09-02, 02:00:00 GMT+03:00 + expected2.Timestamp = time.Unix(1441148400, 0).Unix() + expected2.CreatedAt = testNow.Unix() + systemClock.EXPECT().NowUTC().Return(testNow).Times(1) + dataBase.EXPECT().GetTriggerThrottling(params2.Event.TriggerID).Return(testNow, testNow) + dataBase.EXPECT().GetSubscription(*params2.Event.SubscriptionID).Return(testSubscription, nil) notification := scheduler.ScheduleNotification(params2, logger) So(notification, ShouldResemble, &expected2) @@ -96,9 +152,11 @@ func TestThrottling(t *testing.T) { expected2 := expected expected2.SendFail = 3 - expected2.Timestamp = now.Add(time.Minute).Unix() + expected2.Timestamp = now.Add(10 * time.Minute).Unix() expected2.Throttled = true systemClock.EXPECT().NowUTC().Return(now).Times(1) + dataBase.EXPECT().GetTriggerThrottling(params2.Event.TriggerID).Return(next, now) + dataBase.EXPECT().GetSubscription(*params2.Event.SubscriptionID).Return(subscription, nil) notification := scheduler.ScheduleNotification(params2, logger) So(notification, ShouldResemble, &expected2) @@ -504,3 +562,18 @@ var schedule4 = moira.ScheduleData{ {Enabled: true}, }, } + +var schedule5 = moira.ScheduleData{ + StartOffset: 0, // 00:00 + EndOffset: 1440, // 24:00 + TimezoneOffset: -180, // (GMT +3) + Days: []moira.ScheduleDataDay{ + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + {Enabled: true}, + }, +} From bbff8da3ccd21e44b565bf53ed0694396cc93f20 Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Tue, 5 Nov 2024 16:01:52 +0700 Subject: [PATCH 35/36] feat: remote graphite retries (#1085) --- clock/clock.go | 4 +- cmd/config.go | 61 +++- go.mod | 1 + go.sum | 2 + local/api.yml | 15 +- local/checker.yml | 15 +- local/notifier.yml | 15 +- metric_source/remote/config.go | 21 +- metric_source/remote/config_test.go | 96 ++++++ metric_source/remote/remote.go | 67 +++-- metric_source/remote/remote_test.go | 267 +++++++++++++++-- metric_source/remote/request.go | 122 +++++++- metric_source/remote/request_test.go | 279 +++++++++++++++++- metric_source/remote/response.go | 6 + metric_source/retries/backoff_factory.go | 37 +++ metric_source/retries/backoff_factory_test.go | 169 +++++++++++ metric_source/retries/config.go | 24 ++ metric_source/retries/config_test.go | 95 ++++++ metric_source/retries/retrier.go | 23 ++ metric_source/retries/retrier_test.go | 167 +++++++++++ metric_source/retries/retryable_operation.go | 7 + 21 files changed, 1400 insertions(+), 93 deletions(-) create mode 100644 metric_source/remote/config_test.go create mode 100644 metric_source/retries/backoff_factory.go create mode 100644 metric_source/retries/backoff_factory_test.go create mode 100644 metric_source/retries/config.go create mode 100644 metric_source/retries/config_test.go create mode 100644 metric_source/retries/retrier.go create mode 100644 metric_source/retries/retrier_test.go create mode 100644 metric_source/retries/retryable_operation.go diff --git a/clock/clock.go b/clock/clock.go index f78ed070c..b527ea462 100644 --- a/clock/clock.go +++ b/clock/clock.go @@ -10,12 +10,12 @@ func NewSystemClock() *SystemClock { return &SystemClock{} } -// Now returns now time.Time with UTC location. +// NowUTC returns now time.Time with UTC location. func (t *SystemClock) NowUTC() time.Time { return time.Now().UTC() } -// Now returns now time.Time as a Unix time. +// NowUnix returns current time in a Unix time format. func (t *SystemClock) NowUnix() int64 { return time.Now().Unix() } diff --git a/cmd/config.go b/cmd/config.go index 0d033cf76..8bc59b07d 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/moira-alert/moira" + "github.com/moira-alert/moira/metric_source/retries" "github.com/moira-alert/moira/metrics" "github.com/moira-alert/moira/image_store/s3" @@ -230,15 +231,54 @@ type remoteCommon interface { getRemoteCommon() *RemoteCommonConfig } +// RetriesConfig is a settings for retry policy when performing requests to remote sources. +// Stop retrying when ONE of the following conditions is satisfied: +// - Time passed since first try is greater than MaxElapsedTime; +// - Already MaxRetriesCount done. +type RetriesConfig struct { + // InitialInterval between requests. + InitialInterval string `yaml:"initial_interval"` + // RandomizationFactor is used in exponential backoff to add some randomization + // when calculating next interval between requests. + // It will be used in multiplication like: + // RandomizedInterval = RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) + RandomizationFactor float64 `yaml:"randomization_factor"` + // Each new RetryInterval will be multiplied on Multiplier. + Multiplier float64 `yaml:"multiplier"` + // MaxInterval is the cap for RetryInterval. Note that it doesn't cap the RandomizedInterval. + MaxInterval string `yaml:"max_interval"` + // MaxElapsedTime caps the time passed from first try. If time passed is greater than MaxElapsedTime than stop retrying. + MaxElapsedTime string `yaml:"max_elapsed_time"` + // MaxRetriesCount is the amount of allowed retries. So at most MaxRetriesCount will be performed. + MaxRetriesCount uint64 `yaml:"max_retries_count"` +} + +func (config RetriesConfig) getRetriesSettings() retries.Config { + return retries.Config{ + InitialInterval: to.Duration(config.InitialInterval), + RandomizationFactor: config.RandomizationFactor, + Multiplier: config.Multiplier, + MaxInterval: to.Duration(config.MaxInterval), + MaxElapsedTime: to.Duration(config.MaxElapsedTime), + MaxRetriesCount: config.MaxRetriesCount, + } +} + // GraphiteRemoteConfig is remote graphite settings structure. type GraphiteRemoteConfig struct { RemoteCommonConfig `yaml:",inline"` - // Timeout for remote requests + // Timeout for remote requests. Timeout string `yaml:"timeout"` - // Username for basic auth + // Username for basic auth. User string `yaml:"user"` - // Password for basic auth + // Password for basic auth. Password string `yaml:"password"` + // Retries configuration for general requests to remote graphite. + Retries RetriesConfig `yaml:"retries"` + // HealthcheckTimeout is timeout for remote api health check requests. + HealthcheckTimeout string `yaml:"health_check_timeout"` + // HealthCheckRetries configuration for healthcheck requests to remote graphite. + HealthCheckRetries RetriesConfig `yaml:"health_check_retries"` } func (config GraphiteRemoteConfig) getRemoteCommon() *RemoteCommonConfig { @@ -248,12 +288,15 @@ func (config GraphiteRemoteConfig) getRemoteCommon() *RemoteCommonConfig { // GetRemoteSourceSettings returns remote config parsed from moira config files. func (config *GraphiteRemoteConfig) GetRemoteSourceSettings() *graphiteRemoteSource.Config { return &graphiteRemoteSource.Config{ - URL: config.URL, - CheckInterval: to.Duration(config.CheckInterval), - MetricsTTL: to.Duration(config.MetricsTTL), - Timeout: to.Duration(config.Timeout), - User: config.User, - Password: config.Password, + URL: config.URL, + CheckInterval: to.Duration(config.CheckInterval), + MetricsTTL: to.Duration(config.MetricsTTL), + Timeout: to.Duration(config.Timeout), + User: config.User, + Password: config.Password, + Retries: config.Retries.getRetriesSettings(), + HealthcheckTimeout: to.Duration(config.HealthcheckTimeout), + HealthcheckRetries: config.HealthCheckRetries.getRetriesSettings(), } } diff --git a/go.mod b/go.mod index b309808bd..ad157caf0 100644 --- a/go.mod +++ b/go.mod @@ -171,6 +171,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect github.com/fatih/color v1.16.0 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect diff --git a/go.sum b/go.sum index a013bebd9..74558b6da 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,8 @@ github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4 github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1 h1:hXakhQtPnXH839q1pBl/GqfTSchqE+R5Fqn98Iu7UQM= github.com/carlosdp/twiliogo v0.0.0-20161027183705-b26045ebb9d1/go.mod h1:pAxCBpjl/0JxYZlWGP/Dyi8f/LQSCQD2WAsG/iNzqQ8= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/local/api.yml b/local/api.yml index 631240d4e..d852a09b5 100644 --- a/local/api.yml +++ b/local/api.yml @@ -17,8 +17,21 @@ graphite_remote: cluster_name: Graphite 1 url: "http://graphite:80/render" check_interval: 60s - timeout: 60s metrics_ttl: 168h + timeout: 60s + retries: + initial_interval: 60s + randomization_factor: 0.5 + multiplier: 1.5 + max_interval: 120s + max_retries_count: 3 + health_check_timeout: 6s + health_check_retries: + initial_interval: 20s + randomization_factor: 0.5 + multiplier: 1.5 + max_interval: 80s + max_retries_count: 3 prometheus_remote: - cluster_id: default cluster_name: Prometheus 1 diff --git a/local/checker.yml b/local/checker.yml index 71ff47e40..30f7e0bb9 100644 --- a/local/checker.yml +++ b/local/checker.yml @@ -19,8 +19,21 @@ graphite_remote: cluster_name: Graphite 1 url: "http://graphite:80/render" check_interval: 60s - timeout: 60s metrics_ttl: 168h + timeout: 60s + retries: + initial_interval: 60s + randomization_factor: 0.5 + multiplier: 1.5 + max_interval: 120s + max_retries_count: 3 + health_check_timeout: 6s + health_check_retries: + initial_interval: 20s + randomization_factor: 0.5 + multiplier: 1.5 + max_interval: 80s + max_retries_count: 3 prometheus_remote: - cluster_id: default cluster_name: Prometheus 1 diff --git a/local/notifier.yml b/local/notifier.yml index 6a5cf57a7..d23f24955 100644 --- a/local/notifier.yml +++ b/local/notifier.yml @@ -17,8 +17,21 @@ graphite_remote: cluster_name: Graphite 1 url: "http://graphite:80/render" check_interval: 60s - timeout: 60s metrics_ttl: 168h + timeout: 60s + retries: + initial_interval: 60s + randomization_factor: 0.5 + multiplier: 1.5 + max_interval: 120s + max_retries_count: 3 + health_check_timeout: 6s + health_check_retries: + initial_interval: 20s + randomization_factor: 0.5 + multiplier: 1.5 + max_interval: 80s + max_retries_count: 3 prometheus_remote: - cluster_id: default cluster_name: Prometheus 1 diff --git a/metric_source/remote/config.go b/metric_source/remote/config.go index 06ca22af4..53b2f90cc 100644 --- a/metric_source/remote/config.go +++ b/metric_source/remote/config.go @@ -1,13 +1,20 @@ package remote -import "time" +import ( + "time" + + "github.com/moira-alert/moira/metric_source/retries" +) // Config represents config from remote storage. type Config struct { - URL string - CheckInterval time.Duration - MetricsTTL time.Duration - Timeout time.Duration - User string - Password string + URL string `validate:"required,url"` + CheckInterval time.Duration + MetricsTTL time.Duration + Timeout time.Duration `validate:"required,gt=0s"` + User string + Password string + HealthcheckTimeout time.Duration `validate:"required,gt=0s"` + Retries retries.Config + HealthcheckRetries retries.Config } diff --git a/metric_source/remote/config_test.go b/metric_source/remote/config_test.go new file mode 100644 index 000000000..6e1ab0c4c --- /dev/null +++ b/metric_source/remote/config_test.go @@ -0,0 +1,96 @@ +package remote + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/go-playground/validator/v10" + "github.com/moira-alert/moira" + + "github.com/moira-alert/moira/metric_source/retries" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestConfigWithValidateStruct(t *testing.T) { + Convey("Test validating retries config", t, func() { + type testcase struct { + caseDesc string + conf Config + errIsNil bool + } + + var ( + testInitialInterval = time.Second * 5 + testMaxInterval = time.Second * 10 + testRetriesCount uint64 = 10 + validatorErr = validator.ValidationErrors{} + ) + + testRetriesConf := retries.Config{ + InitialInterval: testInitialInterval, + MaxInterval: testMaxInterval, + MaxRetriesCount: testRetriesCount, + } + + cases := []testcase{ + { + caseDesc: "with empty config", + conf: Config{}, + errIsNil: false, + }, + { + caseDesc: "with retries config set", + conf: Config{ + Retries: testRetriesConf, + HealthcheckRetries: testRetriesConf, + }, + errIsNil: false, + }, + { + caseDesc: "with retries config set and some url", + conf: Config{ + URL: "http://test-graphite", + Retries: testRetriesConf, + HealthcheckRetries: testRetriesConf, + }, + errIsNil: false, + }, + { + caseDesc: "with retries config set, some url, timeout", + conf: Config{ + Timeout: time.Second, + URL: "http://test-graphite", + Retries: testRetriesConf, + HealthcheckRetries: testRetriesConf, + }, + errIsNil: false, + }, + { + caseDesc: "with valid config", + conf: Config{ + Timeout: time.Second, + HealthcheckTimeout: time.Millisecond, + URL: "http://test-graphite", + Retries: testRetriesConf, + HealthcheckRetries: testRetriesConf, + }, + errIsNil: true, // nil, + }, + } + + for i := range cases { + Convey(fmt.Sprintf("Case %d: %s", i+1, cases[i].caseDesc), func() { + err := moira.ValidateStruct(cases[i].conf) + + if cases[i].errIsNil { + So(err, ShouldBeNil) + } else { + So(errors.As(err, &validatorErr), ShouldBeTrue) + } + }) + } + }) +} diff --git a/metric_source/remote/remote.go b/metric_source/remote/remote.go index 853a96240..654437fbd 100644 --- a/metric_source/remote/remote.go +++ b/metric_source/remote/remote.go @@ -1,17 +1,15 @@ package remote import ( - "fmt" "net/http" "time" + "github.com/moira-alert/moira/metric_source/retries" + "github.com/moira-alert/moira" metricSource "github.com/moira-alert/moira/metric_source" ) -// ErrRemoteStorageDisabled is used to prevent remote.Fetch calls when remote storage is disabled. -var ErrRemoteStorageDisabled = fmt.Errorf("remote graphite storage is not enabled") - // ErrRemoteTriggerResponse is a custom error when remote trigger check fails. type ErrRemoteTriggerResponse struct { InternalError error @@ -23,20 +21,38 @@ func (err ErrRemoteTriggerResponse) Error() string { return err.InternalError.Error() } +// ErrRemoteUnavailable is a custom error when remote trigger check fails. +type ErrRemoteUnavailable struct { + InternalError error + Target string +} + +// Error is a representation of Error interface method. +func (err ErrRemoteUnavailable) Error() string { + return err.InternalError.Error() +} + // Remote is implementation of MetricSource interface, which implements fetch metrics method from remote graphite installation. type Remote struct { - config *Config - client *http.Client + config *Config + client *http.Client + retrier retries.Retrier[[]byte] + requestBackoffFactory retries.BackoffFactory + healthcheckBackoffFactory retries.BackoffFactory } // Create configures remote metric source. func Create(config *Config) (metricSource.MetricSource, error) { - if config.URL == "" { - return nil, fmt.Errorf("remote graphite URL should not be empty") + if err := moira.ValidateStruct(config); err != nil { + return nil, err } + return &Remote{ - config: config, - client: &http.Client{Timeout: config.Timeout}, + config: config, + client: &http.Client{}, + retrier: retries.NewStandardRetrier[[]byte](), + requestBackoffFactory: retries.NewExponentialBackoffFactory(config.Retries), + healthcheckBackoffFactory: retries.NewExponentialBackoffFactory(config.HealthcheckRetries), }, nil } @@ -53,13 +69,12 @@ func (remote *Remote) Fetch(target string, from, until int64, allowRealTimeAlert Target: target, } } - body, err := remote.makeRequest(req) + + body, err := remote.makeRequest(req, remote.config.Timeout, remote.requestBackoffFactory.NewBackOff()) if err != nil { - return nil, ErrRemoteTriggerResponse{ - InternalError: err, - Target: target, - } + return nil, internalErrToPublicErr(err, target) } + resp, err := decodeBody(body) if err != nil { return nil, ErrRemoteTriggerResponse{ @@ -67,6 +82,7 @@ func (remote *Remote) Fetch(target string, from, until int64, allowRealTimeAlert Target: target, } } + fetchResult := convertResponse(resp, allowRealTimeAlerting) return &fetchResult, nil } @@ -76,25 +92,18 @@ func (remote *Remote) GetMetricsTTLSeconds() int64 { return int64(remote.config.MetricsTTL.Seconds()) } -// IsConfigured returns false in cases that user does not properly configure remote settings like graphite URL. -func (remote *Remote) IsConfigured() (bool, error) { - return true, nil -} - -// IsRemoteAvailable checks if graphite API is available and returns 200 response. +// IsAvailable checks if graphite API is available and returns 200 response. func (remote *Remote) IsAvailable() (bool, error) { - maxRetries := 3 until := time.Now().Unix() from := until - 600 //nolint + req, err := remote.prepareRequest(from, until, "NonExistingTarget") if err != nil { return false, err } - for attempt := 0; attempt < maxRetries; attempt++ { - _, err = remote.makeRequest(req) - if err == nil { - return true, nil - } - } - return false, err + + _, err = remote.makeRequest(req, remote.config.HealthcheckTimeout, remote.healthcheckBackoffFactory.NewBackOff()) + publicErr := internalErrToPublicErr(err, "") + + return !isRemoteUnavailable(publicErr), publicErr } diff --git a/metric_source/remote/remote_test.go b/metric_source/remote/remote_test.go index fd4539572..ecbe47764 100644 --- a/metric_source/remote/remote_test.go +++ b/metric_source/remote/remote_test.go @@ -4,26 +4,128 @@ import ( "fmt" "net/http" "testing" + "time" metricSource "github.com/moira-alert/moira/metric_source" + "github.com/moira-alert/moira/metric_source/retries" + . "github.com/smartystreets/goconvey/convey" ) -func TestIsRemoteAvailable(t *testing.T) { - Convey("Is available", t, func() { - server := createServer([]byte("Some string"), http.StatusOK) - remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} - isAvailable, err := remote.IsAvailable() - So(isAvailable, ShouldBeTrue) - So(err, ShouldBeEmpty) +var testConfigs = []*Config{ + { + Timeout: time.Second, + Retries: retries.Config{ + InitialInterval: time.Millisecond, + RandomizationFactor: 0.5, + Multiplier: 2, + MaxInterval: time.Millisecond * 20, + MaxRetriesCount: 2, + }, + }, + { + Timeout: time.Millisecond * 200, + Retries: retries.Config{ + InitialInterval: time.Millisecond, + RandomizationFactor: 0.5, + Multiplier: 2, + MaxInterval: time.Second, + MaxElapsedTime: time.Second * 2, + }, + }, +} + +func TestIsAvailable(t *testing.T) { + body := []byte("Some string") + + isAvailableTestConfigs := make([]*Config, 0, len(testConfigs)) + for _, conf := range testConfigs { + isAvailableTestConfigs = append(isAvailableTestConfigs, &Config{ + HealthcheckTimeout: conf.Timeout, + HealthcheckRetries: conf.Retries, + }) + } + + retrier := retries.NewStandardRetrier[[]byte]() + + Convey("Given server returns OK response the remote is available", t, func() { + server := createServer(body, http.StatusOK) + defer server.Close() + + for _, config := range isAvailableTestConfigs { + config.URL = server.URL + + remote := Remote{ + client: server.Client(), + config: config, + retrier: retrier, + healthcheckBackoffFactory: retries.NewExponentialBackoffFactory(config.HealthcheckRetries), + } + + isAvailable, err := remote.IsAvailable() + So(isAvailable, ShouldBeTrue) + So(err, ShouldBeEmpty) + } }) - Convey("Not available", t, func() { - server := createServer([]byte("Some string"), http.StatusInternalServerError) - remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} - isAvailable, err := remote.IsAvailable() - So(isAvailable, ShouldBeFalse) - So(err, ShouldResemble, fmt.Errorf("bad response status %d: %s", http.StatusInternalServerError, "Some string")) + Convey("Given server returns Remote Unavailable responses permanently", t, func() { + for statusCode := range remoteUnavailableStatusCodes { + server := createTestServer(TestResponse{body, statusCode}) + + Convey(fmt.Sprintf( + "request failed with %d response status code and remote is unavailable", statusCode, + ), func() { + for _, config := range isAvailableTestConfigs { + config.URL = server.URL + + remote := Remote{ + client: server.Client(), + config: config, + retrier: retrier, + healthcheckBackoffFactory: retries.NewExponentialBackoffFactory(config.HealthcheckRetries), + } + + isAvailable, err := remote.IsAvailable() + So(err, ShouldResemble, ErrRemoteUnavailable{ + InternalError: fmt.Errorf( + "the remote server is not available. Response status %d: %s", statusCode, string(body), + ), + }) + So(isAvailable, ShouldBeFalse) + } + }) + + server.Close() + } + }) + + Convey("Given server returns Remote Unavailable response temporary", t, func() { + for statusCode := range remoteUnavailableStatusCodes { + Convey(fmt.Sprintf( + "the remote is available with retry after %d response", statusCode, + ), func() { + for _, config := range isAvailableTestConfigs { + server := createTestServer( + TestResponse{body, statusCode}, + TestResponse{body, http.StatusOK}, + ) + config.URL = server.URL + + remote := Remote{ + client: server.Client(), + config: config, + retrier: retrier, + healthcheckBackoffFactory: retries.NewExponentialBackoffFactory(config.HealthcheckRetries), + } + + isAvailable, err := remote.IsAvailable() + So(err, ShouldBeNil) + So(isAvailable, ShouldBeTrue) + + server.Close() + } + }) + } }) } @@ -32,9 +134,22 @@ func TestFetch(t *testing.T) { var until int64 = 500 target := "foo.bar" //nolint + retrier := retries.NewStandardRetrier[[]byte]() + validBody := []byte("[{\"Target\": \"t1\",\"DataPoints\":[[1,2],[3,4]]}]") + Convey("Request success but body is invalid", t, func() { server := createServer([]byte("[]"), http.StatusOK) - remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} + + conf := testConfigs[0] + conf.URL = server.URL + + remote := Remote{ + client: server.Client(), + config: conf, + retrier: retrier, + requestBackoffFactory: retries.NewExponentialBackoffFactory(conf.Retries), + } + result, err := remote.Fetch(target, from, until, false) So(result, ShouldResemble, &FetchResult{MetricsData: []metricSource.MetricData{}}) So(err, ShouldBeEmpty) @@ -42,25 +157,129 @@ func TestFetch(t *testing.T) { Convey("Request success but body is invalid", t, func() { server := createServer([]byte("Some string"), http.StatusOK) - remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} + defer server.Close() + + conf := testConfigs[0] + conf.URL = server.URL + + remote := Remote{ + client: server.Client(), + config: conf, + retrier: retrier, + requestBackoffFactory: retries.NewExponentialBackoffFactory(conf.Retries), + } + result, err := remote.Fetch(target, from, until, false) So(result, ShouldBeEmpty) So(err.Error(), ShouldResemble, "invalid character 'S' looking for beginning of value") + So(err, ShouldHaveSameTypeAs, ErrRemoteTriggerResponse{}) }) Convey("Fail request with InternalServerError", t, func() { server := createServer([]byte("Some string"), http.StatusInternalServerError) - remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} - result, err := remote.Fetch(target, from, until, false) - So(result, ShouldBeEmpty) - So(err.Error(), ShouldResemble, fmt.Sprintf("bad response status %d: %s", http.StatusInternalServerError, "Some string")) + defer server.Close() + + for _, config := range testConfigs { + config.URL = server.URL + + remote := Remote{ + client: server.Client(), + config: config, + retrier: retrier, + requestBackoffFactory: retries.NewExponentialBackoffFactory(config.Retries), + } + + result, err := remote.Fetch(target, from, until, false) + + So(result, ShouldBeEmpty) + So(err.Error(), ShouldResemble, fmt.Sprintf("bad response status %d: %s", http.StatusInternalServerError, "Some string")) + So(err, ShouldHaveSameTypeAs, ErrRemoteTriggerResponse{}) + } }) - Convey("Fail make request", t, func() { + Convey("Client calls bad url", t, func() { + server := createTestServer(TestResponse{[]byte("Some string"), http.StatusOK}) + defer server.Close() + url := "💩%$&TR" - remote := Remote{config: &Config{URL: url}} - result, err := remote.Fetch(target, from, until, false) - So(result, ShouldBeEmpty) - So(err.Error(), ShouldResemble, "parse \"💩%$&TR\": invalid URL escape \"%$&\"") + + for _, config := range testConfigs { + config.URL = url + + remote := Remote{ + client: server.Client(), + config: config, + retrier: retrier, + requestBackoffFactory: retries.NewExponentialBackoffFactory(config.Retries), + } + + result, err := remote.Fetch(target, from, until, false) + So(result, ShouldBeEmpty) + So(err.Error(), ShouldResemble, "parse \"💩%$&TR\": invalid URL escape \"%$&\"") + So(err, ShouldHaveSameTypeAs, ErrRemoteTriggerResponse{}) + } + }) + + Convey("Given server returns Remote Unavailable responses permanently", t, func() { + for statusCode := range remoteUnavailableStatusCodes { + server := createTestServer(TestResponse{validBody, statusCode}) + + Convey(fmt.Sprintf( + "request failed with %d response status code and remote is unavailable", statusCode, + ), func() { + for _, config := range testConfigs { + config.URL = server.URL + remote := Remote{ + client: server.Client(), + config: config, + retrier: retrier, + requestBackoffFactory: retries.NewExponentialBackoffFactory(config.Retries), + } + + result, err := remote.Fetch(target, from, until, false) + So(err, ShouldResemble, ErrRemoteUnavailable{ + InternalError: fmt.Errorf( + "the remote server is not available. Response status %d: %s", statusCode, string(validBody), + ), Target: target, + }) + So(result, ShouldBeNil) + } + }) + + server.Close() + } + }) + + Convey("Given server returns Remote Unavailable response temporary", t, func() { + for statusCode := range remoteUnavailableStatusCodes { + Convey(fmt.Sprintf( + "the remote is available with retry after %d response", statusCode, + ), func() { + for _, config := range testConfigs { + server := createTestServer( + TestResponse{validBody, statusCode}, + TestResponse{validBody, http.StatusOK}, + ) + config.URL = server.URL + + remote := Remote{ + client: server.Client(), + config: config, + retrier: retrier, + requestBackoffFactory: retries.NewExponentialBackoffFactory(config.Retries), + } + + result, err := remote.Fetch(target, from, until, false) + So(err, ShouldBeNil) + So(result, ShouldNotBeNil) + + metricsData := result.GetMetricsData() + So(len(metricsData), ShouldEqual, 1) + So(metricsData[0].Name, ShouldEqual, "t1") + + server.Close() + } + }) + } }) } diff --git a/metric_source/remote/request.go b/metric_source/remote/request.go index 686b1766b..d90884ba4 100644 --- a/metric_source/remote/request.go +++ b/metric_source/remote/request.go @@ -2,10 +2,14 @@ package remote import ( "context" + "errors" "fmt" "io" "net/http" "strconv" + "time" + + "github.com/cenkalti/backoff/v4" ) func (remote *Remote) prepareRequest(from, until int64, target string) (*http.Request, error) { @@ -13,39 +17,133 @@ func (remote *Remote) prepareRequest(from, until int64, target string) (*http.Re if err != nil { return nil, err } + q := req.URL.Query() q.Add("format", "json") q.Add("from", strconv.FormatInt(from, 10)) q.Add("target", target) q.Add("until", strconv.FormatInt(until, 10)) req.URL.RawQuery = q.Encode() + if remote.config.User != "" && remote.config.Password != "" { req.SetBasicAuth(remote.config.User, remote.config.Password) } + return req, nil } -func (remote *Remote) makeRequest(req *http.Request) ([]byte, error) { - var body []byte +func (remote *Remote) makeRequest(req *http.Request, timeout time.Duration, backoffPolicy backoff.BackOff) ([]byte, error) { + return remote.retrier.Retry( + requestToRemoteGraphite{ + client: remote.client, + request: req, + requestTimeout: timeout, + }, + backoffPolicy) +} - resp, err := remote.client.Do(req) - if resp != nil { - defer resp.Body.Close() - } +// requestToRemoteGraphite implements retries.RetryableOperation. +type requestToRemoteGraphite struct { + client *http.Client + request *http.Request + requestTimeout time.Duration +} + +// DoRetryableOperation is a one attempt of performing request to remote graphite. +func (r requestToRemoteGraphite) DoRetryableOperation() ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), r.requestTimeout) + defer cancel() + req := r.request.WithContext(ctx) + + resp, err := r.client.Do(req) if err != nil { - return body, fmt.Errorf("The remote server is not available or the response was reset by timeout. "+ //nolint - "TTL: %s, PATH: %s, ERROR: %v ", remote.client.Timeout.String(), req.URL.RawPath, err) + return nil, errRemoteUnavailable{ + internalErr: fmt.Errorf( + "the remote server is not available or the response was reset by timeout. Url: %s, Error: %w ", + req.URL.String(), + err), + } } + defer resp.Body.Close() - body, err = io.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { - return body, err + return body, errRemoteUnavailable{internalErr: err} } - if resp.StatusCode != http.StatusOK { - return body, fmt.Errorf("bad response status %d: %s", resp.StatusCode, string(body)) + if isRemoteUnavailableStatusCode(resp.StatusCode) { + return body, errRemoteUnavailable{ + internalErr: fmt.Errorf( + "the remote server is not available. Response status %d: %s", + resp.StatusCode, string(body)), + } + } else if resp.StatusCode != http.StatusOK { + return body, backoff.Permanent( + errInvalidRequest{ + internalErr: fmt.Errorf("bad response status %d: %s", resp.StatusCode, string(body)), + }) } return body, nil } + +type errInvalidRequest struct { + internalErr error +} + +func (err errInvalidRequest) Error() string { + return err.internalErr.Error() +} + +type errRemoteUnavailable struct { + internalErr error +} + +func (err errRemoteUnavailable) Error() string { + return err.internalErr.Error() +} + +func isRemoteUnavailable(err error) bool { + var errUnavailable ErrRemoteUnavailable + return errors.As(err, &errUnavailable) +} + +func internalErrToPublicErr(err error, target string) error { + if err == nil { + return nil + } + + var invalidReqErr errInvalidRequest + if errors.As(err, &invalidReqErr) { + return ErrRemoteTriggerResponse{ + InternalError: invalidReqErr.internalErr, + Target: target, + } + } + + var errUnavailable errRemoteUnavailable + if errors.As(err, &errUnavailable) { + return ErrRemoteUnavailable{ + InternalError: errUnavailable.internalErr, + Target: target, + } + } + + return ErrRemoteTriggerResponse{ + InternalError: err, + Target: target, + } +} + +var remoteUnavailableStatusCodes = map[int]struct{}{ + http.StatusUnauthorized: {}, + http.StatusBadGateway: {}, + http.StatusServiceUnavailable: {}, + http.StatusGatewayTimeout: {}, +} + +func isRemoteUnavailableStatusCode(statusCode int) bool { + _, isUnavailableCode := remoteUnavailableStatusCodes[statusCode] + return isUnavailableCode +} diff --git a/metric_source/remote/request_test.go b/metric_source/remote/request_test.go index 340a8cef9..aa98b15fa 100644 --- a/metric_source/remote/request_test.go +++ b/metric_source/remote/request_test.go @@ -5,7 +5,10 @@ import ( "net/http" "net/http/httptest" "testing" + "time" + "github.com/cenkalti/backoff/v4" + "github.com/moira-alert/moira/metric_source/retries" . "github.com/smartystreets/goconvey/convey" ) @@ -44,38 +47,268 @@ func TestPrepareRequest(t *testing.T) { }) } -func TestMakeRequest(t *testing.T) { +func Test_requestToRemoteGraphite_DoRetryableOperation(t *testing.T) { var from int64 = 300 var until int64 = 500 target := "foo.bar" body := []byte("Some string") + testTimeout := time.Millisecond * 10 + Convey("Client returns status OK", t, func() { server := createServer(body, http.StatusOK) + defer server.Close() + remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} request, _ := remote.prepareRequest(from, until, target) - actual, err := remote.makeRequest(request) + + retryableOp := requestToRemoteGraphite{ + request: request, + client: remote.client, + requestTimeout: testTimeout, + } + + actual, err := retryableOp.DoRetryableOperation() + So(err, ShouldBeNil) So(actual, ShouldResemble, body) }) Convey("Client returns status InternalServerError", t, func() { server := createServer(body, http.StatusInternalServerError) + defer server.Close() + remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} request, _ := remote.prepareRequest(from, until, target) - actual, err := remote.makeRequest(request) - So(err, ShouldResemble, fmt.Errorf("bad response status %d: %s", http.StatusInternalServerError, string(body))) + + retryableOp := requestToRemoteGraphite{ + request: request, + client: remote.client, + requestTimeout: testTimeout, + } + + actual, err := retryableOp.DoRetryableOperation() + + So(err, ShouldResemble, backoff.Permanent(errInvalidRequest{ + internalErr: fmt.Errorf("bad response status %d: %s", http.StatusInternalServerError, string(body)), + })) So(actual, ShouldResemble, body) }) Convey("Client calls bad url", t, func() { server := createServer(body, http.StatusOK) - remote := Remote{client: server.Client(), config: &Config{URL: "http://bad/"}} + defer server.Close() + + client := server.Client() + remote := Remote{client: client, config: &Config{URL: "http://bad/"}} request, _ := remote.prepareRequest(from, until, target) - actual, err := remote.makeRequest(request) - So(err, ShouldNotBeEmpty) + + retryableOp := requestToRemoteGraphite{ + request: request, + client: remote.client, + requestTimeout: testTimeout, + } + + actual, err := retryableOp.DoRetryableOperation() + + So(err, ShouldHaveSameTypeAs, errRemoteUnavailable{}) So(actual, ShouldBeEmpty) }) + + Convey("Client returns status Remote Unavailable status codes", t, func() { + for statusCode := range remoteUnavailableStatusCodes { + server := createServer(body, statusCode) + remote := Remote{client: server.Client(), config: &Config{URL: server.URL}} + request, _ := remote.prepareRequest(from, until, target) + + retryableOp := requestToRemoteGraphite{ + request: request, + client: remote.client, + requestTimeout: testTimeout, + } + + actual, err := retryableOp.DoRetryableOperation() + + So(err, ShouldResemble, errRemoteUnavailable{ + internalErr: fmt.Errorf( + "the remote server is not available. Response status %d: %s", statusCode, string(body)), + }) + So(actual, ShouldResemble, body) + + server.Close() + } + }) +} + +func TestMakeRequestWithRetries(t *testing.T) { + var from int64 = 300 + var until int64 = 500 + target := "foo.bar" + body := []byte("Some string") + + retrier := retries.NewStandardRetrier[[]byte]() + + Convey("Given server returns OK response", t, func() { + server := createTestServer(TestResponse{body, http.StatusOK}) + defer server.Close() + + Convey("request is successful", func() { + remote := Remote{ + client: server.Client(), + retrier: retrier, + } + + for _, config := range testConfigs { + config.URL = server.URL + remote.config = config + request, _ := remote.prepareRequest(from, until, target) + backoffPolicy := retries.NewExponentialBackoffFactory(config.Retries).NewBackOff() + + actual, err := remote.makeRequest( + request, + remote.config.Timeout, + backoffPolicy, + ) + + So(err, ShouldBeNil) + So(actual, ShouldResemble, body) + } + }) + }) + + Convey("Given server returns 500 response", t, func() { + server := createTestServer(TestResponse{body, http.StatusInternalServerError}) + defer server.Close() + + expectedErr := errInvalidRequest{ + internalErr: fmt.Errorf("bad response status %d: %s", http.StatusInternalServerError, string(body)), + } + + Convey("request failed with 500 response and remote is available", func() { + remote := Remote{ + client: server.Client(), + retrier: retrier, + } + + for _, config := range testConfigs { + config.URL = server.URL + remote.config = config + request, _ := remote.prepareRequest(from, until, target) + backoffPolicy := retries.NewExponentialBackoffFactory(config.Retries).NewBackOff() + + actual, err := remote.makeRequest( + request, + remote.config.Timeout, + backoffPolicy, + ) + + So(err, ShouldResemble, expectedErr) + So(actual, ShouldResemble, body) + } + }) + }) + + Convey("Given client calls bad url", t, func() { + server := createTestServer(TestResponse{body, http.StatusOK}) + defer server.Close() + + Convey("request failed and remote is unavailable", func() { + remote := Remote{ + client: server.Client(), + retrier: retrier, + } + + for _, config := range testConfigs { + config.URL = "http://bad/" + remote.config = config + + request, _ := remote.prepareRequest(from, until, target) + backoffPolicy := retries.NewExponentialBackoffFactory(config.Retries).NewBackOff() + + actual, err := remote.makeRequest( + request, + remote.config.Timeout, + backoffPolicy, + ) + + So(err, ShouldHaveSameTypeAs, errRemoteUnavailable{}) + So(actual, ShouldBeEmpty) + } + }) + }) + + Convey("Given server returns Remote Unavailable responses permanently", t, func() { + for statusCode := range remoteUnavailableStatusCodes { + server := createTestServer(TestResponse{body, statusCode}) + + Convey(fmt.Sprintf( + "request failed with %d response status code and remote is unavailable", statusCode, + ), func() { + remote := Remote{ + client: server.Client(), + retrier: retrier, + } + + for _, config := range testConfigs { + config.URL = server.URL + remote.config = config + + request, _ := remote.prepareRequest(from, until, target) + backoffPolicy := retries.NewExponentialBackoffFactory(config.Retries).NewBackOff() + + actual, err := remote.makeRequest( + request, + remote.config.Timeout, + backoffPolicy, + ) + + So(err, ShouldResemble, errRemoteUnavailable{ + internalErr: fmt.Errorf( + "the remote server is not available. Response status %d: %s", statusCode, string(body), + ), + }) + So(actual, ShouldResemble, body) + } + }) + + server.Close() + } + }) + + Convey("Given server returns Remote Unavailable response temporary", t, func() { + for statusCode := range remoteUnavailableStatusCodes { + Convey(fmt.Sprintf( + "request is successful with retry after %d response and remote is available", statusCode, + ), func() { + for _, config := range testConfigs { + server := createTestServer( + TestResponse{body, statusCode}, + TestResponse{body, http.StatusOK}, + ) + config.URL = server.URL + remote := Remote{ + client: server.Client(), + config: config, + retrier: retrier, + } + + request, _ := remote.prepareRequest(from, until, target) + backoffPolicy := retries.NewExponentialBackoffFactory(config.Retries).NewBackOff() + + actual, err := remote.makeRequest( + request, + remote.config.Timeout, + backoffPolicy, + ) + + So(err, ShouldBeNil) + So(actual, ShouldResemble, body) + + server.Close() + } + }) + } + }) } func createServer(body []byte, statusCode int) *httptest.Server { @@ -84,3 +317,35 @@ func createServer(body []byte, statusCode int) *httptest.Server { rw.Write(body) //nolint })) } + +func createTestServer(testResponses ...TestResponse) *httptest.Server { + responseWriter := NewTestResponseWriter(testResponses) + return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + response := responseWriter.GetResponse() + rw.WriteHeader(response.statusCode) + rw.Write(response.body) //nolint + })) +} + +type TestResponse struct { + body []byte + statusCode int +} + +type TestResponseWriter struct { + responses []TestResponse + count int +} + +func NewTestResponseWriter(testResponses []TestResponse) *TestResponseWriter { + responseWriter := new(TestResponseWriter) + responseWriter.responses = testResponses + responseWriter.count = 0 + return responseWriter +} + +func (responseWriter *TestResponseWriter) GetResponse() TestResponse { + response := responseWriter.responses[responseWriter.count%len(responseWriter.responses)] + responseWriter.count++ + return response +} diff --git a/metric_source/remote/response.go b/metric_source/remote/response.go index 0af2fbaa0..8cc296948 100644 --- a/metric_source/remote/response.go +++ b/metric_source/remote/response.go @@ -23,6 +23,7 @@ func convertResponse(metricsData []metricSource.MetricData, allowRealTimeAlertin metricData.Values = metricData.Values[:len(metricData.Values)-1] result = append(result, metricData) } + return FetchResult{MetricsData: result} } @@ -32,12 +33,14 @@ func decodeBody(body []byte) ([]metricSource.MetricData, error) { if err != nil { return nil, err } + res := make([]metricSource.MetricData, 0, len(tmp)) for _, m := range tmp { var stepTime int64 = 60 if len(m.DataPoints) > 1 { stepTime = int64(*m.DataPoints[1][1] - *m.DataPoints[0][1]) } + metricData := metricSource.MetricData{ Name: m.Target, StartTime: int64(*m.DataPoints[0][1]), @@ -45,6 +48,7 @@ func decodeBody(body []byte) ([]metricSource.MetricData, error) { StepTime: stepTime, Values: make([]float64, len(m.DataPoints)), } + for i, v := range m.DataPoints { if v[0] == nil { metricData.Values[i] = math.NaN() @@ -52,7 +56,9 @@ func decodeBody(body []byte) ([]metricSource.MetricData, error) { metricData.Values[i] = *v[0] } } + res = append(res, metricData) } + return res, nil } diff --git a/metric_source/retries/backoff_factory.go b/metric_source/retries/backoff_factory.go new file mode 100644 index 000000000..718142161 --- /dev/null +++ b/metric_source/retries/backoff_factory.go @@ -0,0 +1,37 @@ +package retries + +import "github.com/cenkalti/backoff/v4" + +// BackoffFactory is used for creating backoff. It is expected that all backoffs created with one factory instance +// have the same behaviour. +type BackoffFactory interface { + NewBackOff() backoff.BackOff +} + +// ExponentialBackoffFactory is a factory that generates exponential backoffs based on given config. +type ExponentialBackoffFactory struct { + config Config +} + +// NewExponentialBackoffFactory creates new BackoffFactory which will generate exponential backoffs. +func NewExponentialBackoffFactory(config Config) BackoffFactory { + return ExponentialBackoffFactory{ + config: config, + } +} + +// NewBackOff creates new backoff. +func (factory ExponentialBackoffFactory) NewBackOff() backoff.BackOff { + backoffPolicy := backoff.NewExponentialBackOff( + backoff.WithInitialInterval(factory.config.InitialInterval), + backoff.WithRandomizationFactor(factory.config.RandomizationFactor), + backoff.WithMultiplier(factory.config.Multiplier), + backoff.WithMaxInterval(factory.config.MaxInterval), + backoff.WithMaxElapsedTime(factory.config.MaxElapsedTime)) + + if factory.config.MaxRetriesCount > 0 { + return backoff.WithMaxRetries(backoffPolicy, factory.config.MaxRetriesCount) + } + + return backoffPolicy +} diff --git a/metric_source/retries/backoff_factory_test.go b/metric_source/retries/backoff_factory_test.go new file mode 100644 index 000000000..50d12cef1 --- /dev/null +++ b/metric_source/retries/backoff_factory_test.go @@ -0,0 +1,169 @@ +package retries + +import ( + "sync" + "testing" + "time" + + "github.com/cenkalti/backoff/v4" + + . "github.com/smartystreets/goconvey/convey" +) + +const ( + testInitialInterval = time.Millisecond * 5 + testRandomizationFactor = 0.0 + testMultiplier = 2.0 + testMaxInterval = time.Millisecond * 40 +) + +func TestExponentialBackoffFactory(t *testing.T) { + Convey("Test ExponentialBackoffFactory", t, func() { + conf := Config{ + InitialInterval: testInitialInterval, + RandomizationFactor: testRandomizationFactor, + Multiplier: testMultiplier, + MaxInterval: testMaxInterval, + } + + Convey("with MaxRetriesCount != 0 and MaxElapsedTime = 0", func() { + Convey("with retry interval always lower then config.MaxInterval", func() { + conf.MaxRetriesCount = 3 + defer func() { + conf.MaxRetriesCount = 0 + }() + + expectedBackoffs := []time.Duration{ + testInitialInterval, + testInitialInterval * testMultiplier, + testInitialInterval * 4.0, + backoff.Stop, + backoff.Stop, + backoff.Stop, + } + + factory := NewExponentialBackoffFactory(conf) + + b := factory.NewBackOff() + + for i := range expectedBackoffs { + So(b.NextBackOff(), ShouldEqual, expectedBackoffs[i]) + } + }) + + Convey("with retry interval becomes config.MaxInterval", func() { + conf.MaxRetriesCount = 6 + defer func() { + conf.MaxRetriesCount = 0 + }() + + expectedBackoffs := []time.Duration{ + testInitialInterval, + testInitialInterval * testMultiplier, + testInitialInterval * 4.0, + testMaxInterval, + testMaxInterval, + testMaxInterval, + backoff.Stop, + backoff.Stop, + backoff.Stop, + } + + factory := NewExponentialBackoffFactory(conf) + + b := factory.NewBackOff() + + for i := range expectedBackoffs { + So(b.NextBackOff(), ShouldEqual, expectedBackoffs[i]) + } + }) + }) + + Convey("with MaxRetriesCount = 0 and MaxElapsedTime != 0", func() { + conf.MaxElapsedTime = time.Second + defer func() { + conf.MaxElapsedTime = 0 + }() + + once := sync.Once{} + + expectedBackoffs := []time.Duration{ + testInitialInterval, + backoff.Stop, + backoff.Stop, + backoff.Stop, + } + + factory := NewExponentialBackoffFactory(conf) + + b := factory.NewBackOff() + + for i := range expectedBackoffs { + So(b.NextBackOff(), ShouldEqual, expectedBackoffs[i]) + once.Do(func() { + time.Sleep(conf.MaxElapsedTime) + }) + } + }) + + Convey("with MaxRetriesCount != 0 and MaxElapsedTime != 0", func() { + Convey("MaxRetriesCount performed retries before MaxElapsedTime passed", func() { + conf.MaxElapsedTime = time.Second + conf.MaxRetriesCount = 6 + defer func() { + conf.MaxElapsedTime = 0 + conf.MaxRetriesCount = 0 + }() + + expectedBackoffs := []time.Duration{ + testInitialInterval, + testInitialInterval * testMultiplier, + testInitialInterval * 4.0, + testMaxInterval, + testMaxInterval, + testMaxInterval, + backoff.Stop, + backoff.Stop, + backoff.Stop, + } + + factory := NewExponentialBackoffFactory(conf) + + b := factory.NewBackOff() + + for i := range expectedBackoffs { + So(b.NextBackOff(), ShouldEqual, expectedBackoffs[i]) + } + }) + + Convey("MaxElapsedTime passed before MaxRetriesCount performed", func() { + conf.MaxElapsedTime = time.Second + conf.MaxRetriesCount = 6 + defer func() { + conf.MaxElapsedTime = 0 + conf.MaxRetriesCount = 0 + }() + + expectedBackoffs := []time.Duration{ + testInitialInterval, + backoff.Stop, + backoff.Stop, + backoff.Stop, + } + + once := sync.Once{} + + factory := NewExponentialBackoffFactory(conf) + + b := factory.NewBackOff() + + for i := range expectedBackoffs { + So(b.NextBackOff(), ShouldEqual, expectedBackoffs[i]) + once.Do(func() { + time.Sleep(conf.MaxElapsedTime) + }) + } + }) + }) + }) +} diff --git a/metric_source/retries/config.go b/metric_source/retries/config.go new file mode 100644 index 000000000..a7a72cc66 --- /dev/null +++ b/metric_source/retries/config.go @@ -0,0 +1,24 @@ +package retries + +import ( + "time" +) + +// Config for exponential backoff retries. +type Config struct { + // InitialInterval between requests. + InitialInterval time.Duration `validate:"required,gt=0s"` + // RandomizationFactor is used in exponential backoff to add some randomization + // when calculating next interval between requests. + // It will be used in multiplication like: + // RandomizedInterval = RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) + RandomizationFactor float64 + // Each new RetryInterval will be multiplied on Multiplier. + Multiplier float64 + // MaxInterval is the cap for RetryInterval. Note that it doesn't cap the RandomizedInterval. + MaxInterval time.Duration `validate:"required,gt=0s"` + // MaxElapsedTime caps the time passed from first try. If time passed is greater than MaxElapsedTime than stop retrying. + MaxElapsedTime time.Duration `validate:"required_if=MaxRetriesCount 0"` + // MaxRetriesCount is the amount of allowed retries. So at most MaxRetriesCount will be performed. + MaxRetriesCount uint64 `validate:"required_if=MaxElapsedTime 0"` +} diff --git a/metric_source/retries/config_test.go b/metric_source/retries/config_test.go new file mode 100644 index 000000000..976630e2f --- /dev/null +++ b/metric_source/retries/config_test.go @@ -0,0 +1,95 @@ +package retries + +import ( + "errors" + "fmt" + "testing" + "time" + + "github.com/go-playground/validator/v10" + "github.com/moira-alert/moira" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestConfigWithValidateStruct(t *testing.T) { + Convey("Test validating retries config", t, func() { + type testcase struct { + caseDesc string + conf Config + errIsNil bool + } + + var ( + testRetriesCount uint64 = 10 + testMaxElapsedTIme = time.Second * 10 + validatorErr = validator.ValidationErrors{} + ) + + cases := []testcase{ + { + caseDesc: "with empty config", + conf: Config{}, + errIsNil: false, + }, + { + caseDesc: "with only InitialInterval set", + conf: Config{ + InitialInterval: testInitialInterval, + }, + errIsNil: false, + }, + { + caseDesc: "with only MaxInterval set", + conf: Config{ + MaxInterval: testMaxInterval, + }, + errIsNil: false, + }, + { + caseDesc: "with only MaxRetriesCount set", + conf: Config{ + MaxRetriesCount: testRetriesCount, + }, + errIsNil: false, + }, + { + caseDesc: "with only MaxElapsedTime set", + conf: Config{ + MaxElapsedTime: testMaxElapsedTIme, + }, + errIsNil: false, + }, + { + caseDesc: "with valid config but only MaxElapsedTime set", + conf: Config{ + InitialInterval: testInitialInterval, + MaxInterval: testMaxInterval, + MaxElapsedTime: testMaxElapsedTIme, + }, + errIsNil: true, + }, + { + caseDesc: "with valid config but only MaxRetriesCount set", + conf: Config{ + InitialInterval: testInitialInterval, + MaxInterval: testMaxInterval, + MaxRetriesCount: testRetriesCount, + }, + errIsNil: true, + }, + } + + for i := range cases { + Convey(fmt.Sprintf("Case %d: %s", i+1, cases[i].caseDesc), func() { + err := moira.ValidateStruct(cases[i].conf) + + if cases[i].errIsNil { + So(err, ShouldBeNil) + } else { + So(errors.As(err, &validatorErr), ShouldBeTrue) + } + }) + } + }) +} diff --git a/metric_source/retries/retrier.go b/metric_source/retries/retrier.go new file mode 100644 index 000000000..24a8b38c7 --- /dev/null +++ b/metric_source/retries/retrier.go @@ -0,0 +1,23 @@ +package retries + +import ( + "github.com/cenkalti/backoff/v4" +) + +// Retrier retries the given operation with given backoff. +type Retrier[T any] interface { + // Retry the given operation until the op succeeds or op returns backoff.PermanentError or backoffPolicy returns backoff.Stop. + Retry(op RetryableOperation[T], backoffPolicy backoff.BackOff) (T, error) +} + +type standardRetrier[T any] struct{} + +// NewStandardRetrier returns standard retrier. +func NewStandardRetrier[T any]() Retrier[T] { + return standardRetrier[T]{} +} + +// Retry the given operation until the op succeeds or op returns backoff.PermanentError or backoffPolicy returns backoff.Stop. +func (r standardRetrier[T]) Retry(op RetryableOperation[T], backoffPolicy backoff.BackOff) (T, error) { + return backoff.RetryWithData[T](op.DoRetryableOperation, backoffPolicy) +} diff --git a/metric_source/retries/retrier_test.go b/metric_source/retries/retrier_test.go new file mode 100644 index 000000000..360771d86 --- /dev/null +++ b/metric_source/retries/retrier_test.go @@ -0,0 +1,167 @@ +package retries + +import ( + "errors" + "testing" + + "github.com/cenkalti/backoff/v4" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestStandardRetrier(t *testing.T) { + var ( + maxRetriesCount uint64 = 6 + testErr = errors.New("some test err") + errInsidePermanent = errors.New("test err inside permanent") + permanentTestErr = backoff.Permanent(errInsidePermanent) + ) + + conf := Config{ + InitialInterval: testInitialInterval, + RandomizationFactor: testRandomizationFactor, + Multiplier: testMultiplier, + MaxInterval: testMaxInterval, + MaxRetriesCount: maxRetriesCount, + } + + retrier := NewStandardRetrier[int]() + + Convey("Test retrier", t, func() { + Convey("with successful RetryableOperation", func() { + retPairs := []retPair[int]{ + { + returnValue: 25, + err: nil, + }, + { + returnValue: 26, + err: nil, + }, + } + expectedCalls := 1 + + stub := newStubRetryableOperation[int](retPairs) + + backoffPolicy := NewExponentialBackoffFactory(conf).NewBackOff() + + gotRes, gotErr := retrier.Retry(stub, backoffPolicy) + + So(gotRes, ShouldEqual, retPairs[0].returnValue) + So(gotErr, ShouldBeNil) + So(stub.calls, ShouldEqual, expectedCalls) + }) + + Convey("with successful RetryableOperation after some retries", func() { + retPairs := []retPair[int]{ + { + returnValue: 25, + err: testErr, + }, + { + returnValue: 10, + err: testErr, + }, + { + returnValue: 42, + err: nil, + }, + { + returnValue: 41, + err: nil, + }, + } + expectedCalls := 3 + + stub := newStubRetryableOperation[int](retPairs) + + backoffPolicy := NewExponentialBackoffFactory(conf).NewBackOff() + + gotRes, gotErr := retrier.Retry(stub, backoffPolicy) + + So(gotRes, ShouldEqual, retPairs[2].returnValue) + So(gotErr, ShouldBeNil) + So(stub.calls, ShouldEqual, expectedCalls) + }) + + Convey("with permanent error from RetryableOperation after some retries", func() { + retPairs := []retPair[int]{ + { + returnValue: 25, + err: testErr, + }, + { + returnValue: 10, + err: permanentTestErr, + }, + { + returnValue: 42, + err: nil, + }, + { + returnValue: 41, + err: nil, + }, + } + expectedCalls := 2 + + stub := newStubRetryableOperation[int](retPairs) + + backoffPolicy := NewExponentialBackoffFactory(conf).NewBackOff() + + gotRes, gotErr := retrier.Retry(stub, backoffPolicy) + + So(gotRes, ShouldEqual, retPairs[1].returnValue) + So(gotErr, ShouldResemble, errInsidePermanent) + So(stub.calls, ShouldEqual, expectedCalls) + }) + + Convey("with RetryableOperation failed on each retry", func() { + expectedCalls := conf.MaxRetriesCount + 1 + + stub := newStubRetryableOperation[int](nil) + + backoffPolicy := NewExponentialBackoffFactory(conf).NewBackOff() + + gotRes, gotErr := retrier.Retry(stub, backoffPolicy) + + So(gotRes, ShouldEqual, 0) + So(gotErr, ShouldResemble, errStubValuesEnded) + So(stub.calls, ShouldEqual, expectedCalls) + }) + }) +} + +type retPair[T any] struct { + returnValue T + err error +} + +type stubRetryableOperation[T any] struct { + retPairs []retPair[T] + idx int + calls int +} + +func newStubRetryableOperation[T any](pairs []retPair[T]) *stubRetryableOperation[T] { + return &stubRetryableOperation[T]{ + retPairs: pairs, + idx: 0, + calls: 0, + } +} + +var errStubValuesEnded = errors.New("prepared return values and errors for stub ended") + +func (stub *stubRetryableOperation[T]) DoRetryableOperation() (T, error) { + stub.calls += 1 + + if stub.idx >= len(stub.retPairs) { + return *new(T), errStubValuesEnded + } + + res := stub.retPairs[stub.idx] + stub.idx += 1 + + return res.returnValue, res.err +} diff --git a/metric_source/retries/retryable_operation.go b/metric_source/retries/retryable_operation.go new file mode 100644 index 000000000..dfab3d17e --- /dev/null +++ b/metric_source/retries/retryable_operation.go @@ -0,0 +1,7 @@ +package retries + +// RetryableOperation is an action that can be retried after some time interval. +// If there is an error in DoRetryableOperation that should not be retried, wrap the error with backoff.PermanentError. +type RetryableOperation[T any] interface { + DoRetryableOperation() (T, error) +} From a16c39020e224ec7b879fd3d24c9ceaf7d7976aa Mon Sep 17 00:00:00 2001 From: Aleksandr Matsko <90016901+AleksandrMatsko@users.noreply.github.com> Date: Wed, 6 Nov 2024 20:46:52 +0700 Subject: [PATCH 36/36] fix: trigger schedule fill (#1116) --- api/dto/triggers.go | 55 ++++++++++ api/dto/triggers_test.go | 196 +++++++++++++++++++++++++++++++++++ api/handler/triggers_test.go | 32 ++++-- datatypes.go | 48 ++++++--- 4 files changed, 308 insertions(+), 23 deletions(-) diff --git a/api/dto/triggers.go b/api/dto/triggers.go index e22e28990..46aa866b4 100644 --- a/api/dto/triggers.go +++ b/api/dto/triggers.go @@ -7,6 +7,7 @@ import ( "net/http" "regexp" "strconv" + "strings" "time" "unicode/utf8" @@ -40,6 +41,9 @@ var ( // errAsteriskPatternNotAllowed is returned then one of Trigger.Patterns contain only "*". errAsteriskPatternNotAllowed = errors.New("pattern \"*\" is not allowed to use") + + // errNoAllowedDays is returned then all days disabled in moira.ScheduleData. + errNoAllowedDays = errors.New("no allowed days in trigger schedule") ) // TODO(litleleprikon): Remove after https://github.com/moira-alert/moira/issues/550 will be resolved. @@ -257,6 +261,13 @@ func (trigger *Trigger) Bind(request *http.Request) error { if trigger.Schedule == nil { trigger.Schedule = moira.NewDefaultScheduleData() + } else { + correctedSchedule, err := checkScheduleFilling(trigger.Schedule) + if err != nil { + return api.ErrInvalidRequestContent{ValidationError: err} + } + + trigger.Schedule = correctedSchedule } middleware.SetTimeSeriesNames(request, metricsDataNames) @@ -278,6 +289,50 @@ func getDateTime(timestamp *int64) *time.Time { return &datetime } +// checkScheduleFilling ensures that all days are included to schedule, ordered from monday to sunday +// and have proper names (one of [Mon, Tue, Wed, Thu, Fri, Sat Sun]). +func checkScheduleFilling(gotSchedule *moira.ScheduleData) (*moira.ScheduleData, error) { + newSchedule := moira.NewDefaultScheduleData() + + scheduleDaysMap := make(map[moira.DayName]bool, len(newSchedule.Days)) + for _, day := range newSchedule.Days { + scheduleDaysMap[day.Name] = false + } + + badDayNames := make([]string, 0) + for _, day := range gotSchedule.Days { + _, validDayName := scheduleDaysMap[day.Name] + if validDayName { + scheduleDaysMap[day.Name] = day.Enabled + } else { + badDayNames = append(badDayNames, string(day.Name)) + } + } + + if len(badDayNames) != 0 { + return nil, fmt.Errorf("bad day names in schedule: %s", strings.Join(badDayNames, ", ")) + } + + someDayEnabled := false + for i := range newSchedule.Days { + newSchedule.Days[i].Enabled = scheduleDaysMap[newSchedule.Days[i].Name] + + if newSchedule.Days[i].Enabled { + someDayEnabled = true + } + } + + if !someDayEnabled { + return nil, errNoAllowedDays + } + + newSchedule.TimezoneOffset = gotSchedule.TimezoneOffset + newSchedule.StartOffset = gotSchedule.StartOffset + newSchedule.EndOffset = gotSchedule.EndOffset + + return newSchedule, nil +} + func checkTTLSanity(trigger *Trigger, metricsSource metricSource.MetricSource) error { maximumAllowedTTL := metricsSource.GetMetricsTTLSeconds() diff --git a/api/dto/triggers_test.go b/api/dto/triggers_test.go index be944ab20..0d7a92a1d 100644 --- a/api/dto/triggers_test.go +++ b/api/dto/triggers_test.go @@ -3,7 +3,9 @@ package dto import ( "context" "fmt" + "math/rand" "net/http" + "slices" "strings" "testing" "time" @@ -442,3 +444,197 @@ func TestCreateTriggerModel(t *testing.T) { So(CreateTriggerModel(trigger), ShouldResemble, expTriggerModel) }) } + +func Test_checkScheduleFilling(t *testing.T) { + Convey("Testing checking schedule filling", t, func() { + defaultSchedule := moira.NewDefaultScheduleData() + + Convey("With valid schedule", func() { + givenSchedule := moira.NewDefaultScheduleData() + + givenSchedule.Days[len(givenSchedule.Days)-1].Enabled = false + givenSchedule.TimezoneOffset += 1 + givenSchedule.StartOffset += 1 + givenSchedule.EndOffset += 1 + + gotSchedule, err := checkScheduleFilling(givenSchedule) + + So(err, ShouldBeNil) + So(gotSchedule, ShouldResemble, givenSchedule) + }) + + Convey("With not all days, missing days filled with false", func() { + days := moira.GetFilledScheduleDataDays(true) + + givenSchedule := &moira.ScheduleData{ + Days: days[:len(days)-1], + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + days[len(days)-1].Enabled = false + + expectedSchedule := &moira.ScheduleData{ + Days: days, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + gotSchedule, err := checkScheduleFilling(givenSchedule) + + So(err, ShouldBeNil) + So(gotSchedule, ShouldResemble, expectedSchedule) + }) + + Convey("With some days repeated, there is no repeated days and missing days filled with false", func() { + days := moira.GetFilledScheduleDataDays(true) + + days[4].Name = moira.Monday + days[6].Name = moira.Monday + + givenSchedule := &moira.ScheduleData{ + Days: days, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + expectedDays := moira.GetFilledScheduleDataDays(true) + + expectedDays[4].Enabled = false + expectedDays[6].Enabled = false + + expectedSchedule := &moira.ScheduleData{ + Days: expectedDays, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + gotSchedule, err := checkScheduleFilling(givenSchedule) + + So(err, ShouldBeNil) + So(gotSchedule, ShouldResemble, expectedSchedule) + }) + + Convey("When days shuffled return ordered", func() { + days := moira.GetFilledScheduleDataDays(true) + + shuffledDays := shuffleArray(days) + + givenSchedule := &moira.ScheduleData{ + Days: shuffledDays, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + expectedSchedule := &moira.ScheduleData{ + Days: defaultSchedule.Days, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + gotSchedule, err := checkScheduleFilling(givenSchedule) + + So(err, ShouldBeNil) + So(gotSchedule, ShouldResemble, expectedSchedule) + }) + + Convey("When days shuffled and some are missed return ordered and filled missing", func() { + days := moira.GetFilledScheduleDataDays(true) + + shuffledDays := shuffleArray(days[:len(days)-2]) + + days[len(days)-1].Enabled = false + days[len(days)-2].Enabled = false + + givenSchedule := &moira.ScheduleData{ + Days: shuffledDays, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + expectedSchedule := &moira.ScheduleData{ + Days: days, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + gotSchedule, err := checkScheduleFilling(givenSchedule) + + So(err, ShouldBeNil) + So(gotSchedule, ShouldResemble, expectedSchedule) + }) + + Convey("With bad day names error returned", func() { + days := moira.GetFilledScheduleDataDays(true) + + var ( + badMondayName moira.DayName = "Monday" + badFridayName moira.DayName = "Friday" + ) + + days[0].Name = badMondayName + days[4].Name = badFridayName + + givenSchedule := &moira.ScheduleData{ + Days: days, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + gotSchedule, err := checkScheduleFilling(givenSchedule) + + So(err, ShouldResemble, fmt.Errorf("bad day names in schedule: %s, %s", badMondayName, badFridayName)) + So(gotSchedule, ShouldBeNil) + }) + + Convey("With no enabled days error returned", func() { + days := moira.GetFilledScheduleDataDays(false) + + givenSchedule := &moira.ScheduleData{ + Days: days, + TimezoneOffset: defaultSchedule.TimezoneOffset, + StartOffset: defaultSchedule.StartOffset, + EndOffset: defaultSchedule.EndOffset, + } + + gotSchedule, err := checkScheduleFilling(givenSchedule) + + So(err, ShouldResemble, errNoAllowedDays) + So(gotSchedule, ShouldBeNil) + }) + }) +} + +func shuffleArray[S interface{ ~[]E }, E any](slice S) S { + slice = slices.Clone(slice) + shuffledSlice := make(S, 0, len(slice)) + + for len(slice) > 0 { + randomIdx := rand.Intn(len(slice)) + shuffledSlice = append(shuffledSlice, slice[randomIdx]) + + switch { + case randomIdx == len(slice)-1: + slice = slice[:len(slice)-1] + case randomIdx == 0: + if len(slice) > 1 { + slice = slice[1:] + } else { + slice = nil + } + default: + slice = append(slice[:randomIdx], slice[randomIdx+1:]...) + } + } + + return shuffledSlice +} diff --git a/api/handler/triggers_test.go b/api/handler/triggers_test.go index 01c3420d3..275121c15 100644 --- a/api/handler/triggers_test.go +++ b/api/handler/triggers_test.go @@ -72,17 +72,24 @@ func TestGetTriggerFromRequest(t *testing.T) { ttlState := moira.TTLState("NODATA") triggerDTO := dto.Trigger{ TriggerModel: dto.TriggerModel{ - ID: "test_id", - Name: "Test trigger", - Desc: new(string), - Targets: []string{"foo.bar"}, - WarnValue: &triggerWarnValue, - ErrorValue: &triggerErrorValue, - TriggerType: "rising", - Tags: []string{"Normal", "DevOps", "DevOpsGraphite-duty"}, - TTLState: &ttlState, - TTL: 0, - Schedule: &moira.ScheduleData{}, + ID: "test_id", + Name: "Test trigger", + Desc: new(string), + Targets: []string{"foo.bar"}, + WarnValue: &triggerWarnValue, + ErrorValue: &triggerErrorValue, + TriggerType: "rising", + Tags: []string{"Normal", "DevOps", "DevOpsGraphite-duty"}, + TTLState: &ttlState, + TTL: 0, + Schedule: &moira.ScheduleData{ + Days: []moira.ScheduleDataDay{ + { + Name: "Mon", + Enabled: true, + }, + }, + }, Expression: "", Patterns: []string{}, TriggerSource: moira.GraphiteLocal, @@ -102,6 +109,9 @@ func TestGetTriggerFromRequest(t *testing.T) { request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "metricSourceProvider", sourceProvider)) request = request.WithContext(middleware.SetContextValueForTest(request.Context(), "limits", api.GetTestLimitsConfig())) + triggerDTO.Schedule.Days = moira.GetFilledScheduleDataDays(false) + triggerDTO.Schedule.Days[0].Enabled = true + Convey("It should be parsed successfully", func() { triggerDTO.TTL = moira.DefaultTTL diff --git a/datatypes.go b/datatypes.go index 00fcd77d2..a9f317563 100644 --- a/datatypes.go +++ b/datatypes.go @@ -246,7 +246,7 @@ type PlottingData struct { // ScheduleData represents subscription schedule. type ScheduleData struct { - Days []ScheduleDataDay `json:"days"` + Days []ScheduleDataDay `json:"days" validate:"dive"` TimezoneOffset int64 `json:"tzOffset" example:"-60" format:"int64"` StartOffset int64 `json:"startOffset" example:"0" format:"int64"` EndOffset int64 `json:"endOffset" example:"1439" format:"int64"` @@ -254,8 +254,40 @@ type ScheduleData struct { // ScheduleDataDay represents week day of schedule. type ScheduleDataDay struct { - Enabled bool `json:"enabled" example:"true"` - Name string `json:"name,omitempty" example:"Mon"` + Enabled bool `json:"enabled" example:"true"` + Name DayName `json:"name,omitempty" example:"Mon" validate:"oneof=Mon Tue Wed Thu Fri Sat Sun"` +} + +// DayName represents the day name used in ScheduleDataDay. +type DayName string + +// Constants for day names. +const ( + Monday DayName = "Mon" + Tuesday DayName = "Tue" + Wednesday DayName = "Wed" + Thursday DayName = "Thu" + Friday DayName = "Fri" + Saturday DayName = "Sat" + Sunday DayName = "Sun" +) + +// DaysOrder represents the order of days in week. +var DaysOrder = [...]DayName{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday} + +// GetFilledScheduleDataDays returns slice of ScheduleDataDay with ScheduleDataDay.Enabled field set from param. +// Days are ordered with DaysOrder. +func GetFilledScheduleDataDays(enabled bool) []ScheduleDataDay { + days := make([]ScheduleDataDay, 0, len(DaysOrder)) + + for _, d := range DaysOrder { + days = append(days, ScheduleDataDay{ + Name: d, + Enabled: enabled, + }) + } + + return days } const ( @@ -270,15 +302,7 @@ const ( // NewDefaultScheduleData returns the default ScheduleData which can be used in Trigger. func NewDefaultScheduleData() *ScheduleData { return &ScheduleData{ - Days: []ScheduleDataDay{ - {Name: "Mon", Enabled: true}, - {Name: "Tue", Enabled: true}, - {Name: "Wed", Enabled: true}, - {Name: "Thu", Enabled: true}, - {Name: "Fri", Enabled: true}, - {Name: "Sat", Enabled: true}, - {Name: "Sun", Enabled: true}, - }, + Days: GetFilledScheduleDataDays(true), TimezoneOffset: DefaultTimezoneOffset, StartOffset: DefaultStartOffset, EndOffset: DefaultEndOffset,