Path: blob/main/extensions/copilot/src/platform/otel/common/otelConfig.ts
13401 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*--------------------------------------------------------------------------------------------*/45export type OTelExporterType = 'otlp-grpc' | 'otlp-http' | 'console' | 'file';67export type OTelEnabledVia = 'envVar' | 'setting' | 'otlpEndpointEnvVar' | 'dbSpanExporterOnly' | 'disabled';89/** Default OTLP endpoint used when no env var or setting overrides it. */10export const DEFAULT_OTLP_ENDPOINT = 'http://localhost:4318';1112export interface OTelConfig {13readonly enabled: boolean;14/** True when OTel was enabled via setting/env var, not just implied by dbSpanExporter. */15readonly enabledExplicitly: boolean;16/** How OTel was enabled — used for telemetry to track adoption channels. */17readonly enabledVia: OTelEnabledVia;18readonly exporterType: OTelExporterType;19readonly otlpEndpoint: string;20readonly otlpProtocol: 'grpc' | 'http';21readonly captureContent: boolean;22readonly fileExporterPath?: string;23readonly dbSpanExporter: boolean;24readonly logLevel: 'trace' | 'debug' | 'info' | 'warn' | 'error';25readonly httpInstrumentation: boolean;26readonly serviceName: string;27readonly serviceVersion: string;28readonly sessionId: string;29readonly resourceAttributes: Record<string, string>;30}3132/**33* Parse `OTEL_RESOURCE_ATTRIBUTES` format: "key1=val1,key2=val2"34*/35function parseResourceAttributes(raw: string | undefined): Record<string, string> {36if (!raw) {37return {};38}39const result: Record<string, string> = {};40for (const pair of raw.split(',')) {41const eqIdx = pair.indexOf('=');42if (eqIdx > 0) {43const key = pair.substring(0, eqIdx).trim();44const value = pair.substring(eqIdx + 1).trim();45if (key) {46result[key] = value;47}48}49}50return result;51}5253/**54* Parse and validate an OTLP endpoint URL.55* For gRPC: returns origin (scheme://host:port).56* For HTTP: returns full href.57*/58function parseOtlpEndpoint(raw: string | undefined, protocol: 'grpc' | 'http'): string | undefined {59if (!raw) {60return undefined;61}62const trimmed = raw.replace(/^["']|["']$/g, '');63try {64const url = new URL(trimmed);65return protocol === 'grpc' ? url.origin : url.href;66} catch {67return undefined;68}69}7071export interface OTelConfigInput {72env: Record<string, string | undefined>;73settingEnabled?: boolean;74settingExporterType?: OTelExporterType;75settingOtlpEndpoint?: string;76settingCaptureContent?: boolean;77settingOutfile?: string;78settingDbSpanExporter?: boolean;79extensionVersion: string;80sessionId: string;81vscodeTelemetryLevel?: string;82}8384/**85* Resolve OTel configuration with layered precedence:86* 1. COPILOT_OTEL_* env vars (highest)87* 2. OTEL_EXPORTER_OTLP_* standard env vars88* 3. VS Code settings89* 4. Defaults (lowest)90*/91export function resolveOTelConfig(input: OTelConfigInput): OTelConfig {92const { env } = input;9394// Kill switch: respect VS Code telemetry level95if (input.vscodeTelemetryLevel === 'off') {96return createDisabledConfig(input);97}9899// SQLite DB span exporter: setting > default(false)100const dbSpanExporter = input.settingDbSpanExporter ?? false;101102// Determine if enabled: env > setting > dbSpanExporter > default(false)103// When dbSpanExporter is on, OTel must be enabled for the SDK pipeline to work.104const enabled = (envBool(env['COPILOT_OTEL_ENABLED'])105?? input.settingEnabled106?? (!!env['OTEL_EXPORTER_OTLP_ENDPOINT']))107|| dbSpanExporter;108109// OTel was explicitly enabled if the user/env turned it on, not just dbSpanExporter110const enabledExplicitly = (envBool(env['COPILOT_OTEL_ENABLED'])111?? input.settingEnabled112?? (!!env['OTEL_EXPORTER_OTLP_ENDPOINT'])) === true;113114if (!enabled) {115return createDisabledConfig(input);116}117118// Determine how OTel was enabled for telemetry tracking119let enabledVia: OTelEnabledVia;120if (envBool(env['COPILOT_OTEL_ENABLED']) === true) {121enabledVia = 'envVar';122} else if (input.settingEnabled === true) {123enabledVia = 'setting';124} else if (!!env['OTEL_EXPORTER_OTLP_ENDPOINT']) {125enabledVia = 'otlpEndpointEnvVar';126} else {127enabledVia = 'dbSpanExporterOnly';128}129130// Protocol: env > inferred from exporter type > default131const rawProtocol = env['OTEL_EXPORTER_OTLP_PROTOCOL'] ?? env['COPILOT_OTEL_PROTOCOL'];132const protocol: 'grpc' | 'http' = rawProtocol === 'grpc' ? 'grpc' : 'http';133134// Endpoint: COPILOT_OTEL env > OTEL env > setting > default135const rawEndpoint = env['COPILOT_OTEL_ENDPOINT']136?? env['OTEL_EXPORTER_OTLP_ENDPOINT']137?? input.settingOtlpEndpoint138?? DEFAULT_OTLP_ENDPOINT;139const otlpEndpoint = parseOtlpEndpoint(rawEndpoint, protocol) ?? DEFAULT_OTLP_ENDPOINT;140141// File exporter path142const fileExporterPath = env['COPILOT_OTEL_FILE_EXPORTER_PATH'] ?? input.settingOutfile;143144// Exporter type145let exporterType: OTelExporterType;146if (fileExporterPath) {147exporterType = 'file';148} else if (input.settingExporterType) {149exporterType = input.settingExporterType;150} else {151exporterType = protocol === 'grpc' ? 'otlp-grpc' : 'otlp-http';152}153154// Content capture155const captureContent = envBool(env['COPILOT_OTEL_CAPTURE_CONTENT'])156?? input.settingCaptureContent157?? false;158159// Log level160const validLogLevels = new Set<OTelConfig['logLevel']>(['trace', 'debug', 'info', 'warn', 'error']);161const rawLogLevel = env['COPILOT_OTEL_LOG_LEVEL'];162const logLevel: OTelConfig['logLevel'] = rawLogLevel && validLogLevels.has(rawLogLevel as OTelConfig['logLevel'])163? rawLogLevel as OTelConfig['logLevel']164: 'info';165166// HTTP instrumentation167const httpInstrumentation = envBool(env['COPILOT_OTEL_HTTP_INSTRUMENTATION']) ?? false;168169// Service name170const serviceName = env['OTEL_SERVICE_NAME'] ?? 'copilot-chat';171172// Resource attributes173const resourceAttributes = parseResourceAttributes(env['OTEL_RESOURCE_ATTRIBUTES']);174175return Object.freeze({176enabled: true,177enabledExplicitly,178enabledVia,179exporterType,180otlpEndpoint,181otlpProtocol: protocol,182captureContent,183fileExporterPath,184dbSpanExporter,185logLevel,186httpInstrumentation,187serviceName,188serviceVersion: input.extensionVersion,189sessionId: input.sessionId,190resourceAttributes,191});192}193194function createDisabledConfig(input: OTelConfigInput): OTelConfig {195return Object.freeze({196enabled: false,197enabledExplicitly: false,198enabledVia: 'disabled' as const,199exporterType: 'otlp-http' as const,200otlpEndpoint: '',201otlpProtocol: 'http' as const,202captureContent: false,203dbSpanExporter: false,204logLevel: 'info' as const,205httpInstrumentation: false,206serviceName: 'copilot-chat',207serviceVersion: input.extensionVersion,208sessionId: input.sessionId,209resourceAttributes: {},210});211}212213function envBool(val: string | undefined): boolean | undefined {214if (val === undefined) {215return undefined;216}217return val === 'true' || val === '1';218}219220221