Skip to content

Commit fe15d50

Browse files
committed
fix(NO-JIRA): Generate correct URL for pod logs.
log domain supports two styles of query selector: - LogQL query strings for Loki. - k8s:Pod selectors for direct API server log access, or Loki. The troubleshooting panel did not correctly generate URLs for k8s:Pod style queries.
1 parent d93c3e3 commit fe15d50

5 files changed

Lines changed: 105 additions & 18 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/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)