diff --git a/client/src/api/metrics.jsx b/client/src/api/metrics.jsx index 1b34d4f..c16ed66 100644 --- a/client/src/api/metrics.jsx +++ b/client/src/api/metrics.jsx @@ -1,7 +1,7 @@ import wrap from './wrap'; -function get() { - return wrap(fetch('/cosmos/api/metrics', { +function get(metarr) { + return wrap(fetch('/cosmos/api/metrics?metrics=' + metarr.join(','), { method: 'GET', headers: { 'Content-Type': 'application/json' diff --git a/client/src/menu-items/dashboard.jsx b/client/src/menu-items/dashboard.jsx index c628239..cf221cf 100644 --- a/client/src/menu-items/dashboard.jsx +++ b/client/src/menu-items/dashboard.jsx @@ -23,9 +23,9 @@ const dashboard = { }, { id: 'dashboard', - title: 'Dashboard', + title: 'Monitoring', type: 'item', - url: '/cosmos-ui/dashboard', + url: '/cosmos-ui/monitoring', icon: DashboardOutlined, breadcrumbs: false }, diff --git a/client/src/pages/config/users/configman.jsx b/client/src/pages/config/users/configman.jsx index 97e9e27..43951b8 100644 --- a/client/src/pages/config/users/configman.jsx +++ b/client/src/pages/config/users/configman.jsx @@ -122,6 +122,8 @@ const ConfigManagement = () => { Expanded: config && config.HomepageConfig && config.HomepageConfig.Expanded, PrimaryColor: config && config.ThemeConfig && config.ThemeConfig.PrimaryColor, SecondaryColor: config && config.ThemeConfig && config.ThemeConfig.SecondaryColor, + + MonitoringEnabled: !config.MonitoringDisabled, }} validationSchema={Yup.object().shape({ @@ -141,6 +143,7 @@ const ConfigManagement = () => { // AutoUpdate: values.AutoUpdate, BlockedCountries: values.GeoBlocking, CountryBlacklistIsWhitelist: values.CountryBlacklistIsWhitelist, + MonitoringDisabled: !values.MonitoringEnabled, HTTPConfig: { ...config.HTTPConfig, Hostname: values.Hostname, @@ -298,6 +301,12 @@ const ConfigManagement = () => { + + @@ -363,29 +372,6 @@ const ConfigManagement = () => { formik.setFieldValue('PrimaryColor', colorRGB); SetPrimaryColor(colorRGB); }} - colors={[ - '#ab47bc', - '#4527a0', - '#FF6900', - '#FCB900', - '#7BDCB5', - '#00D084', - '#8ED1FC', - '#0693E3', - '#ABB8C3', - '#EB144C', - '#F78DA7', - '#9900EF', - '#FF0000', - '#FFC0CB', - '#20B2AA', - '#FFFF00', - '#8A2BE2', - '#A52A2A', - '#5F9EA0', - '#7FFF00', - '#D2691E' - ]} /> @@ -401,29 +387,6 @@ const ConfigManagement = () => { formik.setFieldValue('SecondaryColor', colorRGB); SetSecondaryColor(colorRGB); }} - colors={[ - '#ab47bc', - '#4527a0', - '#FF6900', - '#FCB900', - '#7BDCB5', - '#00D084', - '#8ED1FC', - '#0693E3', - '#ABB8C3', - '#EB144C', - '#F78DA7', - '#9900EF', - '#FF0000', - '#FFC0CB', - '#20B2AA', - '#FFFF00', - '#8A2BE2', - '#A52A2A', - '#5F9EA0', - '#7FFF00', - '#D2691E' - ]} /> diff --git a/client/src/pages/dashboard/components/mini-plot.jsx b/client/src/pages/dashboard/components/mini-plot.jsx new file mode 100644 index 0000000..caedd5b --- /dev/null +++ b/client/src/pages/dashboard/components/mini-plot.jsx @@ -0,0 +1,187 @@ +import React, { useEffect, useState, useMemo } from 'react'; +// material-ui +import { + Avatar, + AvatarGroup, + Box, + Button, + Grid, + List, + ListItemAvatar, + ListItemButton, + ListItemSecondaryAction, + ListItemText, + MenuItem, + Stack, + TextField, + Typography, + Alert +} from '@mui/material'; +import MainCard from '../../../components/MainCard'; + +// material-ui +import { useTheme } from '@mui/material/styles'; + +// third-party +import ReactApexChart from 'react-apexcharts'; +import { FormaterForMetric, formatDate, toUTC } from './utils'; + +import * as API from '../../../api'; + +const _MiniPlotComponent = ({metrics, labels}) => { + const theme = useTheme(); + const { primary, secondary } = theme.palette.text; + const [dataMetrics, setDataMetrics] = useState([]); + const [series, setSeries] = useState([]); + const slot = 'hourly'; + + useEffect(() => { + let xAxis = []; + + if(slot === 'latest') { + for(let i = 0; i < 100; i++) { + xAxis.unshift(i); + } + } + else if(slot === 'hourly') { + for(let i = 0; i < 48; i++) { + let now = new Date(); + now.setHours(now.getHours() - i); + now.setMinutes(0); + now.setSeconds(0); + xAxis.unshift(formatDate(now, true)); + } + } else if(slot === 'daily') { + for(let i = 0; i < 30; i++) { + let now = new Date(); + now.setDate(now.getDate() - i); + xAxis.unshift(formatDate(now)); + } + } + + let xAxisLength = xAxis.length; + xAxis = xAxis.filter((x, i) => i >= xAxisLength / 4); + + // request data + API.metrics.get(metrics).then((res) => { + const toProcess = res.data || []; + setDataMetrics(toProcess); + + const _series = toProcess.map((serie) => ({ + name: 'Value', + data: xAxis.map((date) => { + if(slot === 'latest') { + return serie.Values[serie.Values.length - 1 - date] ? + serie.Values[serie.Values.length - 1 - date].Value : + 0; + } else { + let key = slot === 'hourly' ? "hour_" : "day_"; + let k = key + toUTC(date, slot === 'hourly'); + if (k in serie.ValuesAggl) { + return serie.ValuesAggl[k].Value; + } else { + return 0; + } + } + }) + })); + + setSeries(_series); + }); + }, [metrics]); + + const chartOptions = { + colors: [ + theme.palette.primary.main.replace('rgb(', 'rgba('), + theme.palette.secondary.main.replace('rgb(', 'rgba(') + ], + chart: { + type: 'area', + toolbar: { + show: false, + }, + zoom: { + enabled: false + }, + }, + grid: { + show: false, + }, + dataLabels: { + enabled: false + }, + stroke: { + show: true, + width: 2, + curve: 'smooth' + }, + xaxis: { + labels: { + show: false + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + }, + yaxis: dataMetrics.map((data) => ({ + labels: { + show: false + }, + axisBorder: { + show: false + }, + axisTicks: { + show: false + } + })), + tooltip: { + enabled: false + }, + legend: { + show: false + }, + markers: { + size: 0 + }, + responsive: [{ + breakpoint: undefined, + options: {}, + }] + }; + + const formaters = dataMetrics.map((data) => FormaterForMetric(data)); + + return + + {dataMetrics && dataMetrics.map((dataMetric, di) => + +
{formaters[di](dataMetric.Values[dataMetric.Values.length - 1].Value)}
+
{(labels && labels[di]) || dataMetric.Label}
+
)} + +
+ +
+
+} + +const MiniPlotComponent = ({ metrics, labels }) => { + const memoizedComponent = useMemo(() => <_MiniPlotComponent metrics={metrics} labels={labels} />, [metrics]); + return memoizedComponent; +}; + +export default MiniPlotComponent; \ No newline at end of file diff --git a/client/src/pages/dashboard/components/plot.jsx b/client/src/pages/dashboard/components/plot.jsx index 58c0620..b7f6f8d 100644 --- a/client/src/pages/dashboard/components/plot.jsx +++ b/client/src/pages/dashboard/components/plot.jsx @@ -27,7 +27,7 @@ import ReactApexChart from 'react-apexcharts'; import { FormaterForMetric, toUTC } from './utils'; -const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, zoom, setZoom }) => { +const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, zoom, setZoom, zoomDisabled }) => { const theme = useTheme(); const { primary, secondary } = theme.palette.text; const line = theme.palette.divider; @@ -127,8 +127,7 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z title: { text: SimpleDesign ? '' : thisdata.Label, }, - min: zoom.yaxis && zoom.yaxis.min, - max: zoom.yaxis && zoom.yaxis.max, + max: thisdata.Max ? thisdata.Max : undefined, })), grid: { borderColor: line @@ -137,16 +136,16 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z theme: theme.palette.mode, }, chart: { + zoom: { + enabled: !zoomDisabled, + }, + selection: { + enabled: !zoomDisabled, + }, events: { - zoomed: function(chartContext, { xaxis, yaxis }) { - // Handle the zoom event here - console.log('Zoomed:', xaxis, yaxis); - setZoom({ xaxis, yaxis }); - }, - selection: function(chartContext, { xaxis, yaxis }) { - // Handle the selection event here - console.log('Selected:', xaxis, yaxis); - }, + zoomed: function(chartContext, { xaxis }) { + setZoom({ xaxis }); + } } } })); @@ -161,11 +160,9 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z useEffect(() => { // if different - if(zoom && zoom.xaxis && zoom.yaxis && (!options.xaxis || !options.yaxis || !options.yaxis[0] || ( + if(zoom && zoom.xaxis && (!options.xaxis || ( zoom.xaxis.min !== options.xaxis.min || - zoom.xaxis.max !== options.xaxis.max || - zoom.yaxis.min !== options.yaxis[0].min || - zoom.yaxis.max !== options.yaxis[0].max + zoom.xaxis.max !== options.xaxis.max ))){ setOptions((prevState) => ({ ...prevState, @@ -174,21 +171,16 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z min: zoom.xaxis.min, max: zoom.xaxis.max, }, - yaxis: prevState.yaxis.map((y, id) => ({ - ...y, - min: zoom.yaxis.min, - max: zoom.yaxis.max, - })) })); } }, [zoom]); - return - + return <> + {title} - + {withSelector &&
- + + - + } export default PlotComponent; \ No newline at end of file diff --git a/client/src/pages/dashboard/components/table.jsx b/client/src/pages/dashboard/components/table.jsx index bc4f839..5c13ab8 100644 --- a/client/src/pages/dashboard/components/table.jsx +++ b/client/src/pages/dashboard/components/table.jsx @@ -21,7 +21,12 @@ import { TableCell, TableContainer, Table, - TableBody + TableBody, + Dialog, + DialogTitle, + DialogContent, + DialogContentText, + DialogActions } from '@mui/material'; import MainCard from '../../../components/MainCard'; @@ -34,6 +39,7 @@ import { object } from 'prop-types'; import { FormaterForMetric } from './utils'; import { set } from 'lodash'; import { DownOutlined, UpOutlined } from '@ant-design/icons'; +import PlotComponent from './plot'; function formatDate(now, time) { // use as UTC @@ -128,6 +134,7 @@ const TableComponent = ({ title, data, displayMax, render, xAxis, slot, zoom}) = const [orderBy, setOrderBy] = useState('name'); const [headCells, setHeadCells] = useState([]); const [rows, setRows] = useState([]); + const [openModal, setOpenModal] = useState(false); useEffect(() => { let heads = {}; @@ -189,11 +196,13 @@ const TableComponent = ({ title, data, displayMax, render, xAxis, slot, zoom}) = fnrows.push({ name, [cat]: render ? render(item, v, formatter(v)) : formatter(v), - ["__" + cat]: v + ["__" + cat]: v, + "__key": [item.Key] }); } else { fnrows.find((row) => row.name === name)[cat] = render ? render(item, v, formatter(v)) : formatter(v) fnrows.find((row) => row.name === name)["__" + cat] = v + fnrows.find((row) => row.name === name)["__key"].push(item.Key) } }); @@ -233,12 +242,41 @@ const TableComponent = ({ title, data, displayMax, render, xAxis, slot, zoom}) = }, [data, slot, xAxis, zoom]); - return + return <> + + {openModal && setOpenModal(false)} maxWidth="md" fullWidth={true}> + Detailed History + + + { + if (!openModal) { + return false; + } + return openModal.includes(item.Key) + })} + xAxis={xAxis} + slot={slot} + zoom={zoom} + zoomDisabled={true} + /> + + + + + + } + + {title} + { + setOpenModal(row.__key); + }} > {headCells.map((headCell) => { return + } export default TableComponent; \ No newline at end of file diff --git a/client/src/pages/dashboard/components/utils.jsx b/client/src/pages/dashboard/components/utils.jsx index 63a8955..38a7b53 100644 --- a/client/src/pages/dashboard/components/utils.jsx +++ b/client/src/pages/dashboard/components/utils.jsx @@ -4,13 +4,13 @@ export const simplifyNumber = (num) => { num = Math.round(num * 100) / 100; if (Math.abs(num) >= 1e12) { - return (num / 1e12).toFixed(1) + 'T'; // Convert to Millions + return (num / 1e12).toFixed(1) + ' T'; // Convert to Millions } else if (Math.abs(num) >= 1e9) { - return (num / 1e9).toFixed(1) + 'G'; // Convert to Millions + return (num / 1e9).toFixed(1) + ' G'; // Convert to Millions } else if (Math.abs(num) >= 1e6) { - return (num / 1e6).toFixed(1) + 'M'; // Convert to Millions + return (num / 1e6).toFixed(1) + ' M'; // Convert to Millions } else if (Math.abs(num) >= 1e3) { - return (num / 1e3).toFixed(1) + 'K'; // Convert to Thousands + return (num / 1e3).toFixed(1) + ' K'; // Convert to Thousands } else { return num.toString(); } @@ -18,12 +18,11 @@ export const simplifyNumber = (num) => { export const FormaterForMetric = (metric, displayMax) => { return (num) => { - if(!num) return 0; - + if(metric.Scale) num /= metric.Scale; - num = simplifyNumber(num); + num = simplifyNumber(num) + metric.Unit; if(displayMax && metric.Max) { num += ` / ${simplifyNumber(metric.Max)}` diff --git a/client/src/pages/dashboard/containerMetrics.jsx b/client/src/pages/dashboard/containerMetrics.jsx new file mode 100644 index 0000000..d9b06b7 --- /dev/null +++ b/client/src/pages/dashboard/containerMetrics.jsx @@ -0,0 +1,234 @@ +import { useEffect, useState } from 'react'; + +// material-ui +import { + Avatar, + AvatarGroup, + Box, + Button, + Grid, + List, + ListItemAvatar, + ListItemButton, + ListItemSecondaryAction, + ListItemText, + MenuItem, + Stack, + TextField, + Typography, + Alert, + LinearProgress, + CircularProgress +} from '@mui/material'; + +// project import +import OrdersTable from './OrdersTable'; +import IncomeAreaChart from './IncomeAreaChart'; +import MonthlyBarChart from './MonthlyBarChart'; +import ReportAreaChart from './ReportAreaChart'; +import SalesColumnChart from './SalesColumnChart'; +import MainCard from '../../components/MainCard'; +import AnalyticEcommerce from '../../components/cards/statistics/AnalyticEcommerce'; + +// assets +import { GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons'; +import avatar1 from '../../assets/images/users/avatar-1.png'; +import avatar2 from '../../assets/images/users/avatar-2.png'; +import avatar3 from '../../assets/images/users/avatar-3.png'; +import avatar4 from '../../assets/images/users/avatar-4.png'; +import IsLoggedIn from '../../isLoggedIn'; + +import * as API from '../../api'; +import AnimateButton from '../../components/@extended/AnimateButton'; +import PlotComponent from './components/plot'; +import TableComponent from './components/table'; +import { HomeBackground, TransparentHeader } from '../home'; +import { formatDate } from './components/utils'; + +// avatar style +const avatarSX = { + width: 36, + height: 36, + fontSize: '1rem' +}; + +// action style +const actionSX = { + mt: 0.75, + ml: 1, + top: 'auto', + right: 'auto', + alignSelf: 'flex-start', + transform: 'none' +}; + +// sales report status +const status = [ + { + value: 'today', + label: 'Today' + }, + { + value: 'month', + label: 'This Month' + }, + { + value: 'year', + label: 'This Year' + } +]; + +// ==============================|| DASHBOARD - DEFAULT ||============================== // + +const ContainerMetrics = ({containerName}) => { + const [value, setValue] = useState('today'); + const [slot, setSlot] = useState('latest'); + + const [zoom, setZoom] = useState({ + xaxis: {} + }); + + const [coStatus, setCoStatus] = useState(null); + const [metrics, setMetrics] = useState(null); + const [isCreatingDB, setIsCreatingDB] = useState(false); + + const resetZoom = () => { + setZoom({ + xaxis: {} + }); + } + + const metricsKey = { + CPU: "cosmos.system.docker.cpu." + containerName, + RAM: "cosmos.system.docker.ram." + containerName, + NET_RX: "cosmos.system.docker.netRx." + containerName, + NET_TX: "cosmos.system.docker.netTx." + containerName, + }; + + const refreshMetrics = () => { + API.metrics.get([ + "cosmos.system.docker.cpu", + "cosmos.system.docker.ram", + "cosmos.system.docker.netRx", + "cosmos.system.docker.netTx", + ].map(c => c + "." + containerName)).then((res) => { + let finalMetrics = {}; + if(res.data) { + res.data.forEach((metric) => { + finalMetrics[metric.Key] = metric; + }); + setMetrics(finalMetrics); + } + setTimeout(refreshMetrics, 10000); + }); + }; + + const refreshStatus = () => { + API.getStatus().then((res) => { + setCoStatus(res.data); + }); + } + + useEffect(() => { + refreshStatus(); + refreshMetrics(); + }, []); + + let xAxis = []; + + if(slot === 'latest') { + for(let i = 0; i < 100; i++) { + xAxis.unshift(i); + } + } + else if(slot === 'hourly') { + for(let i = 0; i < 48; i++) { + let now = new Date(); + now.setHours(now.getHours() - i); + now.setMinutes(0); + now.setSeconds(0); + xAxis.unshift(formatDate(now, true)); + } + } else if(slot === 'daily') { + for(let i = 0; i < 30; i++) { + let now = new Date(); + now.setDate(now.getDate() - i); + xAxis.unshift(formatDate(now)); + } + } + + return ( + <> + + {!metrics && + + } + {metrics &&
+ + + {containerName} Monitoring + + + + + + {zoom.xaxis.min && } + + + + + + + + + + + +
} + + ); +}; + +export default ContainerMetrics; diff --git a/client/src/pages/dashboard/index.jsx b/client/src/pages/dashboard/index.jsx index 5153681..c33adfe 100644 --- a/client/src/pages/dashboard/index.jsx +++ b/client/src/pages/dashboard/index.jsx @@ -44,6 +44,7 @@ import PlotComponent from './components/plot'; import TableComponent from './components/table'; import { HomeBackground, TransparentHeader } from '../home'; import { formatDate } from './components/utils'; +import MiniPlotComponent from './components/mini-plot'; // avatar style const avatarSX = { @@ -85,8 +86,7 @@ const DashboardDefault = () => { const [slot, setSlot] = useState('latest'); const [zoom, setZoom] = useState({ - xaxis: {}, - yaxis: {} + xaxis: {} }); const [coStatus, setCoStatus] = useState(null); @@ -95,13 +95,12 @@ const DashboardDefault = () => { const resetZoom = () => { setZoom({ - xaxis: {}, - yaxis: {} + xaxis: {} }); } const refreshMetrics = () => { - API.metrics.get().then((res) => { + API.metrics.get(["cosmos.system.*"]).then((res) => { let finalMetrics = {}; if(res.data) { res.data.forEach((metric) => { @@ -109,7 +108,6 @@ const DashboardDefault = () => { }); setMetrics(finalMetrics); } - setTimeout(refreshMetrics, 10000); }); }; @@ -121,7 +119,14 @@ const DashboardDefault = () => { useEffect(() => { refreshStatus(); - refreshMetrics(); + + let interval = setInterval(() => { + refreshMetrics(); + }, 10000); + + return () => { + clearInterval(interval); + }; }, []); let xAxis = []; @@ -199,8 +204,7 @@ const DashboardDefault = () => { size="small" onClick={() => { setZoom({ - xaxis: {}, - yaxis: {} + xaxis: {} }); }} color={'primary'} @@ -230,14 +234,17 @@ const DashboardDefault = () => { */} - - + + + key.startsWith("cosmos.system.docker.cpu") || key.startsWith("cosmos.system.docker.ram")).map((key) => metrics[key]) }/> - + + + key.startsWith("cosmos.system.docker.net")).map((key) => metrics[key]) @@ -245,25 +252,31 @@ const DashboardDefault = () => { { + let percent = value / metric.Max * 100; return {formattedValue} - + 95 ? 'error' : (percent > 75 ? 'warning' : 'info')} + value={percent} /> }} data={ Object.keys(metrics).filter((key) => key.startsWith("cosmos.system.disk")).map((key) => metrics[key]) }/> - key.startsWith("cosmos.system.temp")).map((key) => metrics[key])} - /> - + + key.startsWith("cosmos.system.temp")).map((key) => metrics[key])} + /> + + {/* diff --git a/client/src/pages/servapps/containers/index.jsx b/client/src/pages/servapps/containers/index.jsx index 117f9a2..2ba7f51 100644 --- a/client/src/pages/servapps/containers/index.jsx +++ b/client/src/pages/servapps/containers/index.jsx @@ -17,6 +17,7 @@ import DockerContainerSetup from './setup'; import NetworkContainerSetup from './network'; import VolumeContainerSetup from './volumes'; import DockerTerminal from './terminal'; +import ContainerMetrics from '../../dashboard/containerMetrics'; const ContainerIndex = () => { const { containerName } = useParams(); @@ -59,6 +60,10 @@ const ContainerIndex = () => { title: 'Logs', children: }, + { + title: 'Monitoring', + children: + }, { title: 'Terminal', children: diff --git a/client/src/pages/servapps/containers/overview.jsx b/client/src/pages/servapps/containers/overview.jsx index 3022c14..b623f23 100644 --- a/client/src/pages/servapps/containers/overview.jsx +++ b/client/src/pages/servapps/containers/overview.jsx @@ -9,6 +9,7 @@ import * as API from '../../../api'; import RestartModal from '../../config/users/restart'; import GetActions from '../actionBar'; import { ServAppIcon } from '../../../utils/servapp-icon'; +import MiniPlotComponent from '../../dashboard/components/mini-plot'; const info = { backgroundColor: 'rgba(0, 0, 0, 0.1)', @@ -168,6 +169,17 @@ const ContainerOverview = ({ containerInfo, config, refresh, updatesAvailable, s }} />
+ Monitoring +
+ + +
diff --git a/client/src/pages/servapps/servapps.jsx b/client/src/pages/servapps/servapps.jsx index fc8abea..6fc86c0 100644 --- a/client/src/pages/servapps/servapps.jsx +++ b/client/src/pages/servapps/servapps.jsx @@ -20,6 +20,7 @@ import ResponsiveButton from '../../components/responseiveButton'; import DockerComposeImport from './containers/docker-compose'; import { ContainerNetworkWarning } from '../../components/containers'; import { ServAppIcon } from '../../utils/servapp-icon'; +import MiniPlotComponent from '../dashboard/components/mini-plot'; const Item = styled(Paper)(({ theme }) => ({ backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', @@ -296,6 +297,12 @@ const ServApps = () => { {/* } */} +
+ +
diff --git a/client/src/routes/MainRoutes.jsx b/client/src/routes/MainRoutes.jsx index 84b7f49..bec75ba 100644 --- a/client/src/routes/MainRoutes.jsx +++ b/client/src/routes/MainRoutes.jsx @@ -42,7 +42,7 @@ const MainRoutes = { element: }, { - path: '/cosmos-ui/dashboard', + path: '/cosmos-ui/monitoring', element: }, { diff --git a/client/src/store/reducers/menu.jsx b/client/src/store/reducers/menu.jsx index 00aba2e..048550b 100644 --- a/client/src/store/reducers/menu.jsx +++ b/client/src/store/reducers/menu.jsx @@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; // initial state const initialState = { - openItem: ['dashboard'], + openItem: ['home'], openComponent: 'buttons', drawerOpen: false, componentDrawerOpen: true diff --git a/package.json b/package.json index f0003f7..b5f4f42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.12.0-unstable23", + "version": "0.12.0-unstable24", "description": "", "main": "test-server.js", "bugs": { diff --git a/src/metrics/aggl.go b/src/metrics/aggl.go index 2ba9b63..3a19ae7 100644 --- a/src/metrics/aggl.go +++ b/src/metrics/aggl.go @@ -2,10 +2,12 @@ package metrics import ( "time" + "fmt" + "strings" "github.com/jasonlvhit/gocron" "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/mongo" "github.com/azukaar/cosmos-server/src/utils" ) @@ -30,13 +32,14 @@ type DataDefDB struct { Key string AggloType string Scale int + Unit string } -func AggloMetrics() []DataDefDB { +func AggloMetrics(metricsList []string) []DataDefDB { lock <- true defer func() { <-lock }() - utils.Log("Metrics: Agglomeration started") + utils.Log("Metrics: Agglomeration of metrics") utils.Debug("Time: " + time.Now().String()) @@ -47,8 +50,30 @@ func AggloMetrics() []DataDefDB { } // get all metrics from database + findOpts := map[string]interface{}{ + } + + + // If metricsList is not empty, filter by metrics with wildcard matching + if len(metricsList) > 0 { + // Convert wildcards to regex and store them in an array + var regexPatterns []bson.M + for _, metric := range metricsList { + if strings.Contains(metric, "*") { + // Convert wildcard to regex. Replace * with .* + regexPattern := "^" + strings.ReplaceAll(metric, "*", ".*") + regexPatterns = append(regexPatterns, bson.M{"Key": bson.M{"$regex": regexPattern}}) + } else { + // If there's no wildcard, match the metric directly + regexPatterns = append(regexPatterns, bson.M{"Key": metric}) + } + } + // Use the $or operator to match any of the patterns + findOpts["$or"] = regexPatterns + } + var metrics []DataDefDB - cursor, err := c.Find(nil, map[string]interface{}{}) + cursor, err := c.Find(nil, findOpts) if err != nil { utils.Error("Metrics: Error fetching metrics", err) return []DataDefDB{} @@ -162,21 +187,36 @@ func CommitAggl(metrics []DataDefDB) { defer func() { <-lock }() utils.Log("Metrics: Agglomeration done. Saving to DB") - + c, errCo := utils.GetCollection(utils.GetRootAppId(), "metrics") if errCo != nil { - utils.Error("Metrics - Database Connect", errCo) - return + utils.Error("Metrics - Database Connect", errCo) + return } - // save metrics - for _, metric := range metrics { - options := options.Update().SetUpsert(true) + chunkSize := 100 - _, err := c.UpdateOne(nil, bson.M{"Key": metric.Key}, bson.M{"$set": bson.M{"Values": metric.Values, "ValuesAggl": metric.ValuesAggl}}, options) + for i := 0; i < len(metrics); i += chunkSize { + end := i + chunkSize + if end > len(metrics) { + end = len(metrics) + } + + chunk := metrics[i:end] + models := []mongo.WriteModel{} + + for _, metric := range chunk { + update := mongo.NewUpdateOneModel(). + SetFilter(bson.M{"Key": metric.Key}). + SetUpdate(bson.M{"$set": bson.M{"Values": metric.Values, "ValuesAggl": metric.ValuesAggl}}). + SetUpsert(true) + models = append(models, update) + } + + _, err := c.BulkWrite(nil, models) if err != nil { - utils.Error("Metrics: Error saving metrics", err) + utils.Error(fmt.Sprintf("Metrics: Error saving metrics chunk starting at index %d", i), err) return } } @@ -185,7 +225,11 @@ func CommitAggl(metrics []DataDefDB) { } func AggloAndCommitMetrics() { - CommitAggl(AggloMetrics()) + if utils.GetMainConfig().MonitoringDisabled { + return + } + + CommitAggl(AggloMetrics([]string{})) } func InitAggl() { diff --git a/src/metrics/api.go b/src/metrics/api.go index 1ee0fd7..af40e76 100644 --- a/src/metrics/api.go +++ b/src/metrics/api.go @@ -3,6 +3,7 @@ package metrics import ( "net/http" "encoding/json" + "strings" "github.com/azukaar/cosmos-server/src/utils" ) @@ -12,10 +13,23 @@ func API_GetMetrics(w http.ResponseWriter, req *http.Request) { return } + //get query string "metrics" + query := req.URL.Query() + metrics := query.Get("metrics") + + // split by comma + metricsList := []string{} + if metrics != "" { + metricsList = strings.Split(metrics, ",") + } + + if(req.Method == "GET") { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ "status": "OK", - "data": AggloMetrics(), + "data": AggloMetrics(metricsList), }) } else { utils.Error("MetricsGet: Method not allowed" + req.Method, nil) diff --git a/src/metrics/index.go b/src/metrics/index.go index 4e941ca..ac75406 100644 --- a/src/metrics/index.go +++ b/src/metrics/index.go @@ -17,6 +17,8 @@ type DataDef struct { AggloType string SetOperation string Scale int + Unit string + Decumulate bool } type DataPush struct { @@ -29,6 +31,8 @@ type DataPush struct { AvgIndex int AggloType string Scale int + Unit string + Decumulate bool } var dataBuffer = map[string]DataPush{} @@ -44,6 +48,8 @@ func MergeMetric(SetOperation string, currentValue int, newValue int, avgIndex i } else { return currentValue } + } else if SetOperation == "sum" { + return currentValue + newValue } else if SetOperation == "min" { if newValue < currentValue { return newValue @@ -102,6 +108,7 @@ func SaveMetrics() { "Label": dp.Label, "AggloType": dp.AggloType, "Scale": scale, + "Unit": dp.Unit, }, } @@ -132,8 +139,11 @@ func ModuloTime(start time.Time, modulo time.Duration) time.Time { return time.Unix(0, roundedElapsed) } +var lastInserted = map[string]int{} + func PushSetMetric(key string, value int, def DataDef) { go func() { + originalValue := value key = "cosmos." + key date := ModuloTime(time.Now(), def.Period) cacheKey := key + date.String() @@ -141,9 +151,20 @@ func PushSetMetric(key string, value int, def DataDef) { lock <- true defer func() { <-lock }() + if def.Decumulate { + if lastInserted[key] != 0 { + value = value - lastInserted[key] + } else { + value = 0 + } + } + + if dp, ok := dataBuffer[cacheKey]; ok { + value = MergeMetric(def.SetOperation, dp.Value, value, dp.AvgIndex) + dp.Max = def.Max - dp.Value = MergeMetric(def.SetOperation, dp.Value, value, dp.AvgIndex) + dp.Value = value if def.SetOperation == "avg" { dp.AvgIndex++ } @@ -159,18 +180,29 @@ func PushSetMetric(key string, value int, def DataDef) { Label: def.Label, AggloType: def.AggloType, Scale: def.Scale, + Unit: def.Unit, } } + + lastInserted[key] = originalValue }() } func Run() { utils.Debug("Metrics - Run") - + nextTime := ModuloTime(time.Now().Add(time.Second*30), time.Second*30) nextTime = nextTime.Add(time.Second * 2) - utils.Debug("Metrics - Next run at " + nextTime.String()) + + if utils.GetMainConfig().MonitoringDisabled { + time.AfterFunc(nextTime.Sub(time.Now()), func() { + Run() + }) + + return + } + time.AfterFunc(nextTime.Sub(time.Now()), func() { go func() { GetSystemMetrics() @@ -182,8 +214,12 @@ func Run() { } func Init() { + lastInserted = map[string]int{} + InitAggl() Run() - go GetSystemMetrics() + if !utils.GetMainConfig().MonitoringDisabled { + go GetSystemMetrics() + } } \ No newline at end of file diff --git a/src/metrics/system.go b/src/metrics/system.go index 5d46c17..b9e44a8 100644 --- a/src/metrics/system.go +++ b/src/metrics/system.go @@ -60,6 +60,7 @@ func GetSystemMetrics() { Period: time.Second * 30, Label: "CPU " + strconv.Itoa(i), AggloType: "avg", + Unit: "%", }) } } @@ -78,6 +79,7 @@ func GetSystemMetrics() { Period: time.Second * 30, Label: "RAM", AggloType: "avg", + Unit: "B", }) // Get Network Usage @@ -92,65 +94,40 @@ func GetSystemMetrics() { Max: 0, Period: time.Second * 30, Label: "Network Received", - AggloType: "avg", + SetOperation: "max", + AggloType: "sum", + Decumulate: true, + Unit: "B", }) PushSetMetric("system.netTx", int(netIO[0].BytesSent), DataDef{ Max: 0, Period: time.Second * 30, Label: "Network Sent", - AggloType: "avg", + SetOperation: "max", + AggloType: "sum", + Decumulate: true, + Unit: "B", }) PushSetMetric("system.netErr", int(netIO[0].Errin + netIO[0].Errout), DataDef{ Max: 0, Period: time.Second * 30, Label: "Network Errors", - AggloType: "avg", + SetOperation: "max", + AggloType: "sum", + Decumulate: true, }) PushSetMetric("system.netDrop", int(netIO[0].Dropin + netIO[0].Dropout), DataDef{ Max: 0, Period: time.Second * 30, Label: "Network Drops", - AggloType: "avg", + SetOperation: "max", + AggloType: "sum", + Decumulate: true, }) - // docker stats - dockerStats, err := docker.StatsAll() - if err != nil { - utils.Error("Metrics - Error fetching Docker stats:", err) - return - } - - for _, ds := range dockerStats { - PushSetMetric("system.docker.cpu." + ds.Name, int(ds.CPUUsage), DataDef{ - Max: 100, - Period: time.Second * 30, - Label: "Docker CPU " + ds.Name, - AggloType: "avg", - Scale: 1000, - }) - PushSetMetric("system.docker.ram." + ds.Name, int(ds.MemUsage), DataDef{ - Max: 100, - Period: time.Second * 30, - Label: "Docker RAM " + ds.Name, - AggloType: "avg", - }) - PushSetMetric("system.docker.netRx." + ds.Name, int(ds.NetworkRx), DataDef{ - Max: 0, - Period: time.Second * 30, - Label: "Docker Network Received " + ds.Name, - AggloType: "avg", - }) - PushSetMetric("system.docker.netTx." + ds.Name, int(ds.NetworkTx), DataDef{ - Max: 0, - Period: time.Second * 30, - Label: "Docker Network Sent " + ds.Name, - AggloType: "avg", - }) - } - // Get Disk Usage parts, err := disk.PartitionsWithContext(ctx, true) if err != nil { @@ -202,6 +179,7 @@ func GetSystemMetrics() { Max: 0, Period: time.Second * 30, Label: "Temperature " + temp.SensorKey, + Unit: "°C", }) } } @@ -213,4 +191,48 @@ func GetSystemMetrics() { Label: "Temperature - All", }) } + + + // docker stats + dockerStats, err := docker.StatsAll() + if err != nil { + utils.Error("Metrics - Error fetching Docker stats:", err) + return + } + + for _, ds := range dockerStats { + PushSetMetric("system.docker.cpu." + ds.Name, int(ds.CPUUsage), DataDef{ + Max: 100, + Period: time.Second * 30, + Label: "Docker CPU " + ds.Name, + AggloType: "avg", + Scale: 1000, + Unit: "%", + }) + PushSetMetric("system.docker.ram." + ds.Name, int(ds.MemUsage), DataDef{ + Max: 0, + Period: time.Second * 30, + Label: "Docker RAM " + ds.Name, + AggloType: "avg", + Unit: "B", + }) + PushSetMetric("system.docker.netRx." + ds.Name, int(ds.NetworkRx), DataDef{ + Max: 0, + Period: time.Second * 30, + Label: "Docker Network Received " + ds.Name, + SetOperation: "max", + AggloType: "sum", + Decumulate: true, + Unit: "B", + }) + PushSetMetric("system.docker.netTx." + ds.Name, int(ds.NetworkTx), DataDef{ + Max: 0, + Period: time.Second * 30, + Label: "Docker Network Sent " + ds.Name, + SetOperation: "max", + AggloType: "sum", + Decumulate: true, + Unit: "B", + }) + } } \ No newline at end of file diff --git a/src/utils/types.go b/src/utils/types.go index 4eec5a5..9930af2 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -91,6 +91,7 @@ type Config struct { HomepageConfig HomepageConfig ThemeConfig ThemeConfig ConstellationConfig ConstellationConfig + MonitoringDisabled bool } type HomepageConfig struct {