Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/otel/node/inMemoryOTelService.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { AsyncLocalStorage } from 'async_hooks';
7
import { Emitter, type Event } from '../../../util/vs/base/common/event';
8
import type { OTelConfig } from '../common/otelConfig';
9
import { SpanStatusCode, type ICompletedSpanData, type IOTelService, type ISpanEventData, type ISpanEventRecord, type ISpanHandle, type SpanOptions, type TraceContext } from '../common/otelService';
10
11
let nextId = 1;
12
function hexId(len: number): string {
13
return (nextId++).toString(16).padStart(len, '0');
14
}
15
16
/**
17
* Span context stored in AsyncLocalStorage for correct parent-child propagation
18
* across concurrent async operations.
19
*/
20
interface SpanContext {
21
readonly spanId: string;
22
readonly traceId: string;
23
}
24
25
/**
26
* Full-fidelity span handle that tracks all attributes, events, and status in-memory.
27
* Fires onDidCompleteSpan on end() and onDidEmitSpanEvent on addEvent().
28
*/
29
class InMemorySpanHandle implements ISpanHandle {
30
private readonly _attributes: Record<string, string | number | boolean | string[]> = {};
31
private readonly _events: ISpanEventRecord[] = [];
32
private _statusCode = SpanStatusCode.UNSET;
33
private _statusMessage?: string;
34
private readonly _startTime = Date.now();
35
private _ended = false;
36
37
readonly spanId: string;
38
readonly traceId: string;
39
readonly parentSpanId: string | undefined;
40
41
constructor(
42
readonly name: string,
43
private readonly _onDidCompleteSpan: Emitter<ICompletedSpanData>,
44
private readonly _onDidEmitSpanEvent: Emitter<ISpanEventData>,
45
parentContext?: SpanContext,
46
initialAttributes?: Record<string, string | number | boolean | string[]>,
47
) {
48
this.spanId = hexId(16);
49
this.traceId = parentContext?.traceId ?? hexId(32);
50
this.parentSpanId = parentContext?.spanId;
51
if (initialAttributes) {
52
Object.assign(this._attributes, initialAttributes);
53
}
54
}
55
56
setAttribute(key: string, value: string | number | boolean | string[]): void {
57
this._attributes[key] = value;
58
}
59
60
setAttributes(attrs: Record<string, string | number | boolean | string[] | undefined>): void {
61
for (const k in attrs) {
62
if (Object.prototype.hasOwnProperty.call(attrs, k)) {
63
const v = attrs[k];
64
if (v !== undefined) {
65
this._attributes[k] = v;
66
}
67
}
68
}
69
}
70
71
setStatus(code: SpanStatusCode, message?: string): void {
72
this._statusCode = code;
73
this._statusMessage = message;
74
}
75
76
recordException(_error: unknown): void { /* no-op for in-memory */ }
77
78
addEvent(name: string, attributes?: Record<string, string | number | boolean | string[]>): void {
79
const timestamp = Date.now();
80
this._events.push({ name, timestamp, attributes });
81
try {
82
this._onDidEmitSpanEvent.fire({
83
spanId: this.spanId,
84
traceId: this.traceId,
85
parentSpanId: this.parentSpanId,
86
eventName: name,
87
attributes: attributes ?? {},
88
timestamp,
89
});
90
} catch { /* emitter disposed */ }
91
}
92
93
end(): void {
94
if (this._ended) { return; }
95
this._ended = true;
96
try {
97
this._onDidCompleteSpan.fire({
98
name: this.name,
99
spanId: this.spanId,
100
traceId: this.traceId,
101
parentSpanId: this.parentSpanId,
102
startTime: this._startTime,
103
endTime: Date.now(),
104
status: { code: this._statusCode, message: this._statusMessage },
105
attributes: { ...this._attributes },
106
events: [...this._events],
107
});
108
} catch { /* emitter disposed */ }
109
}
110
111
get context(): SpanContext {
112
return { spanId: this.spanId, traceId: this.traceId };
113
}
114
115
getSpanContext(): TraceContext | undefined {
116
return { spanId: this.spanId, traceId: this.traceId };
117
}
118
}
119
120
/**
121
* In-memory OTel service for the debug panel.
122
*
123
* Uses Node.js AsyncLocalStorage for correct parent-child span propagation
124
* across concurrent async operations (e.g., parallel tool calls, subagents).
125
* Does NOT load the OTel SDK or export to any backend.
126
*
127
* Used when OTel external export is disabled (the default).
128
* When OTel export IS enabled, NodeOTelService is used instead (which has
129
* both in-memory tracking AND SDK-based export).
130
*/
131
export class InMemoryOTelService implements IOTelService {
132
declare readonly _serviceBrand: undefined;
133
readonly config: OTelConfig;
134
135
private readonly _onDidCompleteSpan = new Emitter<ICompletedSpanData>();
136
readonly onDidCompleteSpan: Event<ICompletedSpanData> = this._onDidCompleteSpan.event;
137
private readonly _onDidEmitSpanEvent = new Emitter<ISpanEventData>();
138
readonly onDidEmitSpanEvent: Event<ISpanEventData> = this._onDidEmitSpanEvent.event;
139
140
injectCompletedSpan(span: ICompletedSpanData): void {
141
try { this._onDidCompleteSpan.fire(span); } catch { /* emitter may be disposed */ }
142
}
143
144
/** AsyncLocalStorage for correct context propagation across concurrent async ops */
145
private readonly _contextStorage = new AsyncLocalStorage<SpanContext>();
146
147
/** Trace context store for cross-boundary propagation (e.g., subagent invocations) */
148
private static readonly _MAX_TRACE_CONTEXT_STORE_SIZE = 1000;
149
private readonly _traceContextStore = new Map<string, TraceContext>();
150
private readonly _traceContextTimers = new Map<string, ReturnType<typeof setTimeout>>();
151
152
constructor(config: OTelConfig) {
153
this.config = config;
154
}
155
156
startSpan(name: string, options?: SpanOptions): ISpanHandle {
157
const parentCtx = this._resolveParentContext(options);
158
return new InMemorySpanHandle(
159
name,
160
this._onDidCompleteSpan,
161
this._onDidEmitSpanEvent,
162
parentCtx,
163
options?.attributes as Record<string, string | number | boolean | string[]>,
164
);
165
}
166
167
async startActiveSpan<T>(name: string, options: SpanOptions, fn: (span: ISpanHandle) => Promise<T>): Promise<T> {
168
const parentCtx = this._resolveParentContext(options);
169
const handle = new InMemorySpanHandle(
170
name,
171
this._onDidCompleteSpan,
172
this._onDidEmitSpanEvent,
173
parentCtx,
174
options?.attributes as Record<string, string | number | boolean | string[]>,
175
);
176
return this._contextStorage.run(handle.context, async () => {
177
try {
178
return await fn(handle);
179
} finally {
180
handle.end();
181
}
182
});
183
}
184
185
getActiveTraceContext(): TraceContext | undefined {
186
const ctx = this._contextStorage.getStore();
187
return ctx ? { traceId: ctx.traceId, spanId: ctx.spanId } : undefined;
188
}
189
190
storeTraceContext(key: string, context: TraceContext): void {
191
// Evict oldest entry if at capacity
192
if (this._traceContextStore.size >= InMemoryOTelService._MAX_TRACE_CONTEXT_STORE_SIZE) {
193
const oldestKey = this._traceContextStore.keys().next().value;
194
if (oldestKey !== undefined) {
195
this._clearStoredTraceContext(oldestKey);
196
}
197
}
198
this._traceContextStore.set(key, context);
199
// Auto-cleanup after 30 minutes (generous for long-running agent sessions)
200
const timer = setTimeout(() => this._clearStoredTraceContext(key), 30 * 60 * 1000);
201
this._traceContextTimers.set(key, timer);
202
}
203
204
getStoredTraceContext(key: string): TraceContext | undefined {
205
const ctx = this._traceContextStore.get(key);
206
if (ctx) { this._clearStoredTraceContext(key); }
207
return ctx;
208
}
209
210
private _clearStoredTraceContext(key: string): void {
211
this._traceContextStore.delete(key);
212
const timer = this._traceContextTimers.get(key);
213
if (timer) {
214
clearTimeout(timer);
215
this._traceContextTimers.delete(key);
216
}
217
}
218
219
runWithTraceContext<T>(traceContext: TraceContext, fn: () => Promise<T>): Promise<T> {
220
return this._contextStorage.run({ spanId: traceContext.spanId, traceId: traceContext.traceId }, fn);
221
}
222
223
// ── No-ops for metrics/logs (not needed for debug panel for now) ──
224
225
recordMetric(_name: string, _value: number, _attributes?: Record<string, string | number | boolean>): void { }
226
incrementCounter(_name: string, _value?: number, _attributes?: Record<string, string | number | boolean>): void { }
227
emitLogRecord(_body: string, _attributes?: Record<string, unknown>): void { }
228
async flush(): Promise<void> { }
229
230
async shutdown(): Promise<void> {
231
for (const timer of this._traceContextTimers.values()) {
232
clearTimeout(timer);
233
}
234
this._traceContextTimers.clear();
235
this._traceContextStore.clear();
236
this._onDidCompleteSpan.dispose();
237
this._onDidEmitSpanEvent.dispose();
238
}
239
240
// ── Private ──
241
242
private _resolveParentContext(options?: SpanOptions): SpanContext | undefined {
243
// Explicit parent takes priority (e.g., subagent linking)
244
if (options?.parentTraceContext) {
245
return {
246
spanId: options.parentTraceContext.spanId,
247
traceId: options.parentTraceContext.traceId,
248
};
249
}
250
// Otherwise inherit from async context
251
return this._contextStorage.getStore();
252
}
253
}
254
255