feat: add tenant column to eventstore (#3314)

* feat: add tenant column to eventstore

* feat: read tenant from context on push and filter

* Update 07_events_table.sql

* pass tenant to queryFactory

* fix some query tests

* init in tests

* add missing sql files

Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
Fabi
2022-03-15 07:19:02 +01:00
committed by GitHub
parent 5463244376
commit 5132ebe07c
51 changed files with 414 additions and 479 deletions

View File

@@ -11,11 +11,11 @@ import (
)
const (
selectEscaped = `SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, aggregate_type, aggregate_id, aggregate_version FROM eventstore\.events WHERE aggregate_type = \$1`
selectEscaped = `SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, tenant, aggregate_type, aggregate_id, aggregate_version FROM eventstore\.events WHERE aggregate_type = \$1`
)
var (
eventColumns = []string{"creation_date", "event_type", "event_sequence", "previous_aggregate_sequence", "event_data", "editor_service", "editor_user", "resource_owner", "aggregate_type", "aggregate_id", "aggregate_version"}
eventColumns = []string{"creation_date", "event_type", "event_sequence", "previous_aggregate_sequence", "event_data", "editor_service", "editor_user", "resource_owner", "tenant", "aggregate_type", "aggregate_id", "aggregate_version"}
expectedFilterEventsLimitFormat = regexp.MustCompile(selectEscaped + ` ORDER BY event_sequence LIMIT \$2`).String()
expectedFilterEventsDescFormat = regexp.MustCompile(selectEscaped + ` ORDER BY event_sequence DESC`).String()
expectedFilterEventsAggregateIDLimit = regexp.MustCompile(selectEscaped + ` AND aggregate_id = \$2 ORDER BY event_sequence LIMIT \$3`).String()
@@ -23,10 +23,10 @@ var (
expectedGetAllEvents = regexp.MustCompile(selectEscaped + ` ORDER BY event_sequence`).String()
expectedInsertStatement = regexp.MustCompile(`INSERT INTO eventstore\.events ` +
`\(event_type, aggregate_type, aggregate_id, aggregate_version, creation_date, event_data, editor_user, editor_service, resource_owner, previous_aggregate_sequence, previous_aggregate_type_sequence\) ` +
`SELECT \$1, \$2, \$3, \$4, COALESCE\(\$5, now\(\)\), \$6, \$7, \$8, \$9, \$10 ` +
`\(event_type, aggregate_type, aggregate_id, aggregate_version, creation_date, event_data, editor_user, editor_service, resource_owner, tenant, previous_aggregate_sequence, previous_aggregate_type_sequence\) ` +
`SELECT \$1, \$2, \$3, \$4, COALESCE\(\$5, now\(\)\), \$6, \$7, \$8, \$9, \$10, \$11 ` +
`WHERE EXISTS \(` +
`SELECT 1 FROM eventstore\.events WHERE aggregate_type = \$11 AND aggregate_id = \$12 HAVING MAX\(event_sequence\) = \$13 OR \(\$14::BIGINT IS NULL AND COUNT\(\*\) = 0\)\) ` +
`SELECT 1 FROM eventstore\.events WHERE aggregate_type = \$12 AND aggregate_id = \$13 HAVING MAX\(event_sequence\) = \$14 OR \(\$14::BIGINT IS NULL AND COUNT\(\*\) = 0\)\) ` +
`RETURNING event_sequence, creation_date`).String()
)
@@ -99,7 +99,7 @@ func (db *dbMock) expectRollback(err error) *dbMock {
func (db *dbMock) expectInsertEvent(e *models.Event, returnedSequence uint64) *dbMock {
db.mock.ExpectQuery(expectedInsertStatement).
WithArgs(
e.Type, e.AggregateType, e.AggregateID, e.AggregateVersion, sqlmock.AnyArg(), Data(e.Data), e.EditorUser, e.EditorService, e.ResourceOwner, Sequence(e.PreviousSequence),
e.Type, e.AggregateType, e.AggregateID, e.AggregateVersion, sqlmock.AnyArg(), Data(e.Data), e.EditorUser, e.EditorService, e.ResourceOwner, e.Tenant, Sequence(e.PreviousSequence),
e.AggregateType, e.AggregateID, Sequence(e.PreviousSequence), Sequence(e.PreviousSequence),
).
WillReturnRows(
@@ -113,7 +113,7 @@ func (db *dbMock) expectInsertEvent(e *models.Event, returnedSequence uint64) *d
func (db *dbMock) expectInsertEventError(e *models.Event) *dbMock {
db.mock.ExpectQuery(expectedInsertStatement).
WithArgs(
e.Type, e.AggregateType, e.AggregateID, e.AggregateVersion, sqlmock.AnyArg(), Data(e.Data), e.EditorUser, e.EditorService, e.ResourceOwner, Sequence(e.PreviousSequence),
e.Type, e.AggregateType, e.AggregateID, e.AggregateVersion, sqlmock.AnyArg(), Data(e.Data), e.EditorUser, e.EditorService, e.ResourceOwner, e.Tenant, Sequence(e.PreviousSequence),
e.AggregateType, e.AggregateID, Sequence(e.PreviousSequence), Sequence(e.PreviousSequence),
).
WillReturnError(sql.ErrTxDone)
@@ -124,7 +124,7 @@ func (db *dbMock) expectInsertEventError(e *models.Event) *dbMock {
func (db *dbMock) expectFilterEventsLimit(aggregateType string, limit uint64, eventCount int) *dbMock {
rows := sqlmock.NewRows(eventColumns)
for i := 0; i < eventCount; i++ {
rows.AddRow(time.Now(), "eventType", Sequence(i+1), Sequence(i), nil, "svc", "hodor", "org", "aggType", "aggID", "v1.0.0")
rows.AddRow(time.Now(), "eventType", Sequence(i+1), Sequence(i), nil, "svc", "hodor", "org", "tenant", "aggType", "aggID", "v1.0.0")
}
db.mock.ExpectQuery(expectedFilterEventsLimitFormat).
WithArgs(aggregateType, limit).
@@ -135,7 +135,7 @@ func (db *dbMock) expectFilterEventsLimit(aggregateType string, limit uint64, ev
func (db *dbMock) expectFilterEventsDesc(aggregateType string, eventCount int) *dbMock {
rows := sqlmock.NewRows(eventColumns)
for i := eventCount; i > 0; i-- {
rows.AddRow(time.Now(), "eventType", Sequence(i+1), Sequence(i), nil, "svc", "hodor", "org", "aggType", "aggID", "v1.0.0")
rows.AddRow(time.Now(), "eventType", Sequence(i+1), Sequence(i), nil, "svc", "hodor", "org", "tenant", "aggType", "aggID", "v1.0.0")
}
db.mock.ExpectQuery(expectedFilterEventsDescFormat).
WillReturnRows(rows)
@@ -145,7 +145,7 @@ func (db *dbMock) expectFilterEventsDesc(aggregateType string, eventCount int) *
func (db *dbMock) expectFilterEventsAggregateIDLimit(aggregateType, aggregateID string, limit uint64) *dbMock {
rows := sqlmock.NewRows(eventColumns)
for i := limit; i > 0; i-- {
rows.AddRow(time.Now(), "eventType", Sequence(i+1), Sequence(i), nil, "svc", "hodor", "org", "aggType", "aggID", "v1.0.0")
rows.AddRow(time.Now(), "eventType", Sequence(i+1), Sequence(i), nil, "svc", "hodor", "org", "tenant", "aggType", "aggID", "v1.0.0")
}
db.mock.ExpectQuery(expectedFilterEventsAggregateIDLimit).
WithArgs(aggregateType, aggregateID, limit).
@@ -156,7 +156,7 @@ func (db *dbMock) expectFilterEventsAggregateIDLimit(aggregateType, aggregateID
func (db *dbMock) expectFilterEventsAggregateIDTypeLimit(aggregateType, aggregateID string, limit uint64) *dbMock {
rows := sqlmock.NewRows(eventColumns)
for i := limit; i > 0; i-- {
rows.AddRow(time.Now(), "eventType", Sequence(i+1), Sequence(i), nil, "svc", "hodor", "org", "aggType", "aggID", "v1.0.0")
rows.AddRow(time.Now(), "eventType", Sequence(i+1), Sequence(i), nil, "svc", "hodor", "org", "tenant", "aggType", "aggID", "v1.0.0")
}
db.mock.ExpectQuery(expectedFilterEventsAggregateIDTypeLimit).
WithArgs(aggregateType, aggregateID, limit).

View File

@@ -26,7 +26,7 @@ func filter(querier Querier, searchQuery *es_models.SearchQueryFactory) (events
rows, err := querier.Query(query, values...)
if err != nil {
logging.Log("SQL-HP3Uk").WithError(err).Info("query failed")
logging.New().WithError(err).Info("query failed")
return nil, errors.ThrowInternal(err, "SQL-IJuyR", "unable to filter events")
}
defer rows.Close()
@@ -55,7 +55,7 @@ func (db *SQL) LatestSequence(ctx context.Context, queryFactory *es_models.Searc
sequence := new(Sequence)
err := rowScanner(row.Scan, sequence)
if err != nil {
logging.Log("SQL-WsxTg").WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Info("query failed")
logging.New().WithError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Info("query failed")
return 0, errors.ThrowInternal(err, "SQL-Yczyx", "unable to filter latest sequence")
}
return uint64(*sequence), nil

View File

@@ -23,6 +23,7 @@ const (
", editor_service" +
", editor_user" +
", resource_owner" +
", tenant" +
", aggregate_type" +
", aggregate_id" +
", aggregate_version" +
@@ -32,7 +33,7 @@ const (
func buildQuery(queryFactory *es_models.SearchQueryFactory) (query string, limit uint64, values []interface{}, rowScanner func(s scan, dest interface{}) error) {
searchQuery, err := queryFactory.Build()
if err != nil {
logging.Log("SQL-cshKu").WithError(err).Warn("search query factory invalid")
logging.New().WithError(err).Warn("search query factory invalid")
return "", 0, nil, nil
}
query, rowScanner = prepareColumns(searchQuery.Columns)
@@ -116,13 +117,14 @@ func prepareColumns(columns es_models.Columns) (string, func(s scan, dest interf
&event.EditorService,
&event.EditorUser,
&event.ResourceOwner,
&event.Tenant,
&event.AggregateType,
&event.AggregateID,
&event.AggregateVersion,
)
if err != nil {
logging.Log("SQL-kn1Sw").WithError(err).Warn("unable to scan row")
logging.New().WithError(err).Warn("unable to scan row")
return z_errors.ThrowInternal(err, "SQL-J0hFS", "unable to scan row")
}
@@ -175,6 +177,8 @@ func getField(field es_models.Field) string {
return "event_sequence"
case es_models.Field_ResourceOwner:
return "resource_owner"
case es_models.Field_Tenant:
return "tenant"
case es_models.Field_EditorService:
return "editor_service"
case es_models.Field_EditorUser:

View File

@@ -80,6 +80,7 @@ func Test_getField(t *testing.T) {
es_models.Field_AggregateID: "aggregate_id",
es_models.Field_LatestSequence: "event_sequence",
es_models.Field_ResourceOwner: "resource_owner",
es_models.Field_Tenant: "tenant",
es_models.Field_EditorService: "editor_service",
es_models.Field_EditorUser: "editor_user",
es_models.Field_EventType: "event_type",
@@ -234,8 +235,8 @@ func Test_prepareColumns(t *testing.T) {
dest: new(es_models.Event),
},
res: res{
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events",
dbRow: []interface{}{time.Time{}, es_models.EventType(""), uint64(5), Sequence(0), Data(nil), "", "", "", es_models.AggregateType("user"), "hodor", es_models.Version("")},
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, tenant, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events",
dbRow: []interface{}{time.Time{}, es_models.EventType(""), uint64(5), Sequence(0), Data(nil), "", "", "", "", es_models.AggregateType("user"), "hodor", es_models.Version("")},
expected: es_models.Event{AggregateID: "hodor", AggregateType: "user", Sequence: 5, Data: make(Data, 0)},
},
},
@@ -246,7 +247,7 @@ func Test_prepareColumns(t *testing.T) {
dest: new(uint64),
},
res: res{
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events",
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, tenant, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events",
dbErr: errors.IsErrorInvalidArgument,
},
},
@@ -258,7 +259,7 @@ func Test_prepareColumns(t *testing.T) {
dbErr: sql.ErrConnDone,
},
res: res{
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events",
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, tenant, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events",
dbErr: errors.IsInternal,
},
},
@@ -429,7 +430,7 @@ func Test_buildQuery(t *testing.T) {
queryFactory: es_models.NewSearchQueryFactory("user").OrderDesc(),
},
res: res{
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC",
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, tenant, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC",
rowScanner: true,
values: []interface{}{es_models.AggregateType("user")},
},
@@ -440,7 +441,7 @@ func Test_buildQuery(t *testing.T) {
queryFactory: es_models.NewSearchQueryFactory("user").Limit(5),
},
res: res{
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence LIMIT $2",
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, tenant, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence LIMIT $2",
rowScanner: true,
values: []interface{}{es_models.AggregateType("user"), uint64(5)},
limit: 5,
@@ -452,7 +453,7 @@ func Test_buildQuery(t *testing.T) {
queryFactory: es_models.NewSearchQueryFactory("user").Limit(5).OrderDesc(),
},
res: res{
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC LIMIT $2",
query: "SELECT creation_date, event_type, event_sequence, previous_aggregate_sequence, event_data, editor_service, editor_user, resource_owner, tenant, aggregate_type, aggregate_id, aggregate_version FROM eventstore.events WHERE aggregate_type = $1 ORDER BY event_sequence DESC LIMIT $2",
rowScanner: true,
values: []interface{}{es_models.AggregateType("user"), uint64(5)},
limit: 5,

View File

@@ -23,6 +23,7 @@ type Aggregate struct {
editorService string
editorUser string
resourceOwner string
tenant string
Events []*Event
Precondition *precondition
}
@@ -55,6 +56,7 @@ func (a *Aggregate) AppendEvent(typ EventType, payload interface{}) (*Aggregate,
EditorService: a.editorService,
EditorUser: a.editorUser,
ResourceOwner: a.resourceOwner,
Tenant: a.tenant,
}
a.Events = append(a.Events, e)

View File

@@ -20,6 +20,7 @@ func (c *AggregateCreator) NewAggregate(ctx context.Context, id string, typ Aggr
ctxData := authz.GetCtxData(ctx)
editorUser := ctxData.UserID
resourceOwner := ctxData.OrgID
tenant := ctxData.TenantID
aggregate := &Aggregate{
ID: id,
@@ -30,6 +31,7 @@ func (c *AggregateCreator) NewAggregate(ctx context.Context, id string, typ Aggr
editorService: c.serviceName,
editorUser: editorUser,
resourceOwner: resourceOwner,
tenant: tenant,
}
for _, opt := range opts {

View File

@@ -28,6 +28,7 @@ type Event struct {
EditorService string
EditorUser string
ResourceOwner string
Tenant string
}
func eventData(i interface{}) ([]byte, error) {

View File

@@ -11,4 +11,5 @@ const (
Field_EditorUser
Field_EventType
Field_CreationDate
Field_Tenant
)

View File

@@ -8,6 +8,7 @@ type ObjectRoot struct {
AggregateID string `json:"-"`
Sequence uint64 `json:"-"`
ResourceOwner string `json:"-"`
Tenant string `json:"-"`
CreationDate time.Time `json:"-"`
ChangeDate time.Time `json:"-"`
}
@@ -21,6 +22,9 @@ func (o *ObjectRoot) AppendEvent(event *Event) {
if o.ResourceOwner == "" {
o.ResourceOwner = event.ResourceOwner
}
if o.Tenant == "" {
o.Tenant = event.Tenant
}
o.ChangeDate = event.CreationDate
if o.CreationDate.IsZero() {

View File

@@ -17,6 +17,7 @@ type SearchQueryFactory struct {
sequenceTo uint64
eventTypes []EventType
resourceOwner string
tenant string
creationDate time.Time
}
@@ -62,6 +63,8 @@ func FactoryFromSearchQuery(query *SearchQuery) *SearchQueryFactory {
}
case Field_ResourceOwner:
factory = factory.ResourceOwner(filter.value.(string))
case Field_Tenant:
factory = factory.Tenant(filter.value.(string))
case Field_EventType:
factory = factory.EventTypes(filter.value.([]EventType)...)
case Field_EditorService, Field_EditorUser:
@@ -120,6 +123,11 @@ func (factory *SearchQueryFactory) ResourceOwner(resourceOwner string) *SearchQu
return factory
}
func (factory *SearchQueryFactory) Tenant(tenant string) *SearchQueryFactory {
factory.tenant = tenant
return factory
}
func (factory *SearchQueryFactory) CreationDateNewer(time time.Time) *SearchQueryFactory {
factory.creationDate = time
return factory
@@ -151,6 +159,7 @@ func (factory *SearchQueryFactory) Build() (*searchQuery, error) {
factory.sequenceToFilter,
factory.eventTypeFilter,
factory.resourceOwnerFilter,
factory.tenantFilter,
factory.creationDateNewerFilter,
} {
if filter := f(); filter != nil {
@@ -222,6 +231,13 @@ func (factory *SearchQueryFactory) resourceOwnerFilter() *Filter {
return NewFilter(Field_ResourceOwner, factory.resourceOwner, Operation_Equals)
}
func (factory *SearchQueryFactory) tenantFilter() *Filter {
if factory.tenant == "" {
return nil
}
return NewFilter(Field_Tenant, factory.tenant, Operation_Equals)
}
func (factory *SearchQueryFactory) creationDateNewerFilter() *Filter {
if factory.creationDate.IsZero() {
return nil

View File

@@ -69,6 +69,10 @@ func (q *SearchQuery) ResourceOwnerFilter(resourceOwner string) *SearchQuery {
return q.setFilter(NewFilter(Field_ResourceOwner, resourceOwner, Operation_Equals))
}
func (q *SearchQuery) TenantFilter(tenant string) *SearchQuery {
return q.setFilter(NewFilter(Field_Tenant, tenant, Operation_Equals))
}
func (q *SearchQuery) CreationDateNewerFilter(time time.Time) *SearchQuery {
return q.setFilter(NewFilter(Field_CreationDate, time, Operation_Greater))
}

View File

@@ -43,7 +43,7 @@ func ReduceEvent(handler Handler, event *models.Event) {
}()
currentSequence, err := handler.CurrentSequence()
if err != nil {
logging.Log("HANDL-BmpkC").WithError(err).Warn("unable to get current sequence")
logging.New().WithError(err).Warn("unable to get current sequence")
return
}
@@ -65,7 +65,7 @@ func ReduceEvent(handler Handler, event *models.Event) {
return
}
if unprocessedEvent.Sequence < currentSequence {
logging.LogWithFields("QUERY-DOYVN",
logging.WithFields(
"unprocessed", unprocessedEvent.Sequence,
"current", currentSequence,
"view", handler.ViewModel()).

View File

@@ -1,11 +1,12 @@
package spooler
import (
"github.com/caos/zitadel/internal/eventstore/v1"
"math/rand"
"os"
"github.com/caos/logging"
v1 "github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/query"
"github.com/caos/zitadel/internal/id"
)
@@ -21,7 +22,7 @@ func (c *Config) New() *Spooler {
lockID, err := os.Hostname()
if err != nil || lockID == "" {
lockID, err = id.SonyFlakeGenerator.Next()
logging.Log("SPOOL-bdO56").OnError(err).Panic("unable to generate lockID")
logging.OnError(err).Panic("unable to generate lockID")
}
//shuffle the handlers for better balance when running multiple pods

View File

@@ -2,15 +2,14 @@ package spooler
import (
"context"
"github.com/getsentry/sentry-go"
"github.com/caos/zitadel/internal/eventstore/v1"
"strconv"
"sync"
"time"
"github.com/caos/logging"
"github.com/getsentry/sentry-go"
v1 "github.com/caos/zitadel/internal/eventstore/v1"
"github.com/caos/zitadel/internal/eventstore/v1/models"
"github.com/caos/zitadel/internal/eventstore/v1/query"
"github.com/caos/zitadel/internal/telemetry/tracing"
@@ -38,7 +37,7 @@ type spooledHandler struct {
}
func (s *Spooler) Start() {
defer logging.LogWithFields("SPOOL-N0V1g", "lockerID", s.lockID, "workers", s.workers).Info("spooler started")
defer logging.WithFields("lockerID", s.lockID, "workers", s.workers).Info("spooler started")
if s.workers < 1 {
return
}
@@ -116,7 +115,7 @@ func (s *spooledHandler) process(ctx context.Context, events []*models.Event, wo
for i, event := range events {
select {
case <-ctx.Done():
logging.LogWithFields("SPOOL-FTKwH", "view", s.ViewModel(), "worker", workerID, "traceID", tracing.TraceIDFromCtx(ctx)).Debug("context canceled")
logging.WithFields("view", s.ViewModel(), "worker", workerID, "traceID", tracing.TraceIDFromCtx(ctx)).Debug("context canceled")
return nil
default:
if err := s.Reduce(event); err != nil {
@@ -130,7 +129,7 @@ func (s *spooledHandler) process(ctx context.Context, events []*models.Event, wo
}
}
err := s.OnSuccess()
logging.LogWithFields("SPOOL-49ods", "view", s.ViewModel(), "worker", workerID, "traceID", tracing.TraceIDFromCtx(ctx)).OnError(err).Warn("could not process on success func")
logging.WithFields("view", s.ViewModel(), "worker", workerID, "traceID", tracing.TraceIDFromCtx(ctx)).OnError(err).Warn("could not process on success func")
return err
}
@@ -141,7 +140,7 @@ func (s *spooledHandler) query(ctx context.Context) ([]*models.Event, error) {
}
factory := models.FactoryFromSearchQuery(query)
sequence, err := s.eventstore.LatestSequence(ctx, factory)
logging.Log("SPOOL-7SciK").OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to query latest sequence")
logging.OnError(err).WithField("traceID", tracing.TraceIDFromCtx(ctx)).Debug("unable to query latest sequence")
var processedSequence uint64
for _, filter := range query.Filters {
if filter.GetField() == models.Field_LatestSequence {