diff --git a/src/instrumentation/workspace.ts b/src/instrumentation/workspace.ts index 757c41802e..010deec5cf 100644 --- a/src/instrumentation/workspace.ts +++ b/src/instrumentation/workspace.ts @@ -26,8 +26,8 @@ const PROVISIONING_STATUSES: ReadonlySet = new Set([ interface ObservedWorkspaceState { readonly status: WorkspaceStatus; - readonly transition: WorkspaceBuild["transition"]; - readonly reason: WorkspaceBuild["reason"]; + readonly buildTransition: WorkspaceBuild["transition"]; + readonly buildReason: WorkspaceBuild["reason"]; readonly observedAtMs: number; } @@ -53,12 +53,16 @@ export class WorkspaceStateTelemetry { ) {} public observe(workspace: Workspace): void { - const { status, transition, reason } = workspace.latest_build; + const { + status, + transition: buildTransition, + reason: buildReason, + } = workspace.latest_build; const previous = this.observed; if ( previous?.status === status && - previous.transition === transition && - previous.reason === reason + previous.buildTransition === buildTransition && + previous.buildReason === buildReason ) { return; } @@ -86,20 +90,25 @@ export class WorkspaceStateTelemetry { workspaceName: this.workspaceName, from: previous?.status ?? INITIAL_STATE, to: status, - transition, - reason, + "build.transition": buildTransition, + "build.reason": buildReason, }, measurements, ); - this.observed = { status, transition, reason, observedAtMs: now }; + this.observed = { + status, + buildTransition, + buildReason, + observedAtMs: now, + }; } } /** * Emits `workspace.agent.state_transitioned` as the agent's `status` and * `lifecycle_state` change. The agent has two state dimensions so the event - * carries qualified `fromStatus`/`toStatus` and `fromLifecycleState`/ - * `toLifecycleState` properties. Construct one per workspace. + * carries `status.*` and `lifecycle_state.*` properties. Construct one per + * workspace. */ export class WorkspaceAgentTelemetry { private observed: ObservedAgentState | undefined; @@ -124,10 +133,10 @@ export class WorkspaceAgentTelemetry { { workspaceName: this.workspaceName, agentName: agent.name, - fromStatus: previous?.status ?? INITIAL_STATE, - toStatus: agent.status, - fromLifecycleState: previous?.lifecycleState ?? INITIAL_STATE, - toLifecycleState: agent.lifecycle_state, + "status.from": previous?.status ?? INITIAL_STATE, + "status.to": agent.status, + "lifecycle_state.from": previous?.lifecycleState ?? INITIAL_STATE, + "lifecycle_state.to": agent.lifecycle_state, }, previous ? { observedDurationMs: now - previous.observedAtMs } : {}, ); diff --git a/src/logging/httpRequestsTelemetry.ts b/src/logging/httpRequestsTelemetry.ts index dd95b94c80..6069c8f423 100644 --- a/src/logging/httpRequestsTelemetry.ts +++ b/src/logging/httpRequestsTelemetry.ts @@ -175,15 +175,15 @@ export class HttpRequestsTelemetry implements Disposable { { method, route }, { window_seconds: elapsedSeconds, - count_1xx: bucket.count1xx, - count_2xx: bucket.count2xx, - count_3xx: bucket.count3xx, - count_4xx: bucket.count4xx, - count_5xx: bucket.count5xx, - count_network_error: bucket.countNetworkError, - p50_duration_ms: percentile(sortedDurations, 0.5), - p95_duration_ms: percentile(sortedDurations, 0.95), - p99_duration_ms: percentile(sortedDurations, 0.99), + "count.1xx": bucket.count1xx, + "count.2xx": bucket.count2xx, + "count.3xx": bucket.count3xx, + "count.4xx": bucket.count4xx, + "count.5xx": bucket.count5xx, + "count.network_error": bucket.countNetworkError, + "duration.p50_ms": percentile(sortedDurations, 0.5), + "duration.p95_ms": percentile(sortedDurations, 0.95), + "duration.p99_ms": percentile(sortedDurations, 0.99), }, ); } diff --git a/test/unit/api/coderApi.test.ts b/test/unit/api/coderApi.test.ts index c132fb4749..f7170d11ac 100644 --- a/test/unit/api/coderApi.test.ts +++ b/test/unit/api/coderApi.test.ts @@ -256,12 +256,12 @@ describe("CoderApi", () => { { method: "GET", route: "/api/v2/workspaces/{id}" }, expect.objectContaining({ window_seconds: 60, - count_1xx: 0, - count_2xx: 1, - count_3xx: 0, - count_4xx: 0, - count_5xx: 0, - count_network_error: 0, + "count.1xx": 0, + "count.2xx": 1, + "count.3xx": 0, + "count.4xx": 0, + "count.5xx": 0, + "count.network_error": 0, }), ); }); diff --git a/test/unit/instrumentation/workspace.test.ts b/test/unit/instrumentation/workspace.test.ts index 395139131e..f219fa1c80 100644 --- a/test/unit/instrumentation/workspace.test.ts +++ b/test/unit/instrumentation/workspace.test.ts @@ -118,13 +118,23 @@ describe("WorkspaceStateTelemetry.observe", () => { it("emits the first observation with from=none and no duration", () => { const { sink, instance: state } = setup(newState); - state.observe(createWorkspace({ latest_build: { status: "running" } })); + state.observe( + createWorkspace({ + latest_build: { + status: "running", + transition: "start", + reason: "initiator", + }, + }), + ); const event = sink.expectOne("workspace.state_transitioned"); expect(event.properties).toMatchObject({ workspaceName: WORKSPACE_NAME, from: "none", to: "running", + "build.transition": "start", + "build.reason": "initiator", }); expect(event.measurements.observedDurationMs).toBeUndefined(); }); @@ -168,10 +178,10 @@ describe("WorkspaceAgentTelemetry.observe", () => { expect(sink.expectOne("workspace.agent.state_transitioned")).toMatchObject({ properties: { - fromStatus: "none", - toStatus: "connecting", - fromLifecycleState: "none", - toLifecycleState: "created", + "status.from": "none", + "status.to": "connecting", + "lifecycle_state.from": "none", + "lifecycle_state.to": "created", }, }); }); @@ -197,7 +207,7 @@ describe("WorkspaceAgentTelemetry.observe", () => { const events = sink.eventsNamed("workspace.agent.state_transitioned"); expect(events).toHaveLength(2); - expect(events[1].properties.fromStatus).toBe("none"); + expect(events[1].properties["status.from"]).toBe("none"); }); it("includes observedDurationMs between transitions", () => { diff --git a/test/unit/logging/httpRequestsTelemetry.test.ts b/test/unit/logging/httpRequestsTelemetry.test.ts index fce97c26fe..d0415c2d15 100644 --- a/test/unit/logging/httpRequestsTelemetry.test.ts +++ b/test/unit/logging/httpRequestsTelemetry.test.ts @@ -80,22 +80,22 @@ describe("HttpRequestsTelemetry", () => { { method: "GET", route: "/api/v2/workspaces/{id}" }, { window_seconds: WINDOW_SECONDS, - count_1xx: 0, - count_2xx: 2, - count_3xx: 0, - count_4xx: 0, - count_5xx: 0, - count_network_error: 0, - p50_duration_ms: 100, - p95_duration_ms: 200, - p99_duration_ms: 200, + "count.1xx": 0, + "count.2xx": 2, + "count.3xx": 0, + "count.4xx": 0, + "count.5xx": 0, + "count.network_error": 0, + "duration.p50_ms": 100, + "duration.p95_ms": 200, + "duration.p99_ms": 200, }, ); expect(log).toHaveBeenNthCalledWith( 2, "http.requests", { method: "POST", route: "/api/v2/workspaces/{id}" }, - expect.objectContaining({ count_2xx: 1 }), + expect.objectContaining({ "count.2xx": 1 }), ); }); @@ -115,12 +115,12 @@ describe("HttpRequestsTelemetry", () => { "http.requests", { method: "POST", route: "/api/v2/users/{name}/workspaces" }, expect.objectContaining({ - count_1xx: 1, - count_2xx: 1, - count_3xx: 1, - count_4xx: 1, - count_5xx: 1, - count_network_error: 1, + "count.1xx": 1, + "count.2xx": 1, + "count.3xx": 1, + "count.4xx": 1, + "count.5xx": 1, + "count.network_error": 1, }), ); }); @@ -160,9 +160,9 @@ describe("HttpRequestsTelemetry", () => { "http.requests", { method: "GET", route: "/api/v2/workspaces/{id}" }, expect.objectContaining({ - p50_duration_ms: 100, - p95_duration_ms: 190, - p99_duration_ms: 200, + "duration.p50_ms": 100, + "duration.p95_ms": 190, + "duration.p99_ms": 200, }), ); }); @@ -187,10 +187,10 @@ describe("HttpRequestsTelemetry", () => { "http.requests", { method: "GET", route: "/api/v2/workspaces/{id}" }, expect.objectContaining({ - count_2xx: 1, - p50_duration_ms: 0, - p95_duration_ms: 0, - p99_duration_ms: 0, + "count.2xx": 1, + "duration.p50_ms": 0, + "duration.p95_ms": 0, + "duration.p99_ms": 0, }), ); }); @@ -208,7 +208,7 @@ describe("HttpRequestsTelemetry", () => { expect(log).toHaveBeenCalledWith( "http.requests", { method: "GET", route: "/api/v2/workspaces/{id}" }, - expect.objectContaining({ count_2xx: 1 }), + expect.objectContaining({ "count.2xx": 1 }), ); }); diff --git a/test/unit/remote/workspaceStateMachine.test.ts b/test/unit/remote/workspaceStateMachine.test.ts index da563eebeb..3c5c42e2db 100644 --- a/test/unit/remote/workspaceStateMachine.test.ts +++ b/test/unit/remote/workspaceStateMachine.test.ts @@ -445,16 +445,16 @@ describe("WorkspaceStateMachine", () => { const events = sink.eventsNamed("workspace.agent.state_transitioned"); expect(events).toHaveLength(2); expect(events[0].properties).toMatchObject({ - fromStatus: "none", - toStatus: "connecting", - fromLifecycleState: "none", - toLifecycleState: "created", + "status.from": "none", + "status.to": "connecting", + "lifecycle_state.from": "none", + "lifecycle_state.to": "created", }); expect(events[1].properties).toMatchObject({ - fromStatus: "connecting", - toStatus: "connected", - fromLifecycleState: "created", - toLifecycleState: "ready", + "status.from": "connecting", + "status.to": "connected", + "lifecycle_state.from": "created", + "lifecycle_state.to": "ready", }); expect(events[1].measurements.observedDurationMs).toEqual( expect.any(Number), @@ -489,16 +489,16 @@ describe("WorkspaceStateMachine", () => { const events = sink.eventsNamed("workspace.agent.state_transitioned"); expect(events).toHaveLength(2); expect(events[0].properties).toMatchObject({ - fromStatus: "none", - toStatus: "connected", - fromLifecycleState: "none", - toLifecycleState: "ready", + "status.from": "none", + "status.to": "connected", + "lifecycle_state.from": "none", + "lifecycle_state.to": "ready", }); expect(events[1].properties).toMatchObject({ - fromStatus: "none", - toStatus: "connecting", - fromLifecycleState: "none", - toLifecycleState: "created", + "status.from": "none", + "status.to": "connecting", + "lifecycle_state.from": "none", + "lifecycle_state.to": "created", }); }); }); diff --git a/test/unit/workspace/workspaceMonitor.test.ts b/test/unit/workspace/workspaceMonitor.test.ts index feee1aa035..da5667c899 100644 --- a/test/unit/workspace/workspaceMonitor.test.ts +++ b/test/unit/workspace/workspaceMonitor.test.ts @@ -109,14 +109,14 @@ describe("WorkspaceMonitor", () => { properties: { from: "running", to: "stopping", - transition: "stop", - reason: "autostop", + "build.transition": "stop", + "build.reason": "autostop", }, measurements: { observedDurationMs: expect.any(Number) }, }); }); - it("dedupes on (status, transition, reason); re-emits when only reason changes", async () => { + it("dedupes on (status, build transition, build reason); re-emits when only reason changes", async () => { const { stream, sink } = buildSinkContext(); await setup( @@ -153,7 +153,7 @@ describe("WorkspaceMonitor", () => { const reasons = sink .eventsNamed("workspace.state_transitioned") - .map((e) => e.properties.reason); + .map((e) => e.properties["build.reason"]); expect(reasons).toEqual(["autostop", "initiator"]); });