From 050fe7484bd0ef6e200aac80efd5547f764544a1 Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Mon, 6 Nov 2023 19:57:04 +0000 Subject: [PATCH] [release] v0.12.0-unstable43 --- changelog.md | 2 +- client/src/api/downloadButton.jsx | 3 +- client/src/api/metrics.jsx | 10 + client/src/index.jsx | 17 +- client/src/pages/config/routeConfigPage.jsx | 5 + .../src/pages/config/users/formShortcuts.jsx | 8 +- client/src/pages/config/users/proxyman.jsx | 2 +- .../src/pages/dashboard/IncomeAreaChart.jsx | 121 ------- client/src/pages/dashboard/MetricHeaders.jsx | 88 +++++ .../src/pages/dashboard/MonthlyBarChart.jsx | 85 ----- client/src/pages/dashboard/OrdersTable.jsx | 224 ------------ .../src/pages/dashboard/ReportAreaChart.jsx | 105 ------ .../src/pages/dashboard/SalesColumnChart.jsx | 148 -------- .../src/pages/dashboard/components/plot.jsx | 10 +- .../src/pages/dashboard/components/table.jsx | 2 +- .../src/pages/dashboard/containerMetrics.jsx | 66 ---- client/src/pages/dashboard/eventsExplorer.jsx | 208 +++++++++++ .../dashboard/eventsExplorerStandalone.jsx | 50 +++ client/src/pages/dashboard/index.jsx | 324 +----------------- .../src/pages/servapps/containers/index.jsx | 5 + client/src/themes/palette.jsx | 4 +- client/src/themes/theme/index.jsx | 16 + package-lock.json | 226 ++++++++++-- package.json | 5 +- src/CRON.go | 2 +- src/docker/docker.go | 21 +- src/docker/events.go | 38 +- src/httpServer.go | 59 +++- src/metrics/aggl.go | 2 +- src/metrics/api.go | 72 ++++ src/metrics/events.go | 152 ++++++++ src/metrics/http.go | 59 ---- src/metrics/index.go | 8 +- src/proxy/shield.go | 43 +++ src/utils/db.go | 26 +- src/utils/events.go | 31 ++ src/utils/middleware.go | 9 +- src/utils/utils.go | 1 - 38 files changed, 1062 insertions(+), 1195 deletions(-) delete mode 100644 client/src/pages/dashboard/IncomeAreaChart.jsx create mode 100644 client/src/pages/dashboard/MetricHeaders.jsx delete mode 100644 client/src/pages/dashboard/MonthlyBarChart.jsx delete mode 100644 client/src/pages/dashboard/OrdersTable.jsx delete mode 100644 client/src/pages/dashboard/ReportAreaChart.jsx delete mode 100644 client/src/pages/dashboard/SalesColumnChart.jsx create mode 100644 client/src/pages/dashboard/eventsExplorer.jsx create mode 100644 client/src/pages/dashboard/eventsExplorerStandalone.jsx create mode 100644 src/metrics/events.go create mode 100644 src/utils/events.go diff --git a/changelog.md b/changelog.md index 32a7520..d40a656 100644 --- a/changelog.md +++ b/changelog.md @@ -7,7 +7,7 @@ - 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 - - Added a notification when updating a container + - Added a notification when updating a container, renewing certs, etc... - Improved icon loading speed, and added proper placeholder - 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 diff --git a/client/src/api/downloadButton.jsx b/client/src/api/downloadButton.jsx index 3372739..cd2de27 100644 --- a/client/src/api/downloadButton.jsx +++ b/client/src/api/downloadButton.jsx @@ -2,7 +2,7 @@ import { ArrowDownOutlined } from "@ant-design/icons"; import { Button } from "@mui/material"; import ResponsiveButton from "../components/responseiveButton"; -export const DownloadFile = ({ filename, content, contentGetter, label }) => { +export const DownloadFile = ({ filename, content, contentGetter, label, style }) => { const downloadFile = async () => { // Get the content if (contentGetter) { @@ -39,6 +39,7 @@ export const DownloadFile = ({ filename, content, contentGetter, label }) => { } > diff --git a/client/src/api/metrics.jsx b/client/src/api/metrics.jsx index 5822023..bf75ae2 100644 --- a/client/src/api/metrics.jsx +++ b/client/src/api/metrics.jsx @@ -27,8 +27,18 @@ function list() { })) } +function events(from, to, search = '', query = '', page = '', logLevel) { + return wrap(fetch('/cosmos/api/events?from=' + from + '&to=' + to + '&search=' + search + '&query=' + query + '&page=' + page + '&logLevel=' + logLevel, { + method: 'GET', + headers: { + 'Content-Type': 'application/json' + }, + })) +} + export { get, reset, list, + events }; \ No newline at end of file diff --git a/client/src/index.jsx b/client/src/index.jsx index 8195674..8733d56 100644 --- a/client/src/index.jsx +++ b/client/src/index.jsx @@ -1,6 +1,10 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; // import this if you need to parse custom formats +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; // import this for localized formatting +import 'dayjs/locale/en-gb'; // scroll bar import 'simplebar/src/simplebar.css'; @@ -17,6 +21,13 @@ import './index.css'; import App from './App'; import { store } from './store'; import reportWebVitals from './reportWebVitals'; +import { LocalizationProvider } from '@mui/x-date-pickers'; + +import dayjs from 'dayjs'; +import 'dayjs/locale/en-gb'; +dayjs.extend(customParseFormat); // if needed +dayjs.extend(localizedFormat); // if needed +dayjs.locale('en-gb'); // ==============================|| MAIN - REACT DOM RENDER ||============================== // @@ -25,8 +36,10 @@ const root = createRoot(container); // createRoot(container!) if you use TypeScr root.render( - - + + + + diff --git a/client/src/pages/config/routeConfigPage.jsx b/client/src/pages/config/routeConfigPage.jsx index 021edad..8eb21cb 100644 --- a/client/src/pages/config/routeConfigPage.jsx +++ b/client/src/pages/config/routeConfigPage.jsx @@ -9,6 +9,7 @@ import RouteSecurity from "./routes/routeSecurity"; import RouteOverview from "./routes/routeoverview"; import IsLoggedIn from "../../isLoggedIn"; import RouteMetrics from "../dashboard/routeMonitoring"; +import EventExplorerStandalone from "../dashboard/eventsExplorerStandalone"; const RouteConfigPage = () => { const { routeName } = useParams(); @@ -68,6 +69,10 @@ const RouteConfigPage = () => { title: 'Monitoring', children: }, + { + title: 'Events', + children: + }, ]}/>} {!config &&
diff --git a/client/src/pages/config/users/formShortcuts.jsx b/client/src/pages/config/users/formShortcuts.jsx index 46e6c07..fce7fc8 100644 --- a/client/src/pages/config/users/formShortcuts.jsx +++ b/client/src/pages/config/users/formShortcuts.jsx @@ -148,7 +148,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC } -export const CosmosSelect = ({ name, onChange, label, formik, disabled, options }) => { +export const CosmosSelect = ({ name, onChange, label, formik, disabled, options, style }) => { return {label} @@ -171,6 +171,7 @@ export const CosmosSelect = ({ name, onChange, label, formik, disabled, options helperText={ formik.touched[name] && formik.errors[name] } + style={style} > {options.map((option) => ( @@ -212,8 +213,9 @@ export const CosmosCollapse = ({ children, title }) => { aria-controls="panel1a-content" id="panel1a-header" > - - {title} + + {title} + {children} diff --git a/client/src/pages/config/users/proxyman.jsx b/client/src/pages/config/users/proxyman.jsx index 1407a0c..dc237d7 100644 --- a/client/src/pages/config/users/proxyman.jsx +++ b/client/src/pages/config/users/proxyman.jsx @@ -193,7 +193,7 @@ const ProxyManagement = () => { }, { title: 'Network', screenMin: 'lg', clickable:false, field: (r) => -
+
{ - const theme = useTheme(); - - const { primary, secondary } = theme.palette.text; - const line = theme.palette.divider; - - const [options, setOptions] = useState(areaChartOptions); - - useEffect(() => { - setOptions((prevState) => ({ - ...prevState, - colors: [theme.palette.primary.main, theme.palette.primary[700]], - xaxis: { - categories: - slot === 'month' - ? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - : ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], - labels: { - style: { - colors: [ - secondary, - secondary, - secondary, - secondary, - secondary, - secondary, - secondary, - secondary, - secondary, - secondary, - secondary, - secondary - ] - } - }, - axisBorder: { - show: true, - color: line - }, - tickAmount: slot === 'month' ? 11 : 7 - }, - yaxis: { - labels: { - style: { - colors: [secondary] - } - } - }, - grid: { - borderColor: line - }, - tooltip: { - theme: 'light' - } - })); - }, [primary, secondary, line, theme, slot]); - - const [series, setSeries] = useState([ - { - name: 'Page Views', - data: [0, 86, 28, 115, 48, 210, 136] - }, - { - name: 'Sessions', - data: [0, 43, 14, 56, 24, 105, 68] - } - ]); - - useEffect(() => { - setSeries([ - { - name: 'Page Views', - data: slot === 'month' ? [76, 85, 101, 98, 87, 105, 91, 114, 94, 86, 115, 35] : [31, 40, 28, 51, 42, 109, 100] - }, - { - name: 'Sessions', - data: slot === 'month' ? [110, 60, 150, 35, 60, 36, 26, 45, 65, 52, 53, 41] : [11, 32, 45, 32, 34, 52, 41] - } - ]); - }, [slot]); - - return ; -}; - -IncomeAreaChart.propTypes = { - slot: PropTypes.string -}; - -export default IncomeAreaChart; diff --git a/client/src/pages/dashboard/MetricHeaders.jsx b/client/src/pages/dashboard/MetricHeaders.jsx new file mode 100644 index 0000000..a98f8b2 --- /dev/null +++ b/client/src/pages/dashboard/MetricHeaders.jsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from 'react'; + +// material-ui +import { + Button, + Stack, +} from '@mui/material'; + +import { formatDate } from './components/utils'; + +const MetricHeaders = ({loaded, slot, setSlot, zoom, setZoom}) => { + const resetZoom = () => { + setZoom({ + xaxis: {} + }); + } + + 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 ( + <> + {loaded &&
+ + + + + + {zoom.xaxis.min && } + +
} + + ); +}; + +export default MetricHeaders; diff --git a/client/src/pages/dashboard/MonthlyBarChart.jsx b/client/src/pages/dashboard/MonthlyBarChart.jsx deleted file mode 100644 index 10cd0ad..0000000 --- a/client/src/pages/dashboard/MonthlyBarChart.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import { useEffect, useState } from 'react'; - -// material-ui -import { useTheme } from '@mui/material/styles'; - -// third-party -import ReactApexChart from 'react-apexcharts'; - -// chart options -const barChartOptions = { - chart: { - type: 'bar', - height: 365, - toolbar: { - show: false - } - }, - plotOptions: { - bar: { - columnWidth: '45%', - borderRadius: 4 - } - }, - dataLabels: { - enabled: false - }, - xaxis: { - categories: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'], - axisBorder: { - show: false - }, - axisTicks: { - show: false - } - }, - yaxis: { - show: false - }, - grid: { - show: false - } -}; - -// ==============================|| MONTHLY BAR CHART ||============================== // - -const MonthlyBarChart = () => { - const theme = useTheme(); - - const { primary, secondary } = theme.palette.text; - const info = theme.palette.info.light; - - const [series] = useState([ - { - data: [80, 95, 70, 42, 65, 55, 78] - } - ]); - - const [options, setOptions] = useState(barChartOptions); - - useEffect(() => { - setOptions((prevState) => ({ - ...prevState, - colors: [info], - xaxis: { - labels: { - style: { - colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary] - } - } - }, - tooltip: { - theme: 'light' - } - })); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [primary, info, secondary]); - - return ( -
- -
- ); -}; - -export default MonthlyBarChart; diff --git a/client/src/pages/dashboard/OrdersTable.jsx b/client/src/pages/dashboard/OrdersTable.jsx deleted file mode 100644 index a9a1781..0000000 --- a/client/src/pages/dashboard/OrdersTable.jsx +++ /dev/null @@ -1,224 +0,0 @@ -import PropTypes from 'prop-types'; -import { useState } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; - -// material-ui -import { Box, Link, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material'; - -// third-party -import NumberFormat from 'react-number-format'; - -// project import -import Dot from '../../components/@extended/Dot'; - -function createData(trackingNo, name, fat, carbs, protein) { - return { trackingNo, name, fat, carbs, protein }; -} - -const rows = [ - createData(84564564, 'Camera Lens', 40, 2, 40570), - createData(98764564, 'Laptop', 300, 0, 180139), - createData(98756325, 'Mobile', 355, 1, 90989), - createData(98652366, 'Handset', 50, 1, 10239), - createData(13286564, 'Computer Accessories', 100, 1, 83348), - createData(86739658, 'TV', 99, 0, 410780), - createData(13256498, 'Keyboard', 125, 2, 70999), - createData(98753263, 'Mouse', 89, 2, 10570), - createData(98753275, 'Desktop', 185, 1, 98063), - createData(98753291, 'Chair', 100, 0, 14001) -]; - -function descendingComparator(a, b, orderBy) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; -} - -function getComparator(order, orderBy) { - return order === 'desc' ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy); -} - -function stableSort(array, comparator) { - const stabilizedThis = array.map((el, index) => [el, index]); - stabilizedThis.sort((a, b) => { - const order = comparator(a[0], b[0]); - if (order !== 0) { - return order; - } - return a[1] - b[1]; - }); - return stabilizedThis.map((el) => el[0]); -} - -// ==============================|| ORDER TABLE - HEADER CELL ||============================== // - -const headCells = [ - { - id: 'trackingNo', - align: 'left', - disablePadding: false, - label: 'Tracking No.' - }, - { - id: 'name', - align: 'left', - disablePadding: true, - label: 'Product Name' - }, - { - id: 'fat', - align: 'right', - disablePadding: false, - label: 'Total Order' - }, - { - id: 'carbs', - align: 'left', - disablePadding: false, - - label: 'Status' - }, - { - id: 'protein', - align: 'right', - disablePadding: false, - label: 'Total Amount' - } -]; - -// ==============================|| ORDER TABLE - HEADER ||============================== // - -function OrderTableHead({ order, orderBy }) { - return ( - - - {headCells.map((headCell) => ( - - {headCell.label} - - ))} - - - ); -} - -OrderTableHead.propTypes = { - order: PropTypes.string, - orderBy: PropTypes.string -}; - -// ==============================|| ORDER TABLE - STATUS ||============================== // - -const OrderStatus = ({ status }) => { - let color; - let title; - - switch (status) { - case 0: - color = 'warning'; - title = 'Pending'; - break; - case 1: - color = 'success'; - title = 'Approved'; - break; - case 2: - color = 'error'; - title = 'Rejected'; - break; - default: - color = 'primary'; - title = 'None'; - } - - return ( - - - {title} - - ); -}; - -OrderStatus.propTypes = { - status: PropTypes.number -}; - -// ==============================|| ORDER TABLE ||============================== // - -export default function OrderTable() { - const [order] = useState('asc'); - const [orderBy] = useState('trackingNo'); - const [selected] = useState([]); - - const isSelected = (trackingNo) => selected.indexOf(trackingNo) !== -1; - - return ( - - - - - - {stableSort(rows, getComparator(order, orderBy)).map((row, index) => { - const isItemSelected = isSelected(row.trackingNo); - const labelId = `enhanced-table-checkbox-${index}`; - - return ( - - - - {row.trackingNo} - - - {row.name} - {row.fat} - - - - - - - - ); - })} - -
-
-
- ); -} diff --git a/client/src/pages/dashboard/ReportAreaChart.jsx b/client/src/pages/dashboard/ReportAreaChart.jsx deleted file mode 100644 index ed91490..0000000 --- a/client/src/pages/dashboard/ReportAreaChart.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useEffect, useState } from 'react'; - -// material-ui -import { useTheme } from '@mui/material/styles'; - -// third-party -import ReactApexChart from 'react-apexcharts'; - -// chart options -const areaChartOptions = { - chart: { - height: 340, - type: 'line', - toolbar: { - show: false - } - }, - dataLabels: { - enabled: false - }, - stroke: { - curve: 'smooth', - width: 1.5 - }, - grid: { - strokeDashArray: 4 - }, - xaxis: { - type: 'datetime', - categories: [ - '2018-05-19T00:00:00.000Z', - '2018-06-19T00:00:00.000Z', - '2018-07-19T01:30:00.000Z', - '2018-08-19T02:30:00.000Z', - '2018-09-19T03:30:00.000Z', - '2018-10-19T04:30:00.000Z', - '2018-11-19T05:30:00.000Z', - '2018-12-19T06:30:00.000Z' - ], - labels: { - format: 'MMM' - }, - axisBorder: { - show: false - }, - axisTicks: { - show: false - } - }, - yaxis: { - show: false - }, - tooltip: { - x: { - format: 'MM' - } - } -}; - -// ==============================|| REPORT AREA CHART ||============================== // - -const ReportAreaChart = () => { - const theme = useTheme(); - - const { primary, secondary } = theme.palette.text; - const line = theme.palette.divider; - - const [options, setOptions] = useState(areaChartOptions); - - useEffect(() => { - setOptions((prevState) => ({ - ...prevState, - colors: [theme.palette.warning.main], - xaxis: { - labels: { - style: { - colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary, secondary] - } - } - }, - grid: { - borderColor: line - }, - tooltip: { - theme: 'light' - }, - legend: { - labels: { - colors: 'grey.500' - } - } - })); - }, [primary, secondary, line, theme]); - - const [series] = useState([ - { - name: 'Series 1', - data: [58, 115, 28, 83, 63, 75, 35, 55] - } - ]); - - return ; -}; - -export default ReportAreaChart; diff --git a/client/src/pages/dashboard/SalesColumnChart.jsx b/client/src/pages/dashboard/SalesColumnChart.jsx deleted file mode 100644 index beb789b..0000000 --- a/client/src/pages/dashboard/SalesColumnChart.jsx +++ /dev/null @@ -1,148 +0,0 @@ -import { useEffect, useState } from 'react'; - -// material-ui -import { useTheme } from '@mui/material/styles'; - -// third-party -import ReactApexChart from 'react-apexcharts'; - -// chart options -const columnChartOptions = { - chart: { - type: 'bar', - height: 430, - toolbar: { - show: false - } - }, - plotOptions: { - bar: { - columnWidth: '30%', - borderRadius: 4 - } - }, - dataLabels: { - enabled: false - }, - stroke: { - show: true, - width: 8, - colors: ['transparent'] - }, - xaxis: { - categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] - }, - yaxis: { - title: { - text: '$ (thousands)' - } - }, - fill: { - opacity: 1 - }, - tooltip: { - y: { - formatter(val) { - return `$ ${val} thousands`; - } - } - }, - legend: { - show: true, - fontFamily: `'Public Sans', sans-serif`, - offsetX: 10, - offsetY: 10, - labels: { - useSeriesColors: false - }, - markers: { - width: 16, - height: 16, - radius: '50%', - offsexX: 2, - offsexY: 2 - }, - itemMargin: { - horizontal: 15, - vertical: 50 - } - }, - responsive: [ - { - breakpoint: 600, - options: { - yaxis: { - show: false - } - } - } - ] -}; - -// ==============================|| SALES COLUMN CHART ||============================== // - -const SalesColumnChart = () => { - const theme = useTheme(); - - const { primary, secondary } = theme.palette.text; - const line = theme.palette.divider; - - const warning = theme.palette.warning.main; - const primaryMain = theme.palette.primary.main; - const successDark = theme.palette.success.dark; - - const [series] = useState([ - { - name: 'Net Profit', - data: [180, 90, 135, 114, 120, 145] - }, - { - name: 'Revenue', - data: [120, 45, 78, 150, 168, 99] - } - ]); - - const [options, setOptions] = useState(columnChartOptions); - - useEffect(() => { - setOptions((prevState) => ({ - ...prevState, - colors: [warning, primaryMain], - xaxis: { - labels: { - style: { - colors: [secondary, secondary, secondary, secondary, secondary, secondary] - } - } - }, - yaxis: { - labels: { - style: { - colors: [secondary] - } - } - }, - grid: { - borderColor: line - }, - tooltip: { - theme: 'light' - }, - legend: { - position: 'top', - horizontalAlign: 'right', - labels: { - colors: 'grey.500' - } - } - })); - }, [primary, secondary, line, warning, primaryMain, successDark]); - - return ( -
- -
- ); -}; - -export default SalesColumnChart; diff --git a/client/src/pages/dashboard/components/plot.jsx b/client/src/pages/dashboard/components/plot.jsx index ed5068a..91bee58 100644 --- a/client/src/pages/dashboard/components/plot.jsx +++ b/client/src/pages/dashboard/components/plot.jsx @@ -73,8 +73,13 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z name: serie.Label, dataAxis: xAxis.map((date) => { if(slot === 'latest') { - return serie.Values[serie.Values.length - 1 - date] ? - serie.Values[serie.Values.length - 1 - date].Value : + // let old = serie.Values.length - 1 - date; + // let realIndex = parseInt(serie.Values.length / xAxis.length * date); + // let currentIndex = serie.Values.length - 1 - realIndex; + let index = serie.Values.length - 1 - parseInt(date / serie.TimeScale) + + return serie.Values[index] ? + serie.Values[index].Value : 0; } else { let key = slot === 'hourly' ? "hour_" : "day_"; @@ -129,6 +134,7 @@ const PlotComponent = ({ title, slot, data, SimpleDesign, withSelector, xAxis, z text: thisdata && thisdata.Label, }, max: (thisdata && thisdata.Max) ? thisdata.Max : undefined, + min: (thisdata && thisdata.Max) ? 0 : undefined, })), grid: { borderColor: line diff --git a/client/src/pages/dashboard/components/table.jsx b/client/src/pages/dashboard/components/table.jsx index cd6694f..66b2599 100644 --- a/client/src/pages/dashboard/components/table.jsx +++ b/client/src/pages/dashboard/components/table.jsx @@ -173,7 +173,7 @@ const TableComponent = ({ title, data, displayMax, render, xAxis, slot, zoom}) = return 0; } } else { - let realIndex = item.Values.length - 1 - date + let realIndex = item.Values.length - 1 - parseInt(date / item.TimeScale) return item.Values[realIndex] ? item.Values[realIndex].Value : 0; } }) diff --git a/client/src/pages/dashboard/containerMetrics.jsx b/client/src/pages/dashboard/containerMetrics.jsx index a107a54..d099860 100644 --- a/client/src/pages/dashboard/containerMetrics.jsx +++ b/client/src/pages/dashboard/containerMetrics.jsx @@ -2,86 +2,21 @@ 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({ @@ -90,7 +25,6 @@ const ContainerMetrics = ({containerName}) => { const [coStatus, setCoStatus] = useState(null); const [metrics, setMetrics] = useState(null); - const [isCreatingDB, setIsCreatingDB] = useState(false); const resetZoom = () => { setZoom({ diff --git a/client/src/pages/dashboard/eventsExplorer.jsx b/client/src/pages/dashboard/eventsExplorer.jsx new file mode 100644 index 0000000..e94a337 --- /dev/null +++ b/client/src/pages/dashboard/eventsExplorer.jsx @@ -0,0 +1,208 @@ +import { useEffect, useState } from "react"; +import { toUTC } from "./components/utils"; +import * as API from '../../api'; +import { Button, CircularProgress, Stack, TextField } from "@mui/material"; +import { CosmosCollapse, CosmosSelect } from "../config/users/formShortcuts"; +import MainCard from '../../components/MainCard'; +import * as timeago from 'timeago.js'; +import { ExclamationOutlined, SettingOutlined } from "@ant-design/icons"; +import { Alert } from "@mui/material"; +import { DownloadFile } from "../../api/downloadButton"; + +const EventsExplorer = ({from, to, xAxis, zoom, slot, initLevel, initSearch = ''}) => { + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [search, setSearch] = useState(initSearch); + const [debouncedSearch, setDebouncedSearch] = useState(search); + const [total, setTotal] = useState(0); + const [remains, setRemains] = useState(0); + const [page, setPage] = useState(0); + const [logLevel, setLogLevel] = useState(initLevel || 'success'); + + if(typeof from != 'undefined' && typeof to != 'undefined') { + from = new Date(from); + to = new Date(to); + } else { + const zoomedXAxis = xAxis + .filter((date, index) => { + if (zoom && zoom.xaxis && zoom.xaxis.min && zoom.xaxis.max) { + return index >= zoom.xaxis.min && index <= zoom.xaxis.max; + } + return true; + }) + .map((date) => { + if (slot === 'hourly' || slot === 'daily') { + let k = toUTC(date, slot === 'hourly'); + return k; + } else { + let realIndex = xAxis.length - 1 - date + return realIndex; + } + }) + + const firstItem = zoomedXAxis[0]; + const lastItem = zoomedXAxis[zoomedXAxis.length - 1]; + + if (slot === 'hourly' || slot === 'daily') { + from = new Date(firstItem); + to = new Date(lastItem); + + if (slot === 'daily') { + to.setHours(23); + to.setMinutes(59); + to.setSeconds(59); + to.setMilliseconds(999); + } else { + to.setMinutes(to.getMinutes() + 59); + to.setSeconds(to.getSeconds() + 59); + to.setMilliseconds(to.getMilliseconds() + 999); + } + } else { + const now = new Date(); + // round to 30 seconds + now.setSeconds(now.getSeconds() - now.getSeconds() % 30); + // remove microseconds + now.setMilliseconds(0); + + from = new Date(now.getTime() - lastItem * 30000); + to = new Date(now.getTime() - firstItem * 30000); + + // add 29 seconds to the end + to.setSeconds(to.getSeconds() + 29); + } + } + + const refresh = (_page) => { + setLoading(true); + let _search = debouncedSearch; + let _query = ""; + if (_search.startsWith('{') || _search.startsWith('[')) { + _search = "" + _query = debouncedSearch; + } + return API.metrics.events(from.toISOString(), to.toISOString(), _search, _query, _page, logLevel).then((res) => { + setEvents(res.data); + setLoading(false); + setTotal(res.total); + setRemains(res.total - res.data.length); + }); + } + + useEffect(() => { + setPage(0); + if (debouncedSearch.length === 0 || debouncedSearch.length > 2) { + refresh(""); + } + }, [debouncedSearch, xAxis, zoom, slot, logLevel]); + + useEffect(() => { + // Set a timeout to update debounced search after 1 second + const handler = setTimeout(() => { + setDebouncedSearch(search); + }, 500); + + // Clear the timeout if search changes before the 1 second has passed + return () => { + clearTimeout(handler); + }; + }, [search]); // Only re-run if search changes + + useEffect(() => { + setLoading(true); + refresh(page); + }, [page]); + + return (
+ + +
+ +
+
+ +
+
+ {}, + handleChange: () => {} + }} + options={[ + ['debug', 'Debug'], + ['info', 'Info'], + ['success', 'Success'], + ['warning', 'Warning'], + ['important', 'Important'], + ['error', 'Error'], + ]} + onChange={(e) => { + setLogLevel(e.target.value); + }} + style={{ + width: '100px', + margin:0, + }} + /> +
+ setSearch(e.target.value)} placeholder='Search (text or bson)' /> +
+
+ {total} events found from {from.toLocaleString()} to {to.toLocaleString()} +
+
+ {events && + {events.map((event) => { + return
+ : event.level == "important" ? : undefined + }> +
{event.label}
+
{(new Date(event.date)).toLocaleString()} - {timeago.format(event.date)}
+
{event.eventId} - {event.object}
+ }> +
+
+										{JSON.stringify(event, null, 2)}
+									
+
+
+
+ })} + {loading &&
+ +
} + {!loading && (remains > 0) && + + } +
} +
+
+ +
); +} + +export default EventsExplorer; + +const eventStyle = (event) => ({ + padding: 4, + borderRadius: 4, +}); \ No newline at end of file diff --git a/client/src/pages/dashboard/eventsExplorerStandalone.jsx b/client/src/pages/dashboard/eventsExplorerStandalone.jsx new file mode 100644 index 0000000..eaebd6b --- /dev/null +++ b/client/src/pages/dashboard/eventsExplorerStandalone.jsx @@ -0,0 +1,50 @@ +import { useEffect, useRef, useState } from 'react'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; // import this for localized formatting +import 'dayjs/locale/en-gb'; + +// material-ui +import { + Grid, + Stack, + Typography, +} from '@mui/material'; + + +import EventsExplorer from './eventsExplorer'; +import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; + +import dayjs from 'dayjs'; + +dayjs.extend(localizedFormat); // if needed +dayjs.locale('en-gb'); + +const EventExplorerStandalone = ({initSearch, initLevel}) => { + // one hour ago + const now = dayjs(); + const [from, setFrom] = useState(now.subtract(1, 'hour')); + const [to, setTo] = useState(now) + + return ( + <> +
+ + + Events + + + setFrom(e)} /> + setTo(e)} /> + + + + + + + + +
+ + ); +}; + +export default EventExplorerStandalone; diff --git a/client/src/pages/dashboard/index.jsx b/client/src/pages/dashboard/index.jsx index 669a50e..3d09506 100644 --- a/client/src/pages/dashboard/index.jsx +++ b/client/src/pages/dashboard/index.jsx @@ -7,83 +7,22 @@ import { 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'; -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 = { - 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 ||============================== // +import EventsExplorer from './eventsExplorer'; +import MetricHeaders from './MetricHeaders'; const DashboardDefault = () => { const [value, setValue] = useState('today'); @@ -179,8 +118,6 @@ const DashboardDefault = () => { return ( <> - {/* - */} {!metrics && { Server Monitoring - {currentTab <= 2 && - - - - - {zoom.xaxis.min && } - } + {currentTab <= 2 && } {currentTab > 2 &&
}
@@ -262,7 +161,7 @@ const DashboardDefault = () => { }, { title: 'Events', - children: + children: }, { title: 'Alerts', @@ -271,221 +170,6 @@ const DashboardDefault = () => { ]} />
- - - {/* - - Dashboard - - - - - - - - - - - - - - - - */} - {/* - - - - Recent Orders - - - - - - - - - - - - Analytics Report - - - - - - - - +45.14% - - - - 0.58% - - - - Low - - - - - - - - - - Sales Report - - - setValue(e.target.value)} - sx={{ '& .MuiInputBase-input': { py: 0.5, fontSize: '0.875rem' } }} - > - {status.map((option) => ( - - {option.label} - - ))} - - - - - - - Net Profit - - $1560 - - - - - - - - Transaction History - - - - - - - - - - - - Order #002434} secondary="Today, 2:00 AM" /> - - - - + $1,430 - - - 78% - - - - - - - - - - - Order #984947} - secondary="5 August, 1:45 PM" - /> - - - - + $302 - - - 8% - - - - - - - - - - - Order #988784} secondary="7 hours ago" /> - - - - + $682 - - - 16% - - - - - - - - - - - - - Help & Support Chat - - - Typical replay within 5 min - - - - - - - - - - - - - - - - - */}
} diff --git a/client/src/pages/servapps/containers/index.jsx b/client/src/pages/servapps/containers/index.jsx index 2ba7f51..574d961 100644 --- a/client/src/pages/servapps/containers/index.jsx +++ b/client/src/pages/servapps/containers/index.jsx @@ -18,6 +18,7 @@ import NetworkContainerSetup from './network'; import VolumeContainerSetup from './volumes'; import DockerTerminal from './terminal'; import ContainerMetrics from '../../dashboard/containerMetrics'; +import EventExplorerStandalone from '../../dashboard/eventsExplorerStandalone'; const ContainerIndex = () => { const { containerName } = useParams(); @@ -64,6 +65,10 @@ const ContainerIndex = () => { title: 'Monitoring', children: }, + { + title: 'Events', + children: + }, { title: 'Terminal', children: diff --git a/client/src/themes/palette.jsx b/client/src/themes/palette.jsx index 77be183..4e8208f 100644 --- a/client/src/themes/palette.jsx +++ b/client/src/themes/palette.jsx @@ -62,7 +62,7 @@ const Palette = (mode, PrimaryColor, SecondaryColor) => { paper: paletteColor.grey[700], default: paletteColor.grey[800] } - } + }, } : { palette: { mode, @@ -84,7 +84,7 @@ const Palette = (mode, PrimaryColor, SecondaryColor) => { paper: paletteColor.grey[0], default: paletteColor.grey.A50 } - } + }, }); }; diff --git a/client/src/themes/theme/index.jsx b/client/src/themes/theme/index.jsx index 934fd48..8021d6c 100644 --- a/client/src/themes/theme/index.jsx +++ b/client/src/themes/theme/index.jsx @@ -64,6 +64,22 @@ const Theme = (colors, darkMode) => { darker: green[9], contrastText }, + debug: { + lighter: grey[0], + light: grey[3], + main: grey[5], + dark: grey[7], + darker: grey[9], + contrastText + }, + important: { + lighter: pink['100'], + light: pink['200'], + main: pink['400'], + dark: pink['700'], + darker: pink['800'], + contrastText + }, grey: greyColors }; }; diff --git a/package-lock.json b/package-lock.json index 9ed5998..4a25ed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cosmos-server", - "version": "0.12.0-unstable40", + "version": "0.12.0-unstable42", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cosmos-server", - "version": "0.12.0-unstable40", + "version": "0.12.0-unstable42", "dependencies": { "@ant-design/colors": "^6.0.0", "@ant-design/icons": "^4.7.0", @@ -16,6 +16,7 @@ "@jamesives/github-sponsors-readme-action": "^1.2.2", "@mui/lab": "^5.0.0-alpha.100", "@mui/material": "^5.10.6", + "@mui/x-date-pickers": "^6.18.0", "@reduxjs/toolkit": "^1.8.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -24,6 +25,8 @@ "apexcharts": "^3.35.5", "bcryptjs": "^2.4.3", "browserslist": "^4.21.7", + "date-fns": "^2.30.0", + "dayjs": "^1.11.10", "dot": "^1.1.3", "express": "^4.18.2", "formik": "^2.2.9", @@ -2104,11 +2107,11 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.22.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", - "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { - "regenerator-runtime": "^0.13.11" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -2127,6 +2130,11 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/runtime/node_modules/regenerator-runtime": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + }, "node_modules/@babel/template": { "version": "7.21.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.21.9.tgz", @@ -2732,6 +2740,40 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", + "dependencies": { + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", + "dependencies": { + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz", + "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==", + "dependencies": { + "@floating-ui/dom": "^1.5.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", @@ -3270,11 +3312,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", - "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.8.tgz", + "integrity": "sha512-9u0ji+xspl96WPqvrYJF/iO+1tQ1L5GTaDOeG3vCR893yy7VcWwRNiVMmPdPNpMDqx0WV1wtEW9OMwK9acWJzQ==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3283,13 +3325,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.13.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.1.tgz", - "integrity": "sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A==", + "version": "5.14.17", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.17.tgz", + "integrity": "sha512-yxnWgSS4J6DMFPw2Dof85yBkG02VTbEiqsikymMsnZnXDurtVGTIhlNuV24GTmFTuJMzEyTTU9UF+O7zaL8LEQ==", "dependencies": { - "@babel/runtime": "^7.21.0", - "@types/prop-types": "^15.7.5", - "@types/react-is": "^18.2.0", + "@babel/runtime": "^7.23.2", + "@types/prop-types": "^15.7.9", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -3301,7 +3342,117 @@ "url": "https://opencollective.com/mui" }, "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", "react": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.0.tgz", + "integrity": "sha512-y4UlkHQXiNRfb6FWQ/GWir0sZ+9kL+GEEZssG+XWP3KJ+d3lONRteusl4AJkYJBdIAOh+5LnMV9RAQKq9Sl7yw==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.8.6", + "@mui/system": "^5.8.0", + "date-fns": "^2.25.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/@mui/base": { + "version": "5.0.0-beta.23", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.23.tgz", + "integrity": "sha512-9L8SQUGAWtd/Qi7Qem26+oSSgpY7f2iQTuvcz/rsGpyZjSomMMO6lwYeQSA0CpWM7+aN7eGoSY/WV6wxJiIxXw==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@floating-ui/react-dom": "^2.0.2", + "@mui/types": "^7.2.8", + "@mui/utils": "^5.14.17", + "@popperjs/core": "^2.11.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/x-date-pickers/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "engines": { + "node": ">=6" } }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { @@ -3816,9 +3967,9 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, "node_modules/@types/react": { "version": "18.2.8", @@ -3838,18 +3989,10 @@ "@types/react": "*" } }, - "node_modules/@types/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-1vz2yObaQkLL7YFe/pme2cpvDsCwI1WXIfL+5eLz0MI9gFG24Re16RzUsI8t9XZn9ZWvgLNDrJBmrqXJO7GNQQ==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/react-transition-group": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", - "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.8.tgz", + "integrity": "sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg==", "dependencies": { "@types/react": "*" } @@ -5011,6 +5154,26 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "dev": true }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9308,7 +9471,8 @@ "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true }, "node_modules/regenerator-transform": { "version": "0.15.1", diff --git a/package.json b/package.json index d84b8c0..14ad43b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cosmos-server", - "version": "0.12.0-unstable42", + "version": "0.12.0-unstable43", "description": "", "main": "test-server.js", "bugs": { @@ -16,6 +16,7 @@ "@jamesives/github-sponsors-readme-action": "^1.2.2", "@mui/lab": "^5.0.0-alpha.100", "@mui/material": "^5.10.6", + "@mui/x-date-pickers": "^6.18.0", "@reduxjs/toolkit": "^1.8.5", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", @@ -24,6 +25,8 @@ "apexcharts": "^3.35.5", "bcryptjs": "^2.4.3", "browserslist": "^4.21.7", + "date-fns": "^2.30.0", + "dayjs": "^1.11.10", "dot": "^1.1.3", "express": "^4.18.2", "formik": "^2.2.9", diff --git a/src/CRON.go b/src/CRON.go index a817175..9d66655 100644 --- a/src/CRON.go +++ b/src/CRON.go @@ -109,7 +109,7 @@ func checkCerts() { HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["SELFSIGNED"] || HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) { utils.Log("Checking certificates for renewal") - if !CertificateIsValid(HTTPConfig.TLSValidUntil) { + if !CertificateIsExpiredSoon(HTTPConfig.TLSValidUntil) { utils.Log("Certificates are not valid anymore, renewing") RestartServer() } diff --git a/src/docker/docker.go b/src/docker/docker.go index 04a3cbe..2e972a7 100644 --- a/src/docker/docker.go +++ b/src/docker/docker.go @@ -537,6 +537,15 @@ func CheckUpdatesAvailable() map[string]bool { } if needsUpdate && HasAutoUpdateOn(fullContainer) { + utils.TriggerEvent( + "cosmos.docker.container.update", + "Cosmos Container Update", + "success", + "", + map[string]interface{}{ + "container": container.Names[0][1:], + }) + utils.WriteNotification(utils.Notification{ Recipient: "admin", Title: "Container Update", @@ -612,6 +621,16 @@ func SelfRecreate() error { Services: map[string]ContainerCreateRequestContainer {}, } + utils.TriggerEvent( + "cosmos.internal.self-updater", + "Cosmos Self Updater", + "important", + "", + map[string]interface{}{ + "action": "recreate", + "container": containerName, + }) + service.Services["cosmos-self-updater-agent"] = ContainerCreateRequestContainer{ Name: "cosmos-self-updater-agent", Image: "azukaar/docker-self-updater:" + version, @@ -756,7 +775,7 @@ func Stats(container types.Container) (ContainerStats, error) { utils.Error("StatsAll", err) return nil, err } - + var containerStatsList []ContainerStats var wg sync.WaitGroup semaphore := make(chan struct{}, 5) // A channel with a buffer size of 5 for controlling parallelism. diff --git a/src/docker/events.go b/src/docker/events.go index b79156b..92ee1be 100644 --- a/src/docker/events.go +++ b/src/docker/events.go @@ -35,7 +35,7 @@ func DockerListenEvents() error { msgs, errs = DockerClient.Events(context.Background(), types.EventsOptions{}) case msg := <-msgs: - utils.Debug("Docker Event: " + msg.Type + " " + msg.Action + " " + msg.Actor.ID) + utils.Debug("Docker Event: " + msg.Type + " " + msg.Action + " " + msg.Actor.Attributes["name"]) if msg.Type == "container" && msg.Action == "start" { onDockerStarted(msg.Actor.ID) } @@ -59,6 +59,42 @@ func DockerListenEvents() error { if msg.Type == "network" && msg.Action == "connect" { onNetworkConnect(msg.Actor.ID) } + + level := "info" + if msg.Type == "image" { + level = "debug" + } + if msg.Action == "destroy" || msg.Action == "delete" || msg.Action == "kill" || msg.Action == "die" { + level = "warning" + } + if msg.Action == "create" || msg.Action == "start" { + level = "success" + } + + object := "" + if msg.Type == "container" { + object = "container@" + msg.Actor.Attributes["name"] + } else if msg.Type == "network" { + object = "network@" + msg.Actor.Attributes["name"] + } else if msg.Type == "image" { + object = "image@" + msg.Actor.Attributes["name"] + } else if msg.Type == "volume" && msg.Actor.Attributes["name"] != "" { + object = "volume@" + msg.Actor.Attributes["name"] + } + + utils.TriggerEvent( + "cosmos.docker.event." + msg.Type + "." + msg.Action, + "Docker Event " + msg.Type + " " + msg.Action, + level, + object, + map[string]interface{}{ + "Type": msg.Type, + "Action": msg.Action, + "Actor": msg.Actor, + "Status": msg.Status, + "From": msg.From, + "Scope": msg.Scope, + }) } } }() diff --git a/src/httpServer.go b/src/httpServer.go index 537a962..b9dffb4 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -175,9 +175,26 @@ func SecureAPI(userRouter *mux.Router, public bool) { )) } -func CertificateIsValid(validUntil time.Time) bool { +func CertificateIsExpiredSoon(validUntil time.Time) bool { // allow 5 days of leeway - isValid := time.Now().Add(5 * 24 * time.Hour).Before(validUntil) + isValid := time.Now().Add(45 * 24 * time.Hour).Before(validUntil) + if !isValid { + utils.TriggerEvent( + "cosmos.proxy.certificate", + "Cosmos Certificate Expire Soon", + "warning", + "", + map[string]interface{}{ + }) + + utils.Log("Certificate is not valid anymore. Needs refresh") + } + return isValid +} + +func CertificateIsExpired(validUntil time.Time) bool { + // allow 5 days of leeway + isValid := time.Now().Before(validUntil) if !isValid { utils.Log("Certificate is not valid anymore. Needs refresh") } @@ -200,13 +217,13 @@ func InitServer() *mux.Router { oldDomains := baseMainConfig.HTTPConfig.TLSKeyHostsCached falledBack := false - NeedsRefresh := baseMainConfig.HTTPConfig.ForceHTTPSCertificateRenewal || (tlsCert == "" || tlsKey == "") || utils.HasAnyNewItem(domains, oldDomains) || !CertificateIsValid(baseMainConfig.HTTPConfig.TLSValidUntil) + NeedsRefresh := baseMainConfig.HTTPConfig.ForceHTTPSCertificateRenewal || (tlsCert == "" || tlsKey == "") || utils.HasAnyNewItem(domains, oldDomains) || !CertificateIsExpiredSoon(baseMainConfig.HTTPConfig.TLSValidUntil) // If we have a certificate, we can fallback to it if necessary CanFallback := tlsCert != "" && tlsKey != "" && len(config.HTTPConfig.TLSKeyHostsCached) > 0 && config.HTTPConfig.TLSKeyHostsCached[0] == config.HTTPConfig.Hostname && - CertificateIsValid(baseMainConfig.HTTPConfig.TLSValidUntil) + CertificateIsExpired(baseMainConfig.HTTPConfig.TLSValidUntil) if(NeedsRefresh && config.HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) { if(config.HTTPConfig.DNSChallengeProvider != "") { @@ -237,6 +254,22 @@ func InitServer() *mux.Router { utils.SetBaseMainConfig(baseMainConfig) utils.Log("Saved new LETSENCRYPT TLS certificate") + utils.TriggerEvent( + "cosmos.proxy.certificate", + "Cosmos Certificate Renewed", + "important", + "", + map[string]interface{}{ + "domains": domains, + }) + + utils.WriteNotification(utils.Notification{ + Recipient: "admin", + Title: "Cosmos Certificate Renewed", + Message: "The TLS certificate for the following domains has been renewed: " + strings.Join(domains, ", "), + Level: "info", + }) + tlsCert = pub tlsKey = priv } @@ -255,6 +288,22 @@ func InitServer() *mux.Router { utils.SetBaseMainConfig(baseMainConfig) utils.Log("Saved new SELFISGNED TLS certificate") + + utils.TriggerEvent( + "cosmos.proxy.certificate", + "Cosmos Certificate Renewed", + "important", + "", + map[string]interface{}{ + "domains": domains, + }) + + utils.WriteNotification(utils.Notification{ + Recipient: "admin", + Title: "Cosmos Certificate Renewed", + Message: "The TLS certificate for the following domains has been renewed: " + strings.Join(domains, ", "), + Level: "info", + }) } tlsCert = pub @@ -351,6 +400,8 @@ func InitServer() *mux.Router { srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs) srapi.HandleFunc("/api/constellation/block", constellation.DeviceBlock) + srapi.HandleFunc("/api/events", metrics.API_ListEvents) + srapi.HandleFunc("/api/metrics", metrics.API_GetMetrics) srapi.HandleFunc("/api/reset-metrics", metrics.API_ResetMetrics) srapi.HandleFunc("/api/list-metrics", metrics.ListMetrics) diff --git a/src/metrics/aggl.go b/src/metrics/aggl.go index fc9d5b5..2969f3d 100644 --- a/src/metrics/aggl.go +++ b/src/metrics/aggl.go @@ -16,7 +16,6 @@ type DataDefDBEntry struct { Date time.Time Value int Processed bool - // For agglomeration AvgIndex int AggloTo time.Time @@ -27,6 +26,7 @@ type DataDefDB struct { Values []DataDefDBEntry ValuesAggl map[string]DataDefDBEntry LastUpdate time.Time + TimeScale float64 Max uint64 Label string Key string diff --git a/src/metrics/api.go b/src/metrics/api.go index af40e76..4dad6c5 100644 --- a/src/metrics/api.go +++ b/src/metrics/api.go @@ -4,6 +4,9 @@ import ( "net/http" "encoding/json" "strings" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo/options" "github.com/azukaar/cosmos-server/src/utils" ) @@ -58,6 +61,21 @@ func API_ResetMetrics(w http.ResponseWriter, req *http.Request) { return } + c, errCo = utils.GetCollection(utils.GetRootAppId(), "events") + if errCo != nil { + utils.Error("MetricsReset: Database error" , errCo) + utils.HTTPError(w, "Database error ", http.StatusMethodNotAllowed, "HTTP001") + return + } + + // delete all metrics from database + _, err = c.DeleteMany(nil, map[string]interface{}{}) + if err != nil { + utils.Error("MetricsReset: Database error ", err) + utils.HTTPError(w, "Database error ", http.StatusMethodNotAllowed, "HTTP001") + return + } + if(req.Method == "GET") { json.NewEncoder(w).Encode(map[string]interface{}{ "status": "OK", @@ -67,4 +85,58 @@ func API_ResetMetrics(w http.ResponseWriter, req *http.Request) { utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") return } +} + +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 + } } \ No newline at end of file diff --git a/src/metrics/events.go b/src/metrics/events.go new file mode 100644 index 0000000..6d89602 --- /dev/null +++ b/src/metrics/events.go @@ -0,0 +1,152 @@ +package metrics + +import ( + "net/http" + "encoding/json" + "time" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" + + "github.com/azukaar/cosmos-server/src/utils" +) + +type Event struct { + Id primitive.ObjectID `json:"id" bson:"_id"` + Label string `json:"label" bson:"label"` + Application string `json:"application" bson:"application"` + EventId string `json:"eventId" bson:"eventId"` + Date time.Time `json:"date" bson:"date"` + Level string `json:"level" bson:"level"` + Data map[string]interface{} `json:"data" bson:"data"` + Object string `json:"object" bson:"object"` +} + +func API_ListEvents(w http.ResponseWriter, req *http.Request) { + if utils.AdminOnly(w, req) != nil { + return + } + + if(req.Method == "GET") { + + query := req.URL.Query() + from, errF := time.Parse("2006-01-02T15:04:05Z", query.Get("from")) + if errF != nil { + utils.Error("events: Error while parsing from date", errF) + } + to, errF := time.Parse("2006-01-02T15:04:05Z", query.Get("to")) + if errF != nil { + utils.Error("events: Error while parsing from date", errF) + } + + logLevel := query.Get("logLevel") + if logLevel == "" { + logLevel = "info" + } + search := query.Get("search") + dbQuery := query.Get("query") + + page := query.Get("page") + var pageId primitive.ObjectID + if page != "" { + pageId, _ = primitive.ObjectIDFromHex(page) + } + + // decode to bson + dbQueryBson := bson.M{} + if dbQuery != "" { + err := bson.UnmarshalExtJSON([]byte(dbQuery), true, &dbQueryBson) + if err != nil { + utils.Error("events: Error while parsing query " + dbQuery, err) + utils.HTTPError(w, "events Get Error", http.StatusInternalServerError, "UD001") + return + } + } else if search != "" { + dbQueryBson["$text"] = bson.M{ + "$search": search, + } + } + + + // merge date query into dbQueryBson + if dbQueryBson["date"] == nil { + dbQueryBson["date"] = bson.M{} + } + dbQueryBson["date"].(bson.M)["$gte"] = from + dbQueryBson["date"].(bson.M)["$lte"] = to + + if logLevel != "" { + if dbQueryBson["level"] == nil { + dbQueryBson["level"] = bson.M{} + } + levels := []string{"error"} + if logLevel == "debug" { + levels = []string{"debug", "info", "warning", "error", "important", "success"} + } else if logLevel == "info" { + levels = []string{"info", "warning", "error", "important", "success"} + } else if logLevel == "success" { + levels = []string{"warning", "error", "important", "success"} + } else if logLevel == "warning" { + levels = []string{"warning", "error", "important"} + } else if logLevel == "important" { + levels = []string{"important", "error"} + } + dbQueryBson["level"].(bson.M)["$in"] = levels + } + + if pageId != primitive.NilObjectID { + dbQueryBson["_id"] = bson.M{ + "$lt": pageId, + } + } + + c, errCo := utils.GetCollection(utils.GetRootAppId(), "events") + if errCo != nil { + utils.Error("Database Connect", errCo) + utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001") + return + } + + events := []Event{} + + limit := int64(50) + opts := options.Find().SetLimit(limit).SetSort(bson.D{{"date", -1}}) + // .SetProjection(bson.D{{"_id", 1}, {"eventId", 1}, {"date", 1}, {"level", 1}, {"data", 1}}) + + cursor, err := c.Find(nil, dbQueryBson, opts) + + if err != nil { + utils.Error("events: Error while getting events", err) + utils.HTTPError(w, "events Get Error", http.StatusInternalServerError, "UD001") + return + } + + defer cursor.Close(nil) + + if err = cursor.All(nil, &events); err != nil { + utils.Error("events: Error while decoding events", err) + utils.HTTPError(w, "events decode Error", http.StatusInternalServerError, "UD002") + return + } + + totalCount, err := c.CountDocuments(nil, dbQueryBson) + if err != nil { + utils.Error("events: Error while counting events", err) + utils.HTTPError(w, "events count Error", http.StatusInternalServerError, "UD003") + return + } + + w.Header().Set("Content-Type", "application/json") + + json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "OK", + "total": totalCount, + "data": events, + }) + } else { + utils.Error("events: Method not allowed" + req.Method, nil) + utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") + return + } +} \ No newline at end of file diff --git a/src/metrics/http.go b/src/metrics/http.go index 2889f73..7f45069 100644 --- a/src/metrics/http.go +++ b/src/metrics/http.go @@ -2,11 +2,6 @@ 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" ) @@ -118,57 +113,3 @@ func PushShieldMetrics(reason string) { 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 - } -} \ No newline at end of file diff --git a/src/metrics/index.go b/src/metrics/index.go index 23b6f9f..17a0659 100644 --- a/src/metrics/index.go +++ b/src/metrics/index.go @@ -27,6 +27,7 @@ type DataPush struct { Key string Value int Max uint64 + Period time.Duration Expire time.Time Label string AvgIndex int @@ -112,6 +113,7 @@ func SaveMetrics() { "Scale": scale, "Unit": dp.Unit, "Object": dp.Object, + "TimeScale": float64(dp.Period / (time.Second * 30)), }, } @@ -191,6 +193,7 @@ func PushSetMetric(key string, value int, def DataDef) { Scale: def.Scale, Unit: def.Unit, Object: def.Object, + Period: def.Period, } } @@ -199,11 +202,10 @@ func PushSetMetric(key string, value int, def DataDef) { } 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()) + + utils.Debug("Metrics - Run - Next run at " + nextTime.String()) if utils.GetMainConfig().MonitoringDisabled { time.AfterFunc(nextTime.Sub(time.Now()), func() { diff --git a/src/proxy/shield.go b/src/proxy/shield.go index e90d441..113b67b 100644 --- a/src/proxy/shield.go +++ b/src/proxy/shield.go @@ -319,6 +319,27 @@ func SmartShieldMiddleware(shieldID string, route utils.ProxyRouteConfig) func(h wrapper.TimeEnded = time.Now() wrapper.isOver = true + statusText := "success" + level := "info" + if wrapper.Status >= 400 { + statusText = "error" + level = "warning" + } + + utils.TriggerEvent( + "cosmos.proxy.response." + route.Name + "." + statusText, + "Proxy Response " + route.Name + " " + statusText, + level, + "route@" + route.Name, + map[string]interface{}{ + "Route": route.Name, + "Status": wrapper.Status, + "Method": wrapper.Method, + "ClientID": wrapper.ClientID, + "Time": wrapper.TimeEnded.Sub(wrapper.TimeStarted).Seconds(), + "Bytes": wrapper.Bytes, + }) + go metrics.PushRequestMetrics(route, wrapper.Status, wrapper.TimeStarted, wrapper.Bytes) return @@ -400,6 +421,28 @@ func SmartShieldMiddleware(shieldID string, route utils.ProxyRouteConfig) func(h shield.Lock() wrapper.TimeEnded = time.Now() wrapper.isOver = true + + statusText := "success" + level := "info" + if wrapper.Status >= 400 { + statusText = "error" + level = "warning" + } + + utils.TriggerEvent( + "cosmos.proxy.response." + route.Name + "." + statusText, + "Proxy Response " + route.Name + " " + statusText, + level, + "route@" + route.Name, + map[string]interface{}{ + "Route": route.Name, + "Status": wrapper.Status, + "Method": wrapper.Method, + "ClientID": wrapper.ClientID, + "Time": wrapper.TimeEnded.Sub(wrapper.TimeStarted).Seconds(), + "Bytes": wrapper.Bytes, + }) + go metrics.PushRequestMetrics(route, wrapper.Status, wrapper.TimeStarted, wrapper.Bytes) shield.Unlock() })() diff --git a/src/utils/db.go b/src/utils/db.go index 0eae100..9d51ad7 100644 --- a/src/utils/db.go +++ b/src/utils/db.go @@ -10,7 +10,8 @@ import ( "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/readpref" - "go.mongodb.org/mongo-driver/mongo/writeconcern" + "go.mongodb.org/mongo-driver/mongo/writeconcern" + "go.mongodb.org/mongo-driver/bson" ) @@ -50,6 +51,8 @@ func DB() error { return err } + initDB() + Log("Successfully connected to the database.") return nil } @@ -73,7 +76,7 @@ func GetCollection(applicationId string, collection string) (*mongo.Collection, name = "COSMOS" } - Debug("Getting collection " + applicationId + "_" + collection + " from database " + name) + // Debug("Getting collection " + applicationId + "_" + collection + " from database " + name) c := client.Database(name).Collection(applicationId + "_" + collection) @@ -202,4 +205,23 @@ func ListAllUsers(role string) []User { } return users +} + +func initDB() { + c, errCo := GetCollection(GetRootAppId(), "events") + if errCo != nil { + Error("Metrics - Database Connect", errCo) + } else { + // Create a text index on the _search field + model := mongo.IndexModel{ + Keys: bson.M{"_search": "text"}, // Specify the field to index here + } + + // Creating the index + _, err := c.Indexes().CreateOne(context.Background(), model) + if err != nil { + Error("Metrics - Create Index", err) + return // Handle error appropriately + } + } } \ No newline at end of file diff --git a/src/utils/events.go b/src/utils/events.go new file mode 100644 index 0000000..76c55ae --- /dev/null +++ b/src/utils/events.go @@ -0,0 +1,31 @@ +package utils + +import ( + "time" + "encoding/json" +) + +func TriggerEvent(eventId string, label string, level string, object string, data map[string]interface{}) { + Debug("Triggering event " + eventId) + + + // Marshal the data map into a JSON string + dataAsBytes, err := json.Marshal(data) + if err != nil { + Error("Error marshaling data: %v\n", err) + return + } + dataAsString := string(dataAsBytes) + + BufferedDBWrite("events", map[string]interface{}{ + "eventId": eventId, + "label": label, + "application": "Cosmos", + "level": level, + "date": time.Now(), + "data": data, + "object": object, + "_search": eventId + " " + dataAsString, + }) +} + diff --git a/src/utils/middleware.go b/src/utils/middleware.go index 3f5b3f3..0ba426f 100644 --- a/src/utils/middleware.go +++ b/src/utils/middleware.go @@ -203,16 +203,11 @@ func BlockByCountryMiddleware(blockedCountries []string, CountryBlacklistIsWhite countryCode, err := GetIPLocation(ip) if err == nil { - if countryCode == "" { - Debug("Country code is empty") - } else { - Debug("Country code: " + countryCode) - } - config := GetMainConfig() if CountryBlacklistIsWhitelist { if countryCode != "" { + Debug("Country code: " + countryCode) blocked := true for _, blockedCountry := range blockedCountries { if config.ServerCountry != countryCode && countryCode == blockedCountry { @@ -272,8 +267,6 @@ func BlockPostWithoutReferer(next http.Handler) http.Handler { func EnsureHostname(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - Debug("Ensuring origin for requested resource from : " + r.Host) - og := GetMainConfig().HTTPConfig.Hostname ni := GetMainConfig().NewInstall diff --git a/src/utils/utils.go b/src/utils/utils.go index 6ee814d..2e5a147 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -383,7 +383,6 @@ func GetAllHostnames(applyWildCard bool, removePorts bool) []string { uniqueHostnames = filteredHostnames } - Debug("Hostnames are " + strings.Join(uniqueHostnames, ", ")) return uniqueHostnames }