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
33 changes: 29 additions & 4 deletions korrel8r/korrel8r-openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ info:
name: Apache 2.0
url: https://github.com/korrel8r/korrel8r/blob/main/LICENSE
version: v1alpha1
x-korrel8r-version: "0.10.1-dev"
externalDocs:
url: https://korrel8r.github.io/korrel8r/
description: Korrel8r User Guide
Expand Down Expand Up @@ -294,6 +295,13 @@ paths:
required: true
schema:
$ref: "#/components/schemas/Query"
- name: constraint
description: Constrains the objects that will be included in results.
in: query
style: form
explode: true
schema:
$ref: "#/components/schemas/Constraint"
responses:
"200":
description: OK
Expand Down Expand Up @@ -399,7 +407,6 @@ components:
Constraint:
description: Constrains the objects that will be included in search results.
type: object
x-go-type: korrel8r.Constraint
properties:
start:
type: string
Expand Down Expand Up @@ -553,16 +560,16 @@ components:
x-oapi-codegen-extra-tags:
jsonschema: "Full class name in DOMAIN:CLASS format."
queries:
type: array
x-go-type-skip-optional-pointer: true
description: Queries yielding results for this class.
type: array
items:
$ref: "#/components/schemas/QueryCount"
x-go-type-skip-optional-pointer: true
x-oapi-codegen-extra-tags:
jsonschema: "Queries yielding results for this class."
count:
type: integer
description: Number of results for this class, after de-duplication.
type: integer
x-oapi-codegen-extra-tags:
jsonschema: "Number of results for this class, after de-duplication."
result:
Expand Down Expand Up @@ -590,6 +597,24 @@ components:
- $ref: "#/components/schemas/Query"
x-oapi-codegen-extra-tags:
jsonschema: "Query for correlation data in DOMAIN:CLASS:SELECTOR format."
statuses:
description: Statuses found on data objects for this query.
type: array
items:
$ref: "#/components/schemas/StatusCount"
x-go-type-skip-optional-pointer: true

StatusCount:
description: Status with number of instances found.
type: object
required: [status]
properties:
status:
description: Status for correlation data.
type: string
count:
description: Number of instances found, omitted if none.
type: integer

Rule:
type: object
Expand Down
147 changes: 147 additions & 0 deletions web/src/__tests__/status.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
jest.mock('@patternfly/react-topology', () => ({
NodeStatus: {
default: 'default',
info: 'info',
success: 'success',
warning: 'warning',
danger: 'danger',
},
}));

import {
mergeStatusCounts,
Status,
statusForNode,
statusName,
toStatus,
} from '../components/topology/status';
import * as api from '../korrel8r/client';
import * as korrel8r from '../korrel8r/types';

describe('Status enum', () => {
it('has ordered severity levels', () => {
expect(Status.info).toBeLessThan(Status.warning);
expect(Status.warning).toBeLessThan(Status.danger);
});
});

describe('statusName', () => {
it.each([
{ status: Status.info, name: 'info' },
{ status: Status.warning, name: 'warning' },
{ status: Status.danger, name: 'danger' },
])('converts $name', ({ status, name }) => {
expect(statusName(status)).toEqual(name);
});
});

describe('statusForNode', () => {
it.each([
{ status: Status.info, nodeStatus: 'info' },
{ status: Status.warning, nodeStatus: 'warning' },
{ status: Status.danger, nodeStatus: 'danger' },
])('converts $status to NodeStatus', ({ status, nodeStatus }) => {
expect(statusForNode(status)).toEqual(nodeStatus);
});
});

describe('toStatus', () => {
it.each([
{ input: 'error', expected: Status.danger },
{ input: 'Error', expected: Status.danger },
{ input: 'ERROR', expected: Status.danger },
{ input: 'critical', expected: Status.danger },
{ input: 'Fatal', expected: Status.danger },
{ input: 'some error here', expected: Status.danger },
{ input: 'critically fatal', expected: Status.danger },
{ input: 'warn', expected: Status.warning },
{ input: 'warning', expected: Status.warning },
{ input: 'Warning', expected: Status.warning },
{ input: 'WARNING', expected: Status.warning },
{ input: 'info', expected: Status.info },
{ input: 'anything', expected: Status.info },
])('converts "$input" to $expected', ({ input, expected }) => {
expect(toStatus(input)).toEqual(expected);
});

it('returns undefined for empty string', () => {
expect(toStatus('')).toBeUndefined();
});
});

const makeNode = (
queries: Array<{ query: string; statuses: Array<{ status: string; count: number }> }>,
): korrel8r.Node => {
const apiNode: api.Node = {
class: 'log:application',
count: 0,
queries: queries.map((q) => ({
query: q.query,
count: q.statuses.reduce((sum, s) => sum + s.count, 0),
statuses: q.statuses,
})),
};
return new korrel8r.Node(apiNode);
};

describe('mergeStatusCounts', () => {
it('returns empty counts and undefined status for a node with no statuses', () => {
const node = makeNode([{ query: 'log:application:{}', statuses: [] }]);
const [counts, status] = mergeStatusCounts(node);
expect(counts).toEqual([]);
expect(status).toBeUndefined();
});

it('returns the most severe status', () => {
const node = makeNode([
{
query: 'log:application:{}',
statuses: [
{ status: 'info', count: 1 },
{ status: 'error', count: 2 },
{ status: 'warning', count: 3 },
],
},
]);
const [, status] = mergeStatusCounts(node);
expect(status).toEqual(Status.danger);
});

it('merges counts for the same status across queries', () => {
const node = makeNode([
{ query: 'log:application:{}', statuses: [{ status: 'error', count: 5 }] },
{ query: 'log:infrastructure:{}', statuses: [{ status: 'error', count: 3 }] },
]);
const [counts] = mergeStatusCounts(node);
const errorCount = counts.find((c) => c.status === 'error');
expect(errorCount?.count).toEqual(8);
});

it('handles a single status', () => {
const node = makeNode([
{
query: 'log:application:{}',
statuses: [{ status: 'warning', count: 10 }],
},
]);
const [counts, status] = mergeStatusCounts(node);
expect(status).toEqual(Status.warning);
expect(counts).toEqual([{ status: 'warning', count: 10 }]);
});

it('skips entries with empty status', () => {
const node = makeNode([
{
query: 'log:application:{}',
statuses: [
{ status: '', count: 5 },
{ status: 'info', count: 2 },
],
},
]);
const [counts, status] = mergeStatusCounts(node);
expect(status).toEqual(Status.info);
expect(counts).toHaveLength(1);
expect(counts[0].status).toEqual('info');
});
});
4 changes: 3 additions & 1 deletion web/src/__tests__/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,12 @@ describe('Node', () => {
{
query: { class: { domain: 'a', name: 'b' }, selector: 'c' },
count: 5,
statuses: [],
},
{
query: { class: { domain: 'a', name: 'b' }, selector: 'd' },
count: 5,
statuses: [],
},
],
});
Expand All @@ -154,7 +156,7 @@ describe('Node', () => {
expect(new Node({ class: 'foobar', count: 1 })).toEqual({
id: 'foobar',
count: 1,
error: new TypeError('invalid class: foobar'),
disabled: 'invalid class: foobar',
queries: [],
});
});
Expand Down
Loading