Skip to content

Commit

Permalink
chore: visualize connected edges (#9325)
Browse files Browse the repository at this point in the history
https://linear.app/unleash/issue/2-3233/visualize-connected-edge-instances

Adds a new tab in the Network page to visualize connected Edges.

This is behind a `edgeObservability` flag.

Also opens up the Network page even if you don't have a Prometheus API
configured. When accessing the tabs that require it to set, and it
isn't, we show some extra information about this and redirect you to the
respective section in our docs.


![image](https://github.com/user-attachments/assets/1689f785-7544-450b-8c33-159609fc0f7d)


![image](https://github.com/user-attachments/assets/a7a14805-0488-41d2-885f-5e11a8495127)


![image](https://github.com/user-attachments/assets/918cba87-5538-4600-a71f-1143b2e33e2a)
  • Loading branch information
nunogois authored Feb 19, 2025
1 parent c938b0f commit b4bfadd
Show file tree
Hide file tree
Showing 15 changed files with 703 additions and 9 deletions.
1 change: 0 additions & 1 deletion frontend/src/component/admin/adminRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ export const adminRoutes: INavigationMenuItem[] = [
path: '/admin/network/*',
title: 'Network',
menu: { adminSettings: true, mode: ['pro', 'enterprise'] },
configFlag: 'networkViewEnabled',
group: 'instance',
},
{
Expand Down
23 changes: 21 additions & 2 deletions frontend/src/component/admin/network/Network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { Tab, Tabs } from '@mui/material';
import { Route, Routes, useLocation } from 'react-router-dom';
import { TabLink } from 'component/common/TabNav/TabLink';
import { PageContent } from 'component/common/PageContent/PageContent';
import { useUiFlag } from 'hooks/useUiFlag';

const NetworkOverview = lazy(() => import('./NetworkOverview/NetworkOverview'));
const NetworkConnectedEdges = lazy(
() => import('./NetworkConnectedEdges/NetworkConnectedEdges'),
);
const NetworkTraffic = lazy(() => import('./NetworkTraffic/NetworkTraffic'));
const NetworkTrafficUsage = lazy(
() => import('./NetworkTrafficUsage/NetworkTrafficUsage'),
Expand All @@ -20,6 +24,10 @@ const tabs = [
label: 'Traffic',
path: '/admin/network/traffic',
},
{
label: 'Connected Edges',
path: '/admin/network/connected-edges',
},
{
label: 'Data Usage',
path: '/admin/network/data-usage',
Expand All @@ -28,6 +36,11 @@ const tabs = [

export const Network = () => {
const { pathname } = useLocation();
const edgeObservabilityEnabled = useUiFlag('edgeObservability');

const filteredTabs = tabs.filter(
({ label }) => label !== 'Connected Edges' || edgeObservabilityEnabled,
);

return (
<div>
Expand All @@ -41,7 +54,7 @@ export const Network = () => {
variant='scrollable'
allowScrollButtonsMobile
>
{tabs.map(({ label, path }) => (
{filteredTabs.map(({ label, path }) => (
<Tab
key={label}
value={path}
Expand All @@ -57,8 +70,14 @@ export const Network = () => {
}
>
<Routes>
<Route path='traffic' element={<NetworkTraffic />} />
<Route path='*' element={<NetworkOverview />} />
<Route path='traffic' element={<NetworkTraffic />} />
{edgeObservabilityEnabled && (
<Route
path='connected-edges'
element={<NetworkConnectedEdges />}
/>
)}
<Route
path='data-usage'
element={<NetworkTrafficUsage />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { useLocationSettings } from 'hooks/useLocationSettings';
import type { ConnectedEdge } from 'interfaces/connectedEdge';
import CircleIcon from '@mui/icons-material/Circle';
import ExpandMore from '@mui/icons-material/ExpandMore';
import { formatDateYMDHMS } from 'utils/formatDate';
import {
Accordion,
AccordionDetails,
AccordionSummary,
styled,
Tooltip,
} from '@mui/material';
import { Badge } from 'component/common/Badge/Badge';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { NetworkConnectedEdgeInstanceLatency } from './NetworkConnectedEdgeInstanceLatency';

const StyledInstance = styled('div')(({ theme }) => ({
borderRadius: theme.shape.borderRadiusMedium,
border: '1px solid',
borderColor: theme.palette.secondary.border,
backgroundColor: theme.palette.secondary.light,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
padding: 0,
zIndex: 1,
marginTop: theme.spacing(1),
}));

const StyledAccordion = styled(Accordion)({
background: 'transparent',
boxShadow: 'none',
});

const StyledAccordionSummary = styled(AccordionSummary, {
shouldForwardProp: (prop) => prop !== 'connectionStatus',
})<{ connectionStatus: InstanceConnectionStatus }>(
({ theme, connectionStatus }) => ({
fontSize: theme.fontSizes.smallBody,
padding: theme.spacing(1),
minHeight: theme.spacing(3),
'& .MuiAccordionSummary-content': {
alignItems: 'center',
gap: theme.spacing(1),
margin: 0,
'&.Mui-expanded': {
margin: 0,
},
'& svg': {
fontSize: theme.fontSizes.mainHeader,
color:
connectionStatus === 'Stale'
? theme.palette.warning.main
: connectionStatus === 'Disconnected'
? theme.palette.error.main
: theme.palette.success.main,
},
},
}),
);

const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
fontSize: theme.fontSizes.smallerBody,
gap: theme.spacing(2),
}));

const StyledDetailRow = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
gap: theme.spacing(2),
'& > span': {
display: 'flex',
alignItems: 'center',
},
}));

const StyledBadge = styled(Badge)(({ theme }) => ({
padding: theme.spacing(0, 1),
}));

const getConnectionStatus = ({
reportedAt,
}: ConnectedEdge): InstanceConnectionStatus => {
const reportedTime = new Date(reportedAt).getTime();
const reportedSecondsAgo = (Date.now() - reportedTime) / 1000;

if (reportedSecondsAgo > 360) return 'Disconnected';
if (reportedSecondsAgo > 180) return 'Stale';

return 'Connected';
};

const getCPUPercentage = ({
started,
reportedAt,
cpuUsage,
}: ConnectedEdge): string => {
const cpuUsageSeconds = Number(cpuUsage);
if (!cpuUsageSeconds) return 'No usage';

const startedTimestamp = new Date(started).getTime();
const reportedTimestamp = new Date(reportedAt).getTime();

const totalRuntimeSeconds = (reportedTimestamp - startedTimestamp) / 1000;
if (totalRuntimeSeconds === 0) return 'No usage';

return `${((cpuUsageSeconds / totalRuntimeSeconds) * 100).toFixed(2)} %`;
};

const getMemory = ({ memoryUsage }: ConnectedEdge): string => {
if (!memoryUsage) return 'No usage';

const units = ['B', 'KB', 'MB', 'GB'];
let size = memoryUsage;
let unitIndex = 0;

while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}

return `${size.toFixed(2)} ${units[unitIndex]}`;
};

type InstanceConnectionStatus = 'Connected' | 'Stale' | 'Disconnected';

interface INetworkConnectedEdgeInstanceProps {
instance: ConnectedEdge;
}

export const NetworkConnectedEdgeInstance = ({
instance,
}: INetworkConnectedEdgeInstanceProps) => {
const { locationSettings } = useLocationSettings();

const connectionStatus = getConnectionStatus(instance);
const start = formatDateYMDHMS(instance.started, locationSettings?.locale);
const lastReport = formatDateYMDHMS(
instance.reportedAt,
locationSettings?.locale,
);
const cpuPercentage = getCPUPercentage(instance);
const memory = getMemory(instance);
const archWarning = cpuPercentage === 'No usage' &&
memory === 'No usage' && (
<p>Resource metrics are only available when running on Linux</p>
);

return (
<StyledInstance>
<StyledAccordion>
<StyledAccordionSummary
expandIcon={<ExpandMore />}
connectionStatus={connectionStatus}
>
<Tooltip
arrow
title={`${connectionStatus}. Last reported: ${lastReport}`}
>
<CircleIcon />
</Tooltip>
{instance.id || instance.instanceId}
</StyledAccordionSummary>
<StyledAccordionDetails>
<StyledDetailRow>
<strong>ID</strong>
<span>{instance.instanceId}</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>Upstream</strong>
<span>{instance.connectedVia || 'Unleash'}</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>Status</strong>
<StyledBadge
color={
connectionStatus === 'Disconnected'
? 'error'
: connectionStatus === 'Stale'
? 'warning'
: 'success'
}
>
{connectionStatus}
</StyledBadge>
</StyledDetailRow>
<StyledDetailRow>
<strong>Start</strong>
<span>{start}</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>Last report</strong>
<span>{lastReport}</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>App name</strong>
<span>{instance.appName}</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>Region</strong>
<span>{instance.region || 'Unknown'}</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>Version</strong>
<span>{instance.edgeVersion}</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>CPU</strong>
<span>
{cpuPercentage}{' '}
<HelpIcon
tooltip={
<>
<p>
CPU average usage since instance
started
</p>
{archWarning}
</>
}
size='16px'
/>
</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>Memory</strong>
<span>
{memory}{' '}
<HelpIcon
tooltip={
<>
<p>Current memory usage</p>
{archWarning}
</>
}
size='16px'
/>
</span>
</StyledDetailRow>
<StyledDetailRow>
<strong>Stream clients</strong>
<span>{instance.connectedStreamingClients}</span>
</StyledDetailRow>
<StyledDetailRow>
<NetworkConnectedEdgeInstanceLatency
instance={instance}
/>
</StyledDetailRow>
</StyledAccordionDetails>
</StyledAccordion>
</StyledInstance>
);
};
Loading

0 comments on commit b4bfadd

Please sign in to comment.