Skip to content

Commit c795a1a

Browse files
committed
fix: propagate diagnostics timeline phase
1 parent 61223a7 commit c795a1a

3 files changed

Lines changed: 101 additions & 16 deletions

File tree

src/infra/diagnostics-timeline.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,47 @@ describe("diagnostics timeline", () => {
215215
name: "plugins.metadata.scan",
216216
});
217217
});
218+
219+
it("lets nested spans inherit the active timeline phase and parent span", async () => {
220+
const { env, path } = await createTimelineEnv();
221+
222+
const result = await measureDiagnosticsTimelineSpan(
223+
"reply.run_agent_turn",
224+
() =>
225+
measureDiagnosticsTimelineSpanSync("plugins.metadata.scan", () => 42, {
226+
env,
227+
}),
228+
{
229+
env,
230+
phase: "agent-turn",
231+
},
232+
);
233+
234+
expect(result).toBe(42);
235+
const events = await readTimeline(path);
236+
expect(events).toHaveLength(4);
237+
const [parentStart, childStart, childEnd, parentEnd] = events;
238+
expect(parentStart).toMatchObject({
239+
type: "span.start",
240+
name: "reply.run_agent_turn",
241+
phase: "agent-turn",
242+
});
243+
expect(childStart).toMatchObject({
244+
type: "span.start",
245+
name: "plugins.metadata.scan",
246+
phase: "agent-turn",
247+
parentSpanId: parentStart?.spanId,
248+
});
249+
expect(childEnd).toMatchObject({
250+
type: "span.end",
251+
name: "plugins.metadata.scan",
252+
phase: "agent-turn",
253+
parentSpanId: parentStart?.spanId,
254+
});
255+
expect(parentEnd).toMatchObject({
256+
type: "span.end",
257+
name: "reply.run_agent_turn",
258+
phase: "agent-turn",
259+
});
260+
});
218261
});

src/infra/diagnostics-timeline.ts

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AsyncLocalStorage } from "node:async_hooks";
12
import { randomUUID } from "node:crypto";
23
import { mkdirSync } from "node:fs";
34
import { dirname } from "node:path";
@@ -60,8 +61,17 @@ type DiagnosticsTimelineOptions = {
6061
env?: NodeJS.ProcessEnv;
6162
};
6263

64+
export type ActiveDiagnosticsTimelineSpan = {
65+
name: string;
66+
phase?: string;
67+
spanId: string;
68+
parentSpanId?: string;
69+
attributes?: DiagnosticsTimelineAttributes;
70+
};
71+
6372
let warnedAboutTimelineWrite = false;
6473
const createdTimelineDirs = new Set<string>();
74+
const activeDiagnosticsTimelineSpan = new AsyncLocalStorage<ActiveDiagnosticsTimelineSpan>();
6575

6676
function resolveDiagnosticsTimelineOptions(
6777
options: DiagnosticsTimelineOptions = {},
@@ -177,6 +187,10 @@ export function emitDiagnosticsTimelineEvent(
177187
}
178188
}
179189

190+
export function getActiveDiagnosticsTimelineSpan(): ActiveDiagnosticsTimelineSpan | undefined {
191+
return activeDiagnosticsTimelineSpan.getStore();
192+
}
193+
180194
export async function measureDiagnosticsTimelineSpan<T>(
181195
name: string,
182196
run: () => Promise<T> | T,
@@ -186,28 +200,40 @@ export async function measureDiagnosticsTimelineSpan<T>(
186200
if (!isDiagnosticsTimelineEnabled({ config: options.config, env })) {
187201
return await run();
188202
}
203+
const activeSpan = getActiveDiagnosticsTimelineSpan();
189204
const spanId = randomUUID();
205+
const phase = options.phase ?? activeSpan?.phase;
206+
const parentSpanId = options.parentSpanId ?? activeSpan?.spanId;
190207
const startedAt = performance.now();
191208
emitDiagnosticsTimelineEvent(
192209
{
193210
type: "span.start",
194211
name,
195-
phase: options.phase,
212+
phase,
196213
spanId,
197-
parentSpanId: options.parentSpanId,
214+
parentSpanId,
198215
attributes: options.attributes,
199216
},
200217
{ config: options.config, env },
201218
);
202219
try {
203-
const result = await run();
220+
const result = await activeDiagnosticsTimelineSpan.run(
221+
{
222+
name,
223+
...(phase ? { phase } : {}),
224+
spanId,
225+
...(parentSpanId ? { parentSpanId } : {}),
226+
...(options.attributes ? { attributes: options.attributes } : {}),
227+
},
228+
() => run(),
229+
);
204230
emitDiagnosticsTimelineEvent(
205231
{
206232
type: "span.end",
207233
name,
208-
phase: options.phase,
234+
phase,
209235
spanId,
210-
parentSpanId: options.parentSpanId,
236+
parentSpanId,
211237
durationMs: performance.now() - startedAt,
212238
attributes: options.attributes,
213239
},
@@ -219,9 +245,9 @@ export async function measureDiagnosticsTimelineSpan<T>(
219245
{
220246
type: "span.error",
221247
name,
222-
phase: options.phase,
248+
phase,
223249
spanId,
224-
parentSpanId: options.parentSpanId,
250+
parentSpanId,
225251
durationMs: performance.now() - startedAt,
226252
attributes: options.attributes,
227253
errorName: error instanceof Error ? error.name : typeof error,
@@ -242,28 +268,40 @@ export function measureDiagnosticsTimelineSpanSync<T>(
242268
if (!isDiagnosticsTimelineEnabled({ config: options.config, env })) {
243269
return run();
244270
}
271+
const activeSpan = getActiveDiagnosticsTimelineSpan();
245272
const spanId = randomUUID();
273+
const phase = options.phase ?? activeSpan?.phase;
274+
const parentSpanId = options.parentSpanId ?? activeSpan?.spanId;
246275
const startedAt = performance.now();
247276
emitDiagnosticsTimelineEvent(
248277
{
249278
type: "span.start",
250279
name,
251-
phase: options.phase,
280+
phase,
252281
spanId,
253-
parentSpanId: options.parentSpanId,
282+
parentSpanId,
254283
attributes: options.attributes,
255284
},
256285
{ config: options.config, env },
257286
);
258287
try {
259-
const result = run();
288+
const result = activeDiagnosticsTimelineSpan.run(
289+
{
290+
name,
291+
...(phase ? { phase } : {}),
292+
spanId,
293+
...(parentSpanId ? { parentSpanId } : {}),
294+
...(options.attributes ? { attributes: options.attributes } : {}),
295+
},
296+
run,
297+
);
260298
emitDiagnosticsTimelineEvent(
261299
{
262300
type: "span.end",
263301
name,
264-
phase: options.phase,
302+
phase,
265303
spanId,
266-
parentSpanId: options.parentSpanId,
304+
parentSpanId,
267305
durationMs: performance.now() - startedAt,
268306
attributes: options.attributes,
269307
},
@@ -275,9 +313,9 @@ export function measureDiagnosticsTimelineSpanSync<T>(
275313
{
276314
type: "span.error",
277315
name,
278-
phase: options.phase,
316+
phase,
279317
spanId,
280-
parentSpanId: options.parentSpanId,
318+
parentSpanId,
281319
durationMs: performance.now() - startedAt,
282320
attributes: options.attributes,
283321
errorName: error instanceof Error ? error.name : typeof error,

src/plugins/plugin-metadata-snapshot.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
2-
import { measureDiagnosticsTimelineSpanSync } from "../infra/diagnostics-timeline.js";
2+
import {
3+
getActiveDiagnosticsTimelineSpan,
4+
measureDiagnosticsTimelineSpanSync,
5+
} from "../infra/diagnostics-timeline.js";
36
import { resolveInstalledPluginIndexPolicyHash } from "./installed-plugin-index-policy.js";
47
import type { InstalledPluginIndex } from "./installed-plugin-index.js";
58
import {
@@ -173,11 +176,12 @@ export function listPluginOriginsFromMetadataSnapshot(
173176
export function loadPluginMetadataSnapshot(
174177
params: LoadPluginMetadataSnapshotParams,
175178
): PluginMetadataSnapshot {
179+
const activeTimelineSpan = getActiveDiagnosticsTimelineSpan();
176180
return measureDiagnosticsTimelineSpanSync(
177181
"plugins.metadata.scan",
178182
() => loadPluginMetadataSnapshotImpl(params),
179183
{
180-
phase: "startup",
184+
phase: activeTimelineSpan?.phase ?? "startup",
181185
config: params.config,
182186
env: params.env,
183187
attributes: {

0 commit comments

Comments
 (0)