feat: add OpenTelemetry settings UI and sidecar environment wiring

Add a global Telemetry section in the settings panel with:
- Toggle to enable/disable OTLP export
- Text input for the OTLP endpoint URL (default: http://localhost:4317)
- Guidance note about using \un run aspire\ for local testing

Wire the setting through the full stack:
- OpenTelemetrySettings type in shared domain with normalization
- IPC channel, preload binding, and handler for persistence
- SidecarClient forwards settings to createSidecarEnvironment
- Sidecar environment injects OTEL_EXPORTER_OTLP_ENDPOINT when enabled
- Settings loaded from workspace.json on startup and on change

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-16 11:10:23 +02:00
co-authored by Copilot
parent f7d376fc41
commit 67fb950e61
12 changed files with 210 additions and 6 deletions
+9
View File
@@ -140,6 +140,7 @@ import {
type AppearanceTheme,
type LspProfileDefinition,
type McpServerDefinition,
type OpenTelemetrySettings,
type QuickPromptSettings,
type SessionToolingSelection,
type WorkspaceToolingSettings,
@@ -469,6 +470,7 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
async loadWorkspace(): Promise<WorkspaceState> {
if (!this.workspace) {
this.workspace = await this.workspaceRepository.load();
this.sidecar.setOpenTelemetrySettings(this.workspace.settings.openTelemetry);
const selectedProjectId = this.workspace.selectedProjectId;
const selectedProject = selectedProjectId
? this.workspace.projects.find((project) => project.id === selectedProjectId)
@@ -830,6 +832,13 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
return this.persistAndBroadcast(workspace);
}
async setOpenTelemetry(settings: OpenTelemetrySettings): Promise<WorkspaceState> {
const workspace = await this.loadWorkspace();
workspace.settings.openTelemetry = settings;
this.sidecar.setOpenTelemetrySettings(settings);
return this.persistAndBroadcast(workspace);
}
getQuickPromptSettings(): QuickPromptSettings {
return this.workspace?.settings.quickPrompt ?? createDefaultQuickPromptSettings();
}
+5 -1
View File
@@ -56,7 +56,7 @@ import type {
QuickPromptSendInput,
} from '@shared/contracts/ipc';
import type { QuerySessionsInput } from '@shared/domain/sessionLibrary';
import type { AppearanceTheme, QuickPromptSettings } from '@shared/domain/tooling';
import type { AppearanceTheme, OpenTelemetrySettings, QuickPromptSettings } from '@shared/domain/tooling';
import { AryxAppService } from '@main/AryxAppService';
import { AutoUpdateService } from '@main/services/autoUpdater';
@@ -155,6 +155,10 @@ export function registerIpcHandlers(
ipcChannels.setGitAutoRefreshEnabled,
(_event, enabled: boolean) => service.setGitAutoRefreshEnabled(enabled),
);
ipcMain.handle(
ipcChannels.setOpenTelemetry,
(_event, settings: OpenTelemetrySettings) => service.setOpenTelemetry(settings),
);
ipcMain.handle(ipcChannels.checkForUpdates, () => autoUpdateService.checkForUpdates());
ipcMain.handle(ipcChannels.installUpdate, () => {
autoUpdateService.installUpdate();
+10 -1
View File
@@ -1,6 +1,11 @@
import type { OpenTelemetrySettings } from '@shared/domain/tooling';
const blockedEnvironmentPrefixes = ['BUN_', 'COPILOT_', 'ELECTRON_', 'NODE_', 'NPM_'];
export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
export function createSidecarEnvironment(
baseEnvironment: NodeJS.ProcessEnv,
openTelemetry?: OpenTelemetrySettings,
): NodeJS.ProcessEnv {
const sanitizedEnvironment: NodeJS.ProcessEnv = {};
for (const [name, value] of Object.entries(baseEnvironment)) {
@@ -13,5 +18,9 @@ export function createSidecarEnvironment(baseEnvironment: NodeJS.ProcessEnv): No
sanitizedEnvironment[name] = value;
}
if (openTelemetry?.enabled && openTelemetry.endpoint) {
sanitizedEnvironment.OTEL_EXPORTER_OTLP_ENDPOINT = openTelemetry.endpoint;
}
return sanitizedEnvironment;
}
+7 -1
View File
@@ -21,6 +21,7 @@ import type {
} from '@shared/contracts/sidecar';
import type { ApprovalDecision } from '@shared/domain/approval';
import type { ChatMessageRecord } from '@shared/domain/session';
import type { OpenTelemetrySettings } from '@shared/domain/tooling';
import { createSidecarEnvironment } from '@main/sidecar/sidecarEnvironment';
import {
markRunTurnPendingErrored,
@@ -109,6 +110,11 @@ export class SidecarClient {
private processState?: ManagedSidecarProcess;
private nextProcessId = 0;
private readonly pending = new Map<string, PendingCommand>();
private openTelemetrySettings?: OpenTelemetrySettings;
setOpenTelemetrySettings(settings?: OpenTelemetrySettings): void {
this.openTelemetrySettings = settings;
}
async describeCapabilities(): Promise<SidecarCapabilities> {
const command = await this.dispatch<SidecarCapabilities>({
@@ -240,7 +246,7 @@ export class SidecarClient {
});
const childProcess = spawn(sidecar.command, sidecar.args, {
cwd: sidecar.cwd,
env: createSidecarEnvironment(process.env),
env: createSidecarEnvironment(process.env, this.openTelemetrySettings),
stdio: 'pipe',
windowsHide: true,
});