Skip to content

Commit 47a6d61

Browse files
committed
Add support for dynamically querying multiple tenants in trace domain
Signed-off-by: Shweta Padubidri <spadubid@redhat.com>
1 parent 877f363 commit 47a6d61

File tree

6 files changed

+166
-29
lines changed

6 files changed

+166
-29
lines changed

web/src/components/Korrel8rPanel.tsx

Lines changed: 86 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,20 @@ import { TFunction, useTranslation } from 'react-i18next';
2525
import { useDispatch, useSelector } from 'react-redux';
2626
import { useLocationQuery } from '../hooks/useLocationQuery';
2727
import { usePluginAvailable } from '../hooks/usePluginAvailable';
28-
import { getGoalsGraph, getNeighborsGraph } from '../korrel8r-client';
28+
import { getGoalsGraph, getNeighborsGraph, replaceTraceStore } from '../korrel8r-client';
2929
import * as api from '../korrel8r/client';
3030
import * as korrel8r from '../korrel8r/types';
31-
import { defaultSearch, Result, Search, SearchType, setResult, setSearch } from '../redux-actions';
31+
import {
32+
defaultSearch,
33+
Result,
34+
Search,
35+
SearchType,
36+
setResult,
37+
setSearch,
38+
setTraceContext,
39+
} from '../redux-actions';
3240
import { State } from '../redux-reducers';
41+
import { buildTraceStoreConfig, extractTraceContext, TraceContext } from '../utils/traceStoreUtils';
3342
import * as time from '../time';
3443
import { AdvancedSearchForm } from './AdvancedSearchForm';
3544
import './korrel8rpanel.css';
@@ -43,6 +52,9 @@ export default function Korrel8rPanel() {
4352

4453
const search: Search = useSelector((state: State) => state.plugins?.tp?.get('search'));
4554
const result: Result | null = useSelector((state: State) => state.plugins?.tp?.get('result'));
55+
const storedTraceContext: TraceContext | null = useSelector((state: State) =>
56+
state.plugins?.tp?.get('traceContext'),
57+
);
4658

4759
// Disable focus button if the panel is already focused on the current location,
4860
// or the current result is an error.
@@ -92,6 +104,13 @@ export default function Korrel8rPanel() {
92104
// Skip the first fetch if we already have a stored result.
93105
const useStoredResult = React.useRef(result != null);
94106

107+
// Helper function to check if two TraceContexts are different
108+
const traceContextsDiffer = (a: TraceContext | null, b: TraceContext | null): boolean => {
109+
if (!a && !b) return false;
110+
if (!a || !b) return true;
111+
return a.namespace !== b.namespace || a.name !== b.name || a.tenant !== b.tenant;
112+
};
113+
95114
// Fetch a new result from the korrel8r service when the search changes.
96115
React.useEffect(() => {
97116
if (useStoredResult.current) {
@@ -102,37 +121,77 @@ export default function Korrel8rPanel() {
102121
if (!queryStr) return;
103122

104123
let cancelled = false;
105-
const start: api.Start = {
106-
queries: [queryStr],
107-
constraint: constraint?.toAPI(),
124+
125+
// Check if we need to update the trace store based on current URL
126+
const currentTraceContext = extractTraceContext();
127+
const needsTraceStoreUpdate = traceContextsDiffer(currentTraceContext, storedTraceContext);
128+
129+
// Helper to perform the korrel8r fetch
130+
const performFetch = () => {
131+
const start: api.Start = {
132+
queries: [queryStr],
133+
constraint: constraint?.toAPI(),
134+
};
135+
136+
const fetch =
137+
search.searchType === SearchType.Goal
138+
? getGoalsGraph({ start, goals: [search.goal] })
139+
: getNeighborsGraph({ start, depth: search.depth });
140+
fetch
141+
.then((response: api.Graph) => {
142+
if (cancelled) return;
143+
dispatchResult(
144+
Array.isArray(response?.nodes) && response.nodes.length > 0
145+
? { graph: new korrel8r.Graph(response) }
146+
: { title: t('Empty Result'), message: t('No correlated data found') },
147+
);
148+
})
149+
.catch((e: api.ApiError) => {
150+
if (cancelled) return;
151+
dispatchResult({
152+
title: e?.body?.error ? t('Search Error') : t('Search Failed'),
153+
message: e?.body?.error || e.message || 'Unknown Error',
154+
isError: true,
155+
});
156+
});
157+
return fetch;
108158
};
109159

110-
const fetch =
111-
search.searchType === SearchType.Goal
112-
? getGoalsGraph({ start, goals: [search.goal] })
113-
: getNeighborsGraph({ start, depth: search.depth });
114-
fetch
115-
.then((response: api.Graph) => {
116-
if (cancelled) return;
117-
dispatchResult(
118-
Array.isArray(response?.nodes) && response.nodes.length > 0
119-
? { graph: new korrel8r.Graph(response) }
120-
: { title: t('Empty Result'), message: t('No correlated data found') },
121-
);
122-
})
123-
.catch((e: api.ApiError) => {
124-
if (cancelled) return;
125-
dispatchResult({
126-
title: e?.body?.error ? t('Search Error') : t('Search Failed'),
127-
message: e?.body?.error || e.message || 'Unknown Error',
128-
isError: true,
160+
// If trace context changed, update the backend trace store first
161+
if (needsTraceStoreUpdate && currentTraceContext) {
162+
const storeConfig = buildTraceStoreConfig(currentTraceContext);
163+
const timeoutMs = 3000;
164+
165+
Promise.race([
166+
replaceTraceStore(storeConfig),
167+
new Promise((_, reject) =>
168+
setTimeout(() => reject(new Error('Trace store update timed out')), timeoutMs),
169+
),
170+
])
171+
.then(() => {
172+
if (cancelled) return;
173+
// eslint-disable-next-line no-console
174+
console.log('Trace store updated for tenant:', currentTraceContext.tenant);
175+
dispatch(setTraceContext(currentTraceContext));
176+
performFetch();
177+
})
178+
.catch((error) => {
179+
if (cancelled) return;
180+
// Log error but continue with fetch - panel will work with other domains
181+
// eslint-disable-next-line no-console
182+
console.error('Failed to update trace store:', error);
183+
dispatch(setTraceContext(currentTraceContext));
184+
performFetch();
129185
});
130-
});
186+
} else {
187+
// No trace context change, just perform the fetch
188+
performFetch();
189+
}
190+
131191
return () => {
132192
cancelled = true;
133-
fetch.cancel();
134193
};
135-
}, [search, constraint, dispatchResult, t]);
194+
}, [search, constraint, dispatchResult, dispatch, storedTraceContext, t]);
136195

137196
const advancedToggleID = 'query-toggle';
138197
const advancedContentID = 'query-content';

web/src/korrel8r-client.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,26 @@ export const getGoalsGraph = (goals: Goals) => {
3535

3636
return korrel8rClient.default.postGraphsGoals(goals);
3737
};
38+
39+
export interface StoreConfig {
40+
domain: string;
41+
tempoStack?: string;
42+
certificateAuthority?: string;
43+
[key: string]: string | undefined;
44+
}
45+
46+
export const replaceTraceStore = async (storeConfig: StoreConfig): Promise<void> => {
47+
const response = await fetch(`${KORREL8R_ENDPOINT}/stores/trace`, {
48+
method: 'PUT',
49+
headers: {
50+
'Content-Type': 'application/json',
51+
'X-CSRFToken': getCSRFToken(),
52+
},
53+
body: JSON.stringify(storeConfig),
54+
});
55+
56+
if (!response.ok) {
57+
const errorText = await response.text();
58+
throw new Error(`Failed to replace trace store: ${response.status} ${errorText}`);
59+
}
60+
};

web/src/korrel8r/trace.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import { Class, Constraint, Domain, Query, unixMilliseconds, URIRef } from './ty
66
// Search for get selected spans
77
// observe/traces/?namespace=<tempoNamespace>&name=<tempoName>&tenant=<tempoTenant>&q=<traceQL>
88

9-
// TODO hard-coded tempo location, need to make this configurable between console & korrel8r.
10-
// Get from the console page environment (change from using URL as context?)
9+
// NOTE: Default tempo location for generating links.
10+
// When the troubleshooting panel opens, it reads the actual tenant from the URL
11+
// and updates korrel8r's trace store configuration via PUT /stores/trace.
12+
// These defaults are used when generating links back to the console.
1113
const [tempoNamespace, tempoName, tempoTenant] = ['openshift-tracing', 'platform', 'platform'];
1214

1315
export class TraceDomain extends Domain {

web/src/redux-actions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { action, ActionType as Action } from 'typesafe-actions';
22
import { Graph } from './korrel8r/types';
33
import { Duration, HOUR, Period } from './time';
4+
import { TraceContext } from './utils/traceStoreUtils';
45

56
export enum ActionType {
67
CloseTroubleshootingPanel = 'closeTroubleshootingPanel',
78
OpenTroubleshootingPanel = 'openTroubleshootingPanel',
89
SetSearch = 'setSearch',
910
SetResult = 'setResult',
11+
SetTraceContext = 'setTraceContext',
1012
}
1113

1214
export enum SearchType {
@@ -43,12 +45,15 @@ export const closeTP = () => action(ActionType.CloseTroubleshootingPanel);
4345
export const openTP = () => action(ActionType.OpenTroubleshootingPanel);
4446
export const setSearch = (search: Search) => action(ActionType.SetSearch, search);
4547
export const setResult = (result: Result | null) => action(ActionType.SetResult, result);
48+
export const setTraceContext = (traceContext: TraceContext | null) =>
49+
action(ActionType.SetTraceContext, traceContext);
4650

4751
export const actions = {
4852
closeTP,
4953
openTP,
5054
setSearch,
5155
setResult,
56+
setTraceContext,
5257
};
5358

5459
export type TPAction = Action<typeof actions>;

web/src/redux-reducers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const reducer = (state: TPState, action: TPAction): TPState => {
2020
isOpen: false,
2121
search: defaultSearch,
2222
result: null,
23+
traceContext: null,
2324
});
2425
}
2526

@@ -36,6 +37,9 @@ const reducer = (state: TPState, action: TPAction): TPState => {
3637
case ActionType.SetResult:
3738
return state.set('result', action.payload);
3839

40+
case ActionType.SetTraceContext:
41+
return state.set('traceContext', action.payload);
42+
3943
default:
4044
break;
4145
}

web/src/utils/traceStoreUtils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { StoreConfig } from '../korrel8r-client';
2+
3+
export interface TraceContext {
4+
namespace: string;
5+
name: string;
6+
tenant: string;
7+
}
8+
9+
/**
10+
* Extracts trace context (namespace, name, tenant) from the current URL.
11+
* These parameters are set when the user navigates to the traces console.
12+
*/
13+
export const extractTraceContext = (): TraceContext | null => {
14+
const searchParams = new URLSearchParams(window.location.search);
15+
16+
// Check if we're on a traces page with tenant information
17+
const namespace = searchParams.get('namespace');
18+
const name = searchParams.get('name');
19+
const tenant = searchParams.get('tenant');
20+
21+
// If any of these are missing, return null (not on traces page or no tenant selected)
22+
if (!namespace || !name || !tenant) {
23+
return null;
24+
}
25+
26+
return { namespace, name, tenant };
27+
};
28+
29+
/**
30+
* Builds a store configuration for the trace domain based on trace context.
31+
*/
32+
export const buildTraceStoreConfig = (context: TraceContext): StoreConfig => {
33+
const { namespace, name, tenant } = context;
34+
35+
// Build the tempoStack URL with the tenant path
36+
// Format: https://tempo-{name}-gateway.{namespace}.svc.cluster.local:8080/api/traces/v1/{tenant}/tempo/api/search
37+
const tempoStackURL = `https://tempo-${name}-gateway.${namespace}.svc.cluster.local:8080/api/traces/v1/${tenant}/tempo/api/search`;
38+
39+
return {
40+
domain: 'trace',
41+
tempoStack: tempoStackURL,
42+
certificateAuthority: '/var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt',
43+
};
44+
};

0 commit comments

Comments
 (0)