-
Notifications
You must be signed in to change notification settings - Fork 46
feat(workspace): log workspace and agent state changes #1075
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
Open
aqandrew
wants to merge
1
commit into
aqandrew/devex-661-session-id-requests
from
aqandrew/devex-661-connection-state-logs
+221
−0
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import { extractAgents } from "../api/api-helper"; | ||
|
|
||
| import type { | ||
| Workspace, | ||
| WorkspaceAgentLifecycle, | ||
| WorkspaceAgentStatus, | ||
| WorkspaceStatus, | ||
| } from "coder/site/src/api/typesGenerated"; | ||
|
|
||
| import type { Logger } from "../logging/logger"; | ||
|
|
||
| /** Sentinel for the "from" side before any state is observed, and for the | ||
| * agent/lifecycle dimensions while no agent exists yet. `"unknown"` is a real | ||
| * server-reported value, so avoid it. */ | ||
| const INITIAL_STATE = "none"; | ||
|
|
||
| interface ObservedState { | ||
| readonly workspaceStatus: WorkspaceStatus; | ||
| readonly agentStatus: WorkspaceAgentStatus | typeof INITIAL_STATE; | ||
| readonly lifecycleState: WorkspaceAgentLifecycle | typeof INITIAL_STATE; | ||
| } | ||
|
|
||
| /** | ||
| * Logs workspace, agent, and lifecycle status transitions at `info` level so | ||
| * connection debugging has a record of state changes correlated by the session | ||
| * ID. Tracks state per agent (keyed by agent ID) because a workspace can have | ||
| * several. Construct one per workspace; `WorkspaceMonitor` is the sole call | ||
| * site. | ||
| */ | ||
| export class WorkspaceStateLogger { | ||
| private readonly observed = new Map<string, ObservedState>(); | ||
|
|
||
| public constructor( | ||
| private readonly logger: Logger, | ||
| private readonly workspaceName: string, | ||
| ) {} | ||
|
|
||
| public observe(workspace: Workspace): void { | ||
| const workspaceStatus = workspace.latest_build.status; | ||
| const agents = extractAgents(workspace.latest_build.resources); | ||
|
|
||
| if (agents.length === 0) { | ||
| this.observeState(INITIAL_STATE, { | ||
| workspaceStatus, | ||
| agentStatus: INITIAL_STATE, | ||
| lifecycleState: INITIAL_STATE, | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| for (const agent of agents) { | ||
| this.observeState(agent.id, { | ||
| workspaceStatus, | ||
| agentStatus: agent.status, | ||
| lifecycleState: agent.lifecycle_state, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| private observeState(key: string, next: ObservedState): void { | ||
| const previous = this.observed.get(key); | ||
| if ( | ||
| previous?.workspaceStatus === next.workspaceStatus && | ||
| previous?.agentStatus === next.agentStatus && | ||
| previous?.lifecycleState === next.lifecycleState | ||
|
Comment on lines
+63
to
+65
Collaborator
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. IMO I think we should log once for each workspace state change (basically what the workspace telemetry does) and independently track the agent state since they could be independent |
||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| this.logger.info(`Workspace ${this.workspaceName} state changed`, { | ||
| workspaceStatus: { | ||
| from: previous?.workspaceStatus ?? INITIAL_STATE, | ||
| to: next.workspaceStatus, | ||
| }, | ||
| agentStatus: { | ||
| from: previous?.agentStatus ?? INITIAL_STATE, | ||
| to: next.agentStatus, | ||
| }, | ||
| lifecycleState: { | ||
| from: previous?.lifecycleState ?? INITIAL_STATE, | ||
| to: next.lifecycleState, | ||
| }, | ||
| }); | ||
|
|
||
| this.observed.set(key, next); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { WorkspaceStateLogger } from "@/workspace/workspaceStateLogger"; | ||
|
|
||
| import { | ||
| agent as createAgent, | ||
| resource as createResource, | ||
| workspace as createWorkspace, | ||
| } from "@repo/mocks"; | ||
|
|
||
| import { createMockLogger } from "../../mocks/testHelpers"; | ||
|
|
||
| import type { | ||
| Workspace, | ||
| WorkspaceAgent, | ||
| WorkspaceStatus, | ||
| } from "coder/site/src/api/typesGenerated"; | ||
|
|
||
| function workspaceWith( | ||
| status: WorkspaceStatus, | ||
| agents: WorkspaceAgent[] = [], | ||
| ): Workspace { | ||
| return createWorkspace({ | ||
| latest_build: { | ||
| status, | ||
| resources: [createResource({ agents })], | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| describe("WorkspaceStateLogger", () => { | ||
| it("logs the initial observed state with a `none` origin", () => { | ||
| const logger = createMockLogger(); | ||
| const stateLogger = new WorkspaceStateLogger(logger, "testuser/ws"); | ||
|
|
||
| stateLogger.observe( | ||
| workspaceWith("running", [ | ||
| createAgent({ status: "connected", lifecycle_state: "ready" }), | ||
| ]), | ||
| ); | ||
|
|
||
| expect(logger.info).toHaveBeenCalledTimes(1); | ||
| expect(logger.info).toHaveBeenCalledWith( | ||
| "Workspace testuser/ws state changed", | ||
| { | ||
| workspaceStatus: { from: "none", to: "running" }, | ||
| agentStatus: { from: "none", to: "connected" }, | ||
| lifecycleState: { from: "none", to: "ready" }, | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it("logs a transition when the agent status and lifecycle change", () => { | ||
| const logger = createMockLogger(); | ||
| const stateLogger = new WorkspaceStateLogger(logger, "testuser/ws"); | ||
|
|
||
| stateLogger.observe( | ||
| workspaceWith("starting", [ | ||
| createAgent({ status: "connecting", lifecycle_state: "starting" }), | ||
| ]), | ||
| ); | ||
| stateLogger.observe( | ||
| workspaceWith("running", [ | ||
| createAgent({ status: "connected", lifecycle_state: "ready" }), | ||
| ]), | ||
| ); | ||
|
|
||
| expect(logger.info).toHaveBeenCalledTimes(2); | ||
| expect(logger.info).toHaveBeenLastCalledWith( | ||
| "Workspace testuser/ws state changed", | ||
| { | ||
| workspaceStatus: { from: "starting", to: "running" }, | ||
| agentStatus: { from: "connecting", to: "connected" }, | ||
| lifecycleState: { from: "starting", to: "ready" }, | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it("does not log when nothing changes", () => { | ||
| const logger = createMockLogger(); | ||
| const stateLogger = new WorkspaceStateLogger(logger, "testuser/ws"); | ||
| const snapshot = workspaceWith("running", [ | ||
| createAgent({ status: "connected", lifecycle_state: "ready" }), | ||
| ]); | ||
|
|
||
| stateLogger.observe(snapshot); | ||
| stateLogger.observe(snapshot); | ||
|
|
||
| expect(logger.info).toHaveBeenCalledTimes(1); | ||
| }); | ||
|
|
||
| it("uses `none` for the agent dimensions while no agent exists yet", () => { | ||
| const logger = createMockLogger(); | ||
| const stateLogger = new WorkspaceStateLogger(logger, "testuser/ws"); | ||
|
|
||
| stateLogger.observe(workspaceWith("pending")); | ||
|
|
||
| expect(logger.info).toHaveBeenCalledWith( | ||
| "Workspace testuser/ws state changed", | ||
| { | ||
| workspaceStatus: { from: "none", to: "pending" }, | ||
| agentStatus: { from: "none", to: "none" }, | ||
| lifecycleState: { from: "none", to: "none" }, | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it("tracks each agent independently", () => { | ||
| const logger = createMockLogger(); | ||
| const stateLogger = new WorkspaceStateLogger(logger, "testuser/ws"); | ||
|
|
||
| stateLogger.observe( | ||
| workspaceWith("running", [ | ||
| createAgent({ id: "a1", name: "first", status: "connected" }), | ||
| createAgent({ id: "a2", name: "second", status: "connecting" }), | ||
| ]), | ||
| ); | ||
| expect(logger.info).toHaveBeenCalledTimes(2); | ||
|
|
||
| // Only the second agent changes; expect a single new log. | ||
| stateLogger.observe( | ||
| workspaceWith("running", [ | ||
| createAgent({ id: "a1", name: "first", status: "connected" }), | ||
| createAgent({ id: "a2", name: "second", status: "connected" }), | ||
| ]), | ||
| ); | ||
| expect(logger.info).toHaveBeenCalledTimes(3); | ||
| }); | ||
| }); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
There are lots of similarities between this and
WorkspaceStateTelemetry, I suppose one is tracking the workspace states and one is tracking the agent states?