Skip to content

Commit 8955d9d

Browse files
authored
chore(telemetry): dot-namespace grouped telemetry keys (#968)
- Rename grouped `workspace.agent.state_transitioned` properties to dot-namespaced `status.*` and `lifecycle_state.*` keys. - Rename `http.requests` aggregate metric families to `count.*` and `duration.*` keys, preserving the snake_case leaves (`count.{1xx,2xx,3xx,4xx,5xx,network_error}`, `duration.p{50,95,99}_ms`). - Apply the optional `workspace.state_transitioned` clarity rename: `transition` → `build.transition`, `reason` → `build.reason`. - Replace old keys only; do not dual-emit legacy keys. Closes #954
1 parent 9a01bf9 commit 8955d9d

7 files changed

Lines changed: 98 additions & 79 deletions

File tree

src/instrumentation/workspace.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ const PROVISIONING_STATUSES: ReadonlySet<WorkspaceStatus> = new Set([
2626

2727
interface ObservedWorkspaceState {
2828
readonly status: WorkspaceStatus;
29-
readonly transition: WorkspaceBuild["transition"];
30-
readonly reason: WorkspaceBuild["reason"];
29+
readonly buildTransition: WorkspaceBuild["transition"];
30+
readonly buildReason: WorkspaceBuild["reason"];
3131
readonly observedAtMs: number;
3232
}
3333

@@ -53,12 +53,16 @@ export class WorkspaceStateTelemetry {
5353
) {}
5454

5555
public observe(workspace: Workspace): void {
56-
const { status, transition, reason } = workspace.latest_build;
56+
const {
57+
status,
58+
transition: buildTransition,
59+
reason: buildReason,
60+
} = workspace.latest_build;
5761
const previous = this.observed;
5862
if (
5963
previous?.status === status &&
60-
previous.transition === transition &&
61-
previous.reason === reason
64+
previous.buildTransition === buildTransition &&
65+
previous.buildReason === buildReason
6266
) {
6367
return;
6468
}
@@ -86,20 +90,25 @@ export class WorkspaceStateTelemetry {
8690
workspaceName: this.workspaceName,
8791
from: previous?.status ?? INITIAL_STATE,
8892
to: status,
89-
transition,
90-
reason,
93+
"build.transition": buildTransition,
94+
"build.reason": buildReason,
9195
},
9296
measurements,
9397
);
94-
this.observed = { status, transition, reason, observedAtMs: now };
98+
this.observed = {
99+
status,
100+
buildTransition,
101+
buildReason,
102+
observedAtMs: now,
103+
};
95104
}
96105
}
97106

98107
/**
99108
* Emits `workspace.agent.state_transitioned` as the agent's `status` and
100109
* `lifecycle_state` change. The agent has two state dimensions so the event
101-
* carries qualified `fromStatus`/`toStatus` and `fromLifecycleState`/
102-
* `toLifecycleState` properties. Construct one per workspace.
110+
* carries `status.*` and `lifecycle_state.*` properties. Construct one per
111+
* workspace.
103112
*/
104113
export class WorkspaceAgentTelemetry {
105114
private observed: ObservedAgentState | undefined;
@@ -124,10 +133,10 @@ export class WorkspaceAgentTelemetry {
124133
{
125134
workspaceName: this.workspaceName,
126135
agentName: agent.name,
127-
fromStatus: previous?.status ?? INITIAL_STATE,
128-
toStatus: agent.status,
129-
fromLifecycleState: previous?.lifecycleState ?? INITIAL_STATE,
130-
toLifecycleState: agent.lifecycle_state,
136+
"status.from": previous?.status ?? INITIAL_STATE,
137+
"status.to": agent.status,
138+
"lifecycle_state.from": previous?.lifecycleState ?? INITIAL_STATE,
139+
"lifecycle_state.to": agent.lifecycle_state,
131140
},
132141
previous ? { observedDurationMs: now - previous.observedAtMs } : {},
133142
);

src/logging/httpRequestsTelemetry.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,15 @@ export class HttpRequestsTelemetry implements Disposable {
175175
{ method, route },
176176
{
177177
window_seconds: elapsedSeconds,
178-
count_1xx: bucket.count1xx,
179-
count_2xx: bucket.count2xx,
180-
count_3xx: bucket.count3xx,
181-
count_4xx: bucket.count4xx,
182-
count_5xx: bucket.count5xx,
183-
count_network_error: bucket.countNetworkError,
184-
p50_duration_ms: percentile(sortedDurations, 0.5),
185-
p95_duration_ms: percentile(sortedDurations, 0.95),
186-
p99_duration_ms: percentile(sortedDurations, 0.99),
178+
"count.1xx": bucket.count1xx,
179+
"count.2xx": bucket.count2xx,
180+
"count.3xx": bucket.count3xx,
181+
"count.4xx": bucket.count4xx,
182+
"count.5xx": bucket.count5xx,
183+
"count.network_error": bucket.countNetworkError,
184+
"duration.p50_ms": percentile(sortedDurations, 0.5),
185+
"duration.p95_ms": percentile(sortedDurations, 0.95),
186+
"duration.p99_ms": percentile(sortedDurations, 0.99),
187187
},
188188
);
189189
}

test/unit/api/coderApi.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,12 @@ describe("CoderApi", () => {
256256
{ method: "GET", route: "/api/v2/workspaces/{id}" },
257257
expect.objectContaining({
258258
window_seconds: 60,
259-
count_1xx: 0,
260-
count_2xx: 1,
261-
count_3xx: 0,
262-
count_4xx: 0,
263-
count_5xx: 0,
264-
count_network_error: 0,
259+
"count.1xx": 0,
260+
"count.2xx": 1,
261+
"count.3xx": 0,
262+
"count.4xx": 0,
263+
"count.5xx": 0,
264+
"count.network_error": 0,
265265
}),
266266
);
267267
});

test/unit/instrumentation/workspace.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,23 @@ describe("WorkspaceStateTelemetry.observe", () => {
118118
it("emits the first observation with from=none and no duration", () => {
119119
const { sink, instance: state } = setup(newState);
120120

121-
state.observe(createWorkspace({ latest_build: { status: "running" } }));
121+
state.observe(
122+
createWorkspace({
123+
latest_build: {
124+
status: "running",
125+
transition: "start",
126+
reason: "initiator",
127+
},
128+
}),
129+
);
122130

123131
const event = sink.expectOne("workspace.state_transitioned");
124132
expect(event.properties).toMatchObject({
125133
workspaceName: WORKSPACE_NAME,
126134
from: "none",
127135
to: "running",
136+
"build.transition": "start",
137+
"build.reason": "initiator",
128138
});
129139
expect(event.measurements.observedDurationMs).toBeUndefined();
130140
});
@@ -168,10 +178,10 @@ describe("WorkspaceAgentTelemetry.observe", () => {
168178

169179
expect(sink.expectOne("workspace.agent.state_transitioned")).toMatchObject({
170180
properties: {
171-
fromStatus: "none",
172-
toStatus: "connecting",
173-
fromLifecycleState: "none",
174-
toLifecycleState: "created",
181+
"status.from": "none",
182+
"status.to": "connecting",
183+
"lifecycle_state.from": "none",
184+
"lifecycle_state.to": "created",
175185
},
176186
});
177187
});
@@ -197,7 +207,7 @@ describe("WorkspaceAgentTelemetry.observe", () => {
197207

198208
const events = sink.eventsNamed("workspace.agent.state_transitioned");
199209
expect(events).toHaveLength(2);
200-
expect(events[1].properties.fromStatus).toBe("none");
210+
expect(events[1].properties["status.from"]).toBe("none");
201211
});
202212

203213
it("includes observedDurationMs between transitions", () => {

test/unit/logging/httpRequestsTelemetry.test.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -80,22 +80,22 @@ describe("HttpRequestsTelemetry", () => {
8080
{ method: "GET", route: "/api/v2/workspaces/{id}" },
8181
{
8282
window_seconds: WINDOW_SECONDS,
83-
count_1xx: 0,
84-
count_2xx: 2,
85-
count_3xx: 0,
86-
count_4xx: 0,
87-
count_5xx: 0,
88-
count_network_error: 0,
89-
p50_duration_ms: 100,
90-
p95_duration_ms: 200,
91-
p99_duration_ms: 200,
83+
"count.1xx": 0,
84+
"count.2xx": 2,
85+
"count.3xx": 0,
86+
"count.4xx": 0,
87+
"count.5xx": 0,
88+
"count.network_error": 0,
89+
"duration.p50_ms": 100,
90+
"duration.p95_ms": 200,
91+
"duration.p99_ms": 200,
9292
},
9393
);
9494
expect(log).toHaveBeenNthCalledWith(
9595
2,
9696
"http.requests",
9797
{ method: "POST", route: "/api/v2/workspaces/{id}" },
98-
expect.objectContaining({ count_2xx: 1 }),
98+
expect.objectContaining({ "count.2xx": 1 }),
9999
);
100100
});
101101

@@ -115,12 +115,12 @@ describe("HttpRequestsTelemetry", () => {
115115
"http.requests",
116116
{ method: "POST", route: "/api/v2/users/{name}/workspaces" },
117117
expect.objectContaining({
118-
count_1xx: 1,
119-
count_2xx: 1,
120-
count_3xx: 1,
121-
count_4xx: 1,
122-
count_5xx: 1,
123-
count_network_error: 1,
118+
"count.1xx": 1,
119+
"count.2xx": 1,
120+
"count.3xx": 1,
121+
"count.4xx": 1,
122+
"count.5xx": 1,
123+
"count.network_error": 1,
124124
}),
125125
);
126126
});
@@ -160,9 +160,9 @@ describe("HttpRequestsTelemetry", () => {
160160
"http.requests",
161161
{ method: "GET", route: "/api/v2/workspaces/{id}" },
162162
expect.objectContaining({
163-
p50_duration_ms: 100,
164-
p95_duration_ms: 190,
165-
p99_duration_ms: 200,
163+
"duration.p50_ms": 100,
164+
"duration.p95_ms": 190,
165+
"duration.p99_ms": 200,
166166
}),
167167
);
168168
});
@@ -187,10 +187,10 @@ describe("HttpRequestsTelemetry", () => {
187187
"http.requests",
188188
{ method: "GET", route: "/api/v2/workspaces/{id}" },
189189
expect.objectContaining({
190-
count_2xx: 1,
191-
p50_duration_ms: 0,
192-
p95_duration_ms: 0,
193-
p99_duration_ms: 0,
190+
"count.2xx": 1,
191+
"duration.p50_ms": 0,
192+
"duration.p95_ms": 0,
193+
"duration.p99_ms": 0,
194194
}),
195195
);
196196
});
@@ -208,7 +208,7 @@ describe("HttpRequestsTelemetry", () => {
208208
expect(log).toHaveBeenCalledWith(
209209
"http.requests",
210210
{ method: "GET", route: "/api/v2/workspaces/{id}" },
211-
expect.objectContaining({ count_2xx: 1 }),
211+
expect.objectContaining({ "count.2xx": 1 }),
212212
);
213213
});
214214

test/unit/remote/workspaceStateMachine.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -445,16 +445,16 @@ describe("WorkspaceStateMachine", () => {
445445
const events = sink.eventsNamed("workspace.agent.state_transitioned");
446446
expect(events).toHaveLength(2);
447447
expect(events[0].properties).toMatchObject({
448-
fromStatus: "none",
449-
toStatus: "connecting",
450-
fromLifecycleState: "none",
451-
toLifecycleState: "created",
448+
"status.from": "none",
449+
"status.to": "connecting",
450+
"lifecycle_state.from": "none",
451+
"lifecycle_state.to": "created",
452452
});
453453
expect(events[1].properties).toMatchObject({
454-
fromStatus: "connecting",
455-
toStatus: "connected",
456-
fromLifecycleState: "created",
457-
toLifecycleState: "ready",
454+
"status.from": "connecting",
455+
"status.to": "connected",
456+
"lifecycle_state.from": "created",
457+
"lifecycle_state.to": "ready",
458458
});
459459
expect(events[1].measurements.observedDurationMs).toEqual(
460460
expect.any(Number),
@@ -489,16 +489,16 @@ describe("WorkspaceStateMachine", () => {
489489
const events = sink.eventsNamed("workspace.agent.state_transitioned");
490490
expect(events).toHaveLength(2);
491491
expect(events[0].properties).toMatchObject({
492-
fromStatus: "none",
493-
toStatus: "connected",
494-
fromLifecycleState: "none",
495-
toLifecycleState: "ready",
492+
"status.from": "none",
493+
"status.to": "connected",
494+
"lifecycle_state.from": "none",
495+
"lifecycle_state.to": "ready",
496496
});
497497
expect(events[1].properties).toMatchObject({
498-
fromStatus: "none",
499-
toStatus: "connecting",
500-
fromLifecycleState: "none",
501-
toLifecycleState: "created",
498+
"status.from": "none",
499+
"status.to": "connecting",
500+
"lifecycle_state.from": "none",
501+
"lifecycle_state.to": "created",
502502
});
503503
});
504504
});

test/unit/workspace/workspaceMonitor.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,14 @@ describe("WorkspaceMonitor", () => {
109109
properties: {
110110
from: "running",
111111
to: "stopping",
112-
transition: "stop",
113-
reason: "autostop",
112+
"build.transition": "stop",
113+
"build.reason": "autostop",
114114
},
115115
measurements: { observedDurationMs: expect.any(Number) },
116116
});
117117
});
118118

119-
it("dedupes on (status, transition, reason); re-emits when only reason changes", async () => {
119+
it("dedupes on (status, build transition, build reason); re-emits when only reason changes", async () => {
120120
const { stream, sink } = buildSinkContext();
121121

122122
await setup(
@@ -153,7 +153,7 @@ describe("WorkspaceMonitor", () => {
153153

154154
const reasons = sink
155155
.eventsNamed("workspace.state_transitioned")
156-
.map((e) => e.properties.reason);
156+
.map((e) => e.properties["build.reason"]);
157157
expect(reasons).toEqual(["autostop", "initiator"]);
158158
});
159159

0 commit comments

Comments
 (0)