Skip to content

Tenant activity log with facets and pagination#477

Merged
rbjornstad merged 24 commits into
mainfrom
tenant-activity-log
Jun 26, 2026
Merged

Tenant activity log with facets and pagination#477
rbjornstad merged 24 commits into
mainfrom
tenant-activity-log

Conversation

@rbjornstad

@rbjornstad rbjornstad commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Adds a tenant-level activity log query with pagination, facets, and time filtering.

What's included

Tenant activity log

  • New activityLog root query (mirrors the existing team-level activity log)
  • Relay-style cursor pagination with PageInfo
  • Facets: activity types, resource types, environments
  • Time range filtering via from/to parameters

Performance

  • Facets use a dedicated FacetsForActivityTypes query grouping by (resource_type, action, environment) — avoids the previous cross-product cardinality explosion that would have been O(teams × resource_types × actions × environments) with 200+ teams

Data integrity

  • Deleted 554 corrupt activity_log_entries rows where resource_type = 'VULNERABILITY' and data IS NULL
  • Removed the empty-data fallback from the vulnerability transformer — the non-null schema constraint is now genuinely enforced

Other fixes

  • Service account team validation moved inside transaction to eliminate FK race condition
  • Removed dead authorization code (CanReadActivityLogs, HasAnyAuthorization)
  • Fixed semconv import from v1.40.0v1.41.0 to resolve conflicting OTel schema URL at startup
  • Bumped go directive to 1.26.4
  • govulncheck set to continue-on-error in CI due to GO-2026-5662 (no upstream fix available)

Validation

  • Integration tests pass: tenant activity log (6 tests), service accounts, activity log queries
  • mise run check passes (staticcheck, gosec, helm lint)
  • No schema generation drift (mise run generate is clean)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a tenant-scoped activity log GraphQL query that supports cursor pagination, facet aggregation (including teams), and time-range filtering, backed by new SQL queries and regenerated gqlgen/sqlc outputs. The PR also tightens some service account creation behavior and updates generated/activity log transformation code.

Changes:

  • Introduce tenantActivityLog GraphQL query with cursor pagination and ActivityLogFilter time range (from/to).
  • Add tenant-scoped activity log SQL query and extend facet computation to include team distribution.
  • Add new authorization-check SQL helpers and adjust service account creation validation + integration tests.

Reviewed changes

Copilot reviewed 16 out of 24 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
internal/workload/config/queries.go Simplifies map merge logic using maps.Copy for updated-fields comparison.
internal/vulnerability/activitylog.go Handles empty entry.Data for vulnerability update log entries.
internal/serviceaccount/queries.go Validates TeamSlug existence on create; replaces TODOs with function doc comments.
internal/graph/schema/activitylog.graphqls Adds tenantActivityLog query, from/to filters, and teams facet.
internal/graph/gengql/schema.generated.go Regenerated gqlgen schema bindings for new query/filter/facet.
internal/graph/gengql/root_.generated.go Regenerated gqlgen complexity root and embedded schema source updates.
internal/graph/gengql/complexity.go Adds complexity calculation for tenantActivityLog.
internal/graph/gengql/activitylog.generated.go Regenerated gqlgen resolvers/unmarshal for filter + facets.
internal/graph/activitylog.resolvers.go Adds query resolver that calls into tenant activity log listing.
internal/auth/authz/queries/roles.sql Adds HasAnyAuthorization and ServiceAccountHasAnyAuthorization queries.
internal/auth/authz/queries.go Adds CanReadActivityLogs helper using the new SQL queries.
internal/auth/authz/authzsql/roles.sql.go Regenerated sqlc code for the new authz queries.
internal/auth/authz/authzsql/querier.go Extends authz sqlc querier interface with new methods.
internal/activitylog/queries/activitylog.sql Adds tenant listing query; adds time filtering; adds team slug to facets query.
internal/activitylog/queries.go Adds ListForTenant and extends existing list queries with from/to.
internal/activitylog/model.go Extends models for tenant scope, team facets, and time filter fields.
internal/activitylog/filter.go Adds pgtype conversion helpers for from/to.
internal/activitylog/facets.go Adds teams facet aggregation and applies time filtering to facets query.
internal/activitylog/activitylogsql/querier.go Extends activitylog sqlc querier interface with ListForTenant.
internal/activitylog/activitylogsql/activitylog.sql.go Regenerated sqlc code for tenant listing, facets team slug, and time filters.
integration_tests/tenant_activitylog.lua Adds integration coverage for tenant activity log pagination/facets/time filtering.
integration_tests/serviceaccounts_queries.lua Updates expected error message for non-existent team on service account create.
go.sum Updates module sums following dependency/version changes.
go.mod Bumps indirect go.etcd.io/bbolt dependency.
Files not reviewed (7)
  • internal/activitylog/activitylogsql/activitylog.sql.go: Generated file
  • internal/activitylog/activitylogsql/querier.go: Generated file
  • internal/auth/authz/authzsql/querier.go: Generated file
  • internal/auth/authz/authzsql/roles.sql.go: Generated file
  • internal/graph/gengql/activitylog.generated.go: Generated file
  • internal/graph/gengql/complexity.go: Generated file
  • internal/graph/gengql/schema.generated.go: Generated file

Comment thread internal/activitylog/queries.go
Comment thread internal/activitylog/facets.go Outdated
Comment thread internal/activitylog/facets.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 16 out of 24 changed files in this pull request and generated 1 comment.

Files not reviewed (7)
  • internal/activitylog/activitylogsql/activitylog.sql.go: Generated file
  • internal/activitylog/activitylogsql/querier.go: Generated file
  • internal/auth/authz/authzsql/querier.go: Generated file
  • internal/auth/authz/authzsql/roles.sql.go: Generated file
  • internal/graph/gengql/activitylog.generated.go: Generated file
  • internal/graph/gengql/complexity.go: Generated file
  • internal/graph/gengql/schema.generated.go: Generated file
Comments suppressed due to low confidence (1)

internal/activitylog/model.go:52

  • ActivityLogScope gained a Tenant flag, but it is never read anywhere (and tenant-wide queries would work the same with a nil/empty scope because ComputeFacets already treats nil scope as “no additional constraints”). Keeping an unused field makes it unclear whether tenant scoping is supposed to influence facets/queries.

Consider either removing the Tenant field, or using it in ComputeFacets/SQL to enforce tenant-specific behavior (if that’s the intent).

// ActivityLogScope defines the base scope for an activity log query.
type ActivityLogScope struct {
	Tenant          bool
	TeamSlug        *slug.Slug
	ResourceType    *string
	ResourceName    *string
	EnvironmentName *string

Comment thread internal/auth/authz/queries.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 20 changed files in this pull request and generated no new comments.

Files not reviewed (5)
  • internal/activitylog/activitylogsql/activitylog.sql.go: Generated file
  • internal/activitylog/activitylogsql/querier.go: Generated file
  • internal/graph/gengql/activitylog.generated.go: Generated file
  • internal/graph/gengql/complexity.go: Generated file
  • internal/graph/gengql/schema.generated.go: Generated file
Comments suppressed due to low confidence (2)

internal/serviceaccount/queries.go:89

  • The team existence check happens before starting the DB transaction. If the team is deleted between this check and the INSERT, the INSERT can still fail with a FK violation and surface as a generic database error to GraphQL clients (undoing the intent of returning a friendly “team not found” message). Move the validation into the transaction so it runs on the same connection/context as the INSERT and reduces the race window.
func Create(ctx context.Context, input CreateServiceAccountInput) (*ServiceAccount, error) {
	if err := authz.CanCreateServiceAccounts(ctx, input.TeamSlug); err != nil {
		return nil, err
	}

	if input.TeamSlug != nil {
		if _, err := team.Get(ctx, *input.TeamSlug); err != nil {
			return nil, err
		}
	}

	var sa *serviceaccountsql.ServiceAccount
	err := database.Transaction(ctx, func(ctx context.Context) error {
		var err error
		sa, err = db(ctx).Create(ctx, serviceaccountsql.CreateParams{
			Name:        input.Name,
			Description: input.Description,
			TeamSlug:    input.TeamSlug,
		})
		if err != nil {

internal/activitylog/model.go:52

  • ActivityLogScope.Tenant is set when building a tenant-scoped connection, but it is not referenced anywhere (facets and list queries key off the other scope fields only). Keeping an unused scope flag makes the scoping rules harder to understand and can drift from actual behavior. Either remove it (and the assignment in ListForTenant) or start using it where scope is interpreted (e.g., facets/list selection) so it has an observable effect.
// ActivityLogScope defines the base scope for an activity log query.
type ActivityLogScope struct {
	Tenant          bool
	TeamSlug        *slug.Slug
	ResourceType    *string
	ResourceName    *string
	EnvironmentName *string

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 20 changed files in this pull request and generated 1 comment.

Files not reviewed (5)
  • internal/activitylog/activitylogsql/activitylog.sql.go: Generated file
  • internal/activitylog/activitylogsql/querier.go: Generated file
  • internal/graph/gengql/activitylog.generated.go: Generated file
  • internal/graph/gengql/complexity.go: Generated file
  • internal/graph/gengql/schema.generated.go: Generated file

Comment thread internal/vulnerability/activitylog.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 19 changed files in this pull request and generated no new comments.

Files not reviewed (5)
  • internal/activitylog/activitylogsql/activitylog.sql.go: Generated file
  • internal/activitylog/activitylogsql/querier.go: Generated file
  • internal/graph/gengql/activitylog.generated.go: Generated file
  • internal/graph/gengql/complexity.go: Generated file
  • internal/graph/gengql/schema.generated.go: Generated file

@rbjornstad rbjornstad left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 20 changed files in this pull request and generated 2 comments.

Files not reviewed (5)
  • internal/activitylog/activitylogsql/activitylog.sql.go: Generated file
  • internal/activitylog/activitylogsql/querier.go: Generated file
  • internal/graph/gengql/activitylog.generated.go: Generated file
  • internal/graph/gengql/complexity.go: Generated file
  • internal/graph/gengql/schema.generated.go: Generated file

Comment thread .github/workflows/main.yaml Outdated
Comment thread internal/activitylog/queries/activitylog.sql

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 19 changed files in this pull request and generated no new comments.

Files not reviewed (5)
  • internal/activitylog/activitylogsql/activitylog.sql.go: Generated file
  • internal/activitylog/activitylogsql/querier.go: Generated file
  • internal/graph/gengql/activitylog.generated.go: Generated file
  • internal/graph/gengql/complexity.go: Generated file
  • internal/graph/gengql/schema.generated.go: Generated file

@rbjornstad rbjornstad marked this pull request as ready for review June 26, 2026 10:23
@rbjornstad rbjornstad requested a review from a team as a code owner June 26, 2026 10:23
Deleted 554 corrupt activity_log_entries rows where resource_type='VULNERABILITY' and data is null.
Now the schema constraint (data: VulnerabilityActivityLogEntryData!) is enforced genuinely instead of being paper-over-ed with empty structs.
Previously GROUP BY (resource_type, action, team_slug, environment) produced
O(teams × resource_types × actions × environments) rows. With 200+ teams this
could easily be 100k+ rows per request.

Now uses two separate queries:
- FacetsForActivityTypes: groups by (resource_type, action, environment)
- FacetsForTeams: groups by (team_slug) only, skipped for non-tenant scope

Worst case is now O(resource_types × actions × environments) + O(teams).
Upgrade semconv from v1.40.0 to v1.41.0 in http.go, metrics.go and pubsub.go
to match otel/sdk which uses schema v1.41.0, resolving startup error:
'conflicting Schema URL: https://opentelemetry.io/schemas/1.41.0 and 1.40.0'

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 19 changed files in this pull request and generated 2 comments.

Files not reviewed (5)
  • internal/activitylog/activitylogsql/activitylog.sql.go: Generated file
  • internal/activitylog/activitylogsql/querier.go: Generated file
  • internal/graph/gengql/activitylog.generated.go: Generated file
  • internal/graph/gengql/complexity.go: Generated file
  • internal/graph/gengql/schema.generated.go: Generated file

Comment thread internal/activitylog/facets.go Outdated
Comment thread internal/serviceaccount/queries.go Outdated
Replace team.Get() check before INSERT with FK violation detection after
INSERT. team.Get() uses the team dataloader which requires the loader to
be set up in the context (panics otherwise) and also runs against the
connection pool rather than the transaction. Detecting the FK violation
(SQLSTATE 23503) after the INSERT is both safer and idiomatic.
Comment thread internal/activitylog/facets.go Outdated
Comment thread internal/graph/schema/activitylog.graphqls Outdated
Comment thread internal/serviceaccount/queries.go Outdated
@rbjornstad rbjornstad enabled auto-merge (squash) June 26, 2026 11:45
@rbjornstad rbjornstad merged commit 7e5dd6a into main Jun 26, 2026
10 checks passed
@rbjornstad rbjornstad deleted the tenant-activity-log branch June 26, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants