[release] v0.12.0-unstable41
This commit is contained in:
parent
3abd0ee6ea
commit
aa963bb89f
|
@ -1,15 +1,20 @@
|
|||
## Version 0.12.0
|
||||
- New Dashboard
|
||||
- New metrics gathering system
|
||||
- New alerts system
|
||||
- New notification center
|
||||
- New events manager
|
||||
- Integrated a new docker-less mode of functioning for networking
|
||||
- Added Button to force reset HTTPS cert in settings
|
||||
- New color slider with reset buttons
|
||||
- Fixed blinking modals issues
|
||||
- Added a notification when updating a container
|
||||
- Added lazyloading to URL and Servapp pages images
|
||||
- Added a dangerous IP detector that stops sending HTTP response to IPs that are abusing various shields features
|
||||
- Added a button in the servapp page to easily download the docker backup
|
||||
- Redirect static folder to host if possible
|
||||
- New Homescreen look
|
||||
- Added option to disable routes without deleting them
|
||||
- Fixed blinking modals issues
|
||||
- Improve display or icons [fixes #121]
|
||||
- Refactored Mongo connection code [fixes #111]
|
||||
- Forward simultaneously TCP and UDP [fixes #122]
|
||||
|
|
|
@ -6,6 +6,20 @@ function get() {
|
|||
});
|
||||
}
|
||||
|
||||
function reset() {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve()
|
||||
});
|
||||
}
|
||||
|
||||
// function list() {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// resolve()
|
||||
// });
|
||||
// }
|
||||
|
||||
export {
|
||||
get,
|
||||
reset,
|
||||
// list,
|
||||
};
|
|
@ -18,7 +18,17 @@ function reset() {
|
|||
}))
|
||||
}
|
||||
|
||||
function list() {
|
||||
return wrap(fetch('/cosmos/api/list-metrics', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
get,
|
||||
reset,
|
||||
list,
|
||||
};
|
|
@ -110,6 +110,24 @@ function resetPassword(values) {
|
|||
}))
|
||||
}
|
||||
|
||||
function getNotifs() {
|
||||
return wrap(fetch('/cosmos/api/notifications', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
function readNotifs(notifs) {
|
||||
return wrap(fetch('/cosmos/api/notifications/read?ids=' + notifs.join(','), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
export {
|
||||
list,
|
||||
create,
|
||||
|
@ -122,4 +140,6 @@ export {
|
|||
check2FA,
|
||||
reset2FA,
|
||||
resetPassword,
|
||||
getNotifs,
|
||||
readNotifs,
|
||||
};
|
Binary file not shown.
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 126 KiB |
|
@ -3,11 +3,13 @@ import { Card, Chip, Stack, Tooltip } from "@mui/material";
|
|||
import { useState } from "react";
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
||||
export const DeleteButton = ({onDelete}) => {
|
||||
export const DeleteButton = ({onDelete, disabled}) => {
|
||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||
|
||||
return (<>
|
||||
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
|
||||
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={(event) => onDelete(event)}/>)}
|
||||
{!confirmDelete && (<Chip label={<DeleteOutlined />}
|
||||
onClick={() => !disabled && setConfirmDelete(true)}/>)}
|
||||
{confirmDelete && (<Chip label={<CheckOutlined />} color="error"
|
||||
onClick={(event) => !disabled && onDelete(event)}/>)}
|
||||
</>);
|
||||
}
|
|
@ -37,7 +37,7 @@ const a11yProps = (index) => {
|
|||
};
|
||||
};
|
||||
|
||||
const PrettyTabbedView = ({ tabs, isLoading, currentTab, setCurrentTab, fullwidth }) => {
|
||||
const PrettyTabbedView = ({ tabs, isLoading, currentTab, setCurrentTab }) => {
|
||||
const [value, setValue] = useState(0);
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'));
|
||||
|
||||
|
@ -55,8 +55,8 @@ const PrettyTabbedView = ({ tabs, isLoading, currentTab, setCurrentTab, fullwidt
|
|||
};
|
||||
|
||||
return (
|
||||
<Box fullwidth={fullwidth} display="flex" height="100%" flexDirection={isMobile ? 'column' : 'row'}>
|
||||
{(isMobile && !currentTab) ? (
|
||||
<Box display="flex" height="100%" flexDirection={isMobile ? 'column' : 'row'}>
|
||||
{(isMobile) ? (
|
||||
<Select value={value} onChange={handleSelectChange} sx={{ minWidth: 120, marginBottom: '15px' }}>
|
||||
{tabs.map((tab, index) => (
|
||||
<MenuItem key={index} value={index}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
@ -19,12 +19,16 @@ import {
|
|||
Typography,
|
||||
useMediaQuery
|
||||
} from '@mui/material';
|
||||
import * as timeago from 'timeago.js';
|
||||
|
||||
// project import
|
||||
import MainCard from '../../../../components/MainCard';
|
||||
import Transitions from '../../../../components/@extended/Transitions';
|
||||
// assets
|
||||
import { BellOutlined, CloseOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { BellOutlined, CloseOutlined, ExclamationCircleOutlined, GiftOutlined, InfoCircleOutlined, MessageOutlined, SettingOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
|
||||
import * as API from '../../../../api';
|
||||
import { redirectToLocal } from '../../../../utils/indexs';
|
||||
|
||||
// sx styles
|
||||
const avatarSX = {
|
||||
|
@ -48,10 +52,51 @@ const actionSX = {
|
|||
const Notification = () => {
|
||||
const theme = useTheme();
|
||||
const matchesXs = useMediaQuery(theme.breakpoints.down('md'));
|
||||
const [notifications, setNotifications] = useState([]);
|
||||
const [from, setFrom] = useState('');
|
||||
|
||||
const refreshNotifications = () => {
|
||||
API.users.getNotifs(from).then((res) => {
|
||||
setNotifications(() => res.data);
|
||||
});
|
||||
};
|
||||
|
||||
const setAsRead = () => {
|
||||
let unread = [];
|
||||
|
||||
let newN = notifications.map((notif) => {
|
||||
if (!notif.Read) {
|
||||
unread.push(notif.ID);
|
||||
}
|
||||
notif.Read = true;
|
||||
return notif;
|
||||
})
|
||||
|
||||
if (unread.length > 0) {
|
||||
API.users.readNotifs(unread);
|
||||
}
|
||||
|
||||
setNotifications(newN);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshNotifications();
|
||||
|
||||
const interval = setInterval(() => {
|
||||
refreshNotifications();
|
||||
}, 10000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const anchorRef = useRef(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (!open) {
|
||||
setAsRead();
|
||||
}
|
||||
|
||||
setOpen((prevOpen) => !prevOpen);
|
||||
};
|
||||
|
||||
|
@ -62,9 +107,44 @@ const Notification = () => {
|
|||
setOpen(false);
|
||||
};
|
||||
|
||||
const getNotifIcon = (notification) => {
|
||||
switch (notification.Level) {
|
||||
case 'warn':
|
||||
return <Avatar
|
||||
sx={{
|
||||
color: 'warning.main',
|
||||
bgcolor: 'warning.lighter'
|
||||
}}
|
||||
>
|
||||
<WarningOutlined />
|
||||
</Avatar>
|
||||
case 'error':
|
||||
return <Avatar
|
||||
sx={{
|
||||
color: 'error.main',
|
||||
bgcolor: 'error.lighter'
|
||||
}}
|
||||
>
|
||||
<ExclamationCircleOutlined />
|
||||
</Avatar>
|
||||
default:
|
||||
|
||||
return <Avatar
|
||||
sx={{
|
||||
color: 'info.main',
|
||||
bgcolor: 'info.lighter'
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined />
|
||||
</Avatar>
|
||||
}
|
||||
};
|
||||
|
||||
const iconBackColor = theme.palette.mode === 'dark' ? 'grey.700' : 'grey.100';
|
||||
const iconBackColorOpen = theme.palette.mode === 'dark' ? 'grey.800' : 'grey.200';
|
||||
|
||||
const nbUnread = notifications.filter((notif) => !notif.Read).length;
|
||||
|
||||
return (
|
||||
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
|
||||
<IconButton
|
||||
|
@ -77,7 +157,7 @@ const Notification = () => {
|
|||
aria-haspopup="true"
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<Badge badgeContent={4} color="primary">
|
||||
<Badge badgeContent={nbUnread} color="error">
|
||||
<BellOutlined />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
|
@ -127,6 +207,8 @@ const Notification = () => {
|
|||
<List
|
||||
component="nav"
|
||||
sx={{
|
||||
maxHeight: 350,
|
||||
overflow: 'auto',
|
||||
p: 0,
|
||||
'& .MuiListItemButton-root': {
|
||||
py: 0.5,
|
||||
|
@ -135,127 +217,43 @@ const Notification = () => {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
color: 'success.main',
|
||||
bgcolor: 'success.lighter'
|
||||
}}
|
||||
>
|
||||
<GiftOutlined />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6">
|
||||
It's{' '}
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Cristina danny's
|
||||
</Typography>{' '}
|
||||
birthday today.
|
||||
</Typography>
|
||||
}
|
||||
secondary="2 min ago"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
3:00 AM
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
bgcolor: 'primary.lighter'
|
||||
}}
|
||||
>
|
||||
<MessageOutlined />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6">
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Aida Burg
|
||||
</Typography>{' '}
|
||||
commented your post.
|
||||
</Typography>
|
||||
}
|
||||
secondary="5 August"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
6:00 PM
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
color: 'error.main',
|
||||
bgcolor: 'error.lighter'
|
||||
}}
|
||||
>
|
||||
<SettingOutlined />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6">
|
||||
Your Profile is Complete
|
||||
<Typography component="span" variant="subtitle1">
|
||||
60%
|
||||
</Typography>{' '}
|
||||
</Typography>
|
||||
}
|
||||
secondary="7 hours ago"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
2:45 PM
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
<ListItemButton>
|
||||
<ListItemAvatar>
|
||||
<Avatar
|
||||
sx={{
|
||||
color: 'primary.main',
|
||||
bgcolor: 'primary.lighter'
|
||||
}}
|
||||
>
|
||||
C
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6">
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Cristina Danny
|
||||
</Typography>{' '}
|
||||
invited to join{' '}
|
||||
<Typography component="span" variant="subtitle1">
|
||||
Meeting.
|
||||
{notifications && notifications.map(notification => (<>
|
||||
<ListItemButton onClick={() => {
|
||||
notification.Link && redirectToLocal(notification.Link);
|
||||
}}
|
||||
style={{
|
||||
borderLeft: notification.Read ? 'none' : `4px solid ${notification.Level === 'warn' ? theme.palette.warning.main : notification.Level === 'error' ? theme.palette.error.main : theme.palette.info.main}`,
|
||||
paddingLeft: notification.Read ? '14px' : '10px',
|
||||
}}>
|
||||
|
||||
<ListItemAvatar>
|
||||
{getNotifIcon(notification)}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={<>
|
||||
<Typography variant={notification.Read ? 'body' : 'h6'} noWrap>
|
||||
{notification.Title}
|
||||
</Typography>
|
||||
<div style={{
|
||||
overflow: 'hidden',
|
||||
maxHeight: '48px',
|
||||
borderLeft: '1px solid grey',
|
||||
paddingLeft: '8px',
|
||||
margin: '2px'
|
||||
}}>
|
||||
{notification.Message}
|
||||
</div></>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
{timeago.format(notification.Date)}
|
||||
</Typography>
|
||||
}
|
||||
secondary="Daily scrum meeting time"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Typography variant="caption" noWrap>
|
||||
9:10 PM
|
||||
</Typography>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider />
|
||||
<ListItemButton sx={{ textAlign: 'center', py: `${12}px !important` }}>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItemButton>
|
||||
<Divider /></>))}
|
||||
|
||||
{/* <ListItemButton sx={{ textAlign: 'center', py: `${12}px !important` }}>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant="h6" color="primary">
|
||||
|
@ -263,7 +261,7 @@ const Notification = () => {
|
|||
</Typography>
|
||||
}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItemButton> */}
|
||||
</List>
|
||||
</MainCard>
|
||||
</ClickAwayListener>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// material-ui
|
||||
import { Box, Chip, IconButton, Link, useMediaQuery } from '@mui/material';
|
||||
import { Box, Chip, IconButton, Link, Stack, useMediaQuery } from '@mui/material';
|
||||
import { GithubOutlined } from '@ant-design/icons';
|
||||
|
||||
// project import
|
||||
|
@ -18,10 +18,13 @@ const HeaderContent = () => {
|
|||
{!matchesXs && <Search />}
|
||||
{matchesXs && <Box sx={{ width: '100%', ml: 1 }} />}
|
||||
|
||||
<Link href="/cosmos-ui/logout" underline="none">
|
||||
<Chip label="Logout" />
|
||||
</Link>
|
||||
{/* <Notification /> */}
|
||||
<Stack direction="row" spacing={2}>
|
||||
<Notification />
|
||||
|
||||
<Link href="/cosmos-ui/logout" underline="none">
|
||||
<Chip label="Logout" />
|
||||
</Link>
|
||||
</Stack>
|
||||
{/* {!matchesXs && <Profile />}
|
||||
{matchesXs && <MobileSection />} */}
|
||||
</>
|
||||
|
|
|
@ -332,7 +332,7 @@ const ConfigManagement = () => {
|
|||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
formik.setFieldValue('Background', "");
|
||||
|
|
|
@ -27,6 +27,19 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
|
|||
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
|
||||
export const getNestedValue = (values, path) => {
|
||||
return path.split('.').reduce((current, key) => {
|
||||
if (current && current[key] !== undefined) {
|
||||
return current[key];
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
const index = parseInt(key, 10);
|
||||
return current[index];
|
||||
}
|
||||
return undefined;
|
||||
}, values);
|
||||
};
|
||||
|
||||
export const CosmosInputText = ({ name, style, value, errors, multiline, type, placeholder, onChange, label, formik }) => {
|
||||
return <Grid item xs={12}>
|
||||
<Stack spacing={1} style={style}>
|
||||
|
@ -146,7 +159,7 @@ export const CosmosSelect = ({ name, onChange, label, formik, disabled, options
|
|||
id={name}
|
||||
disabled={disabled}
|
||||
select
|
||||
value={formik.values[name]}
|
||||
value={getNestedValue(formik.values, name)}
|
||||
onChange={(...ar) => {
|
||||
onChange && onChange(...ar);
|
||||
formik.handleChange(...ar);
|
||||
|
|
|
@ -66,6 +66,15 @@ const ProxyManagement = () => {
|
|||
const [submitErrors, setSubmitErrors] = React.useState([]);
|
||||
const [needSave, setNeedSave] = React.useState(false);
|
||||
const [openNewModal, setOpenNewModal] = React.useState(false);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
function setRouteEnabled(key) {
|
||||
return (event) => {
|
||||
routes[key].Disabled = !event.target.checked;
|
||||
updateRoutes(routes);
|
||||
setNeedSave(true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateRoutes(routes) {
|
||||
let con = {
|
||||
|
@ -163,6 +172,14 @@ const ProxyManagement = () => {
|
|||
textAlign: 'center',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Enabled',
|
||||
clickable:true,
|
||||
field: (r, k) => <Checkbox disabled={isLoading} size='large' color={!r.Disabled ? 'success' : 'default'}
|
||||
onChange={setRouteEnabled(k)}
|
||||
checked={!r.Disabled}
|
||||
/>,
|
||||
},
|
||||
{ title: 'URL',
|
||||
search: (r) => r.Name + ' ' + r.Description,
|
||||
style: {
|
||||
|
|
507
client/src/pages/dashboard/AlertPage.jsx
Normal file
507
client/src/pages/dashboard/AlertPage.jsx
Normal file
|
@ -0,0 +1,507 @@
|
|||
import * as React from 'react';
|
||||
import IsLoggedIn from '../../isLoggedIn';
|
||||
import * as API from '../../api';
|
||||
import MainCard from '../../components/MainCard';
|
||||
import { Formik, Field, useFormik, FormikProvider } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
FormHelperText,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Skeleton,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import { ExclamationCircleOutlined, InfoCircleOutlined, PlusCircleOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import PrettyTableView from '../../components/tableView/prettyTableView';
|
||||
import { DeleteButton } from '../../components/delete';
|
||||
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
|
||||
import { MetricPicker } from './MetricsPicker';
|
||||
|
||||
const DisplayOperator = (operator) => {
|
||||
switch (operator) {
|
||||
case 'gt':
|
||||
return '>';
|
||||
case 'lt':
|
||||
return '<';
|
||||
case 'eq':
|
||||
return '=';
|
||||
default:
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
const AlertValidationSchema = Yup.object().shape({
|
||||
name: Yup.string().required('Name is required'),
|
||||
trackingMetric: Yup.string().required('Tracking metric is required'),
|
||||
conditionOperator: Yup.string().required('Condition operator is required'),
|
||||
conditionValue: Yup.number().required('Condition value is required'),
|
||||
period: Yup.string().required('Period is required'),
|
||||
});
|
||||
|
||||
const EditAlertModal = ({ open, onClose, onSave }) => {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
name: open.Name || 'New Alert',
|
||||
trackingMetric: open.TrackingMetric || '',
|
||||
conditionOperator: (open.Condition && open.Condition.Operator) || 'gt',
|
||||
conditionValue: (open.Condition && open.Condition.Value) || 0,
|
||||
conditionPercent: (open.Condition && open.Condition.Percent) || false,
|
||||
period: open.Period || 'latest',
|
||||
actions: open.Actions || [],
|
||||
throttled: typeof open.Throttled === 'boolean' ? open.Throttled : true,
|
||||
severity: open.Severity || 'error',
|
||||
},
|
||||
validationSchema: AlertValidationSchema,
|
||||
onSubmit: (values) => {
|
||||
values.actions = values.actions.filter((a) => !a.removed);
|
||||
onSave(values);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Edit Alert</DialogTitle>
|
||||
<FormikProvider value={formik}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<CosmosInputText
|
||||
name="name"
|
||||
label="Name of the alert"
|
||||
formik={formik}
|
||||
required
|
||||
/>
|
||||
<MetricPicker
|
||||
name="trackingMetric"
|
||||
label="Metric to track"
|
||||
formik={formik}
|
||||
required
|
||||
/>
|
||||
<Stack direction="row" spacing={2} alignItems="center">
|
||||
<CosmosSelect
|
||||
name="conditionOperator"
|
||||
label="Trigger Condition Operator"
|
||||
formik={formik}
|
||||
options={[
|
||||
['gt', '>'],
|
||||
['lt', '<'],
|
||||
['eq', '='],
|
||||
]}
|
||||
>
|
||||
</CosmosSelect>
|
||||
<CosmosInputText
|
||||
name="conditionValue"
|
||||
label="Trigger Condition Value"
|
||||
formik={formik}
|
||||
required
|
||||
/>
|
||||
<CosmosCheckbox
|
||||
style={{paddingTop: '20px'}}
|
||||
name="conditionPercent"
|
||||
label="Condition is a percent of max value"
|
||||
formik={formik}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<CosmosSelect
|
||||
name="period"
|
||||
label="Period (how often to check the metric)"
|
||||
formik={formik}
|
||||
options={[
|
||||
['latest', 'Latest'],
|
||||
['hourly', 'Hourly'],
|
||||
['daily', 'Daily'],
|
||||
]}></CosmosSelect>
|
||||
|
||||
<CosmosSelect
|
||||
name="severity"
|
||||
label="Severity"
|
||||
formik={formik}
|
||||
options={[
|
||||
['info', 'Info'],
|
||||
['warn', 'Warning'],
|
||||
['error', 'Error'],
|
||||
]}></CosmosSelect>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="throttled"
|
||||
label="Throttle (only triggers a maximum of once a day)"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosFormDivider title={'Action Triggers'} />
|
||||
|
||||
<Stack direction="column" spacing={2}>
|
||||
{formik.values.actions
|
||||
.map((action, index) => {
|
||||
return !action.removed && <>
|
||||
{action.Type === 'stop' &&
|
||||
<Alert severity="info">Stop action will attempt to stop/disable any resources (ex. Containers, routes, etc... ) attachted to the metric.
|
||||
This will only have an effect on metrics specific to a resources (ex. CPU of a specific container). It will not do anything on global metric such as global used CPU</Alert>
|
||||
}
|
||||
<Stack direction="row" spacing={2} key={index}>
|
||||
<Box style={{
|
||||
width: '100%',
|
||||
}}>
|
||||
<CosmosSelect
|
||||
name={`actions.${index}.Type`}
|
||||
label="Action Type"
|
||||
formik={formik}
|
||||
options={[
|
||||
['notification', 'Send a notification'],
|
||||
['email', 'Send an email'],
|
||||
['stop', 'Stop resources causing the alert'],
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box style={{
|
||||
height: '95px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<DeleteButton
|
||||
onDelete={() => {
|
||||
formik.setFieldValue(`actions.${index}.removed`, true);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
})}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
startIcon={<PlusCircleOutlined />}
|
||||
onClick={() => {
|
||||
formik.setFieldValue('actions', [
|
||||
...formik.values.actions,
|
||||
{
|
||||
Type: 'notification',
|
||||
},
|
||||
]);
|
||||
}}>
|
||||
Add Action
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<Button variant='contained' type="submit">Save</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</FormikProvider>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const AlertPage = () => {
|
||||
const [config, setConfig] = React.useState(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const [metrics, setMetrics] = React.useState({});
|
||||
|
||||
function refresh() {
|
||||
API.config.get().then((res) => {
|
||||
setConfig(res.data);
|
||||
setIsLoading(false);
|
||||
});
|
||||
API.metrics.list().then((res) => {
|
||||
setMetrics(res.data);
|
||||
});
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
const setEnabled = (name) => (event) => {
|
||||
setIsLoading(true);
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: {
|
||||
...config.MonitoringAlerts,
|
||||
[name]: {
|
||||
...config.MonitoringAlerts[name],
|
||||
Enabled: event.target.checked,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
API.config.set(toSave).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const deleteAlert = (name) => {
|
||||
setIsLoading(true);
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: {
|
||||
...config.MonitoringAlerts,
|
||||
}
|
||||
};
|
||||
delete toSave.MonitoringAlerts[name];
|
||||
|
||||
API.config.set(toSave).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const saveAlert = (data) => {
|
||||
setIsLoading(true);
|
||||
|
||||
data.conditionValue = parseInt(data.conditionValue);
|
||||
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: {
|
||||
...config.MonitoringAlerts,
|
||||
[data.name]: {
|
||||
Name: data.name,
|
||||
Enabled: true,
|
||||
TrackingMetric: data.trackingMetric,
|
||||
Condition: {
|
||||
Operator: data.conditionOperator,
|
||||
Value: data.conditionValue,
|
||||
Percent: data.conditionPercent,
|
||||
},
|
||||
Period: data.period,
|
||||
Actions: data.actions,
|
||||
LastTriggered: null,
|
||||
Throttled: data.throttled,
|
||||
Severity: data.severity,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
API.config.set(toSave).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const resetTodefault = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: {
|
||||
"Anti Crypto-Miner": {
|
||||
"Name": "Anti Crypto-Miner",
|
||||
"Enabled": false,
|
||||
"Period": "daily",
|
||||
"TrackingMetric": "cosmos.system.docker.cpu.*",
|
||||
"Condition": {
|
||||
"Operator": "gt",
|
||||
"Value": 80
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "email",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "stop",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": false,
|
||||
"Severity": "warn"
|
||||
},
|
||||
"Anti Memory Leak": {
|
||||
"Name": "Anti Memory Leak",
|
||||
"Enabled": false,
|
||||
"Period": "daily",
|
||||
"TrackingMetric": "cosmos.system.docker.ram.*",
|
||||
"Condition": {
|
||||
"Percent": true,
|
||||
"Operator": "gt",
|
||||
"Value": 80
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "email",
|
||||
"Target": ""
|
||||
},
|
||||
{
|
||||
"Type": "stop",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": false,
|
||||
"Severity": "warn"
|
||||
},
|
||||
"Disk Full Notification": {
|
||||
"Name": "Disk Full Notification",
|
||||
"Enabled": true,
|
||||
"Period": "latest",
|
||||
"TrackingMetric": "cosmos.system.disk./",
|
||||
"Condition": {
|
||||
"Percent": true,
|
||||
"Operator": "gt",
|
||||
"Value": 95
|
||||
},
|
||||
"Actions": [
|
||||
{
|
||||
"Type": "notification",
|
||||
"Target": ""
|
||||
}
|
||||
],
|
||||
"LastTriggered": "0001-01-01T00:00:00Z",
|
||||
"Throttled": true,
|
||||
"Severity": "warn"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
API.config.set(toSave).then(() => {
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
const GetSevIcon = ({level}) => {
|
||||
switch (level) {
|
||||
case 'info':
|
||||
return <span style={{color: '#2196f3'}}><InfoCircleOutlined /></span>;
|
||||
case 'warn':
|
||||
return <span style={{color: '#ff9800'}}><WarningOutlined /></span>;
|
||||
case 'error':
|
||||
return <span style={{color: '#f44336'}}><ExclamationCircleOutlined /></span>;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return <div style={{maxWidth: '1200px', margin: ''}}>
|
||||
<IsLoggedIn />
|
||||
|
||||
{openModal && <EditAlertModal open={openModal} onClose={() => setOpenModal(false)} onSave={saveAlert} />}
|
||||
|
||||
<Stack direction="row" spacing={2} style={{marginBottom: '15px'}}>
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</Button>
|
||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||
setOpenModal(true);
|
||||
}}>Create</Button>
|
||||
<Button variant="outlined" color="warning" startIcon={<WarningOutlined />} onClick={() => {
|
||||
resetTodefault();
|
||||
}}>Reset to default</Button>
|
||||
</Stack>
|
||||
|
||||
{config && <>
|
||||
<Formik
|
||||
initialValues={{
|
||||
Actions: config.MonitoringAlerts
|
||||
}}
|
||||
|
||||
// validationSchema={Yup.object().shape({
|
||||
// })}
|
||||
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
let toSave = {
|
||||
...config,
|
||||
MonitoringAlerts: values.Actions
|
||||
};
|
||||
|
||||
return API.config.set(toSave);
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
<form noValidate onSubmit={formik.handleSubmit}>
|
||||
<Stack spacing={3}>
|
||||
{!config && <Skeleton variant="rectangular" height={300} />}
|
||||
{config && (!config.MonitoringAlerts || !Object.values(config.MonitoringAlerts).length) ? <Alert severity="info">No alerts configured.</Alert> : ''}
|
||||
{config && config.MonitoringAlerts && Object.values(config.MonitoringAlerts).length ? <PrettyTableView
|
||||
data={Object.values(config.MonitoringAlerts)}
|
||||
getKey={(r) => r.Name + r.Target + r.Mode}
|
||||
onRowClick={(r, k) => {
|
||||
setOpenModal(r);
|
||||
}}
|
||||
|
||||
columns={[
|
||||
{
|
||||
title: 'Enabled',
|
||||
clickable:true,
|
||||
field: (r, k) => <Checkbox disabled={isLoading} size='large' color={r.Enabled ? 'success' : 'default'}
|
||||
onChange={setEnabled(Object.keys(config.MonitoringAlerts)[k])}
|
||||
checked={r.Enabled}
|
||||
/>,
|
||||
style: {
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Name',
|
||||
field: (r) => <><GetSevIcon level={r.Severity} /> {r.Name}</>,
|
||||
},
|
||||
{
|
||||
title: 'Tracking Metric',
|
||||
field: (r) => metrics[r.TrackingMetric] ? metrics[r.TrackingMetric] : r.TrackingMetric,
|
||||
},
|
||||
{
|
||||
title: 'Condition',
|
||||
screenMin: 'md',
|
||||
field: (r) => DisplayOperator(r.Condition.Operator) + ' ' + r.Condition.Value + (r.Condition.Percent ? '%' : ''),
|
||||
},
|
||||
{
|
||||
title: 'Period',
|
||||
field: (r) => r.Period,
|
||||
},
|
||||
{
|
||||
title: 'Last Triggered',
|
||||
screenMin: 'md',
|
||||
field: (r) => (r.LastTriggered != "0001-01-01T00:00:00Z") ? new Date(r.LastTriggered).toLocaleString() : 'Never',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
field: (r) => r.Actions.map((a) => a.Type).join(', '),
|
||||
screenMin: 'md',
|
||||
},
|
||||
{ title: '', clickable:true, field: (r, k) => <DeleteButton disabled={isLoading} onDelete={() => {
|
||||
deleteAlert(Object.keys(config.MonitoringAlerts)[k])
|
||||
}}/>,
|
||||
style: {
|
||||
textAlign: 'right',
|
||||
}
|
||||
},
|
||||
]}
|
||||
/> : ''}
|
||||
</Stack>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</>}
|
||||
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default AlertPage;
|
121
client/src/pages/dashboard/MetricsPicker.jsx
Normal file
121
client/src/pages/dashboard/MetricsPicker.jsx
Normal file
|
@ -0,0 +1,121 @@
|
|||
import * as React from 'react';
|
||||
import {
|
||||
Checkbox,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
Grid,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
Typography,
|
||||
FormHelperText,
|
||||
TextField,
|
||||
MenuItem,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
Accordion,
|
||||
Chip,
|
||||
Box,
|
||||
FormControl,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
Autocomplete,
|
||||
|
||||
} from '@mui/material';
|
||||
import { Field } from 'formik';
|
||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||
import * as API from '../../api';
|
||||
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
|
||||
export const MetricPicker = ({ metricsInit, name, style, value, errors, placeholder, onChange, label, formik }) => {
|
||||
const [metrics, setMetrics] = React.useState(metricsInit || {});
|
||||
|
||||
function refresh() {
|
||||
API.metrics.list().then((res) => {
|
||||
let m = [];
|
||||
let wildcards = {};
|
||||
|
||||
Object.keys(res.data).forEach((key) => {
|
||||
m.push({
|
||||
label: res.data[key],
|
||||
value: key,
|
||||
});
|
||||
|
||||
let keysplit = key.split('.');
|
||||
if (keysplit.length > 1) {
|
||||
for (let i = 0; i < keysplit.length - 1; i++) {
|
||||
let wildcard = keysplit.slice(0, i + 1).join('.') + '.*';
|
||||
wildcards[wildcard] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(wildcards).forEach((key) => {
|
||||
m.push({
|
||||
label: "Wildcard for " + key.split('.*')[0],
|
||||
value: key,
|
||||
});
|
||||
});
|
||||
|
||||
setMetrics(m);
|
||||
});
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!metricsInit)
|
||||
refresh();
|
||||
}, []);
|
||||
|
||||
return <Grid item xs={12}>
|
||||
<Stack spacing={1} style={style}>
|
||||
{label && <InputLabel htmlFor={name}>{label}</InputLabel>}
|
||||
{/* <OutlinedInput
|
||||
id={name}
|
||||
type={'text'}
|
||||
value={value || (formik && formik.values[name])}
|
||||
name={name}
|
||||
onBlur={(...ar) => {
|
||||
return formik && formik.handleBlur(...ar);
|
||||
}}
|
||||
onChange={(...ar) => {
|
||||
onChange && onChange(...ar);
|
||||
return formik && formik.handleChange(...ar);
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
fullWidth
|
||||
error={Boolean(formik && formik.touched[name] && formik.errors[name])}
|
||||
/> */}
|
||||
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
name={name}
|
||||
value={value || (formik && formik.values[name])}
|
||||
id="combo-box-demo"
|
||||
isOptionEqualToValue={(option, value) => option.value === value}
|
||||
options={metrics}
|
||||
freeSolo
|
||||
getOptionLabel={(option) => {
|
||||
return option.label ?
|
||||
`${option.value} - ${option.label}` : (formik && formik.values[name]);
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
onChange && onChange(newValue.value);
|
||||
return formik && formik.setFieldValue(name, newValue.value);
|
||||
}}
|
||||
renderInput={(params) => <TextField {...params} />}
|
||||
/>
|
||||
|
||||
{formik && formik.touched[name] && formik.errors[name] && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
{errors && (
|
||||
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||
{formik.errors[name]}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid>
|
||||
}
|
|
@ -48,6 +48,7 @@ import MiniPlotComponent from './components/mini-plot';
|
|||
import ResourceDashboard from './resourceDashboard';
|
||||
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
|
||||
import ProxyDashboard from './proxyDashboard';
|
||||
import AlertPage from './AlertPage';
|
||||
|
||||
// avatar style
|
||||
const avatarSX = {
|
||||
|
@ -108,22 +109,25 @@ const DashboardDefault = () => {
|
|||
let todo = [
|
||||
["cosmos.system.*"],
|
||||
["cosmos.proxy.*"],
|
||||
[],
|
||||
[],
|
||||
]
|
||||
|
||||
let t = typeof override === 'number' ? override : currentTabRef.current;
|
||||
|
||||
API.metrics.get(todo[t]).then((res) => {
|
||||
setMetrics(prevMetrics => {
|
||||
let finalMetrics = prevMetrics ? { ...prevMetrics } : {};
|
||||
if(res.data) {
|
||||
res.data.forEach((metric) => {
|
||||
finalMetrics[metric.Key] = metric;
|
||||
});
|
||||
|
||||
return finalMetrics;
|
||||
}
|
||||
if (t < 2)
|
||||
API.metrics.get(todo[t]).then((res) => {
|
||||
setMetrics(prevMetrics => {
|
||||
let finalMetrics = prevMetrics ? { ...prevMetrics } : {};
|
||||
if(res.data) {
|
||||
res.data.forEach((metric) => {
|
||||
finalMetrics[metric.Key] = metric;
|
||||
});
|
||||
|
||||
return finalMetrics;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const refreshStatus = () => {
|
||||
|
@ -195,7 +199,7 @@ const DashboardDefault = () => {
|
|||
<Grid container rowSpacing={4.5} columnSpacing={2.75} >
|
||||
<Grid item xs={12} sx={{ mb: -2.25 }}>
|
||||
<Typography variant="h4">Server Monitoring</Typography>
|
||||
<Stack direction="row" alignItems="center" spacing={0} style={{marginTop: 10}}>
|
||||
{currentTab <= 2 && <Stack direction="row" alignItems="center" spacing={0} style={{marginTop: 10}}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {setSlot('latest'); resetZoom()}}
|
||||
|
@ -233,7 +237,8 @@ const DashboardDefault = () => {
|
|||
>
|
||||
Reset Zoom
|
||||
</Button>}
|
||||
</Stack>
|
||||
</Stack>}
|
||||
{currentTab > 2 && <div style={{height: 41}}></div>}
|
||||
</Grid>
|
||||
|
||||
|
||||
|
@ -248,12 +253,20 @@ const DashboardDefault = () => {
|
|||
isLoading={!metrics}
|
||||
tabs={[
|
||||
{
|
||||
title: 'Resources',
|
||||
children: <ResourceDashboard xAxis={xAxis} zoom={zoom} setZoom={setZoom} slot={slot} metrics={metrics} />
|
||||
title: 'Resources',
|
||||
children: <ResourceDashboard xAxis={xAxis} zoom={zoom} setZoom={setZoom} slot={slot} metrics={metrics} />
|
||||
},
|
||||
{
|
||||
title: 'Proxy',
|
||||
children: <ProxyDashboard xAxis={xAxis} zoom={zoom} setZoom={setZoom} slot={slot} metrics={metrics} />
|
||||
title: 'Proxy',
|
||||
children: <ProxyDashboard xAxis={xAxis} zoom={zoom} setZoom={setZoom} slot={slot} metrics={metrics} />
|
||||
},
|
||||
{
|
||||
title: 'Events',
|
||||
children: <AlertPage />
|
||||
},
|
||||
{
|
||||
title: 'Alerts',
|
||||
children: <AlertPage />
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -9,7 +9,6 @@ import TableComponent from './components/table';
|
|||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
const ProxyDashboard = ({ xAxis, zoom, setZoom, slot, metrics }) => {
|
||||
console.log(metrics)
|
||||
return (<>
|
||||
|
||||
<Grid container rowSpacing={4.5} columnSpacing={2.75} >
|
||||
|
|
|
@ -78,11 +78,6 @@ export const TransparentHeader = () => {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.MuiDrawer-paper {
|
||||
backdrop-filter: blur(15px);
|
||||
background: rgba(${backColor}, 1) !important;
|
||||
border-right-color: rgba(${backColor},0.45) !important;
|
||||
}
|
||||
`}
|
||||
</style>;
|
||||
}
|
||||
|
@ -184,12 +179,6 @@ const HomePage = () => {
|
|||
useEffect(() => {
|
||||
refreshConfig();
|
||||
refreshStatus();
|
||||
|
||||
// const interval = setInterval(() => {
|
||||
// refreshStatus();
|
||||
// }, 5000);
|
||||
|
||||
// return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const primCol = theme.palette.primary.main.replace('rgb(', 'rgba(')
|
||||
|
@ -268,42 +257,24 @@ const HomePage = () => {
|
|||
},
|
||||
labels: []
|
||||
};
|
||||
|
||||
let latestCPU, latestRAM, latestRAMRaw, maxRAM, maxRAMRaw = 0;
|
||||
|
||||
const bigNb = {
|
||||
fontSize: '23px',
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
color: isDark ? "white" : "black",
|
||||
textShadow: "0px 0px 5px #000",
|
||||
lineHeight: "97px",
|
||||
if(metrics) {
|
||||
|
||||
if(metrics["cosmos.system.cpu.0"] && metrics["cosmos.system.cpu.0"].Values && metrics["cosmos.system.cpu.0"].Values.length > 0)
|
||||
latestCPU = metrics["cosmos.system.cpu.0"].Values[metrics["cosmos.system.cpu.0"].Values.length - 1].Value;
|
||||
|
||||
if(metrics["cosmos.system.ram"] && metrics["cosmos.system.ram"].Values && metrics["cosmos.system.ram"].Values.length > 0) {
|
||||
let formatRAM = metrics && FormaterForMetric(metrics["cosmos.system.ram"], false);
|
||||
latestRAMRaw = metrics["cosmos.system.ram"].Values[metrics["cosmos.system.ram"].Values.length - 1].Value;
|
||||
latestRAM = formatRAM(metrics["cosmos.system.ram"].Values[metrics["cosmos.system.ram"].Values.length - 1].Value);
|
||||
maxRAM = formatRAM(metrics["cosmos.system.ram"].Max);
|
||||
maxRAMRaw = metrics["cosmos.system.ram"].Max;
|
||||
}
|
||||
}
|
||||
|
||||
let latestCPU = metrics && metrics["cosmos.system.cpu.0"] && metrics["cosmos.system.cpu.0"].Values[metrics["cosmos.system.cpu.0"].Values.length - 1].Value;
|
||||
|
||||
let formatRAM = metrics && FormaterForMetric(metrics["cosmos.system.ram"], false);
|
||||
let latestRAMRaw = metrics && metrics["cosmos.system.ram"] && metrics["cosmos.system.ram"].Values[metrics["cosmos.system.ram"].Values.length - 1].Value;
|
||||
let latestRAM = metrics && metrics["cosmos.system.ram"] && formatRAM(metrics["cosmos.system.ram"].Values[metrics["cosmos.system.ram"].Values.length - 1].Value);
|
||||
let maxRAM = metrics && metrics["cosmos.system.ram"] && formatRAM(metrics["cosmos.system.ram"].Max);
|
||||
let maxRAMRaw = metrics && metrics["cosmos.system.ram"] && metrics["cosmos.system.ram"].Max;
|
||||
|
||||
let now = new Date();
|
||||
now = "day_" + formatDate(now);
|
||||
|
||||
let formatNetwork = metrics && FormaterForMetric(metrics["cosmos.system.netTx"], false);
|
||||
let latestNetworkRawTx = (metrics && metrics["cosmos.system.netTx"] && metrics["cosmos.system.netTx"].ValuesAggl[now].Value) || 0;
|
||||
let latestNetworkTx = metrics && formatNetwork(latestNetworkRawTx);
|
||||
let latestNetworkRawRx = (metrics && metrics["cosmos.system.netRx"] && metrics["cosmos.system.netRx"].ValuesAggl[now].Value) || 0;
|
||||
let latestNetworkRx = metrics && formatNetwork(latestNetworkRawRx);
|
||||
let latestNetworkSum = metrics && formatNetwork(latestNetworkRawTx + latestNetworkRawRx);
|
||||
|
||||
let formatRequests = metrics && FormaterForMetric(metrics["cosmos.proxy.all.success"], false);
|
||||
let latestRequestsRaw = (metrics && metrics["cosmos.proxy.all.success"] && metrics["cosmos.proxy.all.success"].ValuesAggl[now].Value) || 0;
|
||||
let latestRequests = metrics && formatRequests(latestRequestsRaw);
|
||||
let latestRequestsErrorRaw = (metrics && metrics["cosmos.proxy.all.error"] && metrics["cosmos.proxy.all.error"].ValuesAggl[now].Value) || 0;
|
||||
let latestRequestsError = metrics && formatRequests(latestRequestsErrorRaw);
|
||||
let latestRequestSum = metrics && formatRequests(latestRequestsRaw + latestRequestsErrorRaw);
|
||||
|
||||
return <Stack spacing={2} style={{maxWidth: '1500px', margin:'auto'}}>
|
||||
return <Stack spacing={2} style={{maxWidth: '1450px', margin:'auto'}}>
|
||||
<IsLoggedIn />
|
||||
<HomeBackground status={coStatus} />
|
||||
<TransparentHeader />
|
||||
|
@ -474,7 +445,7 @@ const HomePage = () => {
|
|||
<Grid2 item xs={12} sm={6} md={6} lg={3} xl={3} xxl={3} key={'001'}>
|
||||
<Box className='app' style={{height: '106px',borderRadius: 5, ...appColor }}>
|
||||
<Stack direction="row" justifyContent={'center'} alignItems={'center'} style={{ height: "100%" }}>
|
||||
<MiniPlotComponent noBackground title='PROXY' agglo metrics={[
|
||||
<MiniPlotComponent noBackground title='URLS' agglo metrics={[
|
||||
"cosmos.proxy.all.success",
|
||||
"cosmos.proxy.all.error",
|
||||
]} labels={{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// ==============================|| PRESET THEME - THEME SELECTOR ||============================== //
|
||||
|
||||
import { purple, pink, deepPurple, blueGrey, lightBlue, lightGreen, orange, teal } from '@mui/material/colors';
|
||||
import { purple, pink, deepPurple, blueGrey, lightBlue, lightGreen, orange, teal, indigo } from '@mui/material/colors';
|
||||
|
||||
const Theme = (colors, darkMode) => {
|
||||
const { blue, red, gold, cyan, green, grey } = colors;
|
||||
|
@ -30,7 +30,7 @@ const Theme = (colors, darkMode) => {
|
|||
main: purple[400],
|
||||
},
|
||||
secondary: {
|
||||
main: blueGrey[500],
|
||||
main: indigo[700],
|
||||
},
|
||||
error: {
|
||||
lighter: red[0],
|
||||
|
|
10
package-lock.json
generated
10
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.12.0-unstable24",
|
||||
"version": "0.12.0-unstable40",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cosmos-server",
|
||||
"version": "0.12.0-unstable24",
|
||||
"version": "0.12.0-unstable40",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^6.0.0",
|
||||
"@ant-design/icons": "^4.7.0",
|
||||
|
@ -56,6 +56,7 @@
|
|||
"semver-compare": "^1.0.0",
|
||||
"simplebar": "^5.3.8",
|
||||
"simplebar-react": "^2.4.1",
|
||||
"timeago.js": "^4.0.2",
|
||||
"typescript": "4.8.3",
|
||||
"vite": "^4.2.0",
|
||||
"web-vitals": "^3.0.2",
|
||||
|
@ -10155,6 +10156,11 @@
|
|||
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/timeago.js": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/timeago.js/-/timeago.js-4.0.2.tgz",
|
||||
"integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w=="
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.12.0-unstable40",
|
||||
"version": "0.12.0-unstable41",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
@ -56,6 +56,7 @@
|
|||
"semver-compare": "^1.0.0",
|
||||
"simplebar": "^5.3.8",
|
||||
"simplebar-react": "^2.4.1",
|
||||
"timeago.js": "^4.0.2",
|
||||
"typescript": "4.8.3",
|
||||
"vite": "^4.2.0",
|
||||
"web-vitals": "^3.0.2",
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<a href="https://github.com/soldier1"><img src="https://avatars.githubusercontent.com/soldier1" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
|
||||
<a href="https://github.com/devcircus"><img src="https://avatars.githubusercontent.com/devcircus" style="border-radius:48px" width="48" height="48" alt="Clayton Stone" title="Clayton Stone" /></a>
|
||||
<a href="https://github.com/BlackrazorNZ"><img src="https://avatars.githubusercontent.com/BlackrazorNZ" style="border-radius:48px" width="48" height="48" alt="null" title="null" /></a>
|
||||
<a href="https://github.com/owengraven"><img src="https://avatars.githubusercontent.com/owengraven" style="border-radius:48px" width="48" height="48" alt="Owen" title="Owen" /></a>
|
||||
<a href="https://github.com/saltyautomation"><img src="https://avatars.githubusercontent.com/saltyautomation" style="border-radius:48px" width="48" height="48" alt="T Morton" title="T Morton" /></a>
|
||||
</p><!-- /sponsors -->
|
||||
|
||||
|
|
|
@ -122,6 +122,7 @@ func CRON() {
|
|||
s.Every(1).Day().At("00:00").Do(checkVersion)
|
||||
s.Every(1).Day().At("01:00").Do(checkCerts)
|
||||
s.Every(6).Hours().Do(checkUpdatesAvailable)
|
||||
s.Every(1).Hours().Do(utils.CleanBannedIPs)
|
||||
s.Start()
|
||||
}()
|
||||
}
|
|
@ -537,10 +537,18 @@ func CheckUpdatesAvailable() map[string]bool {
|
|||
}
|
||||
|
||||
if needsUpdate && HasAutoUpdateOn(fullContainer) {
|
||||
utils.Log("Downlaoded new update for " + container.Image + " ready to install")
|
||||
utils.WriteNotification(utils.Notification{
|
||||
Recipient: "admin",
|
||||
Title: "Container Update",
|
||||
Message: "Container " + container.Names[0][1:] + " updated to the latest version!",
|
||||
Level: "info",
|
||||
Link: "/cosmos-ui/servapps/containers/" + container.Names[0][1:],
|
||||
})
|
||||
|
||||
utils.Log("Downloaded new update for " + container.Image + " ready to install")
|
||||
_, err := RecreateContainer(container.Names[0], fullContainer)
|
||||
if err != nil {
|
||||
utils.Error("CheckUpdatesAvailable - Failed to update - ", err)
|
||||
utils.MajorError("Container failed to update", err)
|
||||
} else {
|
||||
result[container.Names[0]] = false
|
||||
}
|
||||
|
@ -675,8 +683,8 @@ type ContainerStats struct {
|
|||
}
|
||||
|
||||
func Stats(container types.Container) (ContainerStats, error) {
|
||||
utils.Debug("StatsAll - Getting stats for " + container.Names[0])
|
||||
utils.Debug("Time: " + time.Now().String())
|
||||
// utils.Debug("StatsAll - Getting stats for " + container.Names[0])
|
||||
// utils.Debug("Time: " + time.Now().String())
|
||||
|
||||
statsBody, err := DockerClient.ContainerStats(DockerContext, container.ID, false)
|
||||
if err != nil {
|
||||
|
@ -698,7 +706,7 @@ func Stats(container types.Container) (ContainerStats, error) {
|
|||
|
||||
perCore := len(stats.CPUStats.CPUUsage.PercpuUsage)
|
||||
if perCore == 0 {
|
||||
utils.Warn("StatsAll - Docker CPU PercpuUsage is 0")
|
||||
utils.Debug("StatsAll - Docker CPU PercpuUsage is 0")
|
||||
perCore = 1
|
||||
}
|
||||
|
||||
|
@ -715,9 +723,9 @@ func Stats(container types.Container) (ContainerStats, error) {
|
|||
if systemDelta > 0 && cpuDelta > 0 {
|
||||
cpuUsage = (cpuDelta / systemDelta) * float64(perCore) * 100
|
||||
|
||||
utils.Debug("StatsAll - CPU CPUUsage " + strconv.FormatFloat(cpuUsage, 'f', 6, 64))
|
||||
// utils.Debug("StatsAll - CPU CPUUsage " + strconv.FormatFloat(cpuUsage, 'f', 6, 64))
|
||||
} else {
|
||||
utils.Error("StatsAll - Error calculating CPU usage for " + container.Names[0], nil)
|
||||
utils.Debug("StatsAll - Error calculating CPU usage for " + container.Names[0])
|
||||
}
|
||||
|
||||
// memUsage := float64(stats.MemoryStats.Usage) / float64(stats.MemoryStats.Limit) * 100
|
||||
|
@ -780,4 +788,12 @@ func Stats(container types.Container) (ContainerStats, error) {
|
|||
wg.Wait() // Wait for all goroutines to finish.
|
||||
|
||||
return containerStatsList, nil
|
||||
}
|
||||
|
||||
func StopContainer(containerName string) {
|
||||
err := DockerClient.ContainerStop(DockerContext, containerName, container.StopOptions{})
|
||||
if err != nil {
|
||||
utils.Error("StopContainer", err)
|
||||
return
|
||||
}
|
||||
}
|
|
@ -154,11 +154,11 @@ func SecureAPI(userRouter *mux.Router, public bool) {
|
|||
userRouter.Use(proxy.SmartShieldMiddleware(
|
||||
"__COSMOS",
|
||||
utils.ProxyRouteConfig{
|
||||
Name: "_Cosmos",
|
||||
Name: "Cosmos-Internal",
|
||||
SmartShield: utils.SmartShieldPolicy{
|
||||
Enabled: true,
|
||||
PolicyStrictness: 1,
|
||||
PerUserRequestLimit: 5000,
|
||||
PerUserRequestLimit: 6000,
|
||||
},
|
||||
},
|
||||
))
|
||||
|
@ -350,6 +350,10 @@ func InitServer() *mux.Router {
|
|||
|
||||
srapi.HandleFunc("/api/metrics", metrics.API_GetMetrics)
|
||||
srapi.HandleFunc("/api/reset-metrics", metrics.API_ResetMetrics)
|
||||
srapi.HandleFunc("/api/list-metrics", metrics.ListMetrics)
|
||||
|
||||
srapi.HandleFunc("/api/notifications/read", utils.MarkAsRead)
|
||||
srapi.HandleFunc("/api/notifications", utils.NotifGet)
|
||||
|
||||
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
|
||||
srapi.Use(utils.EnsureHostname)
|
||||
|
|
|
@ -23,6 +23,8 @@ func main() {
|
|||
|
||||
LoadConfig()
|
||||
|
||||
utils.InitDBBuffers()
|
||||
|
||||
go CRON()
|
||||
|
||||
docker.ExportDocker()
|
||||
|
|
|
@ -33,6 +33,7 @@ type DataDefDB struct {
|
|||
AggloType string
|
||||
Scale int
|
||||
Unit string
|
||||
Object string
|
||||
}
|
||||
|
||||
func AggloMetrics(metricsList []string) []DataDefDB {
|
||||
|
@ -61,7 +62,7 @@ func AggloMetrics(metricsList []string) []DataDefDB {
|
|||
for _, metric := range metricsList {
|
||||
if strings.Contains(metric, "*") {
|
||||
// Convert wildcard to regex. Replace * with .*
|
||||
regexPattern := "^" + strings.ReplaceAll(metric, "*", ".*")
|
||||
regexPattern := "^" + strings.ReplaceAll(metric, "*", ".*?")
|
||||
regexPatterns = append(regexPatterns, bson.M{"Key": bson.M{"$regex": regexPattern}})
|
||||
} else {
|
||||
// If there's no wildcard, match the metric directly
|
||||
|
@ -90,6 +91,9 @@ func AggloMetrics(metricsList []string) []DataDefDB {
|
|||
hourlyPoolTo := ModuloTime(time.Now().Add(1 * time.Hour), time.Hour)
|
||||
dailyPool := ModuloTime(time.Now(), 24 * time.Hour)
|
||||
dailyPoolTo := ModuloTime(time.Now().Add(24 * time.Hour), 24 * time.Hour)
|
||||
|
||||
previousHourlyPool := ModuloTime(time.Now().Add(-1 * time.Hour), time.Hour)
|
||||
previousDailyPool := ModuloTime(time.Now().Add(-24 * time.Hour), 24 * time.Hour)
|
||||
|
||||
for metInd, metric := range metrics {
|
||||
values := metric.Values
|
||||
|
@ -109,6 +113,15 @@ func AggloMetrics(metricsList []string) []DataDefDB {
|
|||
AggloTo: hourlyPoolTo,
|
||||
AggloExpire: hourlyPoolTo.Add(48 * time.Hour),
|
||||
}
|
||||
|
||||
// check alerts on previous pool
|
||||
if agMet, ok := metric.ValuesAggl["hour_" + previousHourlyPool.UTC().Format("2006-01-02 15:04:05")]; ok {
|
||||
CheckAlerts(metric.Key, "hourly", utils.AlertMetricTrack{
|
||||
Key: metric.Key,
|
||||
Object: metric.Object,
|
||||
Max: metric.Max,
|
||||
}, agMet.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// if daily pool does not exist, create it
|
||||
|
@ -121,6 +134,15 @@ func AggloMetrics(metricsList []string) []DataDefDB {
|
|||
AggloTo: dailyPoolTo,
|
||||
AggloExpire: dailyPoolTo.Add(30 * 24 * time.Hour),
|
||||
}
|
||||
|
||||
// check alerts on previous pool
|
||||
if agMet, ok := metric.ValuesAggl["day_" + previousDailyPool.UTC().Format("2006-01-02 15:04:05")]; ok {
|
||||
CheckAlerts(metric.Key, "daily", utils.AlertMetricTrack{
|
||||
Key: metric.Key,
|
||||
Object: metric.Object,
|
||||
Max: metric.Max,
|
||||
}, agMet.Value)
|
||||
}
|
||||
}
|
||||
|
||||
for valInd, value := range values {
|
||||
|
|
175
src/metrics/alerts.go
Normal file
175
src/metrics/alerts.go
Normal file
|
@ -0,0 +1,175 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"regexp"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
"github.com/azukaar/cosmos-server/src/docker"
|
||||
)
|
||||
|
||||
func CheckAlerts(TrackingMetric string, Period string, metric utils.AlertMetricTrack, Value int) {
|
||||
config := utils.GetMainConfig()
|
||||
ActiveAlerts := config.MonitoringAlerts
|
||||
|
||||
alerts := []utils.Alert{}
|
||||
ok := false
|
||||
|
||||
// if tracking metric contains a wildcard
|
||||
if strings.Contains(TrackingMetric, "*") {
|
||||
regexPattern := "^" + strings.ReplaceAll(TrackingMetric, "*", ".*?")
|
||||
regex, _ := regexp.Compile(regexPattern)
|
||||
|
||||
// Iterate over the map to find a match
|
||||
for _, val := range ActiveAlerts {
|
||||
if regex.MatchString(val.TrackingMetric) && val.Period == Period {
|
||||
alerts = append(alerts, val)
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, val := range ActiveAlerts {
|
||||
if val.TrackingMetric == TrackingMetric && val.Period == Period {
|
||||
alerts = append(alerts, val)
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, alert := range alerts {
|
||||
if !alert.Enabled {
|
||||
continue
|
||||
}
|
||||
|
||||
if alert.Throttled && alert.LastTriggered.Add(time.Hour * 24).After(time.Now()) {
|
||||
continue
|
||||
}
|
||||
|
||||
ValueToTest := Value
|
||||
|
||||
if alert.Condition.Percent {
|
||||
ValueToTest = int(float64(Value) / float64(metric.Max) * 100)
|
||||
|
||||
utils.Debug(fmt.Sprintf("Alert %s: %d / %d = %d%%", alert.Name, Value, metric.Max, ValueToTest))
|
||||
}
|
||||
|
||||
// Check if the condition is met
|
||||
if alert.Condition.Operator == "gt" {
|
||||
if ValueToTest > alert.Condition.Value {
|
||||
ExecuteAllActions(alert, alert.Actions, metric)
|
||||
}
|
||||
} else if alert.Condition.Operator == "lt" {
|
||||
if ValueToTest < alert.Condition.Value {
|
||||
ExecuteAllActions(alert, alert.Actions, metric)
|
||||
}
|
||||
} else if alert.Condition.Operator == "eq" {
|
||||
if ValueToTest == alert.Condition.Value {
|
||||
ExecuteAllActions(alert, alert.Actions, metric)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ExecuteAllActions(alert utils.Alert, actions []utils.AlertAction, metric utils.AlertMetricTrack) {
|
||||
utils.Debug("Alert triggered: " + alert.Name)
|
||||
for _, action := range actions {
|
||||
ExecuteAction(alert, action, metric)
|
||||
}
|
||||
|
||||
// set LastTriggered to now
|
||||
alert.LastTriggered = time.Now()
|
||||
|
||||
// update alert in config
|
||||
config := utils.GetMainConfig()
|
||||
for i, val := range config.MonitoringAlerts {
|
||||
if val.Name == alert.Name {
|
||||
config.MonitoringAlerts[i] = alert
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
utils.SetBaseMainConfig(config)
|
||||
}
|
||||
|
||||
func ExecuteAction(alert utils.Alert, action utils.AlertAction, metric utils.AlertMetricTrack) {
|
||||
utils.Log("Executing action " + action.Type + " on " + metric.Key + " " + metric.Object )
|
||||
|
||||
if action.Type == "email" {
|
||||
utils.Debug("Sending email to " + action.Target)
|
||||
|
||||
if utils.GetMainConfig().EmailConfig.Enabled {
|
||||
users := utils.ListAllUsers("admin")
|
||||
for _, user := range users {
|
||||
if user.Email != "" {
|
||||
utils.SendEmail([]string{user.Email}, "Alert Triggered: " + alert.Name,
|
||||
fmt.Sprintf(`<h1>Alert Triggered [%s]</h1>
|
||||
You are recevining this email because you are admin on a Cosmos
|
||||
server where an Alert has been subscribed to.<br />
|
||||
You can manage your subscriptions in the Monitoring tab.<br />
|
||||
Alert triggered on %s. Please refer to the Monitoring tab for
|
||||
more information.<br />`, alert.Severity, metric.Key))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
utils.Warn("Alert triggered but Email is not enabled")
|
||||
}
|
||||
|
||||
} else if action.Type == "webhook" {
|
||||
utils.Debug("Calling webhook " + action.Target)
|
||||
|
||||
} else if action.Type == "stop" {
|
||||
utils.Debug("Stopping application")
|
||||
|
||||
parts := strings.Split(metric.Object, "@")
|
||||
|
||||
if len(parts) > 1 {
|
||||
object := parts[0]
|
||||
objectName := strings.Join(parts[1:], "@")
|
||||
|
||||
if object == "container" {
|
||||
docker.StopContainer(objectName)
|
||||
} else if object == "route" {
|
||||
config := utils.ReadConfigFromFile()
|
||||
|
||||
objectIndex := -1
|
||||
for i, route := range config.HTTPConfig.ProxyConfig.Routes {
|
||||
if route.Name == objectName {
|
||||
objectIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if objectIndex != -1 {
|
||||
config.HTTPConfig.ProxyConfig.Routes[objectIndex].Disabled = true
|
||||
|
||||
utils.SetBaseMainConfig(config)
|
||||
} else {
|
||||
utils.Warn("No route found, for " + objectName)
|
||||
}
|
||||
|
||||
utils.RestartHTTPServer()
|
||||
}
|
||||
} else {
|
||||
utils.Warn("No object found, for " + metric.Object)
|
||||
}
|
||||
|
||||
} else if action.Type == "notification" {
|
||||
utils.WriteNotification(utils.Notification{
|
||||
Recipient: "admin",
|
||||
Title: "Alert triggered",
|
||||
Message: "The alert \"" + alert.Name + "\" was triggered.",
|
||||
Level: alert.Severity,
|
||||
Link: "/cosmos-ui/monitoring",
|
||||
})
|
||||
|
||||
} else if action.Type == "script" {
|
||||
utils.Debug("Executing script")
|
||||
}
|
||||
}
|
|
@ -2,6 +2,11 @@ package metrics
|
|||
|
||||
import (
|
||||
"time"
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
||||
"github.com/azukaar/cosmos-server/src/utils"
|
||||
)
|
||||
|
@ -25,6 +30,7 @@ func PushRequestMetrics(route utils.ProxyRouteConfig, statusCode int, TimeStarte
|
|||
Label: "Request Errors " + route.Name,
|
||||
AggloType: "sum",
|
||||
SetOperation: "sum",
|
||||
Object: "route@" + route.Name,
|
||||
})
|
||||
} else {
|
||||
PushSetMetric("proxy.all.success", 1, DataDef{
|
||||
|
@ -40,6 +46,7 @@ func PushRequestMetrics(route utils.ProxyRouteConfig, statusCode int, TimeStarte
|
|||
Label: "Request Success " + route.Name,
|
||||
AggloType: "sum",
|
||||
SetOperation: "sum",
|
||||
Object: "route@" + route.Name,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -59,6 +66,7 @@ func PushRequestMetrics(route utils.ProxyRouteConfig, statusCode int, TimeStarte
|
|||
AggloType: "sum",
|
||||
SetOperation: "sum",
|
||||
Unit: "ms",
|
||||
Object: "route@" + route.Name,
|
||||
})
|
||||
|
||||
PushSetMetric("proxy.all.bytes", int(size), DataDef{
|
||||
|
@ -77,6 +85,7 @@ func PushRequestMetrics(route utils.ProxyRouteConfig, statusCode int, TimeStarte
|
|||
AggloType: "sum",
|
||||
SetOperation: "sum",
|
||||
Unit: "B",
|
||||
Object: "route@" + route.Name,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -108,4 +117,58 @@ func PushShieldMetrics(reason string) {
|
|||
AggloType: "sum",
|
||||
SetOperation: "sum",
|
||||
})
|
||||
}
|
||||
|
||||
type MetricList struct {
|
||||
Key string
|
||||
Label string
|
||||
}
|
||||
|
||||
func ListMetrics(w http.ResponseWriter, req *http.Request) {
|
||||
if utils.AdminOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if(req.Method == "GET") {
|
||||
c, errCo := utils.GetCollection(utils.GetRootAppId(), "metrics")
|
||||
if errCo != nil {
|
||||
utils.Error("Database Connect", errCo)
|
||||
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||
return
|
||||
}
|
||||
|
||||
metrics := []MetricList{}
|
||||
|
||||
cursor, err := c.Find(nil, map[string]interface{}{}, options.Find().SetProjection(bson.M{"Key": 1, "Label":1, "_id": 0}))
|
||||
|
||||
if err != nil {
|
||||
utils.Error("metrics: Error while getting metrics", err)
|
||||
utils.HTTPError(w, "metrics Get Error", http.StatusInternalServerError, "UD001")
|
||||
return
|
||||
}
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err = cursor.All(nil, &metrics); err != nil {
|
||||
utils.Error("metrics: Error while decoding metrics", err)
|
||||
utils.HTTPError(w, "metrics decode Error", http.StatusInternalServerError, "UD002")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the names into a string slice
|
||||
metricNames := map[string]string{}
|
||||
|
||||
for _, metric := range metrics {
|
||||
metricNames[metric.Key] = metric.Label
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": metricNames,
|
||||
})
|
||||
} else {
|
||||
utils.Error("metrics: Method not allowed" + req.Method, nil)
|
||||
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ type DataDef struct {
|
|||
Scale int
|
||||
Unit string
|
||||
Decumulate bool
|
||||
Object string
|
||||
}
|
||||
|
||||
type DataPush struct {
|
||||
|
@ -33,6 +34,7 @@ type DataPush struct {
|
|||
Scale int
|
||||
Unit string
|
||||
Decumulate bool
|
||||
Object string
|
||||
}
|
||||
|
||||
var dataBuffer = map[string]DataPush{}
|
||||
|
@ -109,6 +111,7 @@ func SaveMetrics() {
|
|||
"AggloType": dp.AggloType,
|
||||
"Scale": scale,
|
||||
"Unit": dp.Unit,
|
||||
"Object": dp.Object,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -181,9 +184,16 @@ func PushSetMetric(key string, value int, def DataDef) {
|
|||
AggloType: def.AggloType,
|
||||
Scale: def.Scale,
|
||||
Unit: def.Unit,
|
||||
Object: def.Object,
|
||||
}
|
||||
}
|
||||
|
||||
CheckAlerts(key, "latest", utils.AlertMetricTrack{
|
||||
Key: key,
|
||||
Object: def.Object,
|
||||
Max: def.Max,
|
||||
}, value)
|
||||
|
||||
lastInserted[key] = originalValue
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -160,6 +160,7 @@ func GetSystemMetrics() {
|
|||
Period: time.Second * 120,
|
||||
Label: "Disk " + part.Mountpoint,
|
||||
Unit: "B",
|
||||
Object: "disk@" + part.Mountpoint,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -208,13 +209,15 @@ func GetSystemMetrics() {
|
|||
AggloType: "avg",
|
||||
Scale: 100,
|
||||
Unit: "%",
|
||||
Object: "container@" + ds.Name,
|
||||
})
|
||||
PushSetMetric("system.docker.ram." + ds.Name, int(ds.MemUsage), DataDef{
|
||||
Max: 0,
|
||||
Max: memInfo.Total,
|
||||
Period: time.Second * 30,
|
||||
Label: "Docker RAM " + ds.Name,
|
||||
AggloType: "avg",
|
||||
Unit: "B",
|
||||
Object: "container@" + ds.Name,
|
||||
})
|
||||
PushSetMetric("system.docker.netRx." + ds.Name, int(ds.NetworkRx), DataDef{
|
||||
Max: 0,
|
||||
|
@ -224,6 +227,7 @@ func GetSystemMetrics() {
|
|||
AggloType: "sum",
|
||||
Decumulate: true,
|
||||
Unit: "B",
|
||||
Object: "container@" + ds.Name,
|
||||
})
|
||||
PushSetMetric("system.docker.netTx." + ds.Name, int(ds.NetworkTx), DataDef{
|
||||
Max: 0,
|
||||
|
@ -233,6 +237,7 @@ func GetSystemMetrics() {
|
|||
AggloType: "sum",
|
||||
Decumulate: true,
|
||||
Unit: "B",
|
||||
Object: "container@" + ds.Name,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -15,7 +15,9 @@ func BuildFromConfig(router *mux.Router, config utils.ProxyConfig) *mux.Router {
|
|||
|
||||
for i := len(config.Routes)-1; i >= 0; i-- {
|
||||
routeConfig := config.Routes[i]
|
||||
RouterGen(routeConfig, router, RouteTo(routeConfig))
|
||||
if !routeConfig.Disabled {
|
||||
RouterGen(routeConfig, router, RouteTo(routeConfig))
|
||||
}
|
||||
}
|
||||
|
||||
return router
|
||||
|
|
125
src/utils/db.go
125
src/utils/db.go
|
@ -4,6 +4,9 @@ import (
|
|||
"context"
|
||||
"os"
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
|
@ -79,4 +82,124 @@ func GetCollection(applicationId string, collection string) (*mongo.Collection,
|
|||
|
||||
// func query(q string) (*sql.Rows, error) {
|
||||
// return db.Query(q)
|
||||
// }
|
||||
// }
|
||||
|
||||
var (
|
||||
bufferLock sync.Mutex
|
||||
writeBuffer = make(map[string][]map[string]interface{})
|
||||
bufferTicker = time.NewTicker(1 * time.Minute)
|
||||
bufferCapacity = 100
|
||||
)
|
||||
|
||||
func InitDBBuffers() {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-bufferTicker.C:
|
||||
flushAllBuffers()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func flushBuffer(collectionName string) {
|
||||
bufferLock.Lock()
|
||||
objects, exists := writeBuffer[collectionName]
|
||||
if exists && len(objects) > 0 {
|
||||
collection, errG := GetCollection(GetRootAppId(), collectionName)
|
||||
if errG != nil {
|
||||
Error("BulkDBWritter: Error getting collection", errG)
|
||||
}
|
||||
|
||||
if err := WriteToDatabase(collection, objects); err != nil {
|
||||
Error("BulkDBWritter: Error writing to database", err)
|
||||
}
|
||||
writeBuffer[collectionName] = make([]map[string]interface{}, 0)
|
||||
}
|
||||
bufferLock.Unlock()
|
||||
}
|
||||
|
||||
func flushAllBuffers() {
|
||||
bufferLock.Lock()
|
||||
for collectionName, objects := range writeBuffer {
|
||||
if len(objects) > 0 {
|
||||
collection, errG := GetCollection(GetRootAppId(), collectionName)
|
||||
if errG != nil {
|
||||
Error("BulkDBWritter: Error getting collection", errG)
|
||||
}
|
||||
|
||||
if err := WriteToDatabase(collection, objects); err != nil {
|
||||
Error("BulkDBWritter: Error writing to database: ", err)
|
||||
}
|
||||
writeBuffer[collectionName] = make([]map[string]interface{}, 0)
|
||||
}
|
||||
}
|
||||
bufferLock.Unlock()
|
||||
}
|
||||
|
||||
func BufferedDBWrite(collectionName string, object map[string]interface{}) {
|
||||
bufferLock.Lock()
|
||||
writeBuffer[collectionName] = append(writeBuffer[collectionName], object)
|
||||
if len(writeBuffer[collectionName]) >= bufferCapacity {
|
||||
flushBuffer(collectionName)
|
||||
}
|
||||
bufferLock.Unlock()
|
||||
}
|
||||
|
||||
func WriteToDatabase(collection *mongo.Collection, objects []map[string]interface{}) error {
|
||||
if len(objects) == 0 {
|
||||
return nil // Nothing to write
|
||||
}
|
||||
|
||||
// Convert to a slice of interface{} for insertion
|
||||
interfaceSlice := make([]interface{}, len(objects))
|
||||
for i, v := range objects {
|
||||
interfaceSlice[i] = v
|
||||
}
|
||||
|
||||
_, err := collection.InsertMany(context.Background(), interfaceSlice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListAllUsers(role string) []User {
|
||||
// list all users
|
||||
c, errCo := GetCollection(GetRootAppId(), "users")
|
||||
if errCo != nil {
|
||||
Error("Database Connect", errCo)
|
||||
return []User{}
|
||||
}
|
||||
|
||||
users := []User{}
|
||||
|
||||
condition := map[string]interface{}{}
|
||||
|
||||
if role == "admin" {
|
||||
condition = map[string]interface{}{
|
||||
"Role": 2,
|
||||
}
|
||||
} else if role == "user" {
|
||||
condition = map[string]interface{}{
|
||||
"Role": 1,
|
||||
}
|
||||
}
|
||||
|
||||
cursor, err := c.Find(nil, condition)
|
||||
|
||||
if err != nil {
|
||||
Error("Database: Error while getting users", err)
|
||||
return []User{}
|
||||
}
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err = cursor.All(nil, &users); err != nil {
|
||||
Error("Database: Error while decoding users", err)
|
||||
return []User{}
|
||||
}
|
||||
|
||||
return users
|
||||
}
|
|
@ -47,6 +47,24 @@ func Error(message string, err error) {
|
|||
}
|
||||
}
|
||||
|
||||
func MajorError(message string, err error) {
|
||||
ll := LoggingLevelLabels[GetMainConfig().LoggingLevel]
|
||||
errStr := ""
|
||||
if err != nil {
|
||||
errStr = err.Error()
|
||||
}
|
||||
if ll <= ERROR {
|
||||
log.Println(Red + "[ERROR] " + message + " : " + errStr + Reset)
|
||||
}
|
||||
|
||||
WriteNotification(Notification{
|
||||
Recipient: "admin",
|
||||
Title: "Server Error",
|
||||
Message: message + " : " + errStr,
|
||||
Level: "error",
|
||||
})
|
||||
}
|
||||
|
||||
func Fatal(message string, err error) {
|
||||
ll := LoggingLevelLabels[GetMainConfig().LoggingLevel]
|
||||
errStr := ""
|
||||
|
|
|
@ -62,9 +62,11 @@ func BlockBannedIPs(next http.Handler) http.Handler {
|
|||
|
||||
nbAbuse := getIPAbuseCounter(ip)
|
||||
|
||||
// Debug("IP " + ip + " has " + fmt.Sprintf("%d", nbAbuse) + " abuse(s)")
|
||||
if nbAbuse > 275 {
|
||||
Warn("IP " + ip + " has " + fmt.Sprintf("%d", nbAbuse) + " abuse(s) and will soon be banned.")
|
||||
}
|
||||
|
||||
if nbAbuse > 1000 {
|
||||
if nbAbuse > 300 {
|
||||
if hj, ok := w.(http.Hijacker); ok {
|
||||
conn, _, err := hj.Hijack()
|
||||
if err == nil {
|
||||
|
@ -78,6 +80,13 @@ func BlockBannedIPs(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func CleanBannedIPs() {
|
||||
BannedIPs.Range(func(key, value interface{}) bool {
|
||||
BannedIPs.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func MiddlewareTimeout(timeout time.Duration) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
201
src/utils/notifications.go
Normal file
201
src/utils/notifications.go
Normal file
|
@ -0,0 +1,201 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"encoding/json"
|
||||
"time"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type NotificationActions struct {
|
||||
Text string
|
||||
Link string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID primitive.ObjectID `bson:"_id,omitempty"`
|
||||
Title string
|
||||
Message string
|
||||
Icon string
|
||||
Link string
|
||||
Date time.Time
|
||||
Level string
|
||||
Read bool
|
||||
Recipient string
|
||||
Actions []NotificationActions
|
||||
}
|
||||
|
||||
func NotifGet(w http.ResponseWriter, req *http.Request) {
|
||||
_from := req.URL.Query().Get("from")
|
||||
from, _ := primitive.ObjectIDFromHex(_from)
|
||||
|
||||
if LoggedInOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nickname := req.Header.Get("x-cosmos-user")
|
||||
|
||||
if(req.Method == "GET") {
|
||||
c, errCo := GetCollection(GetRootAppId(), "notifications")
|
||||
if errCo != nil {
|
||||
Error("Database Connect", errCo)
|
||||
HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||
return
|
||||
}
|
||||
|
||||
Debug("Notifications: Get notif for " + nickname)
|
||||
|
||||
notifications := []Notification{}
|
||||
|
||||
reqdb := map[string]interface{}{
|
||||
"Recipient": nickname,
|
||||
}
|
||||
|
||||
if from != primitive.NilObjectID {
|
||||
reqdb = map[string]interface{}{
|
||||
// nickname or role
|
||||
"Recipient": nickname,
|
||||
// get notif before from
|
||||
"_id": map[string]interface{}{
|
||||
"$lt": from,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
limit := int64(20)
|
||||
|
||||
cursor, err := c.Find(nil, reqdb, &options.FindOptions{
|
||||
Sort: map[string]interface{}{
|
||||
"Date": -1,
|
||||
},
|
||||
Limit: &limit,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
Error("Notifications: Error while getting notifications", err)
|
||||
HTTPError(w, "notifications Get Error", http.StatusInternalServerError, "UD001")
|
||||
return
|
||||
}
|
||||
|
||||
defer cursor.Close(nil)
|
||||
|
||||
if err = cursor.All(nil, ¬ifications); err != nil {
|
||||
Error("Notifications: Error while decoding notifications", err)
|
||||
HTTPError(w, "notifications Get Error", http.StatusInternalServerError, "UD002")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"data": notifications,
|
||||
})
|
||||
} else {
|
||||
Error("Notifications: Method not allowed" + req.Method, nil)
|
||||
HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func MarkAsRead(w http.ResponseWriter, req *http.Request) {
|
||||
if(req.Method == "GET") {
|
||||
if LoggedInOnly(w, req) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
notificationIDs := []primitive.ObjectID{}
|
||||
nickname := req.Header.Get("x-cosmos-user")
|
||||
|
||||
notificationIDsRawRunes := req.URL.Query().Get("ids")
|
||||
|
||||
notificationIDsRaw := strings.Split(notificationIDsRawRunes, ",")
|
||||
|
||||
Debug(fmt.Sprintf("Marking %v notifications as read",notificationIDsRaw))
|
||||
|
||||
for _, notificationIDRaw := range notificationIDsRaw {
|
||||
notificationID, err := primitive.ObjectIDFromHex(notificationIDRaw)
|
||||
|
||||
if err != nil {
|
||||
HTTPError(w, "Invalid notification ID " + notificationIDRaw, http.StatusBadRequest, "InvalidID")
|
||||
return
|
||||
}
|
||||
|
||||
notificationIDs = append(notificationIDs, notificationID)
|
||||
}
|
||||
|
||||
|
||||
c, errCo := GetCollection(GetRootAppId(), "notifications")
|
||||
if errCo != nil {
|
||||
Error("Database Connect", errCo)
|
||||
HTTPError(w, "Database connection error", http.StatusInternalServerError, "DB001")
|
||||
return
|
||||
}
|
||||
|
||||
filter := bson.M{"_id": bson.M{"$in": notificationIDs}, "Recipient": nickname}
|
||||
update := bson.M{"$set": bson.M{"Read": true}}
|
||||
result, err := c.UpdateMany(nil, filter, update)
|
||||
if err != nil {
|
||||
Error("Notifications: Error while marking notification as read", err)
|
||||
HTTPError(w, "Error updating notification", http.StatusInternalServerError, "UpdateError")
|
||||
return
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
HTTPError(w, "No matching notification found", http.StatusNotFound, "NotFound")
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "OK",
|
||||
"message": "Notification marked as read",
|
||||
})
|
||||
} else {
|
||||
Error("Notifications: Method not allowed" + req.Method, nil)
|
||||
HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func WriteNotification(notification Notification) {
|
||||
notification.Date = time.Now()
|
||||
|
||||
notification.Read = false
|
||||
|
||||
if notification.Recipient == "all" || notification.Recipient == "admin" || notification.Recipient == "user" {
|
||||
// list all users
|
||||
users := ListAllUsers(notification.Recipient)
|
||||
|
||||
Debug("Notifications: Sending notification to " + string(len(users)) + " users")
|
||||
|
||||
for _, user := range users {
|
||||
BufferedDBWrite("notifications", map[string]interface{}{
|
||||
"Title": notification.Title,
|
||||
"Message": notification.Message,
|
||||
"Icon": notification.Icon,
|
||||
"Link": notification.Link,
|
||||
"Date": notification.Date,
|
||||
"Level": notification.Level,
|
||||
"Read": notification.Read,
|
||||
"Recipient": user.Nickname,
|
||||
"Actions": notification.Actions,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
BufferedDBWrite("notifications", map[string]interface{}{
|
||||
"Title": notification.Title,
|
||||
"Message": notification.Message,
|
||||
"Icon": notification.Icon,
|
||||
"Link": notification.Link,
|
||||
"Date": notification.Date,
|
||||
"Level": notification.Level,
|
||||
"Read": notification.Read,
|
||||
"Recipient": notification.Recipient,
|
||||
"Actions": notification.Actions,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -92,6 +92,7 @@ type Config struct {
|
|||
ThemeConfig ThemeConfig
|
||||
ConstellationConfig ConstellationConfig
|
||||
MonitoringDisabled bool
|
||||
MonitoringAlerts map[string]Alert
|
||||
}
|
||||
|
||||
type HomepageConfig struct {
|
||||
|
@ -160,6 +161,7 @@ type AddionalFiltersConfig struct {
|
|||
}
|
||||
|
||||
type ProxyRouteConfig struct {
|
||||
Disabled bool
|
||||
Name string `validate:"required"`
|
||||
Description string
|
||||
UseHost bool
|
||||
|
@ -323,3 +325,32 @@ type Device struct {
|
|||
PrivateKey string `json:"privateKey",omitempty`
|
||||
IP string `json:"ip",validate:"required,ipv4"`
|
||||
}
|
||||
|
||||
type Alert struct {
|
||||
Name string
|
||||
Enabled bool
|
||||
Period string
|
||||
TrackingMetric string
|
||||
Condition AlertCondition
|
||||
Actions []AlertAction
|
||||
LastTriggered time.Time
|
||||
Throttled bool
|
||||
Severity string
|
||||
}
|
||||
|
||||
type AlertCondition struct {
|
||||
Operator string
|
||||
Value int
|
||||
Percent bool
|
||||
}
|
||||
|
||||
type AlertAction struct {
|
||||
Type string
|
||||
Target string
|
||||
}
|
||||
|
||||
type AlertMetricTrack struct {
|
||||
Key string
|
||||
Object string
|
||||
Max uint64
|
||||
}
|
Loading…
Reference in a new issue