Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Expose hooks that retrieve the highest alerts for each entity #3275

Merged
Show file tree
Hide file tree
Changes from 2 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
3 changes: 2 additions & 1 deletion shell-ui/src/alerts/library.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { version } from '../../package.json';
import * as alertLibrary from './AlertProvider';
import * as alertHook from './services/alertHooks';

window.shellUIAlerts = {
///spread shellUI to keep all versions libraries
...window.shellUIAlerts,
[version]: alertLibrary,
[version]: { ...alertLibrary, ...alertHook },
};
40 changes: 40 additions & 0 deletions shell-ui/src/alerts/services/alertHooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//@flow
import { useContext } from 'react';
import { Query } from 'react-query';
import { useAlerts } from '../AlertProvider';
import type { FilterLabels, Alert } from './alertUtils';
import { getHealthStatus } from './alertUtils';

export const getNodesAlertSelectors = (): FilterLabels => {
return { alertname: ['NodeAtRisk', 'NodeDegraded'] };
};

export const getVolumesAlertName = (): FilterLabels => {
return { alertname: ['VolumeAtRisk', 'VolumeDegraded'] };
};

export const getNetworksAlertName = (): FilterLabels => {
return {
alertname: ['ControlPlaneNetworkDegraded', 'WorkloadPlaneNetworkDegraded'],
};
};

export const getServicesAlertName = (): FilterLabels => {
return {
alertname: ['PlatformServicesAtRisk', 'PlatformServicesDegraded'],
};
};

/**
*
* @param {FilterLabels} filterss
* @returns An array of alerts with the highest severity
*/
export const useHighestSeverityAlerts = (filters: FilterLabels): Alert[] => {
const query = useAlerts(useContext)(filters);
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
const filteredAlerts = query && query.alerts;

const health = getHealthStatus(filteredAlerts);
if (!filteredAlerts || !filteredAlerts.length) return [];
return filteredAlerts.filter((alert) => alert.severity === health);
};
112 changes: 112 additions & 0 deletions shell-ui/src/alerts/services/alertHooks.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import '../library';
import React, { createContext, useContext } from 'react';
import { version } from '../../../package.json';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import { renderHook } from '@testing-library/react-hooks';
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import { AlertProvider } from '../AlertProvider.js';
import { useHighestSeverityAlerts } from './alertHooks';
import { afterAll, beforeAll, jest } from '@jest/globals';

const testService = 'http://10.0.0.1/api/alertmanager';

const server = setupServer(
rest.get(`${testService}/api/v2/alerts`, (req, res, ctx) => {
const alerts = [
{
annotations: {
description:
'Filesystem on /dev/vdc at 192.168.1.29:9100 has only 3.13% available space left.',
runbook_url:
'https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-nodefilesystemalmostoutofspace',
summary: 'Filesystem has less than 5% space left.',
},
endsAt: new Date(
new Date().getTime() + 1000 * 60 * 60 * 24,
).toISOString(),
fingerprint: '37b2591ac3cdb320',
startsAt: '2021-01-25T09:12:05.358Z',
updatedAt: '2021-01-29T07:36:11.363Z',
generatorURL:
'http://prometheus-operator-prometheus.metalk8s-monitoring:9090/graph?g0.expr=%28node_filesystem_avail_bytes%7Bfstype%21%3D%22%22%2Cjob%3D%22node-exporter%22%7D+%2F+node_filesystem_size_bytes%7Bfstype%21%3D%22%22%2Cjob%3D%22node-exporter%22%7D+%2A+100+%3C+5+and+node_filesystem_readonly%7Bfstype%21%3D%22%22%2Cjob%3D%22node-exporter%22%7D+%3D%3D+0%29&g0.tab=1',
labels: {
alertname: 'VolumeDegraded',
severity: 'warning',
},
},
{
annotations: {
description:
'Filesystem on /dev/vdc at 192.168.1.29:9100 has only 3.13% available space left.',
runbook_url:
'https://github.com/kubernetes-monitoring/kubernetes-mixin/tree/master/runbook.md#alert-name-nodefilesystemalmostoutofspace',
summary: 'Filesystem has less than 5% space left.',
},
endsAt: new Date(
new Date().getTime() + 1000 * 60 * 60 * 24,
).toISOString(),
fingerprint: '37b2591ac3cdb320',
startsAt: '2021-01-25T09:12:05.358Z',
updatedAt: '2021-01-29T07:36:11.363Z',
generatorURL:
'http://prometheus-operator-prometheus.metalk8s-monitoring:9090/graph?g0.expr=%28node_filesystem_avail_bytes%7Bfstype%21%3D%22%22%2Cjob%3D%22node-exporter%22%7D+%2F+node_filesystem_size_bytes%7Bfstype%21%3D%22%22%2Cjob%3D%22node-exporter%22%7D+%2A+100+%3C+5+and+node_filesystem_readonly%7Bfstype%21%3D%22%22%2Cjob%3D%22node-exporter%22%7D+%3D%3D+0%29&g0.tab=1',
labels: {
alertname: 'VolumeAtRisk',
severity: 'critical',
},
},
];
return res(ctx.json(alerts));
}),
);

describe('useHighestSeverityAlerts hook', () => {
jest.useFakeTimers();
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());

const alertLibrary = window.shellUIAlerts[version];
console.log('window.shellUIAlerts', window.shellUIAlerts);
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
alertLibrary.createAlertContext(createContext);
const wrapper = ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
<AlertProvider alertManagerUrl={testService}>{children}</AlertProvider>
</QueryClientProvider>
);
const AlertProvider = alertLibrary.AlertProvider(useQuery);
const useAlerts = alertLibrary.useAlerts(useContext);

it('should only get the VolumeAtRisk alert when both VolumeAtRisk and VolumeDegraded are active', async () => {
const { result, waitForNextUpdate } = renderHook(
() =>
useHighestSeverityAlerts({
alertname: ['VolumeAtRisk', 'VolumeDegraded'],
}),
{ wrapper },
);
// E
await waitForNextUpdate();
// V
expect(result.current[0].labels.alertname).toEqual('VolumeAtRisk');
expect(result.current.length).toEqual(1);
});

it('should get empty array', async () => {
const { result, waitForNextUpdate } = renderHook(
() =>
useHighestSeverityAlerts({
alertname: ['NodeAtRisk', 'NodeDegraded'],
}),
{ wrapper },
);
// E
await waitForNextUpdate();
// V
expect(result.current).toEqual([]);
});

afterAll(() => {
server.close();
});
});
8 changes: 8 additions & 0 deletions shell-ui/src/platform/library.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { version } from '../../package.json';
import * as k8s from './service/k8s';

window.shellUIAlerts = {
///spread shellUI to keep all versions libraries
...window.shellUIAlerts,
[version]: k8s,
};
113 changes: 113 additions & 0 deletions shell-ui/src/platform/service/k8s.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//@flow
import { useState, useEffect } from 'react';
import { useQuery } from 'react-query';
import axios from 'axios';
import { AUTHENTICATED_EVENT } from '../../navbar/events';

const useGetNodesCount = (useQuery: typeof useQuery, k8sUrl: string) => {
const navbarElement = document.querySelector('solutions-navbar');
const [token, setToken] = useState(null);
// $FlowFixMe
useQuery('initialToken', () => navbarElement.getIdToken(), {
onSucces: (idToken) => setToken(idToken),
});

useEffect(() => {
if (!navbarElement) {
return;
}
const onAuthenticated = (evt: Event) => {
// $FlowFixMe
setToken(evt.detail.id_token);
};
navbarElement.addEventListener(AUTHENTICATED_EVENT, onAuthenticated);
return () =>
navbarElement.removeEventListener(AUTHENTICATED_EVENT, onAuthenticated);
}, []);

const queryNodesResult = useQuery(getNodesCountQuery(k8sUrl, token));
queryNodesResult.nodesCount = queryNodesResult.data;
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
delete queryNodesResult.data;

return queryNodesResult;
};

const useGetVolumesCount = (useQuery: typeof useQuery, k8sUrl: string) => {
const navbarElement = document.querySelector('solutions-navbar');
const [token, setToken] = useState(null);
// $FlowFixMe
useQuery('initialToken', () => navbarElement.getIdToken(), {
onSucces: (idToken) => setToken(idToken),
});

useEffect(() => {
if (!navbarElement) {
return;
}
const onAuthenticated = (evt: Event) => {
// $FlowFixMe
setToken(evt.detail.id_token);
};
navbarElement.addEventListener(AUTHENTICATED_EVENT, onAuthenticated);
return () =>
navbarElement.removeEventListener(AUTHENTICATED_EVENT, onAuthenticated);
}, []);

const queryVolumesResult = useQuery(getVolumesCountQuery(k8sUrl, token));
queryVolumesResult.volumesCount = queryVolumesResult.data;
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
delete queryVolumesResult.data;

return queryVolumesResult;
};

export const getNodesCountQuery = (
k8sUrl: string,
token?: string | null,
): typeof useQuery => {
if (!token) {
console.error('K8s API Not authenticated');
return;
}
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
return {
cacheKey: 'countNodes',
queryFn: () =>
fetch(`${k8sUrl}/api/v1/nodes`)
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
.then((r) => {
if (r.ok) {
return r.json();
}
})
.then((res) => {
return res.items.length;
}),
options: { refetchInterval: 10000 },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is missing enabled options :

Suggested change
options: { refetchInterval: 10000 },
options: { refetchInterval: 10000, enabled: token },

this will only triggers the query if the token is provided.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I've checked the doc, the enabled is a boolean, so maybe the following one is more correct
options: { refetchInterval: 10000, enabled: token ? true : false },

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JS is automatically casting this to a boolean so token ? true : false is strictly equivalent to token. Actually in token ? true : false you are also casting token as a boolean for the ternary condition

Copy link
Contributor Author

@ChengYanJin ChengYanJin Apr 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, the tests won't execute by enabled: token. So I have to switch back to enabled: token ? true : false

};
};

export const getVolumesCountQuery = (
k8sUrl: string,
token?: string | null,
): typeof useQuery => {
if (!token) {
console.error('K8s API Not authenticated');
return;
}
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
return {
cacheKey: 'countVolumes',
queryFn: () =>
fetch(`${k8sUrl}/api/v1/persistentvolumes`, {
headers: {
authorisation: `bearer ${token}`,
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
},
})
.then((r) => {
if (r.ok) {
return r.json();
}
})
.then((res) => {
return res.items.length;
}),
options: { refetchInterval: 10000 },
JBWatenbergScality marked this conversation as resolved.
Show resolved Hide resolved
};
};