diff --git a/backend/src/index.ts b/backend/src/index.ts index 5206dc4e..f4e584f6 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -3,7 +3,10 @@ import cors from 'cors'; import dotenv from 'dotenv'; import uploadRoutes from './routes/upload'; import allLogs from './routes/getAllLogs'; +import getStats from './routes/getStats'; +import logtypeStatus from './routes/logTypeStatus'; import connectToDatabase from './db/connect'; +import severityInfo from './routes/severityInfo'; dotenv.config(); @@ -24,6 +27,9 @@ app.get('/', (req: Request, res: Response): void => { app.use('/api', uploadRoutes); app.use('/api', allLogs); +app.use('/api', getStats) +app.use('/api', logtypeStatus) +app.use('/api', severityInfo) app.listen(PORT, (): void => { console.log(`Server is running on http://localhost:${PORT}`); diff --git a/backend/src/routes/getStats.ts b/backend/src/routes/getStats.ts new file mode 100644 index 00000000..9084ff80 --- /dev/null +++ b/backend/src/routes/getStats.ts @@ -0,0 +1,177 @@ +import express from 'express'; +import { Request, Response } from 'express'; +import { LinuxLogModel } from '../models/LinuxLogModel'; + +const router = express.Router(); + +router.get('/stats', async (req: Request, res: Response) => { + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - 90); // 90 days ago + + const totalLogsAgg = await LinuxLogModel.aggregate([ + { + $match: { timestamp: { $gte: startDate } } + }, + { + $group: { + _id: { + year: { $year: '$timestamp' }, + month: { $month: '$timestamp' }, + day: { $dayOfMonth: '$timestamp' }, + }, + count: { $sum: 1 }, + } + }, + { + $sort: { + "_id.year": 1, + "_id.month": 1, + "_id.day": 1, + } + } + ]); + + const errorLogsAgg = await LinuxLogModel.aggregate([ + { + $match: { + timestamp: { $gte: startDate }, + severity: { $in: ['ERROR'] } + }, + }, + { + $group: { + _id: { + year: { $year: '$timestamp' }, + month: { $month: '$timestamp' }, + day: { $dayOfMonth: '$timestamp' }, + }, + count: { $sum: 1 }, + } + }, + { + $sort: { + "_id.year": 1, + "_id.month": 1, + "_id.day": 1, + } + } + ]); + + const warningLogsAgg = await LinuxLogModel.aggregate([ + { + $match: { + timestamp: { $gte: startDate }, + severity: { $in: ['WARNING'] } // Adjust this based on your severity levels + }, + }, + { + $group: { + _id: { + year: { $year: '$timestamp' }, + month: { $month: '$timestamp' }, + day: { $dayOfMonth: '$timestamp' }, + }, + count: { $sum: 1 }, + } + }, + { + $sort: { + "_id.year": 1, + "_id.month": 1, + "_id.day": 1, + } + } + ]); + + + // Format Helper to build a 90 days array + const totalLogsData = buildDailyArray(totalLogsAgg, startDate); + const errorLogsData = buildDailyArray(errorLogsAgg, startDate); + const warningLogsData = buildDailyArray(warningLogsAgg, startDate); + + // sum up total logs + const totalLogsSum = totalLogsData.reduce((acc, count) => acc + count, 0); + const errorLogsSum = errorLogsData.reduce((acc, count) => acc + count, 0); + const warningLogsSum = warningLogsData.reduce((acc, count) => acc + count, 0); + + //compute tends + const totalLogsTend = computeTrend(totalLogsData); + const errorLogsTend = computeTrend(errorLogsData); + const warningLogsTend = computeTrend(warningLogsData); + + + + res.json([ + { + title: "Total Logs", + value: totalLogsSum, // e.g. "14k" or a number + interval: "Last 90 days", + trend: totalLogsTend, // "up", "down", or "neutral" + data: totalLogsData, // [0, 3, 5, 2, ...] for 180 days + }, + { + title: "Error Count", + value: errorLogsSum, + interval: "Last 90 days", + trend: errorLogsTend, + data: errorLogsData, + }, + { + title: "Warning Count", + value: warningLogsSum, + interval: "Last 90 days", + trend: warningLogsTend, + data: warningLogsData, + }, + ]) + + + + } catch (error) { + console.error(error); + res.status(500).json({ error: "Something went wrong." }); + } +}); + +function buildDailyArray( + aggResults: { _id: { year: number; month: number; day: number }; count: number }[], + startDate: Date +): number[] { + const daysCount = 90; + const dailyArray = new Array(daysCount).fill(0); + + for (const doc of aggResults) { + const { year, month, day } = doc._id; + const thisDate = new Date(year, month - 1, day); // month is 0-indexed + const index = Math.floor((thisDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); + + if (index >= 0 && index < daysCount) { + dailyArray[index] = doc.count; + } + } + + return dailyArray; +} + + +function computeTrend(data: number[]) { + if (data.length < 2) return "neutral"; + + const recentValue = data[data.length - 1]; + const previousValue = data[data.length - 2]; + + if (recentValue > previousValue) { + return "up"; + } else if (recentValue < previousValue) { + return "down"; + } else { + return "neutral"; + } +} + + +export default router; + + + diff --git a/backend/src/routes/logTypeStatus.ts b/backend/src/routes/logTypeStatus.ts new file mode 100644 index 00000000..0531b5ca --- /dev/null +++ b/backend/src/routes/logTypeStatus.ts @@ -0,0 +1,37 @@ +import express from 'express'; +import { Request, Response } from 'express'; +import { LinuxLogModel } from '../models/LinuxLogModel'; + +const router = express.Router(); + +router.get('/logtypeStatus', async (req: Request, res: Response) => { + try { + const syslogCount = await LinuxLogModel.countDocuments({ logType: 'SYSLOG' }); + const windowlogCount = await LinuxLogModel.countDocuments({ logType: 'WINDOWLOG' }); + const authlogCount = await LinuxLogModel.countDocuments({ logType: 'AUTH' }); + const kernelCount = await LinuxLogModel.countDocuments({ logType: 'KERNEL' }); + const unknownCount = await LinuxLogModel.countDocuments({ logType: { $regex: 'UNKNOWN' } }); + + const data = [ + { label: "SYSLOG", value: syslogCount }, + { label: "WINDOWLOG", value: windowlogCount }, + { label: "AUTHLOG", value: authlogCount }, + { label: "KERNEL", value: kernelCount }, + { label: "UNKNOWN", value: unknownCount }, + ]; + + res.json(data); + + + } catch (error) { + console.error(error); + res.status(500).json({ error: "Something went wrong." }); + } +}); + + + +export default router; + + + diff --git a/backend/src/routes/severityInfo.ts b/backend/src/routes/severityInfo.ts new file mode 100644 index 00000000..bd65113a --- /dev/null +++ b/backend/src/routes/severityInfo.ts @@ -0,0 +1,74 @@ +// total no of each severity "INFO" | "WARNING" | "ERROR" | "CRITICAL" and from last 7 months count for each severity in following format const [logData, setLogData] = React.useState({ +// info: [0, 0, 0, 0, 0, 0, 0], +// warning: [0, 0, 0, 0, 0, 0, 0], +// error: [0, 0, 0, 0, 0, 0, 0], +// critical: [0, 0, 0, 0, 0, 0, 0], +// }); + +import express from 'express'; +import { Request, Response } from 'express'; +import { LinuxLogModel } from '../models/LinuxLogModel'; + +const router = express.Router(); + +router.get('/severityInfo', async (req: Request, res: Response) => { + try { + + const info = [0, 0, 0, 0, 0, 0, 0]; + const warning = [0, 0, 0, 0, 0, 0, 0]; + const error = [0, 0, 0, 0, 0, 0, 0]; + const critical = [0, 0, 0, 0, 0, 0, 0]; + + // Get monthly data for the last 7 months + for (let i = 0; i < 7; i++) { + const monthStart = new Date(); + monthStart.setMonth(monthStart.getMonth() - i); + monthStart.setDate(1); + monthStart.setHours(0, 0, 0, 0); + + const monthEnd = new Date(monthStart); + monthEnd.setMonth(monthStart.getMonth() + 1); + monthEnd.setDate(0); + monthEnd.setHours(23, 59, 59, 999); + + info[6 - i] = await LinuxLogModel.countDocuments({ + severity: 'INFO', + timestamp: { $gte: monthStart, $lte: monthEnd } + }); + + warning[6 - i] = await LinuxLogModel.countDocuments({ + severity: 'WARNING', + timestamp: { $gte: monthStart, $lte: monthEnd } + }); + + error[6 - i] = await LinuxLogModel.countDocuments({ + severity: 'ERROR', + timestamp: { $gte: monthStart, $lte: monthEnd } + }); + + critical[6 - i] = await LinuxLogModel.countDocuments({ + severity: 'CRITICAL', + timestamp: { $gte: monthStart, $lte: monthEnd } + }); + } + + // Prepare response with total counts and monthly data + res.json({ + info, + warning, + error, + critical, + }); + + } catch (error) { + console.error(error); + res.status(500).json({ error: "Something went wrong." }); + } +}); + + + +export default router; + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3da335e0..f8040678 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,7 +26,6 @@ "@react-spring/web": "^9.7.5", "d3": "^7.9.0", "dayjs": "^1.11.13", - "leaflet": "^1.9.4", "lucide-react": "^0.485.0", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -4198,7 +4197,8 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/levn": { "version": "0.4.1", diff --git a/frontend/package.json b/frontend/package.json index 9982b006..063dd9ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,7 +28,6 @@ "@react-spring/web": "^9.7.5", "d3": "^7.9.0", "dayjs": "^1.11.13", - "leaflet": "^1.9.4", "lucide-react": "^0.485.0", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/frontend/src/components/Chat.jsx b/frontend/src/components/Chat.jsx index e3b43164..7fa4058e 100644 --- a/frontend/src/components/Chat.jsx +++ b/frontend/src/components/Chat.jsx @@ -1,17 +1,96 @@ -import { Stack } from "@mui/material"; +import { + Box, + IconButton, + Paper, + Stack, + TextField, + Typography, +} from "@mui/material"; +import SendIcon from "@mui/icons-material/Send"; +import { useEffect, useRef, useState } from "react"; export default function Chat() { + const [messages, setMessages] = useState([ + { id: 1, text: "Hello, how can I help you?", sender: "bot" }, + { id: 2, text: "I need help analyzing my logs", sender: "user" }, + ]); + const [newMessage, setNewMessage] = useState(""); + const messagesEndRef = useRef(null); + + const handleSend = () => { + if (newMessage.trim()) { + setMessages([ + ...messages, + { id: messages.length + 1, text: newMessage, sender: "user" }, + ]); + setNewMessage(""); + } + }; + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + return ( -

Chat

+ + {messages.map((message) => ( + + + {message.text} + + + ))} +
+ + + + setNewMessage(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSend()} + /> + + + + ); } diff --git a/frontend/src/components/HighlightedCard.jsx b/frontend/src/components/HighlightedCard.jsx index 940b5fd2..7b655cf8 100644 --- a/frontend/src/components/HighlightedCard.jsx +++ b/frontend/src/components/HighlightedCard.jsx @@ -7,10 +7,13 @@ import ChevronRightRoundedIcon from "@mui/icons-material/ChevronRightRounded"; import InsightsRoundedIcon from "@mui/icons-material/InsightsRounded"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useTheme } from "@mui/material/styles"; +import { useRecoilState } from "recoil"; +import { activeViewState } from "../utils/state"; export default function HighlightedCard() { const theme = useTheme(); const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); + const [_, setActiveViewState] = useRecoilState(activeViewState); return ( @@ -33,6 +36,9 @@ export default function HighlightedCard() { color="primary" endIcon={} fullWidth={isSmallScreen} + onClick={() => { + setActiveViewState("chat"); + }} > Get insights diff --git a/frontend/src/components/LogAnalyzedBarGraph.jsx b/frontend/src/components/LogAnalyzedBarGraph.jsx new file mode 100644 index 00000000..03c81158 --- /dev/null +++ b/frontend/src/components/LogAnalyzedBarGraph.jsx @@ -0,0 +1,164 @@ +import * as React from "react"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Chip from "@mui/material/Chip"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { useTheme } from "@mui/material/styles"; +import severityInfo from "../utils/api/severityInfo"; + +export default function LogAnalyzedBarChart() { + const theme = useTheme(); + const colorPalette = [ + (theme.vars || theme).palette.info.main, // INFO + (theme.vars || theme).palette.warning.main, // WARNING + (theme.vars || theme).palette.error.main, // ERROR + (theme.vars || theme).palette.error.dark, // CRITICAL + ]; + + const [logData, setLogData] = React.useState({ + info: [0, 0, 0, 0, 0, 0, 0], + warning: [0, 0, 0, 0, 0, 0, 0], + error: [0, 0, 0, 0, 0, 0, 0], + critical: [0, 0, 0, 0, 0, 0, 0], + }); + const [totalLogs, setTotalLogs] = React.useState(0); + const [percentageChange, setPercentageChange] = React.useState(0); + const [months, setMonths] = React.useState([]); + + React.useEffect(() => { + // Get last 7 months + const getLastSevenMonths = () => { + const monthNames = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + const result = []; + const currentDate = new Date(); + const currentMonth = currentDate.getMonth(); + + for (let i = 6; i >= 0; i--) { + const monthIndex = (currentMonth - i + 12) % 12; + result.push(monthNames[monthIndex]); + } + + return result; + }; + + setMonths(getLastSevenMonths()); + + // Fetch log data from backend + const fetchLogData = async () => { + try { + const response = await severityInfo(); + setLogData(response); + + const total = Object.values(response) + .flat() + .reduce((sum, val) => sum + val, 0); + setTotalLogs(total); + + if (response.percentageChange !== undefined) { + setPercentageChange(response.percentageChange); + } + } catch (error) { + console.error("Error fetching log data:", error); + } + }; + + fetchLogData(); + }, []); + + // Format number with K suffix if >= 1000 + const formatNumber = (num) => { + return num >= 1000 ? `${(num / 1000).toFixed(1)}K` : num.toString(); + }; + + return ( + + + + Analyzed Logs + + + + + {formatNumber(totalLogs)} + + + + + Logs analyzed in the last 30 days + + + + + + ); +} diff --git a/frontend/src/components/LogTypeStatus.jsx b/frontend/src/components/LogTypeStatus.jsx index 264fc532..4d049d2e 100644 --- a/frontend/src/components/LogTypeStatus.jsx +++ b/frontend/src/components/LogTypeStatus.jsx @@ -15,47 +15,15 @@ import System from "@mui/icons-material/Computer"; import Window from "@mui/icons-material/Window"; import Kernel from "@mui/icons-material/Memory"; import Auth from "@mui/icons-material/LockOpen"; +import logtypeStatus from "../utils/api/logtypeStatus"; -const data = [ - { label: "SYSLOG", value: 50000 }, - { label: "WINDOWLOG", value: 35000 }, - { label: "AUTHLOG", value: 10000 }, - { label: "KERNEL", value: 10000 }, - { label: "UNKNOWN", value: 5000 }, -]; - -const countries = [ - { - name: "SYSLOG", - value: 50, - flag: , - color: "hsl(220, 25%, 65%)", - }, - { - name: "WINDOWLOG", - value: 35, - flag: , - color: "hsl(220, 25%, 45%)", - }, - { - name: "AUTHLOG", - value: 10, - flag: , - color: "hsl(220, 25%, 30%)", - }, - { - name: "KERNEL", - value: 10, - flag: , - color: "hsl(220, 25%, 30%)", - }, - { - name: "UNKNOWN", - value: 5, - flag: , - color: "hsl(220, 25%, 20%)", - }, -]; +// const data = [ +// { label: "SYSLOG", value: 50000 }, +// { label: "WINDOWLOG", value: 35000 }, +// { label: "AUTHLOG", value: 10000 }, +// { label: "KERNEL", value: 10000 }, +// { label: "UNKNOWN", value: 5000 }, +// ]; const StyledText = styled("text", { shouldForwardProp: (prop) => prop !== "variant", @@ -120,6 +88,54 @@ const colors = [ ]; export default function LogTypeStatus() { + const [data, setData] = React.useState([]); + const [totalCount, setTotalCount] = React.useState("0"); + const [loading, setLoading] = React.useState(true); + + React.useEffect(() => { + const fetchData = async () => { + try { + const response = await logtypeStatus(); + setData(response); + + //convert to string with the K notation + const total = response.reduce((acc, item) => acc + item.value, 0); + const totalString = + total >= 1000 ? `${(total / 1000).toFixed(1)}K` : total.toString(); + setTotalCount(totalString); + } catch (error) { + console.error("Error fetching data:", error); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + const total = data.reduce((acc, item) => acc + item.value, 0); + + const iconMap = { + SYSLOG: { flag: , color: "hsl(220, 25%, 65%)" }, + WINDOWLOG: { flag: , color: "hsl(220, 25%, 45%)" }, + AUTHLOG: { flag: , color: "hsl(220, 25%, 30%)" }, + KERNEL: { flag: , color: "hsl(220, 25%, 30%)" }, + UNKNOWN: { flag: , color: "hsl(220, 25%, 20%)" }, + }; + + const logData = data.map((item) => { + const percentage = total > 0 ? Math.round((item.value / total) * 100) : 0; + const defaultIcon = { flag: , color: "hsl(220, 25%, 20%)" }; + const { flag, color } = iconMap[item.label] || defaultIcon; + + return { + name: item.label, + value: percentage, + flag, + color, + rawValue: item.value, + }; + }); + + if (loading) return
Loading...
; return ( - + - {countries.map((country, index) => ( + {logData.map((country, index) => ( { + const fetchData = async () => { + try { + const response = await fetchStats(); + setData(response); + } catch (error) { + console.error("Error fetching data:", error); + } finally { + setLoading(false); + } + }; + fetchData(); + }, []); + if (loading) return
Loading...
; return ( {/* cards */} @@ -68,16 +52,17 @@ export default function MainGrid() { - + - + {/* // TODO: add line Graph to show the trend of the logs over time */} + {/* - + */} ); } diff --git a/frontend/src/components/PageViewsBarChart.jsx b/frontend/src/components/PageViewsBarChart.jsx deleted file mode 100644 index b3457522..00000000 --- a/frontend/src/components/PageViewsBarChart.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import * as React from "react"; -import Card from "@mui/material/Card"; -import CardContent from "@mui/material/CardContent"; -import Chip from "@mui/material/Chip"; -import Typography from "@mui/material/Typography"; -import Stack from "@mui/material/Stack"; -import { BarChart } from "@mui/x-charts/BarChart"; -import { useTheme } from "@mui/material/styles"; - -export default function PageViewsBarChart() { - const theme = useTheme(); - const colorPalette = [ - (theme.vars || theme).palette.primary.dark, - (theme.vars || theme).palette.primary.main, - (theme.vars || theme).palette.primary.light, - ]; - return ( - - - - Analyzed Logs - - - - - 12K - - - - - Logs analyzed in the last 30 days - - - - - - ); -} diff --git a/frontend/src/components/StatCard.jsx b/frontend/src/components/StatCard.jsx index ab804e5e..9f672013 100644 --- a/frontend/src/components/StatCard.jsx +++ b/frontend/src/components/StatCard.jsx @@ -9,18 +9,23 @@ import Typography from "@mui/material/Typography"; import { SparkLineChart } from "@mui/x-charts/SparkLineChart"; import { areaElementClasses } from "@mui/x-charts/LineChart"; -function getDaysInMonth(month, year) { - const date = new Date(year, month, 0); - const monthName = date.toLocaleDateString("en-US", { - month: "short", - }); - const daysInMonth = date.getDate(); +function getDaysInMonth() { + // Create an array of 90 days from today const days = []; - let i = 1; - while (days.length < daysInMonth) { - days.push(`${monthName} ${i}`); - i += 1; + const startDate = new Date(); + + for (let i = 0; i < 90; i++) { + const currentDate = new Date(startDate); + currentDate.setDate(startDate.getDate() + i); + + const monthName = currentDate.toLocaleDateString("en-US", { + month: "short", + }); + const dayOfMonth = currentDate.getDate(); + + days.push(`${monthName} ${dayOfMonth}`); } + return days; } @@ -37,8 +42,7 @@ function AreaGradient({ color, id }) { export default function StatCard({ title, value, interval, trend, data }) { const theme = useTheme(); - const daysInWeek = getDaysInMonth(4, 2024); - + const daysInWeek = getDaysInMonth(); const trendColors = { up: theme.palette.mode === "light" diff --git a/frontend/src/utils/api/fetchStats.js b/frontend/src/utils/api/fetchStats.js new file mode 100644 index 00000000..92b46b73 --- /dev/null +++ b/frontend/src/utils/api/fetchStats.js @@ -0,0 +1,13 @@ +export default async function fetchStats() { + const data = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/stats`) + + if(!data.ok) { + throw new Error("Network response was not ok" + data.statusText); + } + + const dataJson = await data.json(); + if (!dataJson) { + throw new Error("No data found"); + } + return dataJson; +} diff --git a/frontend/src/utils/api/logtypeStatus.js b/frontend/src/utils/api/logtypeStatus.js new file mode 100644 index 00000000..b3bca350 --- /dev/null +++ b/frontend/src/utils/api/logtypeStatus.js @@ -0,0 +1,13 @@ +export default async function logtypeStatus() { + const data = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/logtypeStatus`); + + if(!data.ok) { + throw new Error("Network response was not ok" + data.statusText); + } + + const dataJson = await data.json(); + if (!dataJson) { + throw new Error("No data found"); + } + return dataJson; +} diff --git a/frontend/src/utils/api/severityInfo.js b/frontend/src/utils/api/severityInfo.js new file mode 100644 index 00000000..6c7e8350 --- /dev/null +++ b/frontend/src/utils/api/severityInfo.js @@ -0,0 +1,14 @@ + +export default async function severityInfo() { + const data = await fetch(`${import.meta.env.VITE_BACKEND_URL}/api/severityInfo`) + + if(!data.ok) { + throw new Error("Network response was not ok" + data.statusText); + } + + const dataJson = await data.json(); + if (!dataJson) { + throw new Error("No data found"); + } + return dataJson; +}