Skip to content

Commit da68951

Browse files
author
Hweinstock
committed
refactor(tel): move to metric event instead of attributes recorder
1 parent e8f3d7d commit da68951

10 files changed

Lines changed: 174 additions & 161 deletions

File tree

src/index.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { runWithExitCode } from "./runnable";
1616
import { DefaultGlobalConfigAccessor } from "./globalConfig";
1717
import { DefaultTelemetryClient } from "./telemetry";
1818
import { AgentCoreCLIError } from "./errors";
19-
import { TelemetryAttributesRecorderKey, ValueContext } from "./router";
19+
import { CommandRunMetricEventKey, ValueContext } from "./router";
2020

2121
process.exit(
2222
await runWithExitCode(async (argv: string[]) => {
@@ -50,7 +50,7 @@ process.exit(
5050
globalConfigAccessor,
5151
});
5252

53-
const commandRunTelemetryRecorder = telemetryClient.getAttributesRecorder("cli.command_run", {
53+
const commandRunMetricEvent = telemetryClient.startMetricEvent("cli.command_run", {
5454
exit_reason: "success",
5555
});
5656

@@ -75,29 +75,25 @@ process.exit(
7575
globalConfigAccessor,
7676
});
7777

78-
const context = ValueContext.EmptyContext().withValue<typeof commandRunTelemetryRecorder>(
79-
TelemetryAttributesRecorderKey,
80-
commandRunTelemetryRecorder,
78+
const context = ValueContext.EmptyContext().withValue(
79+
CommandRunMetricEventKey,
80+
commandRunMetricEvent,
8181
);
8282

8383
// Handle the request
8484
await rootHandler.route(argv, context);
8585
} catch (e) {
8686
const error = AgentCoreCLIError.fromError(e);
8787
rootLogger.child({ error: error.json() }).error();
88-
commandRunTelemetryRecorder.record({
88+
commandRunMetricEvent.setAttributes({
8989
exit_reason: "failure",
9090
error_name: error.name,
9191
error_source: error.source,
9292
});
9393
throw error;
9494
} finally {
9595
try {
96-
await telemetryClient.emit(
97-
"cli.command_run",
98-
Date.now() - startTime,
99-
commandRunTelemetryRecorder,
100-
);
96+
await commandRunMetricEvent.end(Date.now() - startTime);
10197
} catch (e) {
10298
const error = AgentCoreCLIError.fromError(e);
10399
rootLogger.child({ error: error.json() }).warn("failed to emit telemetry");

src/router/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export {
55
PathKey,
66
LoggerKey,
77
GlobalConfigAccessorKey,
8-
TelemetryAttributesRecorderKey,
8+
CommandRunMetricEventKey,
99
ProjectKey,
1010
type DefaultHandle,
1111
type DefaultHandlerProvider,

src/router/router.test.ts

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import z from "zod";
44

55
import {
66
Router,
7-
TelemetryAttributesRecorderKey,
7+
CommandRunMetricEventKey,
88
ValueContext,
99
argument,
1010
compile,
@@ -677,40 +677,57 @@ test("commands without long-form flag help have no Parameter details section", a
677677
test.each([
678678
{ scenario: "flag validation succeeds", idFlag: "abc", shouldThrow: false },
679679
{ scenario: "flag validation fails", idFlag: "toolong", shouldThrow: true },
680-
])(
681-
"records command_path on the telemetry recorder when $scenario",
682-
async ({ idFlag, shouldThrow }) => {
683-
// we use a real command name here so that telemetry schemas accept the path produced below
684-
const get = createHandler({
685-
name: "config",
686-
description: "",
687-
flags: [flag("id", "id", z.string().max(3))],
688-
handle: async () => {},
689-
});
690-
691-
const telemetryClient = new DefaultTelemetryClient({
692-
logger: createSilentLogger(),
693-
globalConfigAccessor: new TestGlobalConfigAccessor(),
694-
sessionId: "test-session-id",
695-
});
696-
697-
const root = new Router("agentcore");
698-
root.handler(get);
699-
700-
const recorder = telemetryClient.getAttributesRecorder("cli.command_run");
701-
const ctx = ValueContext.EmptyContext().withValue(TelemetryAttributesRecorderKey, recorder);
702-
const cmd = exitOverrideAll(compile(root, ctx));
703-
704-
if (shouldThrow) {
705-
await expect(
706-
cmd.parseAsync(["node", "agentcore", "config", "--id", idFlag]),
707-
).rejects.toThrow();
708-
recorder.record({ exit_reason: "failure" });
709-
} else {
710-
await cmd.parseAsync(["node", "agentcore", "config", "--id", idFlag]);
711-
recorder.record({ exit_reason: "success" });
712-
}
713-
714-
expect(recorder.getAttributes()).toMatchObject({ command_path: "/agentcore/config" });
715-
},
716-
);
680+
])("records command_path on the metric event when $scenario", async ({ idFlag, shouldThrow }) => {
681+
// we use a real command name here so that telemetry schemas accept the path produced below
682+
const get = createHandler({
683+
name: "config",
684+
description: "",
685+
flags: [flag("id", "id", z.string().max(3))],
686+
handle: async () => {},
687+
});
688+
689+
const recordedMetrics: { metricName: string; value: number; attributes: Record<string, any> }[] =
690+
[];
691+
const inMemorySink = {
692+
getName: () => "InMemorySink",
693+
send: (metricName: string, value: number, attributes: Record<string, any>) => {
694+
recordedMetrics.push({ metricName, value, attributes });
695+
},
696+
shutdown: async () => {},
697+
};
698+
699+
const telemetryClient = new DefaultTelemetryClient({
700+
logger: createSilentLogger(),
701+
globalConfigAccessor: new TestGlobalConfigAccessor(),
702+
sessionId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
703+
metricSinks: [inMemorySink],
704+
});
705+
706+
const root = new Router("agentcore");
707+
root.handler(get);
708+
709+
const commandRunMetricEvent = telemetryClient.startMetricEvent("cli.command_run");
710+
const ctx = ValueContext.EmptyContext().withValue(
711+
CommandRunMetricEventKey,
712+
commandRunMetricEvent,
713+
);
714+
const cmd = compile(root, ctx);
715+
716+
if (shouldThrow) {
717+
await expect(cmd.parseAsync(["node", "agentcore", "config", "--id", idFlag])).rejects.toThrow();
718+
commandRunMetricEvent.setAttributes({ exit_reason: "failure" });
719+
} else {
720+
await cmd.parseAsync(["node", "agentcore", "config", "--id", idFlag]);
721+
commandRunMetricEvent.setAttributes({ exit_reason: "success" });
722+
}
723+
724+
await commandRunMetricEvent.end(100);
725+
726+
expect(recordedMetrics).toHaveLength(1);
727+
expect(recordedMetrics[0]!.metricName).toBe("cli.command_run");
728+
expect(recordedMetrics[0]!.value).toBe(100);
729+
expect(recordedMetrics[0]!.attributes).toMatchObject({
730+
command_path: "/agentcore/config",
731+
exit_reason: shouldThrow ? "failure" : "success",
732+
});
733+
});

src/router/router.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Command } from "commander";
88
import type { Logger } from "../logging";
99
import type { GlobalConfigAccessor } from "../globalConfig";
1010
import type { Project } from "../handlers/project/types";
11-
import { type AttributesOf, type AttributesRecorder } from "../telemetry";
11+
import { type MetricEvent } from "../telemetry";
1212

1313
// CommandKey exposes the Commander Command for the executing leaf via context.
1414
export const CommandKey: ContextKey<Command> = contextKey<Command>("commander.command");
@@ -17,9 +17,8 @@ export const PathKey: ContextKey<string> = contextKey<string>("path");
1717

1818
export const LoggerKey = contextKey<Logger>("logger");
1919

20-
export const TelemetryAttributesRecorderKey = contextKey<
21-
AttributesRecorder<AttributesOf<"cli.command_run">>
22-
>("telemetryAttributesRecorder");
20+
export const CommandRunMetricEventKey =
21+
contextKey<MetricEvent<"cli.command_run">>("commandRunMetricEvent");
2322

2423
export const GlobalConfigAccessorKey: ContextKey<GlobalConfigAccessor> =
2524
contextKey<GlobalConfigAccessor>("globalConfigAccessor");
@@ -99,7 +98,7 @@ function globalFlagsOf(node: Handler): GlobalFlag[] {
9998

10099
/** Add the command path to active command run metric **/
101100
function recordCommandPath(ctx: Context): void {
102-
ctx.value(TelemetryAttributesRecorderKey)?.record({ command_path: ctx.value(PathKey) });
101+
ctx.value(CommandRunMetricEventKey)?.setAttributes({ command_path: ctx.value(PathKey) });
103102
}
104103

105104
// compile walks the Handler tree into a Commander Command tree.

src/telemetry/client.test.tsx

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,20 @@ describe("DefaultTelemetryClient", () => {
4040
metricSinks: [fileSystemSink],
4141
});
4242

43-
const recorder = client.getAttributesRecorder("cli.command_run", {
43+
const metricEvent = client.startMetricEvent("cli.command_run", {
4444
exit_reason: "success",
4545
command_path: "/agentcore",
4646
});
4747

48-
await client.emit("cli.command_run", 123, recorder);
48+
await metricEvent.end(123);
4949

50-
recorder.record({ exit_reason: "failure" });
51-
await client.emit("cli.command_run", 456, recorder);
50+
// Start a second event with failure
51+
const metricEvent2 = client.startMetricEvent("cli.command_run", {
52+
exit_reason: "failure",
53+
command_path: "/agentcore",
54+
});
55+
56+
await metricEvent2.end(456);
5257
await client.shutdown();
5358

5459
expect(fileSystemSink.getName()).toBe("FileSystemSink");
@@ -129,24 +134,20 @@ describe("DefaultTelemetryClient", () => {
129134
auditFilePath,
130135
});
131136

132-
await enabledClient.emit(
133-
"cli.command_run",
134-
123,
135-
enabledClient.getAttributesRecorder("cli.command_run", {
136-
exit_reason: "success",
137-
command_path: "/agentcore",
138-
is_tui: true,
139-
}),
140-
);
141-
await disabledClient.emit(
142-
"cli.command_run",
143-
123,
144-
enabledClient.getAttributesRecorder("cli.command_run", {
145-
exit_reason: "failure",
146-
command_path: "/agentcore",
147-
is_tui: false,
148-
}),
149-
);
137+
const enabledEvent = enabledClient.startMetricEvent("cli.command_run", {
138+
exit_reason: "success",
139+
command_path: "/agentcore",
140+
is_tui: true,
141+
});
142+
await enabledEvent.end(123);
143+
144+
const disabledEvent = disabledClient.startMetricEvent("cli.command_run", {
145+
exit_reason: "failure",
146+
command_path: "/agentcore",
147+
is_tui: false,
148+
});
149+
await disabledEvent.end(123);
150+
150151
await Promise.all([enabledClient.shutdown(), disabledClient.shutdown()]);
151152

152153
const auditLines = (await readFile(auditFilePath, "utf8")).trimEnd().split("\n");
@@ -170,17 +171,17 @@ describe("DefaultTelemetryClient", () => {
170171
});
171172
});
172173

173-
test("throws when recorder has incomplete attributes", async () => {
174+
test("throws when metric event has incomplete attributes", async () => {
174175
const client = new DefaultTelemetryClient({
175176
logger,
176177
sessionId: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
177178
globalConfigAccessor: new TestGlobalConfigAccessor(),
178179
metricSinks: [],
179180
});
180181

181-
const recorder = client.getAttributesRecorder("cli.command_run");
182+
const metricEvent = client.startMetricEvent("cli.command_run");
182183

183-
expect(() => client.emit("cli.command_run", 100, recorder)).toThrow();
184+
await expect(metricEvent.end(100)).rejects.toThrow();
184185
await client.shutdown();
185186
});
186187

@@ -197,11 +198,11 @@ describe("DefaultTelemetryClient", () => {
197198
globalConfigAccessor: new TestGlobalConfigAccessor(),
198199
metricSinks: [sink],
199200
});
200-
const recorder = client.getAttributesRecorder("cli.command_run", {
201+
const metricEvent = client.startMetricEvent("cli.command_run", {
201202
exit_reason: "success",
202203
command_path: "/agentcore",
203204
});
204-
await client.emit("cli.command_run", 1, recorder);
205+
await metricEvent.end(1);
205206
await client.shutdown();
206207

207208
await assertLogsMatch(tempDir, [
@@ -241,13 +242,13 @@ describe("DefaultTelemetryClient", () => {
241242
metricSinks: [badSink, goodSink],
242243
});
243244

244-
const recorder = client.getAttributesRecorder("cli.command_run", {
245+
const metricEvent = client.startMetricEvent("cli.command_run", {
245246
exit_reason: "success",
246247
command_path: "/agentcore",
247248
});
248249

249-
// emit should not throw even though the sink's record() throws
250-
await client.emit("cli.command_run", 100, recorder);
250+
// end should not throw even though the sink's send() throws
251+
await metricEvent.end(100);
251252
// shutdown should not throw even though the sink's shutdown() rejects
252253
await client.shutdown();
253254

0 commit comments

Comments
 (0)