-
Notifications
You must be signed in to change notification settings - Fork 0
CXP-533 Performance improvements to admin_event_feed #114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
5e0b005
eec7d0f
9d11cc1
98dd90f
868d049
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,13 +23,51 @@ import ( | |
| gwclient "github.com/conductorone/baton-google-workspace/pkg/client" | ||
| ) | ||
|
|
||
| // adminEventNames lists the admin-application event names the feed subscribes to. | ||
| // The Google Reports API accepts one eventName per request, so ListEvents issues | ||
| // one ListActivities call per name and merges the results. | ||
| var adminEventNames = []string{ | ||
| // GROUP_SETTINGS | ||
| "CREATE_GROUP", | ||
| "CHANGE_GROUP_DESCRIPTION", | ||
| "CHANGE_GROUP_NAME", | ||
| "CHANGE_GROUP_EMAIL", | ||
| "ADD_GROUP_MEMBER", | ||
| "UPDATE_GROUP_MEMBER", | ||
| "DELETE_GROUP", | ||
| // USER_SETTINGS | ||
| "ACCEPT_USER_INVITATION", | ||
| "CHANGE_USER_ORGANIZATION", | ||
| "ADD_DISPLAY_NAME", | ||
| "CHANGE_DISPLAY_NAME", | ||
| "CHANGE_FIRST_NAME", | ||
| "CHANGE_LAST_NAME", | ||
| "CREATE_USER", | ||
| "RENAME_USER", | ||
| } | ||
|
|
||
| // adminGroupEventNames is the subset of adminEventNames that belong to GROUP_SETTINGS handling. | ||
| var adminGroupEventNames = map[string]bool{ | ||
| "CREATE_GROUP": true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dual-maintenance risk between These two declarations overlap: if a new group event is added to var adminGroupEventNames = map[string]bool{ ... }
var adminUserEventNames = map[string]bool{ ... }
var adminEventNames = func() []string {
names := make([]string, 0, len(adminGroupEventNames)+len(adminUserEventNames))
for k := range adminGroupEventNames { names = append(names, k) }
for k := range adminUserEventNames { names = append(names, k) }
return names
}() |
||
| "CHANGE_GROUP_DESCRIPTION": true, | ||
| "CHANGE_GROUP_NAME": true, | ||
| "CHANGE_GROUP_EMAIL": true, | ||
| "ADD_GROUP_MEMBER": true, | ||
| "UPDATE_GROUP_MEMBER": true, | ||
| "DELETE_GROUP": true, | ||
| } | ||
|
|
||
| type cacheEntry struct { | ||
| Id string | ||
| DisplayName string | ||
| } | ||
|
|
||
| type cacheMap map[string]cacheEntry | ||
|
|
||
| // adminActivitiesPageSize is the number of activity items requested per ListActivities | ||
| // call. The Google Reports API maximum is 1000. | ||
| const adminActivitiesPageSize = 1000 | ||
|
|
||
| type adminEventFeed struct { | ||
| client *gwclient.GoogleWorkspaceClient | ||
|
|
||
|
|
@@ -43,68 +81,78 @@ type adminEventFeed struct { | |
| func (f *adminEventFeed) ListEvents(ctx context.Context, startAt *timestamppb.Timestamp, pToken *pagination.StreamToken) ([]*v2.Event, *pagination.StreamState, annotations.Annotations, error) { | ||
| l := ctxzap.Extract(ctx) | ||
|
|
||
| var streamState *pagination.StreamState | ||
|
|
||
| cursor, err := unmarshalPageToken(pToken, startAt) | ||
| if err != nil { | ||
| return nil, nil, nil, fmt.Errorf("failed to unmarshal page token: %w", err) | ||
| } | ||
|
|
||
| r, err := f.client.ListActivities(ctx, "all", "admin", "", cursor.StartAt, cursor.NextPageToken, int64(pToken.Size)) | ||
| if err != nil { | ||
| return nil, nil, nil, fmt.Errorf("google-workspace: failed to list admin activities: %w", err) | ||
| } | ||
|
|
||
| latestEvent, err := time.Parse(time.RFC3339, cursor.LatestEventSeen) | ||
| if err != nil { | ||
| return nil, nil, nil, fmt.Errorf("failed to parse latest event time in admin event feed: %w", err) | ||
| } | ||
|
|
||
| events := make([]*v2.Event, 0) | ||
| for _, activity := range r.Items { | ||
| occurredAt := convertIdTimeToTimestamp(activity.Id.Time) | ||
| if occurredAt == nil { | ||
| // Set occurred at to epoch so that it should never be after the latest event | ||
| // Unless latest event is before epoch for some reason | ||
| occurredAt = timestamppb.New(time.Unix(0, 0)) | ||
| // On the first call EventPageTokens is nil — fetch every event name from StartAt. | ||
| // On continuation calls fetch only names that still have a next page token. | ||
| fetchNames := adminEventNames | ||
| if len(cursor.EventPageTokens) > 0 { | ||
| fetchNames = make([]string, 0, len(cursor.EventPageTokens)) | ||
| for name := range cursor.EventPageTokens { | ||
| fetchNames = append(fetchNames, name) | ||
| } | ||
| if occurredAt.AsTime().After(latestEvent) { | ||
| cursor.LatestEventSeen = occurredAt.AsTime().Format(time.RFC3339) | ||
| latestEvent = occurredAt.AsTime() | ||
| } | ||
|
|
||
| events := make([]*v2.Event, 0) | ||
| nextTokens := make(map[string]string) | ||
|
|
||
| for _, eventName := range fetchNames { | ||
| isGroupEvent := adminGroupEventNames[eventName] | ||
| r, err := f.client.ListActivities(ctx, "all", "admin", eventName, cursor.StartAt, cursor.EventPageTokens[eventName], adminActivitiesPageSize) | ||
| if err != nil { | ||
| return nil, nil, nil, fmt.Errorf("google-workspace: failed to list admin activities for %s: %w", eventName, err) | ||
| } | ||
| // There can be multiple events, have not found an example of this yet | ||
| for _, evt := range activity.Events { | ||
| switch evt.Type { | ||
| case "GROUP_SETTINGS": | ||
| changeEvents, err := f.handleGroupEvent(ctx, activity.Id.UniqueQualifier, occurredAt, evt) | ||
| if err != nil { | ||
| l.Error("failed to handle group event", zap.Error(err)) | ||
|
|
||
| for _, activity := range r.Items { | ||
| occurredAt := convertIdTimeToTimestamp(activity.Id.Time) | ||
| if occurredAt == nil { | ||
| occurredAt = timestamppb.New(time.Unix(0, 0)) | ||
| } | ||
| if occurredAt.AsTime().After(latestEvent) { | ||
| cursor.LatestEventSeen = occurredAt.AsTime().Format(time.RFC3339) | ||
| latestEvent = occurredAt.AsTime() | ||
| } | ||
| for _, evt := range activity.Events { | ||
|
JavierCarnelli-ConductorOne marked this conversation as resolved.
|
||
| if evt.Name != eventName { | ||
| continue | ||
| } | ||
| events = append(events, changeEvents...) | ||
| case "USER_SETTINGS": | ||
| changeEvents, err := f.handleUserEvent(ctx, activity.Id.UniqueQualifier, occurredAt, evt) | ||
| if err != nil { | ||
| l.Error("failed to handle user event", zap.Error(err)) | ||
| var changeEvents []*v2.Event | ||
| var evtErr error | ||
| if isGroupEvent { | ||
| changeEvents, evtErr = f.handleGroupEvent(ctx, activity.Id.UniqueQualifier, occurredAt, evt) | ||
| } else { | ||
| changeEvents, evtErr = f.handleUserEvent(ctx, activity.Id.UniqueQualifier, occurredAt, evt) | ||
| } | ||
| if evtErr != nil { | ||
| l.Error("failed to handle admin event", zap.String("event_name", evt.Name), zap.Error(evtErr)) | ||
| continue | ||
| } | ||
| events = append(events, changeEvents...) | ||
| default: | ||
| l.Debug("google-workspace-event-feed: skipping event", zap.String("event", evt.Name), zap.String("type", evt.Type)) | ||
| continue | ||
| } | ||
| } | ||
|
|
||
| if r.NextPageToken != "" { | ||
| nextTokens[eventName] = r.NextPageToken | ||
| } | ||
| } | ||
|
|
||
| l.Debug("google-workspace-event-feed: listed events", | ||
| zap.Int("count", len(r.Items)), | ||
| zap.String("next_page_token", r.NextPageToken), | ||
| zap.Any("start_at", startAt), | ||
| zap.Any("latest_event", cursor.LatestEventSeen), | ||
| l.Debug("google-workspace-event-feed: listed admin events", | ||
| zap.Int("event_names_fetched", len(fetchNames)), | ||
| zap.Int("events_produced", len(events)), | ||
| zap.String("latest_event", cursor.LatestEventSeen), | ||
| ) | ||
|
|
||
| cursor.NextPageToken = r.NextPageToken | ||
| if r.NextPageToken == "" { | ||
| hasMore := len(nextTokens) > 0 | ||
| cursor.EventPageTokens = nextTokens | ||
| if !hasMore { | ||
| cursor.StartAt = cursor.LatestEventSeen | ||
| cursor.LatestEventSeen = "" | ||
| } | ||
|
|
@@ -113,12 +161,11 @@ func (f *adminEventFeed) ListEvents(ctx context.Context, startAt *timestamppb.Ti | |
| if err != nil { | ||
| return nil, nil, nil, fmt.Errorf("failed to marshal cursor token in admin event feed: %w", err) | ||
| } | ||
| streamState = &pagination.StreamState{ | ||
| Cursor: cursorToken, | ||
| HasMore: r.NextPageToken != "", | ||
| } | ||
|
|
||
| return events, streamState, nil, nil | ||
| return events, &pagination.StreamState{ | ||
| Cursor: cursorToken, | ||
| HasMore: hasMore, | ||
| }, nil, nil | ||
| } | ||
|
|
||
| func (f *adminEventFeed) handleGroupEvent(ctx context.Context, uniqueQualifier int64, occurredAt *timestamppb.Timestamp, activityEvt *reports.ActivityEvents) ([]*v2.Event, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing
This gap is in the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (low) Silent nil drop on missing In the evt, err = f.newGroupChangedEvent(ctx, uniqueQualifier, occurredAt, "NEW_VALUE", activityEvent)
if evt == nil {
l.Debug("google-workspace-event-feed: CHANGE_GROUP_EMAIL missing NEW_VALUE parameter, skipping")
return events, nil
} |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,10 @@ import ( | |
|
|
||
| var privateAppIDRegex = regexp.MustCompile("[0-9]{21}") | ||
|
|
||
| // usageActivitiesPageSize is the number of activity items requested per ListActivities | ||
| // call. The Google Reports API maximum is 1000. | ||
| const usageActivitiesPageSize = 1000 | ||
|
|
||
| // maxEventFeedLookback caps how far back event feeds query the Google Reports API. | ||
| // Google page tokens expire after ~24h, so a cursor left mid-pagination (e.g. after | ||
| // a connector restart or a transient timeout) would otherwise keep requesting the | ||
|
|
@@ -81,10 +85,13 @@ func hasParameter(name string, parameters []*reportsAdmin.ActivityEventsParamete | |
| } | ||
|
|
||
| type pageToken struct { | ||
| LatestEventSeen string `json:"latest_event_seen,omitempty"` | ||
| NextPageToken string `json:"next_page_token,omitempty"` | ||
| StartAt string `json:"start_at,omitempty"` | ||
| PageSize int `json:"page_size,omitempty"` | ||
| LatestEventSeen string `json:"latest_event_seen,omitempty"` | ||
| NextPageToken string `json:"next_page_token,omitempty"` | ||
| StartAt string `json:"start_at,omitempty"` | ||
| PageSize int `json:"page_size,omitempty"` | ||
| // EventPageTokens holds per-event-name pagination cursors for feeds that | ||
| // issue one ListActivities request per event name (e.g. adminEventFeed). | ||
| EventPageTokens map[string]string `json:"event_page_tokens,omitempty"` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The stale-cursor reset in Clear it alongside the others: add |
||
| } | ||
|
|
||
| func unmarshalPageToken(token *pagination.StreamToken, defaultStart *timestamppb.Timestamp) (*pageToken, error) { | ||
|
|
@@ -151,7 +158,7 @@ func (f *usageEventFeed) ListEvents(ctx context.Context, startAt *timestamppb.Ti | |
| return nil, nil, nil, fmt.Errorf("failed to unmarshal page token in usage event feed: %w", err) | ||
| } | ||
|
|
||
| r, err := f.c.ListActivities(ctx, "all", "token", "authorize", cursor.StartAt, cursor.NextPageToken, int64(pToken.Size)) | ||
| r, err := f.c.ListActivities(ctx, "all", "token", "authorize", cursor.StartAt, cursor.NextPageToken, usageActivitiesPageSize) | ||
| if err != nil { | ||
| return nil, nil, nil, fmt.Errorf("google-workspace: failed to list token activities: %w", err) | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DELETE_GROUPcauses a wasted API call every syncDELETE_GROUPis inadminEventNamessoListEventsissues a realListActivitiesrequest for it on every cycle — buthandleGroupEventdrops all results with a comment explaining the group can't be looked up after deletion. Consider removing it from the list (or adding it to a separate commented-out section) so we don't pay the API cost for events we can't use.