From 29441ce4b6930962fe82e1357af419966cd718ab Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 18 Nov 2022 13:49:38 +0100 Subject: [PATCH] feat: save last occurrence of failed events and fix instance filtering (#4710) * fix: filter failed events and current sequence correctly * fix failed events sorting column * feat: save last occurrence of failed event * fix failedEvents query and update sql statements * change sql statement to only create index * fix linting * fix linting * Update internal/query/failed_events.go Co-authored-by: Silvan * update job name on test-docs to match the one from test-code Co-authored-by: Silvan --- .github/workflows/test-docs.yml | 2 +- .golangci.yaml | 12 +++- cmd/setup/05.go | 25 ++++++++ cmd/setup/05.sql | 11 ++++ cmd/setup/config.go | 1 + cmd/setup/setup.go | 3 + .../failed-events.component.html | 7 +++ .../failed-events/failed-events.component.ts | 1 + console/src/assets/i18n/de.json | 1 + console/src/assets/i18n/en.json | 1 + console/src/assets/i18n/fr.json | 1 + console/src/assets/i18n/it.json | 1 + console/src/assets/i18n/zh.json | 1 + docs/docs/apis/proto/admin.md | 3 +- docs/docs/apis/proto/system.md | 2 + internal/admin/repository/administrator.go | 4 +- .../eventsourcing/eventstore/administrator.go | 8 +-- .../eventsourcing/view/error_event.go | 4 +- .../repository/eventsourcing/view/sequence.go | 4 +- internal/api/grpc/admin/failed_event.go | 19 ++++-- .../api/grpc/admin/failed_event_converter.go | 18 +++++- .../grpc/admin/failed_event_converter_test.go | 11 +++- internal/api/grpc/admin/view.go | 12 +++- internal/api/grpc/admin/view_converter.go | 11 ++-- internal/api/grpc/system/failed_event.go | 6 +- .../api/grpc/system/failed_event_converter.go | 13 ++++ .../system/failed_event_converter_test.go | 6 +- internal/api/grpc/system/view.go | 2 +- internal/api/grpc/system/view_converter.go | 11 ++-- .../eventstore/handler/crdb/db_mock_test.go | 4 +- .../eventstore/handler/crdb/failed_stmt.go | 8 +-- internal/eventstore/v1/spooler/spooler.go | 3 +- .../eventstore/v1/spooler/spooler_test.go | 7 +++ internal/query/current_sequence.go | 4 ++ internal/query/failed_events.go | 50 +++++++--------- internal/query/failed_events_test.go | 11 ++++ internal/view/model/failed_event.go | 4 ++ internal/view/repository/failed_events.go | 60 ++++++++++++++++--- internal/view/repository/sequence.go | 8 ++- proto/zitadel/admin.proto | 11 +++- proto/zitadel/system.proto | 10 ++++ 41 files changed, 291 insertions(+), 90 deletions(-) create mode 100644 cmd/setup/05.go create mode 100644 cmd/setup/05.sql diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml index 5574b835a..77bb66633 100644 --- a/.github/workflows/test-docs.yml +++ b/.github/workflows/test-docs.yml @@ -14,7 +14,7 @@ on: - '**.md' jobs: - Test: + Build-ZITADEL: runs-on: ubuntu-20.04 steps: - run: 'echo "No tests for docs are implemented, yet"' diff --git a/.golangci.yaml b/.golangci.yaml index a364105f6..a09e0ee52 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -33,7 +33,7 @@ linters: # Computes and checks the cognitive complexity of functions [fast: true, auto-fix: false] - gocognit # Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false] - - unused + - unused # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false] - errcheck # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. [fast: false, auto-fix: false] @@ -59,7 +59,7 @@ linters: # Finds naked returns in functions greater than a specified function length [fast: true, auto-fix: false] - nakedret # Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false, auto-fix: false] - - staticcheck + - staticcheck # Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false] - typecheck # Reports ill-formed or insufficient nolint directives [fast: true, auto-fix: false] @@ -294,4 +294,10 @@ linters: - wrapcheck # Whitespace Linter - Forces you to use empty lines! [fast: true, auto-fix: false] # FUTURE: improves code quality by allowing and blocking line breaks - - wsl \ No newline at end of file + - wsl +linters-settings: + gci: + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - prefix(github.com/zitadel/zitadel) # Custom section: groups all imports with the specified Prefix. diff --git a/cmd/setup/05.go b/cmd/setup/05.go new file mode 100644 index 000000000..7813ce8bb --- /dev/null +++ b/cmd/setup/05.go @@ -0,0 +1,25 @@ +package setup + +import ( + "context" + "database/sql" + _ "embed" +) + +var ( + //go:embed 05.sql + lastFailedStmts string +) + +type LastFailed struct { + dbClient *sql.DB +} + +func (mig *LastFailed) Execute(ctx context.Context) error { + _, err := mig.dbClient.ExecContext(ctx, lastFailedStmts) + return err +} + +func (mig *LastFailed) String() string { + return "05_last_failed" +} diff --git a/cmd/setup/05.sql b/cmd/setup/05.sql new file mode 100644 index 000000000..c30f74081 --- /dev/null +++ b/cmd/setup/05.sql @@ -0,0 +1,11 @@ +CREATE INDEX instance_id_idx ON adminapi.current_sequences (instance_id); +CREATE INDEX instance_id_idx ON auth.current_sequences (instance_id); +CREATE INDEX instance_id_idx ON projections.current_sequences (instance_id); + +CREATE INDEX instance_id_idx ON adminapi.failed_events (instance_id); +CREATE INDEX instance_id_idx ON auth.failed_events (instance_id); +CREATE INDEX instance_id_idx ON projections.failed_events (instance_id); + +ALTER TABLE adminapi.failed_events ADD COLUMN last_failed TIMESTAMPTZ; +ALTER TABLE auth.failed_events ADD COLUMN last_failed TIMESTAMPTZ; +ALTER TABLE projections.failed_events ADD COLUMN last_failed TIMESTAMPTZ; diff --git a/cmd/setup/config.go b/cmd/setup/config.go index fe6341b45..0390c5f0c 100644 --- a/cmd/setup/config.go +++ b/cmd/setup/config.go @@ -58,6 +58,7 @@ type Steps struct { s2AssetsTable *AssetTable FirstInstance *FirstInstance s4EventstoreIndexes *EventstoreIndexes + s5LastFailed *LastFailed } type encryptionKeyConfig struct { diff --git a/cmd/setup/setup.go b/cmd/setup/setup.go index 5de3f619f..185b57473 100644 --- a/cmd/setup/setup.go +++ b/cmd/setup/setup.go @@ -82,6 +82,7 @@ func Setup(config *Config, steps *Steps, masterKey string) { steps.FirstInstance.externalPort = config.ExternalPort steps.s4EventstoreIndexes = &EventstoreIndexes{dbClient: dbClient, dbType: config.Database.Type()} + steps.s5LastFailed = &LastFailed{dbClient: dbClient} err = projection.Create(ctx, dbClient, eventstoreClient, config.Projections, nil, nil) logging.OnError(err).Fatal("unable to start projections") @@ -107,6 +108,8 @@ func Setup(config *Config, steps *Steps, masterKey string) { logging.OnError(err).Fatal("unable to migrate step 3") err = migration.Migrate(ctx, eventstoreClient, steps.s4EventstoreIndexes) logging.OnError(err).Fatal("unable to migrate step 4") + err = migration.Migrate(ctx, eventstoreClient, steps.s5LastFailed) + logging.OnError(err).Fatal("unable to migrate step 5") for _, repeatableStep := range repeatableSteps { err = migration.Migrate(ctx, eventstoreClient, repeatableStep) diff --git a/console/src/app/pages/failed-events/failed-events.component.html b/console/src/app/pages/failed-events/failed-events.component.html index 958d187ad..136a41a4d 100644 --- a/console/src/app/pages/failed-events/failed-events.component.html +++ b/console/src/app/pages/failed-events/failed-events.component.html @@ -29,6 +29,13 @@ + + {{ 'IAM.FAILEDEVENTS.LASTFAILED' | translate }} + + {{ event?.lastFailed | timestampToDate | localizedDate: 'EEE dd. MMM, HH:mm' }} + + + {{ 'IAM.FAILEDEVENTS.ERRORMESSAGE' | translate }} diff --git a/console/src/app/pages/failed-events/failed-events.component.ts b/console/src/app/pages/failed-events/failed-events.component.ts index d93a4a44e..c3f5fedb4 100644 --- a/console/src/app/pages/failed-events/failed-events.component.ts +++ b/console/src/app/pages/failed-events/failed-events.component.ts @@ -22,6 +22,7 @@ export class FailedEventsComponent implements AfterViewInit { 'database', 'failedSequence', 'failureCount', + 'lastFailed', 'errorMessage', 'actions', ]; diff --git a/console/src/assets/i18n/de.json b/console/src/assets/i18n/de.json index 29e5fe2b1..21185b5bf 100644 --- a/console/src/assets/i18n/de.json +++ b/console/src/assets/i18n/de.json @@ -743,6 +743,7 @@ "DATABASE": "Datenbank", "FAILEDSEQUENCE": "betroffene Sequenz", "FAILURECOUNT": "Fehleranzahl", + "LASTFAILED": "Zuletzt gescheitert um", "ERRORMESSAGE": "Meldung", "ACTIONS": "Aktionen", "DELETE": "Entfernen", diff --git a/console/src/assets/i18n/en.json b/console/src/assets/i18n/en.json index 100204ed9..274e20f05 100644 --- a/console/src/assets/i18n/en.json +++ b/console/src/assets/i18n/en.json @@ -743,6 +743,7 @@ "DATABASE": "Database", "FAILEDSEQUENCE": "Failed Sequence", "FAILURECOUNT": "Failure Count", + "LASTFAILED": "Last failed at", "ERRORMESSAGE": "Error Message", "ACTIONS": "Actions", "DELETE": "Remove", diff --git a/console/src/assets/i18n/fr.json b/console/src/assets/i18n/fr.json index f3cd110a8..65b3ed2ef 100644 --- a/console/src/assets/i18n/fr.json +++ b/console/src/assets/i18n/fr.json @@ -743,6 +743,7 @@ "DATABASE": "Base de données", "FAILEDSEQUENCE": "Séquence échouée", "FAILURECOUNT": "Nombre d'échecs", + "LASTFAILED": "Dernier échec à", "ERRORMESSAGE": "Message d'erreur", "ACTIONS": "Actions", "DELETE": "Supprimer", diff --git a/console/src/assets/i18n/it.json b/console/src/assets/i18n/it.json index adf81d273..b34ff04d8 100644 --- a/console/src/assets/i18n/it.json +++ b/console/src/assets/i18n/it.json @@ -743,6 +743,7 @@ "DATABASE": "Database", "FAILEDSEQUENCE": "Sequenza fallita", "FAILURECOUNT": "Conteggio dei fallimenti", + "LASTFAILED": "L'ultimo fallimento a", "ERRORMESSAGE": "Messaggio di errore", "ACTIONS": "Azioni", "DELETE": "Rimuovi", diff --git a/console/src/assets/i18n/zh.json b/console/src/assets/i18n/zh.json index 33dae1a70..066e24df3 100644 --- a/console/src/assets/i18n/zh.json +++ b/console/src/assets/i18n/zh.json @@ -743,6 +743,7 @@ "DATABASE": "数据库", "FAILEDSEQUENCE": "失败的序列", "FAILURECOUNT": "失败计数", + "LASTFAILED": "最后一次失败是在", "ERRORMESSAGE": "错误消息", "ACTIONS": "操作", "DELETE": "删除", diff --git a/docs/docs/apis/proto/admin.md b/docs/docs/apis/proto/admin.md index 8b5c477c8..d5886f291 100644 --- a/docs/docs/apis/proto/admin.md +++ b/docs/docs/apis/proto/admin.md @@ -2020,6 +2020,7 @@ This is an empty request | failed_sequence | uint64 | - | | | failure_count | uint64 | - | | | error_message | string | - | | +| last_failed | google.protobuf.Timestamp | - | | @@ -4633,7 +4634,7 @@ this is en empty request | database | string | - | | | view_name | string | - | | | processed_sequence | uint64 | - | | -| event_timestamp | google.protobuf.Timestamp | The timestamp the event occured | | +| event_timestamp | google.protobuf.Timestamp | The timestamp the event occurred | | | last_successful_spooler_run | google.protobuf.Timestamp | - | | diff --git a/docs/docs/apis/proto/system.md b/docs/docs/apis/proto/system.md index ebe4e80a6..d5af548c4 100644 --- a/docs/docs/apis/proto/system.md +++ b/docs/docs/apis/proto/system.md @@ -375,6 +375,7 @@ This is an empty response | failed_sequence | uint64 | - | | | failure_count | uint64 | - | | | error_message | string | - | | +| last_failed | google.protobuf.Timestamp | - | | @@ -556,6 +557,7 @@ This is an empty request | database | string | - | string.min_len: 1
string.max_len: 200
| | view_name | string | - | string.min_len: 1
string.max_len: 200
| | failed_sequence | uint64 | - | | +| instance_id | string | - | | diff --git a/internal/admin/repository/administrator.go b/internal/admin/repository/administrator.go index 7a58b75a6..87e02ee6d 100644 --- a/internal/admin/repository/administrator.go +++ b/internal/admin/repository/administrator.go @@ -7,8 +7,8 @@ import ( ) type AdministratorRepository interface { - GetFailedEvents(context.Context) ([]*model.FailedEvent, error) + GetFailedEvents(ctx context.Context, instanceID string) ([]*model.FailedEvent, error) RemoveFailedEvent(context.Context, *model.FailedEvent) error - GetViews() ([]*model.View, error) + GetViews(instanceID string) ([]*model.View, error) ClearView(ctx context.Context, db, viewName string) error } diff --git a/internal/admin/repository/eventsourcing/eventstore/administrator.go b/internal/admin/repository/eventsourcing/eventstore/administrator.go index c802d9be8..0272a7d3c 100644 --- a/internal/admin/repository/eventsourcing/eventstore/administrator.go +++ b/internal/admin/repository/eventsourcing/eventstore/administrator.go @@ -14,10 +14,10 @@ type AdministratorRepo struct { View *view.View } -func (repo *AdministratorRepo) GetFailedEvents(ctx context.Context) ([]*view_model.FailedEvent, error) { +func (repo *AdministratorRepo) GetFailedEvents(ctx context.Context, instanceID string) ([]*view_model.FailedEvent, error) { allFailedEvents := make([]*view_model.FailedEvent, 0) for _, db := range dbList { - failedEvents, err := repo.View.AllFailedEvents(db) + failedEvents, err := repo.View.AllFailedEvents(db, instanceID) if err != nil { return nil, err } @@ -32,10 +32,10 @@ func (repo *AdministratorRepo) RemoveFailedEvent(ctx context.Context, failedEven return repo.View.RemoveFailedEvent(failedEvent.Database, repository.FailedEventFromModel(failedEvent)) } -func (repo *AdministratorRepo) GetViews() ([]*view_model.View, error) { +func (repo *AdministratorRepo) GetViews(instanceID string) ([]*view_model.View, error) { views := make([]*view_model.View, 0) for _, db := range dbList { - sequences, err := repo.View.AllCurrentSequences(db) + sequences, err := repo.View.AllCurrentSequences(db, instanceID) if err != nil { return nil, err } diff --git a/internal/admin/repository/eventsourcing/view/error_event.go b/internal/admin/repository/eventsourcing/view/error_event.go index e6905b49b..3b4711963 100644 --- a/internal/admin/repository/eventsourcing/view/error_event.go +++ b/internal/admin/repository/eventsourcing/view/error_event.go @@ -21,6 +21,6 @@ func (v *View) latestFailedEvent(viewName, instanceID string, sequence uint64) ( return repository.LatestFailedEvent(v.Db, errTable, viewName, instanceID, sequence) } -func (v *View) AllFailedEvents(db string) ([]*repository.FailedEvent, error) { - return repository.AllFailedEvents(v.Db, db+"."+errColumn) +func (v *View) AllFailedEvents(db, instanceID string) ([]*repository.FailedEvent, error) { + return repository.AllFailedEvents(v.Db, db+"."+errColumn, instanceID) } diff --git a/internal/admin/repository/eventsourcing/view/sequence.go b/internal/admin/repository/eventsourcing/view/sequence.go index e026a6c0c..868d9633b 100644 --- a/internal/admin/repository/eventsourcing/view/sequence.go +++ b/internal/admin/repository/eventsourcing/view/sequence.go @@ -23,8 +23,8 @@ func (v *View) latestSequences(viewName string, instanceIDs ...string) ([]*repos return repository.LatestSequences(v.Db, sequencesTable, viewName, instanceIDs...) } -func (v *View) AllCurrentSequences(db string) ([]*repository.CurrentSequence, error) { - return repository.AllCurrentSequences(v.Db, db+".current_sequences") +func (v *View) AllCurrentSequences(db, instanceID string) ([]*repository.CurrentSequence, error) { + return repository.AllCurrentSequences(v.Db, db+".current_sequences", instanceID) } func (v *View) updateSpoolerRunSequence(viewName string) error { diff --git a/internal/api/grpc/admin/failed_event.go b/internal/api/grpc/admin/failed_event.go index b8e34f5d3..c6bbf98d1 100644 --- a/internal/api/grpc/admin/failed_event.go +++ b/internal/api/grpc/admin/failed_event.go @@ -3,18 +3,25 @@ package admin import ( "context" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" ) -func (s *Server) ListFailedEvents(ctx context.Context, req *admin_pb.ListFailedEventsRequest) (*admin_pb.ListFailedEventsResponse, error) { - failedEventsOld, err := s.administrator.GetFailedEvents(ctx) +func (s *Server) ListFailedEvents(ctx context.Context, _ *admin_pb.ListFailedEventsRequest) (*admin_pb.ListFailedEventsResponse, error) { + instanceID := authz.GetInstance(ctx).InstanceID() + failedEventsOld, err := s.administrator.GetFailedEvents(ctx, instanceID) if err != nil { return nil, err } convertedOld := FailedEventsViewToPb(failedEventsOld) - - failedEvents, err := s.query.SearchFailedEvents(ctx, new(query.FailedEventSearchQueries)) + instanceIDQuery, err := query.NewFailedEventInstanceIDSearchQuery(instanceID) + if err != nil { + return nil, err + } + failedEvents, err := s.query.SearchFailedEvents(ctx, &query.FailedEventSearchQueries{ + Queries: []query.SearchQuery{instanceIDQuery}, + }) if err != nil { return nil, err } @@ -25,9 +32,9 @@ func (s *Server) ListFailedEvents(ctx context.Context, req *admin_pb.ListFailedE func (s *Server) RemoveFailedEvent(ctx context.Context, req *admin_pb.RemoveFailedEventRequest) (*admin_pb.RemoveFailedEventResponse, error) { var err error if req.Database != s.database { - err = s.administrator.RemoveFailedEvent(ctx, RemoveFailedEventRequestToModel(req)) + err = s.administrator.RemoveFailedEvent(ctx, RemoveFailedEventRequestToModel(ctx, req)) } else { - err = s.query.RemoveFailedEvent(ctx, req.ViewName, req.FailedSequence) + err = s.query.RemoveFailedEvent(ctx, req.ViewName, authz.GetInstance(ctx).InstanceID(), req.FailedSequence) } if err != nil { return nil, err diff --git a/internal/api/grpc/admin/failed_event_converter.go b/internal/api/grpc/admin/failed_event_converter.go index afc59a425..90c040e74 100644 --- a/internal/api/grpc/admin/failed_event_converter.go +++ b/internal/api/grpc/admin/failed_event_converter.go @@ -1,6 +1,11 @@ package admin import ( + "context" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/view/model" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -15,12 +20,17 @@ func FailedEventsViewToPb(failedEvents []*model.FailedEvent) []*admin_pb.FailedE } func FailedEventViewToPb(failedEvent *model.FailedEvent) *admin_pb.FailedEvent { + var lastFailed *timestamppb.Timestamp + if !failedEvent.LastFailed.IsZero() { + lastFailed = timestamppb.New(failedEvent.LastFailed) + } return &admin_pb.FailedEvent{ Database: failedEvent.Database, ViewName: failedEvent.ViewName, FailedSequence: failedEvent.FailedSequence, FailureCount: failedEvent.FailureCount, ErrorMessage: failedEvent.ErrMsg, + LastFailed: lastFailed, } } @@ -33,19 +43,25 @@ func FailedEventsToPb(database string, failedEvents *query.FailedEvents) []*admi } func FailedEventToPb(database string, failedEvent *query.FailedEvent) *admin_pb.FailedEvent { + var lastFailed *timestamppb.Timestamp + if !failedEvent.LastFailed.IsZero() { + lastFailed = timestamppb.New(failedEvent.LastFailed) + } return &admin_pb.FailedEvent{ Database: database, ViewName: failedEvent.ProjectionName, FailedSequence: failedEvent.FailedSequence, FailureCount: failedEvent.FailureCount, ErrorMessage: failedEvent.Error, + LastFailed: lastFailed, } } -func RemoveFailedEventRequestToModel(req *admin_pb.RemoveFailedEventRequest) *model.FailedEvent { +func RemoveFailedEventRequestToModel(ctx context.Context, req *admin_pb.RemoveFailedEventRequest) *model.FailedEvent { return &model.FailedEvent{ Database: req.Database, ViewName: req.ViewName, FailedSequence: req.FailedSequence, + InstanceID: authz.GetInstance(ctx).InstanceID(), } } diff --git a/internal/api/grpc/admin/failed_event_converter_test.go b/internal/api/grpc/admin/failed_event_converter_test.go index c88f80103..6edc36c1e 100644 --- a/internal/api/grpc/admin/failed_event_converter_test.go +++ b/internal/api/grpc/admin/failed_event_converter_test.go @@ -1,8 +1,11 @@ package admin import ( + "context" "testing" + "time" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/test" "github.com/zitadel/zitadel/internal/view/model" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" @@ -25,6 +28,7 @@ func TestFailedEventsToPbFields(t *testing.T) { ViewName: "users", FailedSequence: 456, FailureCount: 5, + LastFailed: time.Now(), ErrMsg: "some error", }, }, @@ -57,6 +61,7 @@ func TestFailedEventToPbFields(t *testing.T) { ViewName: "users", FailedSequence: 456, FailureCount: 5, + LastFailed: time.Now(), ErrMsg: "some error", }, }, @@ -70,6 +75,7 @@ func TestFailedEventToPbFields(t *testing.T) { func TestRemoveFailedEventRequestToModelFields(t *testing.T) { type args struct { + ctx context.Context req *admin_pb.RemoveFailedEventRequest } tests := []struct { @@ -79,6 +85,7 @@ func TestRemoveFailedEventRequestToModelFields(t *testing.T) { { "all fields", args{ + ctx: authz.WithInstanceID(context.Background(), "instanceID"), req: &admin_pb.RemoveFailedEventRequest{ Database: "admin", ViewName: "users", @@ -88,7 +95,7 @@ func TestRemoveFailedEventRequestToModelFields(t *testing.T) { }, } for _, tt := range tests { - converted := RemoveFailedEventRequestToModel(tt.args.req) - test.AssertFieldsMapped(t, converted, "FailureCount", "ErrMsg") + converted := RemoveFailedEventRequestToModel(tt.args.ctx, tt.args.req) + test.AssertFieldsMapped(t, converted, "FailureCount", "LastFailed", "ErrMsg") } } diff --git a/internal/api/grpc/admin/view.go b/internal/api/grpc/admin/view.go index 6bc7014fb..d7fa30f42 100644 --- a/internal/api/grpc/admin/view.go +++ b/internal/api/grpc/admin/view.go @@ -3,17 +3,25 @@ package admin import ( "context" + "github.com/zitadel/zitadel/internal/api/authz" "github.com/zitadel/zitadel/internal/query" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" ) func (s *Server) ListViews(ctx context.Context, _ *admin_pb.ListViewsRequest) (*admin_pb.ListViewsResponse, error) { - currentSequences, err := s.query.SearchCurrentSequences(ctx, new(query.CurrentSequencesSearchQueries)) + instanceID := authz.GetInstance(ctx).InstanceID() + instanceIDQuery, err := query.NewCurrentSequencesInstanceIDSearchQuery(instanceID) + if err != nil { + return nil, err + } + currentSequences, err := s.query.SearchCurrentSequences(ctx, &query.CurrentSequencesSearchQueries{ + Queries: []query.SearchQuery{instanceIDQuery}, + }) if err != nil { return nil, err } convertedCurrentSequences := CurrentSequencesToPb(s.database, currentSequences) - views, err := s.administrator.GetViews() + views, err := s.administrator.GetViews(instanceID) if err != nil { return nil, err } diff --git a/internal/api/grpc/admin/view_converter.go b/internal/api/grpc/admin/view_converter.go index 24ee45e4b..f9d28185e 100644 --- a/internal/api/grpc/admin/view_converter.go +++ b/internal/api/grpc/admin/view_converter.go @@ -1,10 +1,11 @@ package admin import ( + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/view/model" admin_pb "github.com/zitadel/zitadel/pkg/grpc/admin" - "google.golang.org/protobuf/types/known/timestamppb" ) func ViewsToPb(views []*model.View) []*admin_pb.View { @@ -35,9 +36,9 @@ func CurrentSequencesToPb(database string, currentSequences *query.CurrentSequen func CurrentSequenceToPb(database string, currentSequence *query.CurrentSequence) *admin_pb.View { return &admin_pb.View{ - Database: database, - ViewName: currentSequence.ProjectionName, - ProcessedSequence: currentSequence.CurrentSequence, - EventTimestamp: timestamppb.New(currentSequence.Timestamp), + Database: database, + ViewName: currentSequence.ProjectionName, + ProcessedSequence: currentSequence.CurrentSequence, + LastSuccessfulSpoolerRun: timestamppb.New(currentSequence.Timestamp), } } diff --git a/internal/api/grpc/system/failed_event.go b/internal/api/grpc/system/failed_event.go index bb2972b9d..c01afea60 100644 --- a/internal/api/grpc/system/failed_event.go +++ b/internal/api/grpc/system/failed_event.go @@ -7,8 +7,8 @@ import ( system_pb "github.com/zitadel/zitadel/pkg/grpc/system" ) -func (s *Server) ListFailedEvents(ctx context.Context, req *system_pb.ListFailedEventsRequest) (*system_pb.ListFailedEventsResponse, error) { - failedEventsOld, err := s.administrator.GetFailedEvents(ctx) +func (s *Server) ListFailedEvents(ctx context.Context, _ *system_pb.ListFailedEventsRequest) (*system_pb.ListFailedEventsResponse, error) { + failedEventsOld, err := s.administrator.GetFailedEvents(ctx, "") if err != nil { return nil, err } @@ -27,7 +27,7 @@ func (s *Server) RemoveFailedEvent(ctx context.Context, req *system_pb.RemoveFai if req.Database != s.database { err = s.administrator.RemoveFailedEvent(ctx, RemoveFailedEventRequestToModel(req)) } else { - err = s.query.RemoveFailedEvent(ctx, req.ViewName, req.FailedSequence) + err = s.query.RemoveFailedEvent(ctx, req.ViewName, req.InstanceId, req.FailedSequence) } if err != nil { return nil, err diff --git a/internal/api/grpc/system/failed_event_converter.go b/internal/api/grpc/system/failed_event_converter.go index 9d194692c..65316477a 100644 --- a/internal/api/grpc/system/failed_event_converter.go +++ b/internal/api/grpc/system/failed_event_converter.go @@ -1,6 +1,8 @@ package system import ( + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/view/model" system_pb "github.com/zitadel/zitadel/pkg/grpc/system" @@ -15,12 +17,17 @@ func FailedEventsViewToPb(failedEvents []*model.FailedEvent) []*system_pb.Failed } func FailedEventViewToPb(failedEvent *model.FailedEvent) *system_pb.FailedEvent { + var lastFailed *timestamppb.Timestamp + if !failedEvent.LastFailed.IsZero() { + lastFailed = timestamppb.New(failedEvent.LastFailed) + } return &system_pb.FailedEvent{ Database: failedEvent.Database, ViewName: failedEvent.ViewName, FailedSequence: failedEvent.FailedSequence, FailureCount: failedEvent.FailureCount, ErrorMessage: failedEvent.ErrMsg, + LastFailed: lastFailed, } } @@ -33,12 +40,17 @@ func FailedEventsToPb(database string, failedEvents *query.FailedEvents) []*syst } func FailedEventToPb(database string, failedEvent *query.FailedEvent) *system_pb.FailedEvent { + var lastFailed *timestamppb.Timestamp + if !failedEvent.LastFailed.IsZero() { + lastFailed = timestamppb.New(failedEvent.LastFailed) + } return &system_pb.FailedEvent{ Database: database, ViewName: failedEvent.ProjectionName, FailedSequence: failedEvent.FailedSequence, FailureCount: failedEvent.FailureCount, ErrorMessage: failedEvent.Error, + LastFailed: lastFailed, } } @@ -47,5 +59,6 @@ func RemoveFailedEventRequestToModel(req *system_pb.RemoveFailedEventRequest) *m Database: req.Database, ViewName: req.ViewName, FailedSequence: req.FailedSequence, + InstanceID: req.InstanceId, } } diff --git a/internal/api/grpc/system/failed_event_converter_test.go b/internal/api/grpc/system/failed_event_converter_test.go index 71cb175ce..f91c2d1b4 100644 --- a/internal/api/grpc/system/failed_event_converter_test.go +++ b/internal/api/grpc/system/failed_event_converter_test.go @@ -2,6 +2,7 @@ package system_test import ( "testing" + "time" system_grpc "github.com/zitadel/zitadel/internal/api/grpc/system" "github.com/zitadel/zitadel/internal/test" @@ -26,6 +27,7 @@ func TestFailedEventsToPbFields(t *testing.T) { ViewName: "users", FailedSequence: 456, FailureCount: 5, + LastFailed: time.Now(), ErrMsg: "some error", }, }, @@ -58,6 +60,7 @@ func TestFailedEventToPbFields(t *testing.T) { ViewName: "users", FailedSequence: 456, FailureCount: 5, + LastFailed: time.Now(), ErrMsg: "some error", }, }, @@ -84,12 +87,13 @@ func TestRemoveFailedEventRequestToModelFields(t *testing.T) { Database: "admin", ViewName: "users", FailedSequence: 456, + InstanceId: "instanceID", }, }, }, } for _, tt := range tests { converted := system_grpc.RemoveFailedEventRequestToModel(tt.args.req) - test.AssertFieldsMapped(t, converted, "FailureCount", "ErrMsg") + test.AssertFieldsMapped(t, converted, "FailureCount", "LastFailed", "ErrMsg") } } diff --git a/internal/api/grpc/system/view.go b/internal/api/grpc/system/view.go index 488a7de35..261b1d40b 100644 --- a/internal/api/grpc/system/view.go +++ b/internal/api/grpc/system/view.go @@ -13,7 +13,7 @@ func (s *Server) ListViews(ctx context.Context, _ *system_pb.ListViewsRequest) ( return nil, err } convertedCurrentSequences := CurrentSequencesToPb(s.database, currentSequences) - views, err := s.administrator.GetViews() + views, err := s.administrator.GetViews("") if err != nil { return nil, err } diff --git a/internal/api/grpc/system/view_converter.go b/internal/api/grpc/system/view_converter.go index b4b97afc0..6b11376c5 100644 --- a/internal/api/grpc/system/view_converter.go +++ b/internal/api/grpc/system/view_converter.go @@ -1,10 +1,11 @@ package system import ( + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/internal/view/model" system_pb "github.com/zitadel/zitadel/pkg/grpc/system" - "google.golang.org/protobuf/types/known/timestamppb" ) func ViewsToPb(views []*model.View) []*system_pb.View { @@ -35,9 +36,9 @@ func CurrentSequencesToPb(database string, currentSequences *query.CurrentSequen func CurrentSequenceToPb(database string, currentSequence *query.CurrentSequence) *system_pb.View { return &system_pb.View{ - Database: database, - ViewName: currentSequence.ProjectionName, - ProcessedSequence: currentSequence.CurrentSequence, - EventTimestamp: timestamppb.New(currentSequence.Timestamp), + Database: database, + ViewName: currentSequence.ProjectionName, + ProcessedSequence: currentSequence.CurrentSequence, + LastSuccessfulSpoolerRun: timestamppb.New(currentSequence.Timestamp), } } diff --git a/internal/eventstore/handler/crdb/db_mock_test.go b/internal/eventstore/handler/crdb/db_mock_test.go index 445b35224..33c7984e1 100644 --- a/internal/eventstore/handler/crdb/db_mock_test.go +++ b/internal/eventstore/handler/crdb/db_mock_test.go @@ -28,8 +28,8 @@ func expectFailureCount(tableName string, projectionName, instanceID string, fai func expectUpdateFailureCount(tableName string, projectionName, instanceID string, seq, failureCount uint64) func(sqlmock.Sqlmock) { return func(m sqlmock.Sqlmock) { - m.ExpectExec(`INSERT INTO `+tableName+` \(projection_name, failed_sequence, failure_count, error, instance_id\) VALUES \(\$1, \$2, \$3, \$4\, \$5\) ON CONFLICT \(projection_name, failed_sequence, instance_id\) DO UPDATE SET failure_count = EXCLUDED\.failure_count, error = EXCLUDED\.error`). - WithArgs(projectionName, seq, failureCount, sqlmock.AnyArg(), instanceID).WillReturnResult(sqlmock.NewResult(1, 1)) + m.ExpectExec(`INSERT INTO `+tableName+` \(projection_name, failed_sequence, failure_count, error, instance_id, last_failed\) VALUES \(\$1, \$2, \$3, \$4\, \$5\, \$6\) ON CONFLICT \(projection_name, failed_sequence, instance_id\) DO UPDATE SET failure_count = EXCLUDED\.failure_count, error = EXCLUDED\.error, last_failed = EXCLUDED\.last_failed`). + WithArgs(projectionName, seq, failureCount, sqlmock.AnyArg(), instanceID, "NOW()").WillReturnResult(sqlmock.NewResult(1, 1)) } } diff --git a/internal/eventstore/handler/crdb/failed_stmt.go b/internal/eventstore/handler/crdb/failed_stmt.go index acf918a8f..b7b10844e 100644 --- a/internal/eventstore/handler/crdb/failed_stmt.go +++ b/internal/eventstore/handler/crdb/failed_stmt.go @@ -11,9 +11,9 @@ import ( const ( setFailureCountStmtFormat = "INSERT INTO %s" + - " (projection_name, failed_sequence, failure_count, error, instance_id)" + - " VALUES ($1, $2, $3, $4, $5) ON CONFLICT (projection_name, failed_sequence, instance_id)" + - " DO UPDATE SET failure_count = EXCLUDED.failure_count, error = EXCLUDED.error" + " (projection_name, failed_sequence, failure_count, error, instance_id, last_failed)" + + " VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (projection_name, failed_sequence, instance_id)" + + " DO UPDATE SET failure_count = EXCLUDED.failure_count, error = EXCLUDED.error, last_failed = EXCLUDED.last_failed" failureCountStmtFormat = "WITH failures AS (SELECT failure_count FROM %s WHERE projection_name = $1 AND failed_sequence = $2 AND instance_id = $3)" + " SELECT COALESCE((SELECT failure_count FROM failures), 0) AS failure_count" ) @@ -43,7 +43,7 @@ func (h *StatementHandler) failureCount(tx *sql.Tx, seq uint64, instanceID strin } func (h *StatementHandler) setFailureCount(tx *sql.Tx, seq uint64, count uint, err error, instanceID string) error { - _, dbErr := tx.Exec(h.setFailureCountStmt, h.ProjectionName, seq, count, err.Error(), instanceID) + _, dbErr := tx.Exec(h.setFailureCountStmt, h.ProjectionName, seq, count, err.Error(), instanceID, "NOW()") if dbErr != nil { return errors.ThrowInternal(dbErr, "CRDB-4Ht4x", "set failure count failed") } diff --git a/internal/eventstore/v1/spooler/spooler.go b/internal/eventstore/v1/spooler/spooler.go index 5cfa22044..cd9133fe9 100644 --- a/internal/eventstore/v1/spooler/spooler.go +++ b/internal/eventstore/v1/spooler/spooler.go @@ -135,7 +135,7 @@ func (s *spooledHandler) awaitError(cancel func(), errs chan error, workerID str select { case err := <-errs: cancel() - logging.Log("SPOOL-OT8di").OnError(err).WithField("view", s.ViewModel()).WithField("worker", workerID).Debug("load canceled") + logging.OnError(err).WithField("view", s.ViewModel()).WithField("worker", workerID).Debug("load canceled") } } @@ -216,6 +216,7 @@ func HandleError(event *models.Event, failedErr error, failedEvent.FailureCount++ failedEvent.ErrMsg = failedErr.Error() failedEvent.InstanceID = event.InstanceID + failedEvent.LastFailed = time.Now() err = processFailedEvent(failedEvent) if err != nil { return err diff --git a/internal/eventstore/v1/spooler/spooler_test.go b/internal/eventstore/v1/spooler/spooler_test.go index e97181229..315a16a2a 100644 --- a/internal/eventstore/v1/spooler/spooler_test.go +++ b/internal/eventstore/v1/spooler/spooler_test.go @@ -17,6 +17,10 @@ import ( "github.com/zitadel/zitadel/internal/view/repository" ) +var ( + testNow = time.Now() +) + type testHandler struct { cycleDuration time.Duration processSleep time.Duration @@ -429,6 +433,7 @@ func TestHandleError(t *testing.T) { FailureCount: 6, ViewName: "super.table", InstanceID: instanceID, + LastFailed: testNow, }, nil }, errorCountUntilSkip: 5, @@ -449,6 +454,7 @@ func TestHandleError(t *testing.T) { FailureCount: 5, ViewName: "super.table", InstanceID: instanceID, + LastFailed: testNow, }, nil }, errorCountUntilSkip: 6, @@ -469,6 +475,7 @@ func TestHandleError(t *testing.T) { FailureCount: 3, ViewName: "super.table", InstanceID: instanceID, + LastFailed: testNow, }, nil }, errorCountUntilSkip: 5, diff --git a/internal/query/current_sequence.go b/internal/query/current_sequence.go index 040929dcb..5b604dc8e 100644 --- a/internal/query/current_sequence.go +++ b/internal/query/current_sequence.go @@ -42,6 +42,10 @@ type CurrentSequencesSearchQueries struct { Queries []SearchQuery } +func NewCurrentSequencesInstanceIDSearchQuery(instanceID string) (SearchQuery, error) { + return NewTextQuery(CurrentSequenceColInstanceID, instanceID, TextEquals) +} + func (q *CurrentSequencesSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder { query = q.SearchRequest.toQuery(query) for _, q := range q.Queries { diff --git a/internal/query/failed_events.go b/internal/query/failed_events.go index 7853f3210..203380e0b 100644 --- a/internal/query/failed_events.go +++ b/internal/query/failed_events.go @@ -3,7 +3,7 @@ package query import ( "context" "database/sql" - errs "errors" + "time" sq "github.com/Masterminds/squirrel" @@ -15,6 +15,7 @@ const ( failedEventsColumnProjectionName = "projection_name" failedEventsColumnFailedSequence = "failed_sequence" failedEventsColumnFailureCount = "failure_count" + failedEventsColumnLastFailed = "last_failed" failedEventsColumnError = "error" failedEventsColumnInstanceID = "instance_id" ) @@ -36,10 +37,18 @@ var ( name: failedEventsColumnFailureCount, table: failedEventsTable, } + FailedEventsColumnLastFailed = Column{ + name: failedEventsColumnLastFailed, + table: failedEventsTable, + } FailedEventsColumnError = Column{ name: failedEventsColumnError, table: failedEventsTable, } + FailedEventsColumnInstanceID = Column{ + name: failedEventsColumnInstanceID, + table: failedEventsTable, + } ) type FailedEvents struct { @@ -52,6 +61,7 @@ type FailedEvent struct { FailedSequence uint64 FailureCount uint64 Error string + LastFailed time.Time } type FailedEventSearchQueries struct { @@ -73,26 +83,27 @@ func (q *Queries) SearchFailedEvents(ctx context.Context, queries *FailedEventSe return scan(rows) } -func (q *Queries) RemoveFailedEvent(ctx context.Context, projectionName string, sequence uint64) (err error) { +func (q *Queries) RemoveFailedEvent(ctx context.Context, projectionName, instanceID string, sequence uint64) (err error) { stmt, args, err := sq.Delete(projection.FailedEventsTable). Where(sq.Eq{ failedEventsColumnProjectionName: projectionName, failedEventsColumnFailedSequence: sequence, + failedEventsColumnInstanceID: instanceID, }). PlaceholderFormat(sq.Dollar). ToSql() if err != nil { return errors.ThrowInternal(err, "QUERY-DGgh3", "Errors.RemoveFailed") } - _, err = q.client.Exec(stmt, args...) + _, err = q.client.ExecContext(ctx, stmt, args...) if err != nil { return errors.ThrowInternal(err, "QUERY-0kbFF", "Errors.RemoveFailed") } return nil } -func NewFailedEventProjectionNameSearchQuery(method TextComparison, value string) (SearchQuery, error) { - return NewTextQuery(FailedEventsColumnProjectionName, value, method) +func NewFailedEventInstanceIDSearchQuery(instanceID string) (SearchQuery, error) { + return NewTextQuery(FailedEventsColumnInstanceID, instanceID, TextEquals) } func (r *ProjectSearchQueries) AppendProjectionNameQuery(projectionName string) error { @@ -112,36 +123,12 @@ func (q *FailedEventSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuil return query } -func prepareFailedEventQuery(instanceIDs ...string) (sq.SelectBuilder, func(*sql.Row) (*FailedEvent, error)) { - return sq.Select( - FailedEventsColumnProjectionName.identifier(), - FailedEventsColumnFailedSequence.identifier(), - FailedEventsColumnFailureCount.identifier(), - FailedEventsColumnError.identifier()). - From(failedEventsTable.identifier()).PlaceholderFormat(sq.Dollar), - func(row *sql.Row) (*FailedEvent, error) { - p := new(FailedEvent) - err := row.Scan( - &p.ProjectionName, - &p.FailedSequence, - &p.FailureCount, - &p.Error, - ) - if err != nil { - if errs.Is(err, sql.ErrNoRows) { - return nil, errors.ThrowNotFound(err, "QUERY-5N00f", "Errors.FailedEvents.NotFound") - } - return nil, errors.ThrowInternal(err, "QUERY-0oJf3", "Errors.Internal") - } - return p, nil - } -} - func prepareFailedEventsQuery() (sq.SelectBuilder, func(*sql.Rows) (*FailedEvents, error)) { return sq.Select( FailedEventsColumnProjectionName.identifier(), FailedEventsColumnFailedSequence.identifier(), FailedEventsColumnFailureCount.identifier(), + FailedEventsColumnLastFailed.identifier(), FailedEventsColumnError.identifier(), countColumn.identifier()). From(failedEventsTable.identifier()).PlaceholderFormat(sq.Dollar), @@ -150,16 +137,19 @@ func prepareFailedEventsQuery() (sq.SelectBuilder, func(*sql.Rows) (*FailedEvent var count uint64 for rows.Next() { failedEvent := new(FailedEvent) + var lastFailed sql.NullTime err := rows.Scan( &failedEvent.ProjectionName, &failedEvent.FailedSequence, &failedEvent.FailureCount, + &lastFailed, &failedEvent.Error, &count, ) if err != nil { return nil, err } + failedEvent.LastFailed = lastFailed.Time failedEvents = append(failedEvents, failedEvent) } diff --git a/internal/query/failed_events_test.go b/internal/query/failed_events_test.go index 5ca209199..495a66716 100644 --- a/internal/query/failed_events_test.go +++ b/internal/query/failed_events_test.go @@ -28,6 +28,7 @@ func Test_FailedEventsPrepares(t *testing.T) { regexp.QuoteMeta(`SELECT projections.failed_events.projection_name,`+ ` projections.failed_events.failed_sequence,`+ ` projections.failed_events.failure_count,`+ + ` projections.failed_events.last_failed,`+ ` projections.failed_events.error,`+ ` COUNT(*) OVER ()`+ ` FROM projections.failed_events`), @@ -45,6 +46,7 @@ func Test_FailedEventsPrepares(t *testing.T) { regexp.QuoteMeta(`SELECT projections.failed_events.projection_name,`+ ` projections.failed_events.failed_sequence,`+ ` projections.failed_events.failure_count,`+ + ` projections.failed_events.last_failed,`+ ` projections.failed_events.error,`+ ` COUNT(*) OVER ()`+ ` FROM projections.failed_events`), @@ -52,6 +54,7 @@ func Test_FailedEventsPrepares(t *testing.T) { "projection_name", "failed_sequence", "failure_count", + "last_failed", "error", "count", }, @@ -60,6 +63,7 @@ func Test_FailedEventsPrepares(t *testing.T) { "projection-name", uint64(20211108), uint64(2), + testNow, "error", }, }, @@ -74,6 +78,7 @@ func Test_FailedEventsPrepares(t *testing.T) { ProjectionName: "projection-name", FailedSequence: 20211108, FailureCount: 2, + LastFailed: testNow, Error: "error", }, }, @@ -87,6 +92,7 @@ func Test_FailedEventsPrepares(t *testing.T) { regexp.QuoteMeta(`SELECT projections.failed_events.projection_name,`+ ` projections.failed_events.failed_sequence,`+ ` projections.failed_events.failure_count,`+ + ` projections.failed_events.last_failed,`+ ` projections.failed_events.error,`+ ` COUNT(*) OVER ()`+ ` FROM projections.failed_events`), @@ -94,6 +100,7 @@ func Test_FailedEventsPrepares(t *testing.T) { "projection_name", "failed_sequence", "failure_count", + "last_failed", "error", "count", }, @@ -102,12 +109,14 @@ func Test_FailedEventsPrepares(t *testing.T) { "projection-name", uint64(20211108), 2, + testNow, "error", }, { "projection-name-2", uint64(20211108), 2, + nil, "error", }, }, @@ -122,6 +131,7 @@ func Test_FailedEventsPrepares(t *testing.T) { ProjectionName: "projection-name", FailedSequence: 20211108, FailureCount: 2, + LastFailed: testNow, Error: "error", }, { @@ -141,6 +151,7 @@ func Test_FailedEventsPrepares(t *testing.T) { regexp.QuoteMeta(`SELECT projections.failed_events.projection_name,`+ ` projections.failed_events.failed_sequence,`+ ` projections.failed_events.failure_count,`+ + ` projections.failed_events.last_failed,`+ ` projections.failed_events.error,`+ ` COUNT(*) OVER ()`+ ` FROM projections.failed_events`), diff --git a/internal/view/model/failed_event.go b/internal/view/model/failed_event.go index de01b4b9d..f270ae419 100644 --- a/internal/view/model/failed_event.go +++ b/internal/view/model/failed_event.go @@ -1,9 +1,13 @@ package model +import "time" + type FailedEvent struct { Database string ViewName string FailedSequence uint64 FailureCount uint64 ErrMsg string + InstanceID string + LastFailed time.Time } diff --git a/internal/view/repository/failed_events.go b/internal/view/repository/failed_events.go index 31e2cc08f..6eb75c203 100644 --- a/internal/view/repository/failed_events.go +++ b/internal/view/repository/failed_events.go @@ -2,6 +2,7 @@ package repository import ( "strings" + "time" "github.com/jinzhu/gorm" @@ -11,11 +12,47 @@ import ( ) type FailedEvent struct { - ViewName string `gorm:"column:view_name;primary_key"` - FailedSequence uint64 `gorm:"column:failed_sequence;primary_key"` - FailureCount uint64 `gorm:"column:failure_count"` - ErrMsg string `gorm:"column:err_msg"` - InstanceID string `gorm:"column:instance_id"` + ViewName string `gorm:"column:view_name;primary_key"` + FailedSequence uint64 `gorm:"column:failed_sequence;primary_key"` + FailureCount uint64 `gorm:"column:failure_count"` + ErrMsg string `gorm:"column:err_msg"` + InstanceID string `gorm:"column:instance_id"` + LastFailed time.Time `gorm:"column:last_failed"` +} + +type failedEventSearchRequest struct { + Offset uint64 + Limit uint64 + SortingColumn failedEventSearchKey + Asc bool + Queries []*FailedEventSearchQuery +} + +func (f failedEventSearchRequest) GetLimit() uint64 { + return f.Limit +} + +func (f failedEventSearchRequest) GetOffset() uint64 { + return f.Offset +} + +func (f failedEventSearchRequest) GetSortingColumn() ColumnKey { + if f.SortingColumn == failedEventSearchKey(FailedEventKeyUndefined) { + return nil + } + return f.SortingColumn +} + +func (f failedEventSearchRequest) GetAsc() bool { + return f.Asc +} + +func (f failedEventSearchRequest) GetQueries() []SearchQuery { + result := make([]SearchQuery, len(f.Queries)) + for i, q := range f.Queries { + result[i] = q + } + return result } type FailedEventSearchQuery struct { @@ -43,6 +80,7 @@ const ( FailedEventKeyViewName FailedEventKeyFailedSequence FailedEventKeyInstanceID + FailedEventKeyLastFailed ) type failedEventSearchKey FailedEventSearchKey @@ -55,6 +93,8 @@ func (key failedEventSearchKey) ToColumnName() string { return "failed_sequence" case FailedEventKeyInstanceID: return "instance_id" + case FailedEventKeyLastFailed: + return "last_failed" default: return "" } @@ -65,6 +105,7 @@ func FailedEventFromModel(failedEvent *view_model.FailedEvent) *FailedEvent { ViewName: failedEvent.Database + "." + failedEvent.ViewName, FailureCount: failedEvent.FailureCount, FailedSequence: failedEvent.FailedSequence, + InstanceID: failedEvent.InstanceID, ErrMsg: failedEvent.ErrMsg, } } @@ -76,6 +117,7 @@ func FailedEventToModel(failedEvent *FailedEvent) *view_model.FailedEvent { FailureCount: failedEvent.FailureCount, FailedSequence: failedEvent.FailedSequence, ErrMsg: failedEvent.ErrMsg, + LastFailed: failedEvent.LastFailed, } } @@ -123,9 +165,13 @@ func LatestFailedEvent(db *gorm.DB, table, viewName, instanceID string, sequence } -func AllFailedEvents(db *gorm.DB, table string) ([]*FailedEvent, error) { +func AllFailedEvents(db *gorm.DB, table, instanceID string) ([]*FailedEvent, error) { + queries := make([]*FailedEventSearchQuery, 0, 1) + if instanceID != "" { + queries = append(queries, &FailedEventSearchQuery{Key: FailedEventKeyInstanceID, Method: domain.SearchMethodEquals, Value: instanceID}) + } failedEvents := make([]*FailedEvent, 0) - query := PrepareSearchQuery(table, GeneralSearchRequest{}) + query := PrepareSearchQuery(table, &failedEventSearchRequest{SortingColumn: failedEventSearchKey(FailedEventKeyLastFailed), Queries: queries}) _, err := query(db, &failedEvents) if err != nil { return nil, err diff --git a/internal/view/repository/sequence.go b/internal/view/repository/sequence.go index 8c4b5b0d7..2f1a2e1b2 100644 --- a/internal/view/repository/sequence.go +++ b/internal/view/repository/sequence.go @@ -192,9 +192,13 @@ func LatestSequences(db *gorm.DB, table, viewName string, instanceIDs ...string) return sequences, nil } -func AllCurrentSequences(db *gorm.DB, table string) ([]*CurrentSequence, error) { +func AllCurrentSequences(db *gorm.DB, table, instanceID string) ([]*CurrentSequence, error) { + queries := make([]sequenceSearchQuery, 0, 1) + if instanceID != "" { + queries = append(queries, sequenceSearchQuery{key: sequenceSearchKey(SequenceSearchKeyInstanceID), value: instanceID}) + } sequences := make([]*CurrentSequence, 0) - query := PrepareSearchQuery(table, GeneralSearchRequest{}) + query := PrepareSearchQuery(table, &sequenceSearchRequest{queries: queries}) _, err := query(db, &sequences) if err != nil { return nil, err diff --git a/proto/zitadel/admin.proto b/proto/zitadel/admin.proto index 1a8d9da80..ded7ff7d7 100644 --- a/proto/zitadel/admin.proto +++ b/proto/zitadel/admin.proto @@ -4648,12 +4648,12 @@ message View { google.protobuf.Timestamp event_timestamp = 4 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { example: "\"2019-04-01T08:45:00.000000Z\""; - description: "The timestamp the event occured"; + description: "The timestamp the event occurred"; } - ]; // The timestamp the event occured + ]; // The timestamp the event occurred google.protobuf.Timestamp last_successful_spooler_run = 5 [ (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { - description: "The timestamp the event occured"; + description: "The timestamp the event occurred"; } ]; } @@ -4684,6 +4684,11 @@ message FailedEvent { example: "\"ID=EXAMP-ID3ER Message=Example message\""; } ]; + google.protobuf.Timestamp last_failed = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The timestamp the failure last occurred"; + } + ]; } message ImportDataRequest { diff --git a/proto/zitadel/system.proto b/proto/zitadel/system.proto index f9b7582a6..7e5a49810 100644 --- a/proto/zitadel/system.proto +++ b/proto/zitadel/system.proto @@ -569,6 +569,11 @@ message RemoveFailedEventRequest { example: "\"9823758\""; } ]; + string instance_id = 4 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + example: "\"840498034930840\""; + } + ]; } //This is an empty response @@ -634,4 +639,9 @@ message FailedEvent { example: "\"ID=EXAMP-ID3ER Message=Example message\""; } ]; + google.protobuf.Timestamp last_failed = 6 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "The timestamp the failure last occurred"; + } + ]; }