Skip to content

Commit 2da72ca

Browse files
Merge pull request #168 from alanconway/podlog-url
NO-JIRA: fix: Generate correct URL for pod logs.
2 parents d93c3e3 + 96082e2 commit 2da72ca

7 files changed

Lines changed: 127 additions & 127 deletions

File tree

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ start-backend:
4949
install: install-frontend install-backend
5050

5151
.PHONY: build-image
52-
build-image: test-frontend
52+
build-image: build-frontend test-frontend
5353
./scripts/build-image.sh
5454

5555
.PHONY: start-forward
5656
start-forward:
5757
./scripts/start-forward.sh
5858

59-
export REGISTRY_ORG?=openshift-observability-ui
59+
export REGISTRY_ORG?= openshift-observability-ui
6060
export TAG?=latest
6161
IMAGE=quay.io/${REGISTRY_ORG}/troubleshooting-panel-console-plugin:${TAG}
6262

web/src/__tests__/log.spec.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,23 @@
11
import { LogDomain } from '../korrel8r/log';
22
import { Constraint, Query, URIRef } from '../korrel8r/types';
33

4+
beforeAll(() => {
5+
// Mock API pod resource for pod log queries.
6+
const resources = {
7+
consoleVersion: 'x.y.z',
8+
models: [
9+
{
10+
kind: 'Pod',
11+
apiVersion: 'v1',
12+
path: 'pods',
13+
verbs: ['watch'],
14+
},
15+
],
16+
};
17+
localStorage.setItem('bridge/api-discovery-resources', JSON.stringify(resources));
18+
window['SERVER_FLAGS'] = { consoleVersion: 'x.y.z' };
19+
});
20+
421
describe('LogDomain.linkToQuery', () => {
522
it.each([
623
{
@@ -55,27 +72,31 @@ describe('LogDomain.linkToQuery', () => {
5572
describe('LogDomain.queryToLink', () => {
5673
it.each([
5774
{
58-
url: `monitoring/logs?q=${encodeURIComponent(
59-
'{kubernetes_namespace_name="default",kubernetes_pod_name="foo"}',
60-
)}&tenant=infrastructure&start=1742896800000&end=1742940000000`,
75+
// LogQL query
6176
query: `log:infrastructure:{kubernetes_namespace_name="default",kubernetes_pod_name="foo"}`,
6277
constraint: Constraint.fromAPI({
6378
start: '2025-03-25T10:00:00.000Z',
6479
end: '2025-03-25T22:00:00.000Z',
6580
}),
81+
url: new URIRef(`monitoring/logs`, {
82+
q: '{kubernetes_namespace_name="default",kubernetes_pod_name="foo"}',
83+
tenant: 'infrastructure',
84+
start: 1742896800000,
85+
end: 1742940000000,
86+
}),
6687
},
6788
{
68-
url: `monitoring/logs?q=${encodeURIComponent(
69-
'{kubernetes_namespace_name="default",log_type="infrastructure"}',
70-
)}&tenant=infrastructure&start=1742896800000&end=1742940000000`,
71-
query: 'log:infrastructure:{kubernetes_namespace_name="default",log_type="infrastructure"}',
89+
// k8s Pod query
90+
query: 'log:infrastructure:{"namespace":"default","name":"foo"}',
7291
constraint: Constraint.fromAPI({
7392
start: '2025-03-25T10:00:00.000Z',
7493
end: '2025-03-25T22:00:00.000Z',
7594
}),
95+
96+
url: new URIRef('k8s/ns/default/core~v1~Pod/foo/aggregated-logs'),
7697
},
7798
])('$query', ({ url, query, constraint }) =>
78-
expect(new LogDomain().queryToLink(Query.parse(query), constraint)).toEqual(new URIRef(url)),
99+
expect(new LogDomain().queryToLink(Query.parse(query), constraint)).toEqual(url),
79100
);
80101
});
81102

web/src/__tests__/types.spec.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Node,
1111
Query,
1212
URIRef,
13+
joinPath,
1314
} from '../korrel8r/types';
1415

1516
describe('Query', () => {
@@ -184,3 +185,46 @@ describe('Graph', () => {
184185
expect(g.nodes).toEqual(a.nodes.map((n) => new Node(n)));
185186
expect(g.edges).toEqual(a.edges.map((e) => new Edge(g.node(e.start), g.node(e.goal))));
186187
});
188+
189+
describe('joinPath', () => {
190+
it.each([
191+
// Basic path joining
192+
['path1', 'path2', 'path1/path2'],
193+
['path1', 'path2', 'path3', 'path1/path2/path3'],
194+
195+
// Handling trailing slash on first path
196+
['path1/', 'path2', 'path1/path2'],
197+
['path1//', 'path2', 'path1/path2'],
198+
199+
// Handling leading slashes on subsequent paths
200+
['path1', '/path2', 'path1/path2'],
201+
['path1', '//path2', 'path1/path2'],
202+
203+
// Handling trailing slashes on subsequent paths
204+
['path1', 'path2/', 'path1/path2'],
205+
['path1', 'path2//', 'path1/path2'],
206+
207+
// Complex combinations
208+
['path1/', '/path2/', '/path3/', 'path1/path2/path3'],
209+
['/path1/', '//path2//', '///path3///', '/path1/path2/path3'],
210+
211+
// Empty paths
212+
['', 'path2', '/path2'],
213+
['path1', '', 'path1/'],
214+
['', '', '/'],
215+
216+
// Single path
217+
['single', 'single'],
218+
['single/', 'single'],
219+
['/single/', '/single'],
220+
221+
// Absolute paths
222+
['/absolute', 'relative', '/absolute/relative'],
223+
['/absolute/', '/relative/', '/absolute/relative'],
224+
] as Array<string[]>)('joins paths correctly: %s', (...args: string[]) => {
225+
const expected = args.pop() as string;
226+
const paths = args as string[];
227+
const [first, ...rest] = paths;
228+
expect(joinPath(first, ...rest)).toEqual(expected);
229+
});
230+
});

web/src/components/Korrel8rPanel.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
FlexItem,
1313
Form,
1414
FormGroup,
15+
Spinner,
1516
TextArea,
1617
Title,
1718
Tooltip,
@@ -42,7 +43,6 @@ import './korrel8rpanel.css';
4243
import { SearchFormGroup } from './SearchFormGroup';
4344
import TimeRangeFormGroup from './TimeRangeFormGroup';
4445
import { Korrel8rTopology } from './topology/Korrel8rTopology';
45-
import { LoadingTopology } from './topology/LoadingTopology';
4646

4747
export default function Korrel8rPanel() {
4848
const { t } = useTranslation('plugin__troubleshooting-panel-console-plugin');
@@ -156,14 +156,14 @@ export default function Korrel8rPanel() {
156156
<Tooltip content={locationQuery ? focusTip : cannotFocus}>
157157
<Button
158158
isAriaDisabled={!locationQuery}
159-
onClick={() =>
159+
onClick={() => {
160160
runSearch({
161161
...defaultSearch,
162162
queryStr: locationQuery?.toString(),
163163
constraint: searchResult?.search?.constraint,
164164
period: searchResult?.search?.period,
165-
})
166-
}
165+
});
166+
}}
167167
>
168168
{t('Focus')}
169169
</Button>
@@ -185,7 +185,12 @@ export default function Korrel8rPanel() {
185185

186186
const refreshButton = (
187187
<Tooltip content={t('Refresh the graph using the current settings')}>
188-
<Button isAriaDisabled={!search?.queryStr} onClick={() => runSearch(search)}>
188+
<Button
189+
isAriaDisabled={!search?.queryStr}
190+
onClick={() => {
191+
runSearch(search);
192+
}}
193+
>
189194
<SyncIcon />
190195
</Button>
191196
</Tooltip>
@@ -301,18 +306,20 @@ const Topology: React.FC<TopologyProps> = ({ domains, result, t, constraint }) =
301306
);
302307
};
303308

304-
const Loading: React.FC = () => (
305-
<>
309+
const Loading: React.FC = () => {
310+
const { t } = useTranslation('plugin__troubleshooting-panel-console-plugin');
311+
return (
306312
<div className="tp-plugin__panel-topology-info">
307-
<div className={'co-m-loader co-an-fade-in-out tp-plugin__panel-topology-info'}>
308-
<div className="co-m-loader-dot__one" />
309-
<div className="co-m-loader-dot__two" />
310-
<div className="co-m-loader-dot__three" />
311-
</div>
313+
<EmptyState variant={EmptyStateVariant.sm}>
314+
<EmptyStateHeader
315+
titleText={t('Loading')}
316+
headingLevel="h4"
317+
icon={<EmptyStateIcon icon={Spinner} />}
318+
/>
319+
</EmptyState>
312320
</div>
313-
<LoadingTopology />
314-
</>
315-
);
321+
);
322+
};
316323

317324
interface TopologyInfoStateProps {
318325
titleText: string;

web/src/components/topology/LoadingTopology.tsx

Lines changed: 0 additions & 94 deletions
This file was deleted.

web/src/korrel8r/log.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,30 @@
1-
import { Class, Constraint, Domain, Query, unixMilliseconds, URIRef } from './types';
1+
import { K8sDomain } from './k8s';
2+
import { Class, Constraint, Domain, joinPath, Query, unixMilliseconds, URIRef } from './types';
23

34
enum LogClass {
45
application = 'application',
56
infrastructure = 'infrastructure',
67
audit = 'audit',
78
}
89

10+
// TODO: Aggregated log links and k8s:pod style log queries ignore the containers parameter.
11+
912
export class LogDomain extends Domain {
13+
private k8s: K8sDomain;
14+
private pod: Class;
15+
1016
constructor() {
1117
super('log');
18+
this.k8s = new K8sDomain();
19+
this.pod = new Class('k8s', 'Pod');
1220
}
1321

1422
class(name: string): Class {
1523
if (!LogClass[name]) throw this.badClass(name);
1624
return new Class(this.name, name);
1725
}
1826

19-
// There are 2 types of URL: pod logs, and log search.
27+
// There are 2 types of URL: pod logs, and logQL searches.
2028
linkToQuery(link: URIRef): Query {
2129
// First check for aggregated pod logs URL
2230
const [, namespace, name] =
@@ -42,11 +50,19 @@ export class LogDomain extends Domain {
4250
queryToLink(query: Query, constraint?: Constraint): URIRef {
4351
const logClass = LogClass[query.class.name as keyof typeof LogClass];
4452
if (!logClass) throw this.badQuery(query, 'unknown class');
45-
return new URIRef('monitoring/logs', {
46-
q: query.selector,
47-
tenant: logClass,
48-
start: unixMilliseconds(constraint?.start),
49-
end: unixMilliseconds(constraint?.end),
50-
});
53+
try {
54+
// First try to parse the selector as k8s pod selector
55+
const link = this.k8s.queryToLink(this.pod.query(query.selector));
56+
link.pathname = joinPath(link.pathname, 'aggregated-logs');
57+
return link;
58+
} catch {
59+
// Otherwise assume it is a LogQL query.
60+
return new URIRef('monitoring/logs', {
61+
q: query.selector,
62+
tenant: logClass,
63+
start: unixMilliseconds(constraint?.start),
64+
end: unixMilliseconds(constraint?.end),
65+
});
66+
}
5167
}
5268
}

web/src/korrel8r/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,12 @@ export class URIRef {
170170
}
171171
}
172172

173+
// Join and normalize paths, no leading, trailing or repeated '/' charactersa
174+
export const joinPath = (path0: string, ...paths: string[]): string => {
175+
const clean = paths.map((path: string) => path.replaceAll(/(^\/*)|(\/*$)/g, ''));
176+
return [path0.replace(/\/*$/, ''), ...clean].join('/');
177+
};
178+
173179
// Encode an object as a comma-separated, key=value list: 'key=value,key=value...'. No URI encoding.
174180
export const keyValueList = (obj: { [key: string]: string }): string => {
175181
return Object.keys(obj || {})

0 commit comments

Comments
 (0)