Yann Stepienik 1 год назад
Родитель
Сommit
6ba22dc4cb

+ 3 - 0
client/src/api/index.jsx

@@ -4,6 +4,7 @@ import * as _config from './config';
 import * as _docker from './docker';
 import * as _docker from './docker';
 import * as _market from './market';
 import * as _market from './market';
 import * as _constellation from './constellation';
 import * as _constellation from './constellation';
+import * as _metrics from './metrics';
 
 
 import * as authDemo from './authentication.demo';
 import * as authDemo from './authentication.demo';
 import * as usersDemo from './users.demo';
 import * as usersDemo from './users.demo';
@@ -214,6 +215,7 @@ let config = _config;
 let docker = _docker;
 let docker = _docker;
 let market = _market;
 let market = _market;
 let constellation = _constellation;
 let constellation = _constellation;
+let metrics = _metrics;
 
 
 if(isDemo) {
 if(isDemo) {
   auth = authDemo;
   auth = authDemo;
@@ -242,5 +244,6 @@ export {
   isOnline,
   isOnline,
   checkHost,
   checkHost,
   getDNS,
   getDNS,
+  metrics,
   uploadBackground
   uploadBackground
 };
 };

+ 14 - 0
client/src/api/metrics.jsx

@@ -0,0 +1,14 @@
+import wrap from './wrap';
+
+function get() {
+  return wrap(fetch('/cosmos/api/metrics', {
+    method: 'GET',
+    headers: {
+        'Content-Type': 'application/json'
+    },
+  }))
+}
+
+export {
+  get,
+};

+ 4 - 0
client/src/index.css

@@ -144,4 +144,8 @@
 
 
 .pulsing {
 .pulsing {
   animation: pulsing 2s ease-in-out infinite;
   animation: pulsing 2s ease-in-out infinite;
+}
+
+.force-light > * {
+  color: black !important;
 }
 }

+ 221 - 0
client/src/pages/dashboard/components/plot.jsx

@@ -0,0 +1,221 @@
+import { useEffect, useState } 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';
+
+function formatDate(now, time) {
+  // use as UTC
+  // now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
+
+  const year = now.getFullYear();
+  const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are 0-based
+  const day = String(now.getDate()).padStart(2, '0');
+  const hours = String(now.getHours()).padStart(2, '0');
+  const minutes = String(now.getMinutes()).padStart(2, '0');
+  const seconds = String(now.getSeconds()).padStart(2, '0');
+
+  return time ? `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` : `${year}-${month}-${day}`;
+}
+
+function toUTC(date, time) {
+  let now = new Date(date);
+  now.setMinutes(now.getMinutes() + now.getTimezoneOffset());
+  return formatDate(now, time);
+}
+
+const PlotComponent = ({ data, defaultSlot = 'day' }) => {
+  const [slot, setSlot] = useState(defaultSlot);
+  const theme = useTheme();
+
+  // chart options
+  const areaChartOptions = {
+    chart: {
+      height: 450,
+      type: 'area',
+      toolbar: {
+        show: false
+      }
+    },
+    dataLabels: {
+      enabled: false
+    },
+    stroke: {
+      curve: 'smooth',
+      width: 2
+    },
+    grid: {
+      strokeDashArray: 0
+    }
+  };
+
+  let hourlyDates = [];
+  for(let i = 0; i < 48; i++) {
+    let now = new Date();
+    now.setHours(now.getHours() - i);
+    now.setMinutes(0);
+    now.setSeconds(0);
+    // get as YYYY-MM-DD HH:MM:SS
+    hourlyDates.unshift(formatDate(now, true));
+  }
+
+  let dailyDates = [];
+  for(let i = 0; i < 30; i++) {
+    let now = new Date();
+    now.setDate(now.getDate() - i);
+    dailyDates.unshift(formatDate(now));
+  }
+
+  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.secondary.main],
+      xaxis: {
+        categories:
+          slot === 'hourly'
+            ? hourlyDates.map((date) => date.split(' ')[1])
+            : dailyDates,
+        labels: {
+          style: {
+            fontSize: '11px',
+          }
+        },
+        axisBorder: {
+          show: true,
+          color: line
+        },
+        tickAmount: slot === 'hourly' ? hourlyDates.length : dailyDates.length,
+      },
+      yaxis: [{
+        labels: {
+          style: {
+            colors: [secondary]
+          }
+        },
+        title: {
+          text: data[0].Label,
+        }
+      },
+      {
+        opposite: true,
+        labels: {
+          style: {
+            colors: [secondary]
+          }
+        },
+        title: {
+          text: data[1].Label,
+        }
+      }
+      ],
+      grid: {
+        borderColor: line
+      },
+      tooltip: {
+        theme: 'light'
+      }
+    }));
+  }, [primary, secondary, line, theme, slot]);
+
+  let dataSeries = [];
+  data.forEach((serie) => {
+    dataSeries.push({
+      name: serie.Label,
+      dataDaily: dailyDates.map((date) => {
+        let k = "day_" + toUTC(date);
+        if (k in serie.ValuesAggl) {
+          return serie.ValuesAggl[k].Value;
+        } else {
+          console.log(k)
+          return 0;
+        }
+      }),
+      dataHourly: hourlyDates.map((date) => {
+        let k = "hour_" + toUTC(date, true);
+        if (k in serie.ValuesAggl) {
+          return serie.ValuesAggl[k].Value;
+        } else {
+          return 0;
+        }
+      }),
+    });
+  });
+
+  const [series, setSeries] = useState(dataSeries.map((serie) => {
+    return {
+      name: serie.name,
+      data: slot === 'hourly' ? serie.dataHourly : serie.dataDaily
+    }
+  }));
+
+  useEffect(() => {
+    setSeries(dataSeries.map((serie) => {
+      return {
+        name: serie.name,
+        data: slot === 'hourly' ? serie.dataHourly : serie.dataDaily
+      }
+    }));
+  }, [slot, data]);
+
+
+  return <Grid item xs={12} md={7} lg={8}>
+    <Grid container alignItems="center" justifyContent="space-between">
+      <Grid item>
+        <Typography variant="h5">Server Resources</Typography>
+      </Grid>
+      <Grid item>
+        <Stack direction="row" alignItems="center" spacing={0}>
+          <Button
+            size="small"
+            onClick={() => setSlot('hourly')}
+            color={slot === 'hourly' ? 'primary' : 'secondary'}
+            variant={slot === 'hourly' ? 'outlined' : 'text'}
+          >
+            Hourly
+          </Button>
+          <Button
+            size="small"
+            onClick={() => setSlot('daily')}
+            color={slot === 'daily' ? 'primary' : 'secondary'}
+            variant={slot === 'daily' ? 'outlined' : 'text'}
+          >
+            Daily
+          </Button>
+        </Stack>
+      </Grid>
+    </Grid>
+    <MainCard content={false} sx={{ mt: 1.5 }}>
+      <Box sx={{ pt: 1, pr: 2 }} className="force-light">
+        <ReactApexChart options={options} series={series} type="area" height={450} />;
+      </Box>
+    </MainCard>
+  </Grid>
+}
+
+export default PlotComponent;

+ 19 - 41
client/src/pages/dashboard/index.jsx

@@ -38,6 +38,7 @@ import IsLoggedIn from '../../isLoggedIn';
 
 
 import * as API from '../../api';
 import * as API from '../../api';
 import AnimateButton from '../../components/@extended/AnimateButton';
 import AnimateButton from '../../components/@extended/AnimateButton';
+import PlotComponent from './components/plot';
 
 
 // avatar style
 // avatar style
 const avatarSX = {
 const avatarSX = {
@@ -79,8 +80,20 @@ const DashboardDefault = () => {
     const [slot, setSlot] = useState('week');
     const [slot, setSlot] = useState('week');
 
 
     const [coStatus, setCoStatus] = useState(null);
     const [coStatus, setCoStatus] = useState(null);
+    const [metrics, setMetrics] = useState(null);
     const [isCreatingDB, setIsCreatingDB] = useState(false);
     const [isCreatingDB, setIsCreatingDB] = useState(false);
 
 
+    const refreshMetrics = () => {
+        API.metrics.get().then((res) => {
+            let finalMetrics = {};
+            res.data.forEach((metric) => {
+                finalMetrics[metric.Key] = metric;
+            });
+            setMetrics(finalMetrics);
+            setTimeout(refreshMetrics, 10000);
+        });
+    };
+
     const refreshStatus = () => {
     const refreshStatus = () => {
         API.getStatus().then((res) => {
         API.getStatus().then((res) => {
             setCoStatus(res.data);
             setCoStatus(res.data);
@@ -89,15 +102,9 @@ const DashboardDefault = () => {
 
 
     useEffect(() => {
     useEffect(() => {
         refreshStatus();
         refreshStatus();
+        refreshMetrics();
     }, []);
     }, []);
 
 
-    const setupDB = () => {
-        setIsCreatingDB(true);
-        API.docker.newDB().then((res) => {
-            refreshStatus();
-        });
-    }
-
     return (
     return (
         <>
         <>
         <IsLoggedIn />
         <IsLoggedIn />
@@ -145,7 +152,7 @@ const DashboardDefault = () => {
                 <Alert severity="info">Dashboard implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord!</Alert>
                 <Alert severity="info">Dashboard implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord!</Alert>
             </Stack>
             </Stack>
         </div>
         </div>
-        <div style={{filter: 'blur(10px)', marginTop: '30px', pointerEvents: 'none'}}>
+        {metrics && <div style={{marginTop: '30px'}}>
             <Grid container rowSpacing={4.5} columnSpacing={2.75}>
             <Grid container rowSpacing={4.5} columnSpacing={2.75}>
                 {/* row 1 */}
                 {/* row 1 */}
                 <Grid item xs={12} sx={{ mb: -2.25 }}>
                 <Grid item xs={12} sx={{ mb: -2.25 }}>
@@ -167,38 +174,9 @@ const DashboardDefault = () => {
                 <Grid item md={8} sx={{ display: { sm: 'none', md: 'block', lg: 'none' } }} />
                 <Grid item md={8} sx={{ display: { sm: 'none', md: 'block', lg: 'none' } }} />
 
 
                 {/* row 2 */}
                 {/* row 2 */}
-                <Grid item xs={12} md={7} lg={8}>
-                    <Grid container alignItems="center" justifyContent="space-between">
-                        <Grid item>
-                            <Typography variant="h5">Unique Visitor</Typography>
-                        </Grid>
-                        <Grid item>
-                            <Stack direction="row" alignItems="center" spacing={0}>
-                                <Button
-                                    size="small"
-                                    onClick={() => setSlot('month')}
-                                    color={slot === 'month' ? 'primary' : 'secondary'}
-                                    variant={slot === 'month' ? 'outlined' : 'text'}
-                                >
-                                    Month
-                                </Button>
-                                <Button
-                                    size="small"
-                                    onClick={() => setSlot('week')}
-                                    color={slot === 'week' ? 'primary' : 'secondary'}
-                                    variant={slot === 'week' ? 'outlined' : 'text'}
-                                >
-                                    Week
-                                </Button>
-                            </Stack>
-                        </Grid>
-                    </Grid>
-                    <MainCard content={false} sx={{ mt: 1.5 }}>
-                        <Box sx={{ pt: 1, pr: 2 }}>
-                            <IncomeAreaChart slot={slot} />
-                        </Box>
-                    </MainCard>
-                </Grid>
+                
+                <PlotComponent data={[metrics["cosmos.system.cpu.0"], metrics["cosmos.system.ram"]]}/>
+                
                 <Grid item xs={12} md={5} lg={4}>
                 <Grid item xs={12} md={5} lg={4}>
                     <Grid container alignItems="center" justifyContent="space-between">
                     <Grid container alignItems="center" justifyContent="space-between">
                         <Grid item>
                         <Grid item>
@@ -413,7 +391,7 @@ const DashboardDefault = () => {
                     </MainCard>
                     </MainCard>
                 </Grid>
                 </Grid>
             </Grid>
             </Grid>
-        </div>
+        </div>}
       </>
       </>
     );
     );
 };
 };

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "cosmos-server",
   "name": "cosmos-server",
-  "version": "0.12.0-unstable",
+  "version": "0.12.0-unstable2",
   "description": "",
   "description": "",
   "main": "test-server.js",
   "main": "test-server.js",
   "bugs": {
   "bugs": {

+ 3 - 0
src/httpServer.go

@@ -10,6 +10,7 @@ import (
 		"github.com/azukaar/cosmos-server/src/authorizationserver"
 		"github.com/azukaar/cosmos-server/src/authorizationserver"
 		"github.com/azukaar/cosmos-server/src/market"
 		"github.com/azukaar/cosmos-server/src/market"
 		"github.com/azukaar/cosmos-server/src/constellation"
 		"github.com/azukaar/cosmos-server/src/constellation"
+		"github.com/azukaar/cosmos-server/src/metrics"
 		"github.com/gorilla/mux"
 		"github.com/gorilla/mux"
 		"strconv"
 		"strconv"
 		"time"
 		"time"
@@ -339,6 +340,8 @@ func InitServer() *mux.Router {
 	srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)
 	srapi.HandleFunc("/api/constellation/logs", constellation.API_GetLogs)
 	srapi.HandleFunc("/api/constellation/block", constellation.DeviceBlock)
 	srapi.HandleFunc("/api/constellation/block", constellation.DeviceBlock)
 
 
+	srapi.HandleFunc("/api/metrics", metrics.API_GetMetrics)
+
 	if(!config.HTTPConfig.AcceptAllInsecureHostname) {
 	if(!config.HTTPConfig.AcceptAllInsecureHostname) {
 		srapi.Use(utils.EnsureHostname)
 		srapi.Use(utils.EnsureHostname)
 	}
 	}

+ 16 - 16
src/metrics/aggl.go

@@ -74,8 +74,8 @@ func AggloMetrics() {
 		} 
 		} 
 
 
 		// if hourly pool does not exist, create it
 		// if hourly pool does not exist, create it
-		if _, ok := metric.ValuesAggl["hour_" + hourlyPool.String()]; !ok {
-			metric.ValuesAggl["hour_" + hourlyPool.String()] = DataDefDBEntry{
+		if _, ok := metric.ValuesAggl["hour_" + hourlyPool.UTC().Format("2006-01-02 15:04:05")]; !ok {
+			metric.ValuesAggl["hour_" + hourlyPool.UTC().Format("2006-01-02 15:04:05")] = DataDefDBEntry{
 				Date: hourlyPool,
 				Date: hourlyPool,
 				Value: 0,
 				Value: 0,
 				Processed: false,
 				Processed: false,
@@ -86,8 +86,8 @@ func AggloMetrics() {
 		}
 		}
 	
 	
 		// if daily pool does not exist, create it
 		// if daily pool does not exist, create it
-		if _, ok := metric.ValuesAggl["day_" + dailyPool.String()]; !ok {
-			metric.ValuesAggl["day_" + dailyPool.String()] = DataDefDBEntry{
+		if _, ok := metric.ValuesAggl["day_" + dailyPool.UTC().Format("2006-01-02")]; !ok {
+			metric.ValuesAggl["day_" + dailyPool.UTC().Format("2006-01-02")] = DataDefDBEntry{
 				Date: dailyPool,
 				Date: dailyPool,
 				Value: 0,
 				Value: 0,
 				Processed: false,
 				Processed: false,
@@ -100,33 +100,33 @@ func AggloMetrics() {
 		for valInd, value := range values {
 		for valInd, value := range values {
 			// if not processed
 			// if not processed
 			if !value.Processed {
 			if !value.Processed {
-				valueHourlyPool := ModuloTime(value.Date, time.Hour)
-				valueDailyPool := ModuloTime(value.Date, 24 * time.Hour)
+				valueHourlyPool := ModuloTime(value.Date, time.Hour).UTC().Format("2006-01-02 15:04:05")
+				valueDailyPool := ModuloTime(value.Date, 24 * time.Hour).UTC().Format("2006-01-02")
 
 
-				if _, ok := metric.ValuesAggl["hour_" + valueHourlyPool.String()]; ok {
-					currentPool := metric.ValuesAggl["hour_" + valueHourlyPool.String()]
+				if _, ok := metric.ValuesAggl["hour_" + valueHourlyPool]; ok {
+					currentPool := metric.ValuesAggl["hour_" + valueHourlyPool]
 					
 					
 					currentPool.Value = MergeMetric(metric.AggloType, currentPool.Value, value.Value, currentPool.AvgIndex)    
 					currentPool.Value = MergeMetric(metric.AggloType, currentPool.Value, value.Value, currentPool.AvgIndex)    
 					if metric.AggloType == "avg" {
 					if metric.AggloType == "avg" {
 						currentPool.AvgIndex++
 						currentPool.AvgIndex++
 					}
 					}
 
 
-					metric.ValuesAggl["hour_" + valueHourlyPool.String()] = currentPool
+					metric.ValuesAggl["hour_" + valueHourlyPool] = currentPool
 				} else {
 				} else {
-					utils.Warn("Metrics: Agglomeration - Pool not found : " + "hour_" + valueHourlyPool.String())
+					utils.Warn("Metrics: Agglomeration - Pool not found : " + "hour_" + valueHourlyPool)
 				}
 				}
 
 
-				if _, ok := metric.ValuesAggl["day_" + valueDailyPool.String()]; ok {
-					currentPool := metric.ValuesAggl["day_" + valueDailyPool.String()]
-
+				if _, ok := metric.ValuesAggl["day_" + valueDailyPool]; ok {
+					currentPool := metric.ValuesAggl["day_" + valueDailyPool]
+					
 					currentPool.Value = MergeMetric(metric.AggloType, currentPool.Value, value.Value, currentPool.AvgIndex)
 					currentPool.Value = MergeMetric(metric.AggloType, currentPool.Value, value.Value, currentPool.AvgIndex)
 					if metric.AggloType == "avg" {
 					if metric.AggloType == "avg" {
 						currentPool.AvgIndex++
 						currentPool.AvgIndex++
 					}
 					}
 
 
-					metric.ValuesAggl["day_" + valueDailyPool.String()] = currentPool
+					metric.ValuesAggl["day_" + valueDailyPool] = currentPool
 				} else {
 				} else {
-					utils.Warn("Metrics: Agglomeration - Pool not found: " + "day_" + valueDailyPool.String())
+					utils.Warn("Metrics: Agglomeration - Pool not found: " + "day_" + valueDailyPool)
 				}
 				}
 
 
 				values[valInd].Processed = true
 				values[valInd].Processed = true
@@ -171,7 +171,7 @@ func AggloMetrics() {
 func InitAggl() {
 func InitAggl() {
 	go func() {
 	go func() {
 		s := gocron.NewScheduler()
 		s := gocron.NewScheduler()
-		s.Every(1).Hour().At("00.00").From(gocron.NextTick()).Do(AggloMetrics)
+		s.Every(1).Hour().From(gocron.NextTick()).Do(AggloMetrics)
 		// s.Every(3).Minute().From(gocron.NextTick()).Do(AggloMetrics)
 		// s.Every(3).Minute().From(gocron.NextTick()).Do(AggloMetrics)
 
 
 		s.Start()
 		s.Start()

+ 45 - 0
src/metrics/api.go

@@ -0,0 +1,45 @@
+package metrics 
+
+import (
+	"net/http"
+	"encoding/json"
+	
+	"github.com/azukaar/cosmos-server/src/utils"
+)
+
+func API_GetMetrics(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("Metrics - Database Connect", errCo)
+				return
+		}
+
+		// get all metrics from database
+		var metrics []DataDefDB
+		cursor, err := c.Find(nil, map[string]interface{}{})
+		if err != nil {
+			utils.Error("Metrics: Error fetching metrics", err)
+			return
+		}
+		defer cursor.Close(nil)
+		
+		if err = cursor.All(nil, &metrics); err != nil {
+			utils.Error("Metrics: Error decoding metrics", err)
+			return
+		}
+		
+		json.NewEncoder(w).Encode(map[string]interface{}{
+			"status": "OK",
+			"data": metrics,
+		})
+	} else {
+		utils.Error("SettingGet: Method not allowed" + req.Method, nil)
+		utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
+		return
+	}
+}