Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/otel/common/test/capturingOTelService.ts
13406 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 { Emitter, type Event } from '../../../../util/vs/base/common/event';
7
import { resolveOTelConfig, type OTelConfig } from '../otelConfig';
8
import { SpanStatusCode, type ICompletedSpanData, type IOTelService, type ISpanEventData, type ISpanEventRecord, type ISpanHandle, type SpanOptions, type TraceContext } from '../otelService';
9
10
/**
11
* Captured span record for test assertions.
12
*/
13
export interface CapturedSpan {
14
name: string;
15
kind?: number;
16
attributes: Record<string, string | number | boolean | string[] | undefined>;
17
statusCode?: SpanStatusCode;
18
statusMessage?: string;
19
exceptions: unknown[];
20
ended: boolean;
21
parentTraceContext?: TraceContext;
22
events: ISpanEventRecord[];
23
}
24
25
/**
26
* Captured metric record.
27
*/
28
export interface CapturedMetric {
29
name: string;
30
value: number;
31
attributes?: Record<string, string | number | boolean>;
32
}
33
34
/**
35
* Captured log record.
36
*/
37
export interface CapturedLogRecord {
38
body: string;
39
attributes?: Record<string, unknown>;
40
}
41
42
/**
43
* IOTelService implementation that captures all operations for test verification.
44
* Unlike NoopOTelService, this records spans, metrics, and logs so tests can
45
* assert on the OTel output without a real SDK.
46
*/
47
export class CapturingOTelService implements IOTelService {
48
declare readonly _serviceBrand: undefined;
49
readonly config: OTelConfig;
50
51
readonly spans: CapturedSpan[] = [];
52
readonly metrics: CapturedMetric[] = [];
53
readonly counters: CapturedMetric[] = [];
54
readonly logRecords: CapturedLogRecord[] = [];
55
private readonly _traceContextStore = new Map<string, TraceContext>();
56
private readonly _onDidCompleteSpan = new Emitter<ICompletedSpanData>();
57
readonly onDidCompleteSpan: Event<ICompletedSpanData> = this._onDidCompleteSpan.event;
58
private readonly _onDidEmitSpanEvent = new Emitter<ISpanEventData>();
59
readonly onDidEmitSpanEvent: Event<ISpanEventData> = this._onDidEmitSpanEvent.event;
60
61
injectCompletedSpan(span: ICompletedSpanData): void {
62
this._onDidCompleteSpan.fire(span);
63
}
64
65
constructor(config?: Partial<OTelConfig>) {
66
this.config = {
67
...resolveOTelConfig({ env: { 'COPILOT_OTEL_ENABLED': 'true' }, extensionVersion: '1.0.0', sessionId: 'test' }),
68
...config,
69
};
70
}
71
72
startSpan(name: string, options?: SpanOptions): ISpanHandle {
73
const captured: CapturedSpan = {
74
name,
75
kind: options?.kind,
76
attributes: { ...options?.attributes },
77
exceptions: [],
78
ended: false,
79
parentTraceContext: options?.parentTraceContext,
80
events: [],
81
};
82
this.spans.push(captured);
83
return new CapturingSpanHandle(captured, this._onDidCompleteSpan, this._onDidEmitSpanEvent);
84
}
85
86
async startActiveSpan<T>(name: string, options: SpanOptions, fn: (span: ISpanHandle) => Promise<T>): Promise<T> {
87
const span = this.startSpan(name, options);
88
try {
89
return await fn(span);
90
} finally {
91
span.end();
92
}
93
}
94
95
getActiveTraceContext(): TraceContext | undefined {
96
return undefined;
97
}
98
99
storeTraceContext(key: string, context: TraceContext): void {
100
this._traceContextStore.set(key, context);
101
}
102
103
getStoredTraceContext(key: string): TraceContext | undefined {
104
const ctx = this._traceContextStore.get(key);
105
if (ctx) {
106
this._traceContextStore.delete(key);
107
}
108
return ctx;
109
}
110
111
async runWithTraceContext<T>(_traceContext: TraceContext, fn: () => Promise<T>): Promise<T> {
112
return fn();
113
}
114
115
recordMetric(name: string, value: number, attributes?: Record<string, string | number | boolean>): void {
116
this.metrics.push({ name, value, attributes });
117
}
118
119
incrementCounter(name: string, value = 1, attributes?: Record<string, string | number | boolean>): void {
120
this.counters.push({ name, value, attributes });
121
}
122
123
emitLogRecord(body: string, attributes?: Record<string, unknown>): void {
124
this.logRecords.push({ body, attributes });
125
}
126
127
async flush(): Promise<void> { }
128
async shutdown(): Promise<void> { }
129
130
/** Find spans by name prefix. */
131
findSpans(namePrefix: string): CapturedSpan[] {
132
return this.spans.filter(s => s.name.startsWith(namePrefix));
133
}
134
135
/** Reset all captured data. */
136
reset(): void {
137
this.spans.length = 0;
138
this.metrics.length = 0;
139
this.counters.length = 0;
140
this.logRecords.length = 0;
141
}
142
}
143
144
class CapturingSpanHandle implements ISpanHandle {
145
private static _nextSpanId = 1;
146
private readonly _spanId: string;
147
148
constructor(
149
private readonly _captured: CapturedSpan,
150
private readonly _onDidCompleteSpan: Emitter<ICompletedSpanData>,
151
private readonly _onDidEmitSpanEvent: Emitter<ISpanEventData>,
152
) {
153
this._spanId = String(CapturingSpanHandle._nextSpanId++).padStart(16, '0');
154
}
155
156
setAttribute(key: string, value: string | number | boolean | string[]): void {
157
this._captured.attributes[key] = value;
158
}
159
160
setAttributes(attrs: Record<string, string | number | boolean | string[] | undefined>): void {
161
for (const k in attrs) {
162
if (Object.prototype.hasOwnProperty.call(attrs, k)) {
163
this._captured.attributes[k] = attrs[k];
164
}
165
}
166
}
167
168
setStatus(code: SpanStatusCode, message?: string): void {
169
this._captured.statusCode = code;
170
this._captured.statusMessage = message;
171
}
172
173
recordException(error: unknown): void {
174
this._captured.exceptions.push(error);
175
}
176
177
addEvent(name: string, attributes?: Record<string, string | number | boolean | string[]>): void {
178
const timestamp = Date.now();
179
const record: ISpanEventRecord = { name, timestamp, attributes };
180
this._captured.events.push(record);
181
this._onDidEmitSpanEvent.fire({
182
spanId: this._spanId,
183
traceId: '00000000000000000000000000000000',
184
eventName: name,
185
attributes: attributes ?? {},
186
timestamp,
187
});
188
}
189
190
getSpanContext(): TraceContext | undefined {
191
return { spanId: this._spanId, traceId: '00000000000000000000000000000000' };
192
}
193
194
end(): void {
195
this._captured.ended = true;
196
const attrs: Record<string, string | number | boolean | string[]> = {};
197
for (const [k, v] of Object.entries(this._captured.attributes)) {
198
if (v !== undefined) {
199
attrs[k] = v;
200
}
201
}
202
this._onDidCompleteSpan.fire({
203
name: this._captured.name,
204
spanId: this._spanId,
205
traceId: '00000000000000000000000000000000',
206
startTime: Date.now(),
207
endTime: Date.now(),
208
status: { code: this._captured.statusCode ?? SpanStatusCode.UNSET, message: this._captured.statusMessage },
209
attributes: attrs,
210
events: [...this._captured.events],
211
});
212
}
213
}
214
215