Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/otel/vscode-node/otelContrib.ts
13399 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 * as os from 'os';
7
import * as vscode from 'vscode';
8
import { ConfigKey } from '../../../platform/configuration/common/configurationService';
9
import { ILogService } from '../../../platform/log/common/logService';
10
import { DEFAULT_OTLP_ENDPOINT } from '../../../platform/otel/common/otelConfig';
11
import { IOTelService } from '../../../platform/otel/common/otelService';
12
import { IOTelSqliteStore, type OTelSqliteStore } from '../../../platform/otel/node/sqlite/otelSqliteStore';
13
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
14
import { Disposable } from '../../../util/vs/base/common/lifecycle';
15
import type { IExtensionContribution } from '../../common/contributions';
16
17
/**
18
* Lifecycle contribution that logs OTel status, wires the SQLite store,
19
* and shuts down the SDK on extension deactivation.
20
*/
21
export class OTelContrib extends Disposable implements IExtensionContribution {
22
23
constructor(
24
@IOTelService private readonly _otelService: IOTelService,
25
@IOTelSqliteStore private readonly _sqliteStore: OTelSqliteStore,
26
@ILogService private readonly _logService: ILogService,
27
@ITelemetryService private readonly _telemetryService: ITelemetryService,
28
) {
29
super();
30
if (this._otelService.config.enabled) {
31
this._logService.info(`[OTel] Instrumentation enabled — exporter=${this._otelService.config.exporterType} endpoint=${this._otelService.config.otlpEndpoint} captureContent=${this._otelService.config.captureContent}`);
32
} else {
33
this._logService.trace('[OTel] Instrumentation disabled');
34
}
35
36
this._fireActivatedTelemetry();
37
38
this._register(vscode.commands.registerCommand('github.copilot.chat.otel.flush', async () => {
39
if (!this._otelService.config.enabled) {
40
return;
41
}
42
this._logService.info('[OTel] Flush requested — exporting pending traces, metrics, and events');
43
await this._otelService.flush();
44
this._logService.info('[OTel] Flush complete');
45
}));
46
47
// Prompt for reload when OTel settings change — these are read once at
48
// activation and the OTel SDK cannot be reconfigured at runtime.
49
this._watchForReloadRequiredChanges();
50
51
// Export the agent-traces.db file.
52
// Programmatic (eval harness): called with savePath URI or string → copies DB there.
53
// Interactive (command palette): shows save dialog with default filename.
54
this._register(vscode.commands.registerCommand('github.copilot.chat.otel.exportAgentTracesDB', async (savePath?: vscode.Uri | string) => {
55
const dbPath = this._sqliteStore.dbPath;
56
if (!dbPath) {
57
return;
58
}
59
const src = vscode.Uri.file(dbPath);
60
let dest: vscode.Uri;
61
62
if (savePath) {
63
const saveUri = typeof savePath === 'string' ? vscode.Uri.file(savePath) : savePath;
64
dest = vscode.Uri.joinPath(saveUri, 'agent-traces.db');
65
} else {
66
// Interactive: show save dialog with default filename
67
const result = await vscode.window.showSaveDialog({
68
defaultUri: vscode.Uri.file(os.homedir() + '/agent-traces.db'),
69
filters: { 'SQLite Database': ['db'] },
70
title: 'Export Agent Traces DB',
71
});
72
if (!result) {
73
return;
74
}
75
dest = result;
76
}
77
78
// Flush BatchSpanProcessors so all buffered spans are written to SQLite
79
// before we checkpoint + copy. Without this, the root invoke_agent span
80
// (which ends last) may still be in the processor's buffer.
81
await this._otelService.flush();
82
83
// Checkpoint WAL so all data is flushed into the main .db file before copying.
84
// Without this, the copy would be empty because data lives in the -wal file.
85
this._sqliteStore.checkpoint();
86
87
await vscode.workspace.fs.copy(src, dest, { overwrite: true });
88
this._logService.info(`[OTel] Exported agent-traces.db to ${dest.fsPath}`);
89
90
/* __GDPR__
91
"otel.exportAgentTracesDB" : {
92
"owner": "zhichli",
93
"comment": "Fired when the user exports the agent-traces.db file",
94
"interactive": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the export was interactive (save dialog) or programmatic (eval harness)" }
95
}
96
*/
97
this._telemetryService.sendMSFTTelemetryEvent('otel.exportAgentTracesDB', {
98
interactive: String(!savePath),
99
});
100
}));
101
}
102
103
private _watchForReloadRequiredChanges(): void {
104
const reloadSettings = [
105
ConfigKey.Advanced.OTelEnabled,
106
ConfigKey.Advanced.OTelExporterType,
107
ConfigKey.Advanced.OTelOtlpEndpoint,
108
ConfigKey.Advanced.OTelCaptureContent,
109
ConfigKey.Advanced.OTelOutfile,
110
ConfigKey.Advanced.OTelDbSpanExporter,
111
];
112
113
// Snapshot initial values to avoid prompting when the setting hasn't actually changed
114
const initialValues = new Map(reloadSettings.map(s => [s.fullyQualifiedId, vscode.workspace.getConfiguration().get(s.fullyQualifiedId)]));
115
116
this._register(vscode.workspace.onDidChangeConfiguration(async e => {
117
const currentConfig = vscode.workspace.getConfiguration();
118
const changed = reloadSettings.some(s =>
119
e.affectsConfiguration(s.fullyQualifiedId) &&
120
currentConfig.get(s.fullyQualifiedId) !== initialValues.get(s.fullyQualifiedId)
121
);
122
if (!changed) {
123
return;
124
}
125
const reloadWindowLabel = vscode.l10n.t("Reload Window");
126
const selection = await vscode.window.showInformationMessage(
127
vscode.l10n.t("Copilot OTel settings changed - a reload is required for the change to take effect."),
128
reloadWindowLabel,
129
);
130
if (selection === reloadWindowLabel) {
131
await vscode.commands.executeCommand('workbench.action.reloadWindow');
132
}
133
}));
134
}
135
136
override dispose(): void {
137
// Close SQLite store before OTel shutdown
138
this._sqliteStore.close();
139
if (this._otelService.config.enabled) {
140
this._logService.info('[OTel] Shutting down — flushing pending traces, metrics, and events');
141
}
142
this._otelService.shutdown().catch((err: Error) => {
143
this._logService.error('[OTel] Error during shutdown:', String(err));
144
});
145
super.dispose();
146
}
147
148
private _fireActivatedTelemetry(): void {
149
const config = this._otelService.config;
150
/* __GDPR__
151
"otel.activated" : {
152
"owner": "zhichli",
153
"comment": "Fired once at activation to capture OTel configuration for adoption tracking",
154
"enabled": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the full OTel SDK is loaded" },
155
"enabledVia": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "How OTel was enabled: envVar, setting, otlpEndpointEnvVar, dbSpanExporterOnly, or disabled" },
156
"dbSpanExporter": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether the SQLite local DB span exporter is enabled" },
157
"exporterType": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The OTel exporter type: otlp-grpc, otlp-http, console, or file" },
158
"captureContent": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether prompt/response content capture is enabled" },
159
"protocol": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "OTLP protocol: grpc or http" },
160
"hasCustomEndpoint": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether a non-default OTLP endpoint was configured" },
161
"hasCustomServiceName": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether OTEL_SERVICE_NAME was customized from the default" },
162
"hasResourceAttributes": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Whether custom OTEL_RESOURCE_ATTRIBUTES were set" }
163
}
164
*/
165
this._telemetryService.sendMSFTTelemetryEvent('otel.activated', {
166
enabled: String(config.enabled),
167
enabledVia: config.enabledVia,
168
dbSpanExporter: String(config.dbSpanExporter),
169
exporterType: config.exporterType,
170
captureContent: String(config.captureContent),
171
protocol: config.otlpProtocol,
172
hasCustomEndpoint: String(config.enabled && config.otlpEndpoint !== DEFAULT_OTLP_ENDPOINT && config.otlpEndpoint !== DEFAULT_OTLP_ENDPOINT + '/'),
173
hasCustomServiceName: String(config.serviceName !== 'copilot-chat'),
174
hasResourceAttributes: String(Object.keys(config.resourceAttributes).length > 0),
175
});
176
}
177
}
178
179