Skip to content

Commit 882db65

Browse files
gally47todvoramoesterheld
authored
Cluster Nodes UI improvements (#24493)
* new optional externalSearch prop for PaginatedEntityTable * removed non derived attributes from ColumnDefinitions * added onDataLoaded so the cluster nodes sections could update their header counts * fetch cluster nodes files * ClusterGraylogNode type * cluster node sections using PaginatedEntityTable * adjust tests * useProductName instead of Graylog * Replacing reduce with Object.fromEntries * fix wrapper top spacing * increased refetch intervals * initial static widths * onDataLoaded avoid double-calling * Rename titles in server and datanode cluster tables, add opensearch roles * integrated staticWidth: 'matchHeader' * add data node cpu percent * add cpu load metric * integrated CPU percent metric cell * staticWidth change createColumnRenderers * fix merge issues * finalize col widths * fix GraylogNodeActions menu display * implement CPU threshold status * code cleanup and small refactorings, seamless cpu load computation * fix type casting * use PaperSection --------- Co-authored-by: Tomas Dvorak <[email protected]> Co-authored-by: Matthias Oesterheld <[email protected]>
1 parent 53a017b commit 882db65

36 files changed

+737
-1150
lines changed

data-node/src/main/java/org/graylog/datanode/metrics/NodeStatMetrics.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
public enum NodeStatMetrics {
2626
CPU_LOAD("float", new RollupAction.IsmRollup.AvgMetric(), "$.os.cpu.load_average.1m"),
27+
CPU_PERCENT("integer", new RollupAction.IsmRollup.AvgMetric(), "$.os.cpu.percent"),
2728
MEM_FREE("float", new RollupAction.IsmRollup.AvgMetric(), "$.os.mem.free_in_bytes", NodeStatMetrics::bytesToMb),
2829
MEM_TOTAL("float", new RollupAction.IsmRollup.AvgMetric(), "$.os.mem.total_in_bytes", NodeStatMetrics::bytesToGb),
2930
MEM_TOTAL_USED_BYTES("float", new RollupAction.IsmRollup.AvgMetric(), "$.os.mem.used_in_bytes", NodeStatMetrics::bytesToGb),

graylog2-server/src/main/java/org/graylog2/bindings/PeriodicalBindings.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.graylog2.periodical.IndexRotationThread;
3535
import org.graylog2.periodical.IndexerClusterCheckerThread;
3636
import org.graylog2.periodical.LeaderPresenceCheckPeriodical;
37+
import org.graylog2.periodical.NodeMetricPeriodical;
3738
import org.graylog2.periodical.NodePingThread;
3839
import org.graylog2.periodical.OrphanedTokenCleaner;
3940
import org.graylog2.periodical.SearchVersionCheckPeriodical;
@@ -72,5 +73,6 @@ protected void configure() {
7273
periodicalBinder.addBinding().to(InputDiagnosisMetricsPeriodical.class);
7374
periodicalBinder.addBinding().to(ExpiredTokenCleaner.class);
7475
periodicalBinder.addBinding().to(OrphanedTokenCleaner.class);
76+
periodicalBinder.addBinding().to(NodeMetricPeriodical.class);
7577
}
7678
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog2.periodical;
18+
19+
import com.codahale.metrics.Gauge;
20+
import oshi.SystemInfo;
21+
import oshi.hardware.CentralProcessor;
22+
23+
public class CpuLoadGauge implements Gauge<Double> {
24+
25+
private long[] lastTicks = processor().getSystemCpuLoadTicks();
26+
private Double cpuLoad;
27+
28+
@Override
29+
public Double getValue() {
30+
return cpuLoad;
31+
}
32+
33+
public void update() {
34+
final CentralProcessor processor = processor();
35+
final long[] newTicks = processor.getSystemCpuLoadTicks();
36+
cpuLoad = processor.getSystemCpuLoadBetweenTicks(lastTicks, newTicks) * 100.0d;
37+
lastTicks = newTicks;
38+
}
39+
40+
private static CentralProcessor processor() {
41+
SystemInfo si = new SystemInfo();
42+
return si.getHardware().getProcessor();
43+
}
44+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
18+
package org.graylog2.periodical;
19+
20+
import com.codahale.metrics.MetricRegistry;
21+
import jakarta.annotation.Nonnull;
22+
import jakarta.inject.Inject;
23+
import jakarta.inject.Singleton;
24+
import org.graylog2.plugin.periodical.Periodical;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
27+
28+
@Singleton
29+
public class NodeMetricPeriodical extends Periodical {
30+
31+
private static final Logger LOG = LoggerFactory.getLogger(NodeMetricPeriodical.class);
32+
33+
private final CpuLoadGauge cpuLoadGauge = new CpuLoadGauge();
34+
35+
private final MetricRegistry metricRegistry;
36+
37+
@Inject
38+
public NodeMetricPeriodical(MetricRegistry metricRegistry) {
39+
this.metricRegistry = metricRegistry;
40+
}
41+
42+
@Override
43+
public boolean runsForever() {
44+
return false;
45+
}
46+
47+
@Override
48+
public boolean stopOnGracefulShutdown() {
49+
return true;
50+
}
51+
52+
@Override
53+
public boolean startOnThisNode() {
54+
return true;
55+
}
56+
57+
@Override
58+
public boolean isDaemon() {
59+
return false;
60+
}
61+
62+
@Override
63+
public int getInitialDelaySeconds() {
64+
return 0;
65+
}
66+
67+
@Override
68+
public int getPeriodSeconds() {
69+
return 5;
70+
}
71+
72+
@Nonnull
73+
@Override
74+
protected Logger getLogger() {
75+
return LOG;
76+
}
77+
78+
@Override
79+
public boolean leaderOnly() {
80+
return false;
81+
}
82+
83+
84+
@Override
85+
public void initialize() {
86+
metricRegistry.registerGauge("org.graylog2.system.cpu.percent", cpuLoadGauge);
87+
}
88+
89+
@Override
90+
public void doRun() {
91+
cpuLoadGauge.update();
92+
}
93+
}

graylog2-server/src/main/java/org/graylog2/rest/resources/datanodes/DatanodeResource.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,12 @@ public class DatanodeResource extends RestResource {
6868
private static final String DEFAULT_SORT_FIELD = DataNodeDto.FIELD_HOSTNAME;
6969
private static final String DEFAULT_SORT_DIRECTION = "asc";
7070
private static final List<EntityAttribute> attributes = List.of(
71-
EntityAttribute.builder().id(DataNodeDto.FIELD_HOSTNAME).title("Hostname").type(SearchQueryField.Type.STRING).sortable(true).searchable(true).build(),
71+
EntityAttribute.builder().id(DataNodeDto.FIELD_HOSTNAME).title("Node").type(SearchQueryField.Type.STRING).sortable(true).searchable(true).build(),
7272
EntityAttribute.builder().id(DataNodeDto.FIELD_DATANODE_STATUS).sortable(true).filterable(true).filterOptions(danodeStatusOptions()).title("Status").build(),
7373
EntityAttribute.builder().id(DataNodeDto.FIELD_CLUSTER_ADDRESS).title("Transport address").type(SearchQueryField.Type.STRING).searchable(true).sortable(true).build(),
7474
EntityAttribute.builder().id(DataNodeDto.FIELD_CERT_VALID_UNTIL).title("Certificate valid until").type(SearchQueryField.Type.DATE).sortable(true).build(),
75-
EntityAttribute.builder().id(DataNodeDto.FIELD_DATANODE_VERSION).title("Datanode version").type(SearchQueryField.Type.STRING).sortable(true).build()
75+
EntityAttribute.builder().id(DataNodeDto.FIELD_DATANODE_VERSION).title("Version").type(SearchQueryField.Type.STRING).sortable(true).build(),
76+
EntityAttribute.builder().id(DataNodeDto.FIELD_OPENSEARCH_ROLES).title("Roles").type(SearchQueryField.Type.STRING).sortable(true).build()
7677
);
7778

7879
private static Set<FilterOption> danodeStatusOptions() {

graylog2-server/src/main/java/org/graylog2/rest/resources/system/ClusterResource.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ public class ClusterResource extends RestResource {
9696
EntityAttribute.builder().id("is_leader").title("Leader").filterable(true).sortable(true).build(),
9797
EntityAttribute.builder().id("transport_address").title("Transport address").searchable(true).sortable(true).build(),
9898
EntityAttribute.builder().id("last_seen").title("Last seen").sortable(true).build(),
99-
EntityAttribute.builder().id("hostname").title("Hostname").searchable(true).sortable(true).build(),
99+
EntityAttribute.builder().id("hostname").title("Node").searchable(true).sortable(true).build(),
100100
EntityAttribute.builder().id("node_id").title("Node ID").searchable(true).sortable(true).type(SearchQueryField.Type.STRING).build(),
101101
EntityAttribute.builder().id("short_node_id").title("Short node ID").sortable(true).build(),
102-
EntityAttribute.builder().id(ServerNodeDto.FIELD_LOAD_BALANCER_STATUS).title("Load balancer status").sortable(true).filterable(true).filterOptions(loadBalancerOptions()).build(),
103-
EntityAttribute.builder().id(ServerNodeDto.FIELD_LIFECYCLE).title("Lifecycle").sortable(true).filterable(true).filterOptions(lifecycleOptions()).build(),
102+
EntityAttribute.builder().id(ServerNodeDto.FIELD_LOAD_BALANCER_STATUS).title("Load balancer").sortable(true).filterable(true).filterOptions(loadBalancerOptions()).build(),
103+
EntityAttribute.builder().id(ServerNodeDto.FIELD_LIFECYCLE).title("Status").sortable(true).filterable(true).filterOptions(lifecycleOptions()).build(),
104104
EntityAttribute.builder().id(ServerNodeDto.FIELD_IS_PROCESSING).title("Processing").sortable(true).filterable(true).build()
105105
);
106106

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright (C) 2020 Graylog, Inc.
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the Server Side Public License, version 1,
6+
* as published by MongoDB, Inc.
7+
*
8+
* This program is distributed in the hope that it will be useful,
9+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
* Server Side Public License for more details.
12+
*
13+
* You should have received a copy of the Server Side Public License
14+
* along with this program. If not, see
15+
* <http://www.mongodb.com/licensing/server-side-public-license>.
16+
*/
17+
package org.graylog2.periodical;
18+
19+
import org.assertj.core.api.Assertions;
20+
import org.junit.jupiter.api.Test;
21+
22+
class CpuLoadGaugeTest {
23+
@Test
24+
void testGauge() {
25+
final CpuLoadGauge gauge = new CpuLoadGauge();
26+
Assertions.assertThat(gauge.getValue()).isNull();
27+
gauge.update();
28+
Assertions.assertThat(gauge.getValue())
29+
.isNotNull()
30+
.isGreaterThan(0.0d);
31+
}
32+
}

graylog2-web-interface/src/components/cluster-configuration/ClusterConfigurationNodes.test.tsx

Lines changed: 27 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -19,71 +19,19 @@ import React from 'react';
1919
import { act, render, screen, waitFor } from 'wrappedTestingLibrary';
2020

2121
import { SEARCH_DEBOUNCE_THRESHOLD } from 'components/common/SearchForm';
22+
import asMock from 'helpers/mocking/AsMock';
2223

2324
import ClusterConfigurationNodes from './ClusterConfigurationNodes';
24-
import useClusterDataNodes from './data-nodes/useClusterDataNodes';
25-
import useClusterDataNodesTableLayout from './data-nodes/useClusterDataNodesTableLayout';
26-
import useClusterGraylogNodes from './graylog-nodes/useClusterGraylogNodes';
27-
import useClusterGraylogNodesTableLayout from './graylog-nodes/useClusterGraylogNodesTableLayout';
28-
29-
jest.mock('./data-nodes/useClusterDataNodes');
30-
jest.mock('./data-nodes/useClusterDataNodesTableLayout');
31-
jest.mock('./graylog-nodes/useClusterGraylogNodes');
32-
jest.mock('./graylog-nodes/useClusterGraylogNodesTableLayout');
33-
jest.mock('./data-nodes/useAddMetricsToDataNodes');
34-
jest.mock('./graylog-nodes/useAddMetricsToGraylogNodes');
3525

36-
describe('<ClusterConfigurationNodes />', () => {
37-
const defaultDataLayout = {
38-
defaultDisplayedColumns: ['hostname'],
39-
defaultColumnOrder: ['hostname'],
40-
layoutPreferences: {
41-
attributes: undefined,
42-
order: undefined,
43-
pageSize: 0,
44-
sort: { attributeId: 'hostname', direction: 'asc' },
45-
},
46-
searchParams: { sort: { attributeId: 'hostname', direction: 'asc' }, query: '' },
47-
isLoadingLayout: false,
48-
handleLayoutPreferencesChange: jest.fn(),
49-
handleSortChange: jest.fn(),
50-
};
51-
52-
const defaultGraylogLayout = {
53-
defaultDisplayedColumns: ['hostname'],
54-
defaultColumnOrder: ['hostname'],
55-
layoutPreferences: {
56-
attributes: undefined,
57-
order: undefined,
58-
pageSize: 0,
59-
sort: { attributeId: 'hostname', direction: 'asc' },
60-
},
61-
searchParams: { sort: { attributeId: 'hostname', direction: 'asc' }, query: '' },
62-
isLoadingLayout: false,
63-
handleLayoutPreferencesChange: jest.fn(),
64-
handleSortChange: jest.fn(),
65-
};
26+
jest.mock('components/common/PaginatedEntityTable', () => ({
27+
__esModule: true,
28+
default: jest.fn(() => <div role="table">paginated-table</div>),
29+
useTableFetchContext: jest.fn(),
30+
}));
6631

32+
describe('<ClusterConfigurationNodes />', () => {
6733
beforeEach(() => {
6834
jest.useFakeTimers();
69-
(useClusterDataNodesTableLayout as jest.Mock).mockReturnValue(defaultDataLayout);
70-
(useClusterGraylogNodesTableLayout as jest.Mock).mockReturnValue(defaultGraylogLayout);
71-
(useClusterDataNodes as jest.Mock).mockReturnValue({
72-
nodes: [{ id: 'data-1', hostname: 'data-host', metrics: {} }],
73-
total: 1,
74-
refetch: jest.fn(),
75-
isLoading: false,
76-
setPollingEnabled: jest.fn(),
77-
pollingEnabled: true,
78-
});
79-
(useClusterGraylogNodes as jest.Mock).mockReturnValue({
80-
nodes: [{ id: 'graylog-1', hostname: 'graylog-host', metrics: {} }],
81-
total: 1,
82-
refetch: jest.fn(),
83-
isLoading: false,
84-
setPollingEnabled: jest.fn(),
85-
pollingEnabled: true,
86-
});
8735
});
8836

8937
afterEach(() => {
@@ -92,41 +40,45 @@ describe('<ClusterConfigurationNodes />', () => {
9240
});
9341

9442
it('renders both node types with default paging and refresh settings in "all" view', () => {
43+
const { default: MockPaginatedEntityTable } = jest.requireMock('components/common/PaginatedEntityTable');
44+
const mockPaginatedEntityTable = asMock(MockPaginatedEntityTable);
45+
9546
render(<ClusterConfigurationNodes />);
9647

9748
expect(screen.getAllByRole('table')).toHaveLength(2);
98-
expect(useClusterDataNodesTableLayout).toHaveBeenCalledWith('', 10);
99-
expect(useClusterGraylogNodesTableLayout).toHaveBeenCalledWith('', 10);
100-
expect(useClusterDataNodes).toHaveBeenCalledWith(defaultDataLayout.searchParams, { refetchInterval: 5000 });
101-
expect(useClusterGraylogNodes).toHaveBeenCalledWith(defaultGraylogLayout.searchParams, { refetchInterval: 5000 });
49+
expect(mockPaginatedEntityTable).toHaveBeenCalledTimes(2);
10250
});
10351

10452
it('switches to a specific node type when segmented control is used', async () => {
53+
const { default: MockPaginatedEntityTable } = jest.requireMock('components/common/PaginatedEntityTable');
54+
const mockPaginatedEntityTable = asMock(MockPaginatedEntityTable);
55+
10556
render(<ClusterConfigurationNodes />);
10657

107-
await userEvent.click(screen.getByRole('radio', { name: 'Data Nodes' }));
58+
mockPaginatedEntityTable.mockClear();
10859

109-
await waitFor(() => {
110-
expect(useClusterDataNodesTableLayout).toHaveBeenLastCalledWith(expect.anything(), 100);
111-
});
60+
await userEvent.click(screen.getByRole('radio', { name: 'Data Nodes' }));
11261

113-
expect(useClusterGraylogNodesTableLayout).toHaveBeenCalledTimes(1);
114-
expect(useClusterDataNodes).toHaveBeenLastCalledWith(expect.objectContaining({ query: '' }), {
115-
refetchInterval: 10000,
116-
});
62+
await waitFor(() => expect(mockPaginatedEntityTable).toHaveBeenCalledTimes(1));
11763
});
11864

11965
it('uses child "select node type" handler to switch view', async () => {
66+
const { default: MockPaginatedEntityTable } = jest.requireMock('components/common/PaginatedEntityTable');
67+
const mockPaginatedEntityTable = asMock(MockPaginatedEntityTable);
68+
12069
render(<ClusterConfigurationNodes />);
12170

71+
mockPaginatedEntityTable.mockClear();
72+
12273
await userEvent.click(screen.getByRole('radio', { name: 'Data Nodes' }));
12374

124-
await waitFor(() => {
125-
expect(useClusterDataNodesTableLayout).toHaveBeenLastCalledWith('', 100);
126-
});
75+
await waitFor(() => expect(mockPaginatedEntityTable).toHaveBeenCalledTimes(1));
12776
});
12877

12978
it('passes trimmed search query to children', async () => {
79+
const { default: MockPaginatedEntityTable } = jest.requireMock('components/common/PaginatedEntityTable');
80+
const mockPaginatedEntityTable = asMock(MockPaginatedEntityTable);
81+
13082
render(<ClusterConfigurationNodes />);
13183

13284
const searchInput = screen.getByPlaceholderText('Search nodes…');
@@ -136,7 +88,6 @@ describe('<ClusterConfigurationNodes />', () => {
13688
jest.advanceTimersByTime(SEARCH_DEBOUNCE_THRESHOLD + 10);
13789
});
13890

139-
await waitFor(() => expect(useClusterDataNodesTableLayout).toHaveBeenLastCalledWith('nodes', expect.anything()));
140-
expect(useClusterGraylogNodesTableLayout).toHaveBeenLastCalledWith('nodes', expect.anything());
91+
await waitFor(() => expect(mockPaginatedEntityTable).toHaveBeenCalled());
14192
});
14293
});

0 commit comments

Comments
 (0)