-
Notifications
You must be signed in to change notification settings - Fork 669
Description
Fetch storage capabilities from backend during Vite dev mode
Problem
During development (npm start), the UI is served by Vite, not the Jaeger backend. The backend injects config and capabilities into index.html via search-replace at serve time, but Vite serves its own copy of index.html with hardcoded defaults:
// index.html — always returns defaults during npm start
function getJaegerStorageCapabilities() {
const DEFAULT_STORAGE_CAPABILITIES = { "archiveStorage": false };
const JAEGER_STORAGE_CAPABILITIES = DEFAULT_STORAGE_CAPABILITIES;
return JAEGER_STORAGE_CAPABILITIES;
}This means archiveStorage is always false during development, and the proposed metricsStorage (#3540) would also be false, hiding the Monitor tab even when the local backend has metrics storage configured.
The existing jaeger-ui.config.js override mechanism (handled by the Vite plugin) only covers getJaegerUiConfig() — it does not cover capabilities or version.
Proposed Solution
Extend the existing jaegerUiConfigPlugin in vite.config.mts to dynamically fetch config from the running backend during HTML transformation.
Prerequisites: Backend API endpoint
Add an endpoint to the Jaeger query service that returns the same data it currently injects into HTML:
GET /api/ui/config
Response:
{
"uiConfig": { ... },
"storageCapabilities": { "archiveStorage": true, "metricsStorage": true },
"version": { "gitCommit": "abc123", "gitVersion": "1.60.0", "buildDate": "2025-01-15" }
}This endpoint returns the exact same values the server would inject via search-replace into index.html. It requires no authentication (the existing UI config is already public — it's embedded in the HTML served to browsers).
UI Changes
1. Extend the Vite plugin (packages/jaeger-ui/vite.config.mts)
The jaegerUiConfigPlugin already has a transformIndexHtml hook that runs in Node.js on every page load during dev mode. Extend it to fetch config from the backend and merge it with local overrides.
Config precedence (highest to lowest)
There are three types of data the backend provides:
| Data | Backend | Local file override | Notes |
|---|---|---|---|
| UI config | uiConfig from /api/ui/config |
jaeger-ui.config.js or .json |
Local file merges on top of backend config |
| Capabilities | storageCapabilities from /api/ui/config |
(none) | Always from backend; reflects actual storage configuration |
| Version | version from /api/ui/config |
(none) | Always from backend |
For UI config, the merge order is:
- Backend's
uiConfig(base layer — reflects server-side defaults) - Local
jaeger-ui.config.jsor.json(override layer — developer customizations)
This means a developer can start with the backend's config as a baseline and selectively override specific fields in their local config file. For example, if the backend returns { "tracking": { "gaID": "UA-xxx" } }, a local config with { "tracking": { "gaID": "" } } would disable tracking during development without affecting other backend-provided settings.
Implementation sketch
function jaegerUiConfigPlugin() {
const jsConfigPath = path.resolve(__dirname, 'jaeger-ui.config.js');
const jsonConfigPath = path.resolve(__dirname, 'jaeger-ui.config.json');
// Cache the backend config to avoid fetching on every request.
let cachedBackendConfig: any = null;
let cacheTimestamp = 0;
const CACHE_TTL_MS = 30_000; // re-fetch every 30s
async function fetchBackendConfig() {
const now = Date.now();
if (cachedBackendConfig && now - cacheTimestamp < CACHE_TTL_MS) {
return cachedBackendConfig;
}
try {
const response = await fetch('http://localhost:16686/api/ui/config');
if (response.ok) {
cachedBackendConfig = await response.json();
cacheTimestamp = now;
console.log('[jaeger-ui-config] Fetched config from backend');
}
} catch {
// Backend not running — use defaults silently
}
return cachedBackendConfig;
}
function readLocalConfig(): Record<string, any> | null {
// JS config (higher priority)
if (fs.existsSync(jsConfigPath)) {
try {
// For JS config, we can't easily merge at the Vite level because
// the JS config is injected as a UIConfig() function body.
// Return a marker so the caller knows to use JS injection.
return { __jsConfig: fs.readFileSync(jsConfigPath, 'utf-8') };
} catch { /* ignore */ }
}
// JSON config
if (fs.existsSync(jsonConfigPath)) {
try {
return JSON.parse(fs.readFileSync(jsonConfigPath, 'utf-8'));
} catch { /* ignore */ }
}
return null;
}
return {
name: 'jaeger-ui-config',
configureServer(server) { /* ... existing watcher code, unchanged ... */ },
transformIndexHtml: {
order: 'pre' as const,
async handler(html: string) {
const backendConfig = await fetchBackendConfig();
const localConfig = readLocalConfig();
// 1. Inject capabilities from backend (no local override for these)
if (backendConfig?.storageCapabilities) {
html = html.replace(
'const JAEGER_STORAGE_CAPABILITIES = DEFAULT_STORAGE_CAPABILITIES;',
`const JAEGER_STORAGE_CAPABILITIES = ${JSON.stringify(backendConfig.storageCapabilities)};`
);
}
// 2. Inject version from backend (no local override for these)
if (backendConfig?.version) {
html = html.replace(
'const JAEGER_VERSION = DEFAULT_VERSION;',
`const JAEGER_VERSION = ${JSON.stringify(backendConfig.version)};`
);
}
// 3. Inject UI config: local overrides merged on top of backend config
if (localConfig?.__jsConfig) {
// JS config takes full control (existing behavior, unchanged)
const uiConfigFn = `function UIConfig() { ${localConfig.__jsConfig} }`;
html = html.replace('// JAEGER_CONFIG_JS', uiConfigFn);
console.log('[jaeger-ui-config] Loaded config from jaeger-ui.config.js');
} else {
// Merge: backend uiConfig as base, local JSON config as override
const mergedUiConfig = {
...(backendConfig?.uiConfig || {}),
...(localConfig || {}),
};
if (Object.keys(mergedUiConfig).length > 0) {
html = html.replace(
'const JAEGER_CONFIG = DEFAULT_CONFIG;',
`const JAEGER_CONFIG = ${JSON.stringify(mergedUiConfig)};`
);
const source = localConfig
? 'backend + jaeger-ui.config.json'
: 'backend';
console.log(`[jaeger-ui-config] Loaded config from ${source}`);
}
}
return html;
},
},
};
}Note on JS config files and merging
When jaeger-ui.config.js is used, the entire file content is injected as a UIConfig() function body. This takes full control of getJaegerUiConfig() and bypasses any merging with the backend config. This is the existing behavior and is appropriate — JS config files are used for advanced scenarios where the developer wants complete control.
For jaeger-ui.config.json, the merge is straightforward: spread the backend's uiConfig first, then the local JSON on top. A deep merge could be added if needed, but shallow merge matches the existing { ...defaultConfig, ...embedded } pattern in get-config.ts:41.
2. No changes needed to get-config.ts
The browser-side code (getCapabilities(), getConfig()) remains unchanged. It reads window.getJaegerStorageCapabilities() as before — the only difference is that during dev mode, the Vite plugin now injects real values instead of defaults.
3. The index.html placeholders remain unchanged
The existing placeholder pattern in index.html continues to serve as both the production injection target (for the Go server) and the dev injection target (for the Vite plugin). No changes needed.
Benefits
- Dev mode matches production — capabilities and config reflect the actual backend configuration.
- Local overrides preserved — developers can still use
jaeger-ui.config.jsor.jsonto customize UI config, merged on top of backend-provided values. - No manual config files needed for basic dev — features like Monitor and Archive "just work" if the backend supports them, without needing a local config file.
- Backward compatible — if the backend doesn't have the
/api/ui/configendpoint (older version), the fetch fails silently and defaults are used. Local config files continue to work as before. - Extends naturally — any new capability or config the server reports is automatically available in dev mode.
Verification
- Backend + no local config: Start backend with metrics storage on
localhost:16686, runnpm start. Monitor tab should appear based on backend capabilities. - Backend + local JSON config: Create
jaeger-ui.config.jsonwith{ "tracking": { "gaID": "" } }. Backend capabilities should still be used, and the tracking override should apply. - Backend + local JS config: Create
jaeger-ui.config.js. Backend capabilities should still be injected. UI config comes entirely from the JS file (no merge with backend). - No backend running: Stop backend, run
npm start. Defaults should apply (Monitor tab hidden). Local config files still work for UI config. - Backend restart: Stop backend, refresh (defaults applied). Start backend, wait 30s, refresh — capabilities should update.