mirror of
https://github.com/Graylog2/graylog2-server.git
synced 2026-03-13 09:32:21 +08:00
Add input failures column to Inputs list (#25048)
* first draft of getting a related identifier * add composite suggestion display * add computed field registry logic * fix linter * add input failures column to Inputs list * fix rebasing * fix import * update to use useMetrics hook and fix review --------- Co-authored-by: Maxwell Anipah <maxwell.anipah@graylog.com> Co-authored-by: Laura Bergenthal-Grotlüschen <197286649+laura-b-g@users.noreply.github.com>
This commit is contained in:
@@ -21,6 +21,7 @@ import type { InputSummary } from 'hooks/usePaginatedInputs';
|
||||
import type { InputTypesSummary } from 'hooks/useInputTypes';
|
||||
import type { InputStates } from 'hooks/useInputsStates';
|
||||
import { TypeCell, NodeCell, ThroughputCell, ExpandedSectionToggleWrapper } from 'components/inputs/InputsOveriew';
|
||||
import FailuresCell from 'components/inputs/InputsOveriew/cells/FailuresCell';
|
||||
import { InputStateBadge } from 'components/inputs';
|
||||
import Routes from 'routing/Routes';
|
||||
import { Link } from 'components/common';
|
||||
@@ -74,6 +75,14 @@ const customColumnRenderers = ({ inputTypes, inputStates }: Props): ColumnRender
|
||||
),
|
||||
staticWidth: 180,
|
||||
},
|
||||
input_failures: {
|
||||
renderCell: (_failures: string, input: InputSummary) => (
|
||||
<ExpandedSectionToggleWrapper id={input.id}>
|
||||
<FailuresCell input={input} />
|
||||
</ExpandedSectionToggleWrapper>
|
||||
),
|
||||
staticWidth: 130,
|
||||
},
|
||||
address: {
|
||||
renderCell: (_address: string, input: InputSummary) => (
|
||||
<ExpandedSectionToggleWrapper id={input.id}>
|
||||
|
||||
@@ -27,15 +27,17 @@ const getInputsTableElements = () => {
|
||||
'direction',
|
||||
'desired_state',
|
||||
'traffic',
|
||||
'input_failures',
|
||||
'node_id',
|
||||
'address',
|
||||
'port',
|
||||
],
|
||||
defaultColumnOrder: ['title', 'type', 'direction', 'desired_state', 'traffic', 'node_id', 'address', 'port'],
|
||||
defaultColumnOrder: ['title', 'type', 'direction', 'desired_state', 'traffic', 'input_failures', 'node_id', 'address', 'port'],
|
||||
};
|
||||
|
||||
const additionalAttributes = [
|
||||
{ id: 'traffic', title: 'Traffic Last Minute' },
|
||||
{ id: 'input_failures', title: 'Input Failures' },
|
||||
{ id: 'address', title: 'Address' },
|
||||
{ id: 'port', title: 'Port' },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Graylog, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import { render, screen } from 'wrappedTestingLibrary';
|
||||
|
||||
import { asMock } from 'helpers/mocking';
|
||||
import { useMetrics } from 'hooks/useMetrics';
|
||||
import type { ClusterMetric } from 'types/metrics';
|
||||
|
||||
import FailuresCell from './FailuresCell';
|
||||
|
||||
jest.mock('hooks/useMetrics', () => ({
|
||||
useMetrics: jest.fn(),
|
||||
}));
|
||||
|
||||
const input = {
|
||||
id: 'input-1',
|
||||
title: 'My Test Input',
|
||||
type: 'org.graylog2.inputs.raw.tcp.RawTCPInput',
|
||||
name: 'Raw/Plaintext TCP',
|
||||
global: true,
|
||||
node: '',
|
||||
created_at: '2024-01-01T00:00:00Z',
|
||||
creator_user_id: 'admin',
|
||||
attributes: {},
|
||||
static_fields: {},
|
||||
content_pack: '',
|
||||
};
|
||||
|
||||
const gauge = (fullName: string, value: number) => ({
|
||||
full_name: fullName,
|
||||
name: fullName.split('.').pop()!,
|
||||
type: 'gauge' as const,
|
||||
metric: { value },
|
||||
});
|
||||
|
||||
const defaultMetricsData: ClusterMetric = {
|
||||
'node-1': {
|
||||
'org.graylog2.inputs.input-1.failures.input': gauge('org.graylog2.inputs.input-1.failures.input', 5),
|
||||
'org.graylog2.inputs.input-1.failures.processing': gauge('org.graylog2.inputs.input-1.failures.processing', 3),
|
||||
'org.graylog2.inputs.input-1.failures.indexing': gauge('org.graylog2.inputs.input-1.failures.indexing', 2),
|
||||
},
|
||||
};
|
||||
|
||||
describe('FailuresCell', () => {
|
||||
beforeEach(() => {
|
||||
asMock(useMetrics).mockReturnValue({ data: defaultMetricsData, isLoading: false });
|
||||
});
|
||||
|
||||
it('renders the sum of all failure metrics across nodes', () => {
|
||||
render(<FailuresCell input={input} />);
|
||||
|
||||
expect(screen.getByText('10')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('links to the Input Diagnosis page', () => {
|
||||
render(<FailuresCell input={input} />);
|
||||
|
||||
const link = screen.getByRole('link', { name: /show input diagnosis/i });
|
||||
|
||||
expect(link).toHaveAttribute('href', '/system/input/diagnosis/input-1');
|
||||
});
|
||||
|
||||
it('calls useMetrics with the correct metric names', () => {
|
||||
render(<FailuresCell input={input} />);
|
||||
|
||||
expect(useMetrics).toHaveBeenCalledWith([
|
||||
'org.graylog2.inputs.input-1.failures.input',
|
||||
'org.graylog2.inputs.input-1.failures.processing',
|
||||
'org.graylog2.inputs.input-1.failures.indexing',
|
||||
]);
|
||||
});
|
||||
|
||||
it('aggregates failure metrics from multiple nodes', () => {
|
||||
const multiNodeData: ClusterMetric = {
|
||||
'node-1': {
|
||||
'org.graylog2.inputs.input-1.failures.input': gauge('org.graylog2.inputs.input-1.failures.input', 5),
|
||||
'org.graylog2.inputs.input-1.failures.processing': gauge('org.graylog2.inputs.input-1.failures.processing', 3),
|
||||
'org.graylog2.inputs.input-1.failures.indexing': gauge('org.graylog2.inputs.input-1.failures.indexing', 2),
|
||||
},
|
||||
'node-2': {
|
||||
'org.graylog2.inputs.input-1.failures.input': gauge('org.graylog2.inputs.input-1.failures.input', 10),
|
||||
'org.graylog2.inputs.input-1.failures.processing': gauge('org.graylog2.inputs.input-1.failures.processing', 7),
|
||||
'org.graylog2.inputs.input-1.failures.indexing': gauge('org.graylog2.inputs.input-1.failures.indexing', 3),
|
||||
},
|
||||
};
|
||||
|
||||
asMock(useMetrics).mockReturnValue({ data: multiNodeData, isLoading: false });
|
||||
|
||||
render(<FailuresCell input={input} />);
|
||||
|
||||
expect(screen.getByText('30')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows 0 when no failure metrics exist for the input', () => {
|
||||
asMock(useMetrics).mockReturnValue({ data: { 'node-1': {} }, isLoading: false });
|
||||
|
||||
render(<FailuresCell input={input} />);
|
||||
|
||||
expect(screen.getByText('0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a spinner while loading', async () => {
|
||||
asMock(useMetrics).mockReturnValue({ data: {}, isLoading: true });
|
||||
|
||||
render(<FailuresCell input={input} />);
|
||||
|
||||
expect(await screen.findByText(/loading/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Graylog, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the Server Side Public License, version 1,
|
||||
* as published by MongoDB, Inc.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* Server Side Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the Server Side Public License
|
||||
* along with this program. If not, see
|
||||
* <http://www.mongodb.com/licensing/server-side-public-license>.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import type { InputSummary } from 'hooks/usePaginatedInputs';
|
||||
import { useMetrics } from 'hooks/useMetrics';
|
||||
import { Link, Spinner } from 'components/common';
|
||||
import { formatCount, getValueFromMetric } from 'components/inputs/helpers/InputThroughputUtils';
|
||||
import Routes from 'routing/Routes';
|
||||
|
||||
type Props = {
|
||||
input: InputSummary;
|
||||
};
|
||||
|
||||
const FAILURE_SUFFIXES = ['failures.input', 'failures.processing', 'failures.indexing'] as const;
|
||||
|
||||
const buildMetricName = (inputId: string, suffix: string) => `org.graylog2.inputs.${inputId}.${suffix}`;
|
||||
|
||||
const StyledLink = styled(Link)<{ $hasFailures: boolean }>(
|
||||
({ theme, $hasFailures }) => css`
|
||||
color: ${$hasFailures ? theme.colors.variant.danger : 'inherit'};
|
||||
font-weight: ${$hasFailures ? 'bold' : 'normal'};
|
||||
`,
|
||||
);
|
||||
|
||||
const FailuresCell = ({ input }: Props) => {
|
||||
const metricNames = FAILURE_SUFFIXES.map((suffix) => buildMetricName(input.id, suffix));
|
||||
const { data: metrics, isLoading } = useMetrics(metricNames);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner size="xs" />;
|
||||
}
|
||||
|
||||
const totalFailures = metricNames.reduce((sum, metricName) => {
|
||||
const aggregated = Object.keys(metrics).reduce(
|
||||
(prev, nodeId) => prev + getValueFromMetric(metrics[nodeId]?.[metricName]),
|
||||
0,
|
||||
);
|
||||
|
||||
return sum + aggregated;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<StyledLink
|
||||
to={Routes.SYSTEM.INPUT_DIAGNOSIS(input.id)}
|
||||
title={`Show input diagnosis for ${input.title}`}
|
||||
$hasFailures={totalFailures > 0}>
|
||||
{formatCount(totalFailures)}
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default FailuresCell;
|
||||
@@ -21,6 +21,7 @@ export { default as InputsActions } from './InputsActions';
|
||||
export { default as TypeCell } from './cells/TypeCell';
|
||||
export { default as NodeCell } from './cells/NodeCell';
|
||||
export { default as ThroughputCell } from './cells/ThroughputCell';
|
||||
export { default as FailuresCell } from './cells/FailuresCell';
|
||||
export { default as ExpandedSectionToggleWrapper } from './ExpandedSectionToggleWrapper';
|
||||
export { default as ThroughputSection } from './expanded-sections/ThroughputSection';
|
||||
export { default as Connections } from './expanded-sections/Connections';
|
||||
|
||||
Reference in New Issue
Block a user