mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
-3
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user