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