Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions web/src/__tests__/otellog.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { OtellogDomain } from '../korrel8r/otellog';
import { Constraint, Query, URIRef } from '../korrel8r/types';

describe('OtellogDomain.fromURL', () => {
it.each([
{
url: `monitoring/logs?q=${encodeURIComponent(
'{kubernetes_namespace_name="default",kubernetes_pod_name="foo"}',
)}&tenant=infrastructure`,
query:
`otellog:infrastructure:{kubernetes_namespace_name="default"` +
`,kubernetes_pod_name="foo"}`,
},
{
url: `monitoring/logs?q=${encodeURIComponent(
'{kubernetes_namespace_name="default",' +
'kubernetes_pod_name="foo",log_type="infrastructure"}',
)}`,
query:
`otellog:infrastructure:{kubernetes_namespace_name="default",` +
`kubernetes_pod_name="foo",log_type="infrastructure"}`,
},
{
url: `monitoring/logs?q=${encodeURIComponent(
'{kubernetes_namespace_name="default",kubernetes_pod_name="foo"}',
)}&tenant=infrastructure`,
query:
`otellog:infrastructure:{kubernetes_namespace_name="default",` +
`kubernetes_pod_name="foo"}`,
},
{
url: `monitoring/logs?q=${encodeURIComponent(
'{kubernetes_namespace_name="default",kubernetes_pod_name="foo",log_type="infrastructure"}',
)}&tenant=infrastructure`,
query:
`otellog:infrastructure:{kubernetes_namespace_name="default",` +
`kubernetes_pod_name="foo",log_type="infrastructure"}`,
},
{
url: `/k8s/ns/foo/pods/bar/aggregated-logs`,
query: `otellog:application:{kubernetes_namespace_name="foo",kubernetes_pod_name="bar"}`,
},
{
url: `/k8s/ns/kube/pods/bar/aggregated-logs`,
query: `otellog:infrastructure:{kubernetes_namespace_name="kube",kubernetes_pod_name="bar"}`,
},
{
url: '/monitoring/logs?q=%7Bkubernetes_namespace_name%3D%22openshift-image-registry%22%7D%7Cjson%7Ckubernetes_labels_docker_registry%3D%22default%22&tenant=infrastructure',
query:
'otellog:infrastructure:{kubernetes_namespace_name="openshift-image-registry"}|json|kubernetes_labels_docker_registry="default"',
},
])('$url', ({ url, query }) =>
expect(new OtellogDomain().linkToQuery(new URIRef(url))).toEqual(Query.parse(query)),
);
});

describe('OtellogDomain.fromQuery', () => {
it.each([
{
url: `monitoring/logs?q=${encodeURIComponent(
'{kubernetes_namespace_name="default",kubernetes_pod_name="foo"}',
)}&tenant=infrastructure&start=1742896800000&end=1742940000000`,
// eslint-disable-next-line max-len
query: `otellog:infrastructure:{kubernetes_namespace_name="default",kubernetes_pod_name="foo"}`,
constraint: Constraint.fromAPI({
start: '2025-03-25T10:00:00.000Z',
end: '2025-03-25T22:00:00.000Z',
}),
},
{
url: `monitoring/logs?q=${encodeURIComponent(
'{kubernetes_namespace_name="default",log_type="infrastructure"}',
)}&tenant=infrastructure&start=1742896800000&end=1742940000000`,
query:
'otellog:infrastructure:{kubernetes_namespace_name="default",log_type="infrastructure"}',
constraint: Constraint.fromAPI({
start: '2025-03-25T10:00:00.000Z',
end: '2025-03-25T22:00:00.000Z',
}),
},
])('$query', ({ url, query, constraint }) =>
expect(new OtellogDomain().queryToLink(Query.parse(query), constraint)).toEqual(
new URIRef(url),
),
);
});

describe('expected errors', () => {
it.each([
{
url: 'monitoring/log',
expected: 'domain otellog: invalid link: monitoring/log',
},
{
url: 'monitoring/logs?q={kubernetes_namespace_name="default",kubernetes_pod_name="foo"}',
expected:
'domain otellog: invalid link: monitoring/logs?q=%7Bkubernetes_namespace_name%3D%22default%22%2Ckubernetes_pod_name%3D%22foo%22%7D',
},
])('error from url: $url', ({ url, expected }) => {
expect(() => new OtellogDomain().linkToQuery(new URIRef(url))).toThrow(expected);
});

it.each([
{
query: 'foo:bar:baz',
expected: 'domain otellog: invalid query, unknown class: foo:bar:baz',
},
{
query: 'log:incorrect:{}',
expected: 'domain otellog: invalid query, unknown class: log:incorrect:{}',
},
])('error from query: $query', ({ query, expected }) => {
expect(() => new OtellogDomain().queryToLink(Query.parse(query))).toThrow(expected);
});
});
2 changes: 2 additions & 0 deletions web/src/korrel8r/all-domains.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AlertDomain } from './alert';
import { K8sDomain } from './k8s';
import { LogDomain } from './log';
import { OtellogDomain } from './otellog';
import { MetricDomain } from './metric';
import { NetflowDomain } from './netflow';
import { TraceDomain } from './trace';
Expand All @@ -10,6 +11,7 @@ export const allDomains = [
new AlertDomain(),
new K8sDomain(),
new LogDomain(),
new OtellogDomain(),
new MetricDomain(),
new NetflowDomain(),
new TraceDomain(),
Expand Down
52 changes: 52 additions & 0 deletions web/src/korrel8r/otellog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Class, Constraint, Domain, Query, unixMilliseconds, URIRef } from './types';

enum LogClass {
application = 'application',
infrastructure = 'infrastructure',
audit = 'audit',
}

export class OtellogDomain extends Domain {
constructor() {
super('otellog');
}

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

// There are 2 types of URL: pod logs, and log search.
linkToQuery(link: URIRef): Query {
// First check for aggregated pod logs URL
const [, namespace, name] =
link.pathname.match(/k8s\/ns\/([^/]+)\/pods\/([^/]+)\/aggregated-logs/) || [];
if (namespace && name) {
const logClass = namespace.match(/^kube|^openshift-/)
? LogClass.infrastructure
: LogClass.application;
return new Query(
this.class(logClass),
`{kubernetes_namespace_name="${namespace}",kubernetes_pod_name="${name}"}`,
);
}
// Assume this is a search URL
const logQL = link.searchParams.get('q');
const logClassStr =
link.searchParams.get('tenant') || logQL?.match(/{[^}]*log_type(?:=~?)"([^"]+)"/)?.at(1);
const logClass = LogClass[logClassStr as keyof typeof LogClass];
if (!logClass) throw this.badLink(link);
return this.class(logClass).query(logQL);
}

queryToLink(query: Query, constraint?: Constraint): URIRef {
const logClass = LogClass[query.class.name as keyof typeof LogClass];
if (!logClass) throw this.badQuery(query, 'unknown class');
return new URIRef('monitoring/logs', {
q: query.selector,
tenant: logClass,
start: unixMilliseconds(constraint?.start),
end: unixMilliseconds(constraint?.end),
});
}
}