Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 3 additions & 11 deletions extensions/copilot/src/extension/extension/vscode-node/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ import { IFetcherService } from '../../../platform/networking/common/fetcherServ
import { IToolDeferralService } from '../../../platform/networking/common/toolDeferralService';
import { ChatWebSocketManager, IChatWebSocketManager } from '../../../platform/networking/node/chatWebSocketManager';
import { FetcherService } from '../../../platform/networking/vscode-node/fetcherServiceImpl';
import { resolveOTelConfig } from '../../../platform/otel/common/otelConfig';
import { readOTelPolicyConfig, resolveOTelConfig } from '../../../platform/otel/common/otelConfig';
import { IOTelService } from '../../../platform/otel/common/otelService';
import { InMemoryOTelService } from '../../../platform/otel/node/inMemoryOTelService';
import { IOTelSqliteStore, OTelSqliteStore } from '../../../platform/otel/node/sqlite/otelSqliteStore';
Expand Down Expand Up @@ -295,7 +295,7 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio

// OTel service — resolve config from env + settings, create appropriate impl
const otelSettings = workspace.getConfiguration('github.copilot.chat.otel');
const policyValue = <T>(key: string): T | undefined => (otelSettings.inspect<T>(key) as { policyValue?: T } | undefined)?.policyValue;
const coreOtelSettings = workspace.getConfiguration('chat.agentHost.otel');
const otelConfig = resolveOTelConfig({
env: process.env,
settingEnabled: otelSettings.get<boolean>('enabled'),
Expand All @@ -306,18 +306,10 @@ export function registerServices(builder: IInstantiationServiceBuilder, extensio
settingOutfile: otelSettings.get<string>('outfile') || undefined,
settingDbSpanExporter: otelSettings.get<boolean>('dbSpanExporter.enabled'),
settingProtocol: otelSettings.get<string>('protocol') || undefined,
policyEnabled: policyValue<boolean>('enabled'),
policyExporterType: policyValue<'otlp-grpc' | 'otlp-http' | 'console' | 'file'>('exporterType'),
policyOtlpEndpoint: policyValue<string>('otlpEndpoint'),
policyCaptureContent: policyValue<boolean>('captureContent'),
policyOutfile: policyValue<string>('outfile'),
policyProtocol: policyValue<string>('protocol'),
settingServiceName: otelSettings.get<string>('serviceName') || undefined,
policyServiceName: policyValue<string>('serviceName'),
settingResourceAttributes: otelSettings.get<Record<string, string>>('resourceAttributes'),
policyResourceAttributes: policyValue<Record<string, string>>('resourceAttributes'),
settingHeaders: otelSettings.get<Record<string, string>>('headers'),
policyHeaders: policyValue<Record<string, string>>('headers'),
...readOTelPolicyConfig(otelSettings, coreOtelSettings),
extensionVersion: extensionContext.extension.packageJSON.version ?? '0.0.0',
sessionId: env.sessionId,
});
Expand Down
38 changes: 38 additions & 0 deletions extensions/copilot/src/platform/otel/common/otelConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,44 @@ export interface OTelConfigInput {
vscodeTelemetryLevel?: string;
}

interface OTelPolicyConfiguration {
inspect<T>(section: string): unknown;
}

type OTelPolicyConfig = Pick<OTelConfigInput,
'policyEnabled' |
'policyExporterType' |
'policyOtlpEndpoint' |
'policyCaptureContent' |
'policyOutfile' |
'policyProtocol' |
'policyServiceName' |
'policyResourceAttributes' |
'policyHeaders'>;

/**
* Reads extension-owned policy references, falling back to their core policy owners because
* core settings are registered before extension configuration contributes its references.
*/
export function readOTelPolicyConfig(extensionSettings: OTelPolicyConfiguration, coreSettings: OTelPolicyConfiguration): OTelPolicyConfig {
const policyValue = <T>(key: string, coreKey = key): T | undefined => {
const extensionValue = (extensionSettings.inspect<T>(key) as { policyValue?: T } | undefined)?.policyValue;
return extensionValue ?? (coreSettings.inspect<T>(coreKey) as { policyValue?: T } | undefined)?.policyValue;
};

return {
policyEnabled: policyValue<boolean>('enabled'),
policyExporterType: policyValue<OTelExporterType>('exporterType'),
policyOtlpEndpoint: policyValue<string>('otlpEndpoint'),
policyCaptureContent: policyValue<boolean>('captureContent'),
policyOutfile: policyValue<string>('outfile'),
policyProtocol: policyValue<string>('protocol', 'otlpProtocol'),
policyServiceName: policyValue<string>('serviceName'),
policyResourceAttributes: policyValue<Record<string, string>>('resourceAttributes'),
policyHeaders: policyValue<Record<string, string>>('headers'),
};
}

/**
* Resolve OTel configuration with layered precedence:
* 1. Enterprise policy values from managed settings (highest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { describe, expect, it } from 'vitest';
import { resolveOTelConfig, type OTelConfigInput } from '../otelConfig';
import { readOTelPolicyConfig, resolveOTelConfig, type OTelConfigInput } from '../otelConfig';

function makeInput(overrides: Partial<OTelConfigInput> = {}): OTelConfigInput {
return {
Expand Down Expand Up @@ -114,6 +114,52 @@ describe('resolveOTelConfig', () => {
});
});

describe('readOTelPolicyConfig', () => {

it('falls back to early core policy values when extension policy references are not registered yet', () => {
const coreValues = {
enabled: true,
exporterType: 'otlp-http',
otlpEndpoint: 'https://collector.example.com',
captureContent: true,
outfile: '',
otlpProtocol: 'http/json',
serviceName: 'github-copilot',
resourceAttributes: { deployment: 'managed' },
headers: { authorization: 'core' },
};
const configuration = (values: Record<string, unknown>) => ({
inspect: <T>(key: string) => ({ policyValue: values[key] as T | undefined }),
});

expect(readOTelPolicyConfig(configuration({}), configuration(coreValues))).toEqual({
policyEnabled: true,
policyExporterType: 'otlp-http',
policyOtlpEndpoint: 'https://collector.example.com',
policyCaptureContent: true,
policyOutfile: '',
policyProtocol: 'http/json',
policyServiceName: 'github-copilot',
policyResourceAttributes: { deployment: 'managed' },
policyHeaders: { authorization: 'core' },
});
});

it('prefers extension policy values including false and empty values', () => {
const configuration = (values: Record<string, unknown>) => ({
inspect: <T>(key: string) => ({ policyValue: values[key] as T | undefined }),
});

expect(readOTelPolicyConfig(
configuration({ enabled: false, outfile: '' }),
configuration({ enabled: true, outfile: 'core.jsonl' })
)).toMatchObject({
policyEnabled: false,
policyOutfile: '',
});
});
});

it('merges resource attributes with precedence policy > env > setting', () => {
const config = resolveOTelConfig(makeInput({
settingResourceAttributes: { fromSetting: 'setting', shared: 'setting' },
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHostConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function lookUp(tree: unknown, key: string) {
export type ConfigurationInspect<T> = {
key: string;

policyValue?: T;
defaultValue?: T;
globalLocalValue?: T;
globalRemoteValue?: T;
Expand Down Expand Up @@ -269,6 +270,7 @@ export class ExtHostConfigProvider {
return {
key,

policyValue: deepClone(config.policy?.value),
defaultValue: deepClone(config.policy?.value ?? config.default?.value),
globalLocalValue: deepClone(config.userLocal?.value),
globalRemoteValue: deepClone(config.userRemote?.value),
Expand Down
36 changes: 32 additions & 4 deletions src/vs/workbench/api/test/browser/extHostConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,19 @@ suite('ExtHostConfiguration', function () {
return new ExtHostWorkspace(new TestRPCProtocol(), new class extends mock<IExtHostInitDataService>() { }, new class extends mock<IExtHostFileSystemInfo>() { override getCapabilities() { return isLinux ? FileSystemProviderCapabilities.PathCaseSensitive : undefined; } }, new NullLogService(), new class extends mock<IURITransformerService>() { });
}

function createExtHostConfiguration(contents: any = Object.create(null), shape?: MainThreadConfigurationShape) {
function createExtHostConfiguration(contents: any = Object.create(null), shape?: MainThreadConfigurationShape, policyContents?: any) {
if (!shape) {
shape = new class extends mock<MainThreadConfigurationShape>() { };
}
return new ExtHostConfigProvider(shape, createExtHostWorkspace(), createConfigurationData(contents), new NullLogService());
return new ExtHostConfigProvider(shape, createExtHostWorkspace(), createConfigurationData(contents, policyContents), new NullLogService());
}

function createConfigurationData(contents: any): IConfigurationInitData {
function createConfigurationData(contents: any, policyContents?: any): IConfigurationInitData {
return {
defaults: new ConfigurationModel(contents, [], [], undefined, new NullLogService()),
policy: ConfigurationModel.createEmptyModel(new NullLogService()),
policy: policyContents === undefined
? ConfigurationModel.createEmptyModel(new NullLogService())
: new ConfigurationModel(policyContents, [], [], undefined, new NullLogService()),
application: ConfigurationModel.createEmptyModel(new NullLogService()),
userLocal: new ConfigurationModel(contents, [], [], undefined, new NullLogService()),
userRemote: ConfigurationModel.createEmptyModel(new NullLogService()),
Expand Down Expand Up @@ -74,6 +76,32 @@ suite('ExtHostConfiguration', function () {
assert.strictEqual(extHostConfig.getConfiguration('search').has('exclude.**/node_modules'), true);
});

test('inspect exposes policy value separately from the effective default value', function () {
const configuration = createExtHostConfiguration(
{ setting: { enabled: false } },
undefined,
{ setting: { enabled: true } }
);

assert.deepStrictEqual(configuration.getConfiguration('setting').inspect<boolean>('enabled'), {
key: 'setting.enabled',
policyValue: true,
defaultValue: true,
globalLocalValue: false,
globalRemoteValue: undefined,
globalValue: false,
workspaceValue: undefined,
workspaceFolderValue: undefined,
defaultLanguageValue: undefined,
globalLocalLanguageValue: undefined,
globalRemoteLanguageValue: undefined,
globalLanguageValue: undefined,
workspaceLanguageValue: undefined,
workspaceFolderLanguageValue: undefined,
languageIds: []
});
});

test('has/get', () => {

const all = createExtHostConfiguration({
Expand Down
Loading