feat: persist boundary logs (#24812)

Add database persistence to `ReportBoundaryLogs`. On first log for a
session, the handler lazy-creates a `boundary_sessions` row, then
batch-inserts all `BoundaryLog` entries into `boundary_logs`. Structured
logging and usage tracking are preserved. Old boundary clients (no
`session_id`) fall back to log-only mode.

> [!NOTE]
> This PR was authored by Coder Agents.
This commit is contained in:
Sas Swart
2026-06-15 12:34:48 +02:00
committed by GitHub
parent e1c7e61eb9
commit f0ac52e83c
17 changed files with 868 additions and 130 deletions
+1 -6
View File
@@ -5710,12 +5710,7 @@ func (q *querier) InsertAuditLog(ctx context.Context, arg database.InsertAuditLo
}
func (q *querier) InsertBoundaryLogs(ctx context.Context, arg database.InsertBoundaryLogsParams) ([]database.BoundaryLog, error) {
session, err := q.db.GetBoundarySessionByID(ctx, arg.SessionID)
if err != nil {
return nil, xerrors.Errorf("get boundary session for owner: %w", err)
}
if err := q.authorizeContext(ctx, policy.ActionCreate,
rbac.ResourceBoundaryLog.WithOwner(session.OwnerID.UUID.String())); err != nil {
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceBoundaryLog); err != nil {
return nil, err
}
return q.db.InsertBoundaryLogs(ctx, arg)
+4 -12
View File
@@ -460,22 +460,13 @@ func (s *MethodTestSuite) TestBoundaryLogs() {
dbm.EXPECT().GetBoundarySessionByID(gomock.Any(), uuid.Nil).Return(database.BoundarySession{}, nil).AnyTimes()
check.Args(uuid.Nil).Asserts(rbac.ResourceBoundaryLog, policy.ActionRead)
}))
s.Run("InsertBoundaryLogs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
ownerID := uuid.New()
sessionID := uuid.New()
session := database.BoundarySession{
ID: sessionID,
OwnerID: uuid.NullUUID{UUID: ownerID, Valid: true},
}
s.Run("InsertBoundaryLogs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
arg := database.InsertBoundaryLogsParams{
SessionID: sessionID,
SessionID: uuid.New(),
ID: []uuid.UUID{uuid.New(), uuid.New()},
}
dbm.EXPECT().GetBoundarySessionByID(gomock.Any(), sessionID).Return(session, nil).AnyTimes()
dbm.EXPECT().InsertBoundaryLogs(gomock.Any(), arg).Return([]database.BoundaryLog{}, nil).AnyTimes()
check.Args(arg).Asserts(
rbac.ResourceBoundaryLog.WithOwner(ownerID.String()), policy.ActionCreate,
)
check.Args(arg).Asserts(rbac.ResourceBoundaryLog, policy.ActionCreate)
}))
s.Run("GetBoundaryLogByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().GetBoundaryLogByID(gomock.Any(), uuid.Nil).Return(database.BoundaryLog{}, nil).AnyTimes()
@@ -486,6 +477,7 @@ func (s *MethodTestSuite) TestBoundaryLogs() {
dbm.EXPECT().ListBoundaryLogsBySessionID(gomock.Any(), arg).Return([]database.BoundaryLog{}, nil).AnyTimes()
check.Args(arg).Asserts(rbac.ResourceBoundaryLog, policy.ActionRead)
}))
s.Run("DeleteOldBoundaryLogs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().DeleteOldBoundaryLogs(gomock.Any(), database.DeleteOldBoundaryLogsParams{}).Return(int64(0), nil).AnyTimes()
check.Args(database.DeleteOldBoundaryLogsParams{}).Asserts(rbac.ResourceBoundaryLog, policy.ActionDelete)
-3
View File
@@ -4798,9 +4798,6 @@ ALTER TABLE ONLY aibridge_interceptions
ALTER TABLE ONLY api_keys
ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ALTER TABLE ONLY boundary_logs
ADD CONSTRAINT boundary_logs_session_id_fkey FOREIGN KEY (session_id) REFERENCES boundary_sessions(id) ON DELETE CASCADE;
ALTER TABLE ONLY boundary_sessions
ADD CONSTRAINT boundary_sessions_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL;
-1
View File
@@ -12,7 +12,6 @@ const (
ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id);
ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
ForeignKeyBoundaryLogsSessionID ForeignKeyConstraint = "boundary_logs_session_id_fkey" // ALTER TABLE ONLY boundary_logs ADD CONSTRAINT boundary_logs_session_id_fkey FOREIGN KEY (session_id) REFERENCES boundary_sessions(id) ON DELETE CASCADE;
ForeignKeyBoundarySessionsOwnerID ForeignKeyConstraint = "boundary_sessions_owner_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE SET NULL;
ForeignKeyBoundarySessionsWorkspaceAgentID ForeignKeyConstraint = "boundary_sessions_workspace_agent_id_fkey" // ALTER TABLE ONLY boundary_sessions ADD CONSTRAINT boundary_sessions_workspace_agent_id_fkey FOREIGN KEY (workspace_agent_id) REFERENCES workspace_agents(id);
ForeignKeyChatDebugRunsChatID ForeignKeyConstraint = "chat_debug_runs_chat_id_fkey" // ALTER TABLE ONLY chat_debug_runs ADD CONSTRAINT chat_debug_runs_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
@@ -0,0 +1,10 @@
-- Delete orphaned logs that have no matching session before restoring
-- the FK constraint.
DELETE FROM boundary_logs bl
WHERE NOT EXISTS (
SELECT 1 FROM boundary_sessions bs WHERE bs.id = bl.session_id
);
ALTER TABLE boundary_logs
ADD CONSTRAINT boundary_logs_session_id_fkey
FOREIGN KEY (session_id) REFERENCES boundary_sessions(id) ON DELETE CASCADE;
@@ -0,0 +1,6 @@
-- Drop the foreign key so that boundary logs can be inserted before
-- the session row exists. The session is created lazily and may fail
-- on transient errors; removing the FK lets logs persist regardless.
-- The session row will be created on a subsequent batch, retroactively
-- linking the orphaned logs via session_id.
ALTER TABLE boundary_logs DROP CONSTRAINT boundary_logs_session_id_fkey;
-7
View File
@@ -1023,10 +1023,3 @@ type UpsertConnectionLogParams struct {
func (r GetLatestWorkspaceBuildWithStatusByWorkspaceIDRow) RBACObject() rbac.Object {
return r.WorkspaceTable.RBACObject()
}
func (s BoundarySession) RBACObject() rbac.Object {
if s.OwnerID.Valid {
return rbac.ResourceBoundaryLog.WithOwner(s.OwnerID.UUID.String())
}
return rbac.ResourceBoundaryLog
}
+1 -1
View File
@@ -3682,7 +3682,7 @@ SELECT
unnest($6 :: text[]),
unnest($7 :: text[]),
unnest($8 :: text[]),
unnest($9 :: text[])
NULLIF(unnest($9 :: text[]), '')
RETURNING id, session_id, sequence_number, captured_at, created_at, proto, method, detail, matched_rule
`
+1 -1
View File
@@ -39,7 +39,7 @@ SELECT
unnest(@proto :: text[]),
unnest(@method :: text[]),
unnest(@detail :: text[]),
unnest(@matched_rule :: text[])
NULLIF(unnest(@matched_rule :: text[]), '')
RETURNING *;
-- name: GetBoundaryLogByID :one