Skip to content

Commit

Permalink
feat: Display alerts on cluster overview, pods table and pod page
Browse files Browse the repository at this point in the history
  • Loading branch information
tiithansen committed Jun 13, 2024
1 parent 9e9a5f3 commit 167b72d
Show file tree
Hide file tree
Showing 15 changed files with 472 additions and 28 deletions.
7 changes: 7 additions & 0 deletions src/common/seriesHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ export function getSeriesValue(asyncData: any, name: string, pred: (value: any)
const val = getSeries(asyncData, name, pred)
return val ? val[`Value #${name}`] : 0
}

export function getAllSeries(asyncData: any, name: string, pred: (value: any) => boolean) {
if (asyncData && asyncData.get(name)) {
return asyncData.get(name).filter(pred)
}
return []
}
61 changes: 61 additions & 0 deletions src/components/AlertsTable/AlertExpandedRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";
import { SceneComponentProps, SceneFlexLayout, SceneObjectBase, SceneObjectState } from "@grafana/scenes";
import { TableRow } from "./types";
import { TagList, useStyles2 } from "@grafana/ui";
import { isString } from "lodash";
import { GrafanaTheme2 } from "@grafana/data";
import { css } from "@emotion/css";

const getStyles = (theme: GrafanaTheme2) => ({
justifyStart: css`
justify-content: flex-start;
`,
});

interface SceneTagListState extends SceneObjectState {
tags: string[];
}

class SceneTagList extends SceneObjectBase<SceneTagListState> {
static Component = (props: SceneComponentProps<SceneTagList>) => {
const styles = useStyles2(getStyles);
const { tags } = props.model.useState();
return (<TagList className={styles.justifyStart} tags={tags}/>)
}
}

const KNOWN_LABELS = [
'alertname',
'severity',
'alertstate',
'cluster',
'namespace',
]

function isKnownLabelKey(key: string) {
return KNOWN_LABELS.includes(key);
}

export function expandedRowSceneBuilder(rowIdBuilder: (row: TableRow) => string) {

return (row: TableRow) => {

const tags: string[] = []
Object.entries(row).sort().map(([key, value]) => {
if (isString(value) && value.length > 0 && !isKnownLabelKey(key) && !key.startsWith('__')) {
tags.push(`${key}=${value}`);
}
});

return new SceneFlexLayout({
key: rowIdBuilder(row),
width: '100%',
height: 500,
children: [
new SceneTagList({
tags: tags
}),
],
});
}
}
231 changes: 231 additions & 0 deletions src/components/AlertsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import {
EmbeddedScene,
SceneFlexLayout,
SceneFlexItem,
SceneQueryRunner,
TextBoxVariable,
SceneVariableSet,
VariableValueSelectors,
SceneVariables,
} from '@grafana/scenes';
import { createNamespaceVariable, resolveVariable } from 'common/variableHelpers';
import { SortingState } from 'common/sortingHelpers';
import { AsyncTable, Column, ColumnSortingConfig, QueryBuilder } from 'components/AsyncTable';
import { TextColor } from 'common/types';
import { TableRow } from './types';
import { alertLabelValues } from './utils';
import { expandedRowSceneBuilder } from './AlertExpandedRow';
import { LabelFilters, serializeLabelFilters } from 'common/queryHelpers';

const KNOWN_SEVERITIES = ['critical', 'high', 'warning', 'info'];

interface SeverityColors {
[key: string]: TextColor;
}

const KNOWN_SEVERITY_COLORS: SeverityColors = {
'critical': 'error',
'high': 'warning',
'warning': 'warning',
'info': 'primary',
}

const namespaceVariable = createNamespaceVariable();

const searchVariable = new TextBoxVariable({
name: 'search',
label: 'Search',
value: '',
});

const columns: Array<Column<TableRow>> = [
{
id: 'alertname',
header: 'ALERT NAME',
accessor: (row: TableRow) => row.alertname,
cellProps: {
color: (row: TableRow) => KNOWN_SEVERITY_COLORS[row.severity],
},
sortingConfig: {
enabled: true,
type: 'label',
local: true,
compare: (a, b, direction) => {
return direction === 'asc' ? a.alertname.localeCompare(b.alertname) : b.alertname.localeCompare(a.alertname);
}
},
},
{
id: 'alertstate',
header: 'STATE',
accessor: (row: TableRow) => row.alertstate.toLocaleUpperCase(),
cellProps: {},
sortingConfig: {
enabled: true,
type: 'label',
local: true,
compare: (a, b, direction) => {
return direction === 'asc' ? a.alertstate.localeCompare(b.alertstate) : b.alertstate.localeCompare(a.alertstate);
}
},
},
{
id: 'namespace',
header: 'NAMESPACE',
accessor: (row: TableRow) => row.namespace,
cellType: 'link',
cellProps: {},
sortingConfig: {
enabled: true,
type: 'label',
local: true,
compare: (a, b, direction) => {
return direction === 'asc' ? a.namespace.localeCompare(b.namespace) : b.namespace.localeCompare(a.namespace);
}
},
},
{
id: 'cluster',
header: 'CLUSTER',
accessor: (row: TableRow) => row.cluster,
cellType: 'link',
cellProps: {},
sortingConfig: {
enabled: true,
type: 'label',
local: true,
compare: (a, b, direction) => {
return direction === 'asc' ? a.cluster.localeCompare(b.cluster) : b.cluster.localeCompare(a.cluster);
}
},
},
{
id: 'severity',
header: 'SEVERITY',
accessor: (row: TableRow) => row.severity.toLocaleUpperCase(),
cellProps: {
color: (row: TableRow) => KNOWN_SEVERITY_COLORS[row.severity],
},
sortingConfig: {
enabled: true,
type: 'label',
local: true,
compare: (a, b, direction) => {
return direction === 'asc' ? a.severity.localeCompare(b.severity) : b.severity.localeCompare(a.severity);
}
},
},
{
id: 'Value',
header: 'AGE',
accessor: (row: TableRow) => Date.now()/1000 - row.Value,
cellType: 'formatted',
cellProps: {
format: 'dtdurations'
},
sortingConfig: {
enabled: true,
type: 'value',
local: true,
compare: (a, b, direction) => {
return direction === 'asc' ? a.Value - b.Value : b.Value - a.Value;
}
}
}
]

const serieMatcherPredicate = (row: TableRow) => (value: any) => value.alertname === row.alertname;

function rowMapper(row: TableRow, asyncRowData: any) {

}

function createRowId(row: TableRow) {
return alertLabelValues(row).join('/');
}

class AlertsQueryBuilder implements QueryBuilder<TableRow> {

constructor(private labelFilters?: LabelFilters) {}

rootQueryBuilder(variables: SceneVariableSet | SceneVariables, sorting: SortingState, sortingConfig?: ColumnSortingConfig<TableRow> | undefined) {

const serializedFilters = this.labelFilters ? serializeLabelFilters(this.labelFilters) : '';
const hasNamespaceVariable = variables.getByName('namespace') !== undefined;

const finalQuery = `
ALERTS{
cluster="$cluster",
${ hasNamespaceVariable ? `namespace=~"$namespace",` : '' }
alertstate="firing",
${serializedFilters}
}
* ignoring(alertstate) group_right(alertstate) ALERTS_FOR_STATE{
cluster="$cluster",
${ hasNamespaceVariable ? `namespace=~"$namespace",` : '' }
${serializedFilters}
}
`

return new SceneQueryRunner({
datasource: {
uid: 'prometheus',
type: 'prometheus',
},
queries: [
{
refId: 'namespaces',
expr: finalQuery,
instant: true,
format: 'table'
}
],
});
}

rowQueryBuilder(rows: TableRow[], variables: SceneVariableSet | SceneVariables) {
return []
}
}

export function AlertsTable(labelFilters?: LabelFilters, showVariableControls = true, shouldCreateVariables = true) {

const variables = new SceneVariableSet({
variables: shouldCreateVariables ? [
namespaceVariable,
searchVariable,
]: []
})

const controls = showVariableControls ? [
new VariableValueSelectors({})
] : [];

const defaultSorting: SortingState = {
columnId: 'alertname',
direction: 'asc'
}

const queryBuilder = new AlertsQueryBuilder(labelFilters);

return new EmbeddedScene({
$variables: variables,
controls: controls,
body: new SceneFlexLayout({
children: [
new SceneFlexItem({
width: '100%',
height: '100%',
body: new AsyncTable<TableRow>({
columns: columns,
createRowId: createRowId,
asyncDataRowMapper: rowMapper,
$data: queryBuilder.rootQueryBuilder(variables, defaultSorting),
queryBuilder: queryBuilder,
expandedRowBuilder: expandedRowSceneBuilder(createRowId)
}),
}),
],
}),
})
}
8 changes: 8 additions & 0 deletions src/components/AlertsTable/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface TableRow {
cluster: string;
namespace: string;
alertname: string;
severity: string;
alertstate: string;
Value: number;
}
12 changes: 12 additions & 0 deletions src/components/AlertsTable/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { isString } from "lodash";
import { TableRow } from "./types";

export function alertLabelValues(row: TableRow) {
const labels: any[] = []
Object.entries(row).sort().map(([key, value]) => {
if (isString(value) && value.length > 0) {
labels.push(value);
}
});
return labels;
}
18 changes: 11 additions & 7 deletions src/components/AsyncTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ type LinkCellProps<TableRow> = {
export type FormattedCellProps<TableRow> = {
decimals?: number;
format?: string;
color?: TextColor | ((row: TableRow) => TextColor)
}

export type CellProps<TableRow> = LinkCellProps<TableRow> | FormattedCellProps<TableRow>;
export type CellProps<TableRow> = { color?: TextColor | ((row: TableRow) => TextColor) } & (LinkCellProps<TableRow> | FormattedCellProps<TableRow>);

export interface ColumnSortingConfig<TableRow> {
enabled: boolean;
Expand Down Expand Up @@ -92,6 +91,7 @@ function ExpandedRow<TableRow>({ table, row }: ExpandedRowProps<TableRow>) {
function mapColumn<TableRow>(column: Column<TableRow>): ColumnDef<TableRow> {

let cell = undefined;
const cellProps = column.cellProps || {}
switch (column.cellType) {
case 'link':
const linkCellProps = column.cellProps as LinkCellProps<TableRow>;
Expand All @@ -108,16 +108,21 @@ function mapColumn<TableRow>(column: Column<TableRow>): ColumnDef<TableRow> {
value: props.row.getValue(column.id),
decimals: formattedCellProps.decimals,
format: formattedCellProps.format,
color: (formattedCellProps.color && isFunction(formattedCellProps.color))
? formattedCellProps.color(props.row.original)
: formattedCellProps.color,
color: (cellProps.color && isFunction(cellProps.color))
? cellProps.color(props.row.original)
: cellProps.color,
})
break;
case 'custom':
cell = (props: CellContext<TableRow, any>) => column.cellBuilder!(props.row.original)
break;
default:
cell = (props: CellContext<TableRow, any>) => DefaultCell(props.row.getValue(column.id))
cell = (props: CellContext<TableRow, any>) => DefaultCell({
text: props.row.getValue(column.id),
color: (cellProps.color && isFunction(cellProps.color))
? cellProps.color(props.row.original)
: cellProps.color,
})
break;
}

Expand Down Expand Up @@ -193,7 +198,6 @@ export class AsyncTable<TableRow> extends SceneObjectBase<TableState<TableRow>>
uid: datasourceVariable?.toString(),
type: 'prometheus',
},

queries: [
...this.state.queryBuilder.rowQueryBuilder(rows.map(row => row.original), sceneVariables),
],
Expand Down
10 changes: 8 additions & 2 deletions src/components/Cell/DefaultCell.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import React from 'react';
import { Text } from '@grafana/ui';
import { TextColor } from 'common/types';

export const DefaultCell = (text: string | number) => {
interface DefaultCellProps {
text: string | number;
color?: TextColor;
}

export const DefaultCell = ({ text, color }: DefaultCellProps) => {
return (
<Text>
<Text color={color}>
{text}
</Text>
);
Expand Down
Loading

0 comments on commit 167b72d

Please sign in to comment.