├── .gitignore ├── README.md ├── client ├── Components │ ├── DropdownMenu.jsx │ ├── NodeID.jsx │ ├── UsageDetails.jsx │ └── UsageDisplay.jsx ├── Containers │ ├── DropDownContainer.jsx │ ├── LogContainer.jsx │ ├── MainContainer.jsx │ └── NodeIDContainer.jsx ├── app.css ├── app.jsx ├── images │ ├── KlusterBud-Logo-NoBG.png │ ├── KlusterGif-Animated Image (Large).gif │ └── logo.png ├── index.html ├── index.jsx └── main.js ├── package-lock.json ├── package.json ├── server ├── controller │ ├── RSController.js │ └── queryController.js └── server.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | klusterbudlogo 3 |

4 |

KlusterBud

5 | 6 |

Klusterbud is a Kubernetes developers tool used to detect anomalies in the performance of nodes within a replica set.

7 | 8 |
9 | 10 | ## Built Using 11 | 12 | ![JavaScript](https://img.shields.io/badge/-javascript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=white) 13 | ![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB) 14 | ![Bootstrap](https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white) 15 | ![Node](https://img.shields.io/badge/-node-339933?style=for-the-badge&logo=node.js&logoColor=white) 16 | ![Express](https://img.shields.io/badge/-Express-000000?style=for-the-badge&logo=express&logoColor=white) 17 | ![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white) 18 | ![Kubernetes](https://img.shields.io/badge/kubernetes-326ce5.svg?&style=for-the-badge&logo=kubernetes&logoColor=white) 19 | ![Prometheus](https://img.shields.io/badge/Prometheus-000000?style=for-the-badge&logo=prometheus&labelColor=000000) 20 | ![Helm](https://img.shields.io/badge/Helm-0F1689?style=for-the-badge&logo=Helm&labelColor=0F1689) 21 | ![HTML](https://img.shields.io/badge/HTML5-E34F26?style=for-the-badge&logo=html5&logoColor=white) 22 | ![CSS](https://img.shields.io/badge/CSS3-1572B6?style=for-the-badge&logo=css3&logoColor=white) 23 | 24 |
25 | 26 | ## Table of Contents 27 | 28 | - [About KlusterBud](#about-klusterbud) 29 | - [Vision Timeline](#vision-timeline) 30 | - [Getting Started](#getting-started-using-klusterbud) 31 | - [Demo](#demo) 32 | - [Publications](#publications) 33 | - [Website](#website) 34 | - [Contributors](#contributors) 35 | 36 | ## About Klusterbud 37 | 38 | KlusterBud makes PromQL queries to the user's running instance of prometheus, collecting metrics from nodes and comparing them to other nodes within the same replica set. When an anomaly is found, the identifying information of the node is served back to the user. Users can choose from a dropdown list of replica sets in their cluster, and start watching for any anomalies. 39 | 40 | ## Vision Timeline 41 | 42 | - ✅ Develop application to pull CPU resonse time from a replica set 43 | - ✅ Consolidate data from a replica set to isolate a single outlier node within it 44 | - ⏳ Containerize application to be deployed directly on monitored cluster 45 | - ⏳ Remove need for the user to manually initialize their own Prometheus server 46 | - ⏳ Develop functionality for monitoring multiple replica sets concurrently 47 | - 🤝 Allow users to input their own margins and metrics to define outlier nodes 48 | - 🤝 Develop live monitoring features for a production workflow 49 | - 🤝 Generate live monitoring visualization 50 | 51 | ✅ = Completed 52 | ⏳ = Coming soon 53 | 🤝 = Seeking Support 54 | 55 | ## Getting Started Using KlusterBud 56 | 57 | - Have Prometheus installed on Kubernetes cluster 58 | - Have Prometheus port-forwarded to `localhost:9090` 59 | - Run these commands 60 | 61 | - ```js 62 | npm install 63 | npm run build 64 | npm start 65 | ``` 66 | 67 | - Select node from drop down menu 68 | 69 | ## Demo 70 | 71 |

72 | 73 |

74 | 75 | ## Publications 76 | 77 | [![Medium Badge](https://img.shields.io/badge/Medium-12100E?style=for-the-badge&logo=medium&logoColor=white)](https://medium.com/@AMitchell-Smith/klusterbud-k8s-anomaly-detection-made-simple-060ca496aebc) 78 | 79 | ## Website 80 | 81 | ### Check out our [Website](https://www.klusterbud.com) and [![LinkedIn Badge](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/company/klusterbud) 82 | 83 | ## Contributors: 84 | 85 | ###

Asher Mitchell-Smith

[![GitHub Badge](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/The-Onion-Man) [![LinkedIn Badge](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/asher-mitchell-smith-507683154/)

86 | 87 | ###

Christopher Lu

[![GitHub Badge](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/christopher-lu) [![LinkedIn Badge](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/christopher-lu19/)

88 | 89 | ###

Fredy Rubio

[![GitHub Badge](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/FredyRubio) [![LinkedIn Badge](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](https://www.linkedin.com/in/fredy-rubio/)

90 | 91 | ###

Michael Wang

[![GitHub Badge](https://img.shields.io/badge/GitHub-100000?style=for-the-badge&logo=github&logoColor=white)](https://github.com/mwang934) [![LinkedIn Badge](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white)](www.linkedin.com/in/michael-w-9475412a2)

92 | 93 | [Back to top of page](#klusterbud) 94 | -------------------------------------------------------------------------------- /client/Components/DropdownMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from "react"; 2 | import { Dropdown, DropdownButton } from "react-bootstrap"; 3 | import axios from "axios"; 4 | 5 | export const DropdownMenu = (props) => { 6 | const handleSelect = (event) =>{ 7 | const selectedValue = event.target.value; 8 | props.setSelectedRS(selectedValue) 9 | } 10 | return ( 11 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /client/Components/NodeID.jsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from "react"; 2 | 3 | export const NodeID = (props) => { 4 | return( 5 | //insert logic to pull node ID 6 | <> 7 | {props.abnormalNode !== null && ( 8 |
Abnormal Node: {props.abnormalNode}
9 | )} 10 | 11 | ) 12 | } -------------------------------------------------------------------------------- /client/Components/UsageDetails.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const UsageDetails = ({value, responseData}) => { 4 | return( 5 | //insert logic for retrieving the node details to be displayed 6 |
7 | 'Usage Details Inserted here' 8 |

9 |

10 | ) 11 | } -------------------------------------------------------------------------------- /client/Components/UsageDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export const UsageDisplay = (props) => { 4 | return ( 5 | <> 6 | {props.abnormalNode !== null && ( 7 |
8 |
Average response time for all nodes in the replica set: {props.medianTime} seconds
9 |
10 | )} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /client/Containers/DropDownContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Container } from 'react-bootstrap'; 3 | import ReplicaSetSelector, { DropdownMenu } from '../Components/DropdownMenu'; 4 | import { Dropdown } from 'react-bootstrap'; 5 | 6 | export const DropDownContainer = (props) => { 7 | return( 8 | 13 | ) 14 | } -------------------------------------------------------------------------------- /client/Containers/LogContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { NodeSelector } from "../Components/DropdownMenu"; 3 | import { Container } from "react-bootstrap"; 4 | import { UsageDisplay } from "../Components/UsageDisplay"; 5 | import { UsageDetails } from "../Components/UsageDetails"; 6 | 7 | 8 | 9 | export const LogContainer = (props) => { 10 | return( 11 |
12 | 13 | 14 | {/* */} 15 | 16 |
17 | ) 18 | } -------------------------------------------------------------------------------- /client/Containers/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useState, useEffect } from "react"; 3 | import axios from "axios"; // not using axios just yet, possible iteration 4 | import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom"; 5 | import { Container, Button } from "react-bootstrap"; 6 | import { ReplicaSetSelector } from "../Components/DropdownMenu"; 7 | import { LogContainer } from "./LogContainer"; 8 | import { DropDownContainer } from "./DropDownContainer"; 9 | import { NodeIDContainer } from "./NodeIDContainer"; 10 | 11 | 12 | 13 | const MainContainer = () => { 14 | const [replicaSets, setReplicaSets] = useState([]); //array of names of replica sets in our cluster 15 | const [selectedRS, setSelectedRS] = useState(''); //the name of the replica set selected by the user from the dropdown 16 | const [abnormalNode, setAbnormalNode] = useState(null); //the name of the node behaving abnormally(within our selected replica set) 17 | const [medianTime, setMedianTime] = useState(''); //the median cpu response time from nodes in our selected replica set 18 | const [watchInterval, setWatchInterval] = useState(null); //indicates whether we should be actively watching (we watch by fetching repeatedly at an interval) 19 | 20 | 21 | useEffect(() => { 22 | //the getRS function sends a request to /loadRS, and expects an array of replica set names as a response. 23 | function getRS() { 24 | fetch('/loadRS') 25 | .then(response => response.json()) 26 | .then(data => { 27 | //we then use the setReplicaSet setter function to set this piece of state to that array of replica set names. 28 | setReplicaSets(data.result); 29 | document.getElementById('info').innerText = JSON.stringify(data.result, null, 2); 30 | }) 31 | .catch(err => console.log('ERROR:', err)) 32 | } 33 | getRS(); 34 | }, []) 35 | 36 | 37 | const handleWatchButtonClick = () => { 38 | if(watchInterval){ // if detects the anomaly stops repeating 39 | clearInterval(watchInterval); // to stop repeating 40 | setWatchInterval(null) // set the interval back to null 41 | setAbnormalNode(null) 42 | setMedianTime("") 43 | } else { 44 | handleAbnormalNode(); 45 | const intervalId = setInterval(()=> { 46 | handleAbnormalNode(); 47 | },3000); // every 3s its repeating the functionallity 48 | setWatchInterval(intervalId); // set the state of watchInterval to intervalId so it stops iterating 49 | } 50 | }; 51 | 52 | const handleAbnormalNode = () => { 53 | fetch(`/query/${selectedRS}`) 54 | .then(response => response.json()) 55 | .then(data => { 56 | if(data.abnormalNodeFound === true){ 57 | setAbnormalNode(data.result) // sets the result state to the abnormal it found 58 | setMedianTime(data.median) 59 | clearInterval(watchInterval); // stops repeating reading through 60 | setWatchInterval(null); // set state back to null (false) 61 | }else{ 62 | setAbnormalNode(data.result) 63 | setMedianTime(data.median)} 64 | }) 65 | .catch(err => console.log('ERROR:', err)) 66 | } 67 | 68 | return ( 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | 78 | 79 | 80 |
81 | ) 82 | } 83 | 84 | export default MainContainer; -------------------------------------------------------------------------------- /client/Containers/NodeIDContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Container } from "react-bootstrap"; 3 | import { NodeID } from "../Components/NodeID"; 4 | 5 | export const NodeIDContainer = (props) => { 6 | return( 7 | <> 8 | 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /client/app.css: -------------------------------------------------------------------------------- 1 | /* Global Styles */ 2 | * { 3 | font-family: 'Ubuntu Light', 'Century Gothic', sans-serif; 4 | color: #1b1b1b; 5 | } 6 | 7 | /* html { 8 | background-color: #f0f0f0; 9 | } */ 10 | 11 | body { 12 | background-color: rgb(255, 248, 238); 13 | } 14 | 15 | /* Container Styles */ 16 | .container { 17 | background-color: transparent; 18 | padding-top: 10px; 19 | margin-top: 10px; 20 | } 21 | 22 | #MainContainer { 23 | background-color: #e4eaff; 24 | padding: 20px; 25 | border-radius: 8px; 26 | box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.1); 27 | } 28 | #klusterbud{ 29 | display: flex; 30 | justify-content: center; 31 | align-self: center; 32 | color: black; 33 | font-family: 'Brush Script MT', cursive; 34 | font-size: 700%; 35 | } 36 | 37 | #TitleContainer { 38 | display: flex; 39 | align-items: center; 40 | justify-content: center; 41 | } 42 | 43 | #title { 44 | text-align: center; 45 | color: #333; 46 | } 47 | 48 | /* Watch Container Styles */ 49 | #watchContainer { 50 | text-align: center; 51 | background-color: transparent; 52 | /* border: 2px solid #333; */ 53 | padding: 15px; 54 | margin-top: 20px; 55 | border-radius: 5px; 56 | transition: border-color 0.3s ease-in-out; 57 | } 58 | 59 | .watchButton { 60 | background-color: #3498db; 61 | color: #fff; 62 | padding: 10px 20px; 63 | border: none; 64 | border-radius: 5px; 65 | cursor: pointer; 66 | transition: background-color 0.3s ease-in-out; 67 | } 68 | 69 | .watchButton:hover { 70 | background-color: #2980b9; 71 | } 72 | 73 | /* Dropdown Styles */ 74 | #replicaset-select, 75 | #replica-menu, 76 | #default-dropdown { 77 | background-color: #3498db; 78 | /* color: #fff; */ 79 | border-radius: 5px; 80 | padding: 10px; 81 | margin-top: 10px; 82 | transition: background-color 0.3s ease-in-out; 83 | } 84 | 85 | #default-dropdown { 86 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 87 | } 88 | 89 | /* Log Container Styles */ 90 | #logcontainer { 91 | display: flex; 92 | justify-content: center; 93 | } 94 | 95 | #dropdown-container { 96 | text-align: center; 97 | justify-content: center; 98 | } 99 | 100 | /* Usage Styles */ 101 | #usage { 102 | display: flex; 103 | justify-content: space-between; 104 | background-color: transparent; 105 | margin-top: 20px; 106 | } 107 | 108 | #nodeid, 109 | #cpu-header, 110 | #usage-details, 111 | #pulled-usage-details { 112 | background-color: transparent; 113 | } 114 | 115 | /* Modern Card Styles */ 116 | .card { 117 | background-color: #fff; 118 | border-radius: 8px; 119 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 120 | padding: 20px; 121 | margin-top: 20px; 122 | transition: box-shadow 0.3s ease-in-out; 123 | } 124 | 125 | #usage-details{ 126 | display: flex; 127 | background-color: rgba(0,0,0,0); 128 | } 129 | #pulled-usage-details{ 130 | background-color: rgba(0,0,0,0); 131 | } 132 | 133 | #replica-menu{ 134 | background-color: #296de8; 135 | color: white; 136 | border-radius: 5%; 137 | } 138 | -------------------------------------------------------------------------------- /client/app.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ReactDOM } from "react-dom/client"; 3 | import './app.css' 4 | import MainContainer from './Containers/MainContainer'; 5 | import { Container } from "react-bootstrap"; 6 | import logo from './images/Logo.png'; 7 | 8 | // const root = ReactDOM.createRoot(document.getElementById('root')); 9 | // root.render() 10 | const App = () => { 11 | return( 12 | <> 13 | 14 |
15 | 16 |

KlusterBud

17 |
18 |
19 |
20 | 21 |
22 | ) 23 | } 24 | export default App; -------------------------------------------------------------------------------- /client/images/KlusterBud-Logo-NoBG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterBud/88d6ae1859f6e41dda3c4f3080189c691614eb39/client/images/KlusterBud-Logo-NoBG.png -------------------------------------------------------------------------------- /client/images/KlusterGif-Animated Image (Large).gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterBud/88d6ae1859f6e41dda3c4f3080189c691614eb39/client/images/KlusterGif-Animated Image (Large).gif -------------------------------------------------------------------------------- /client/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/KlusterBud/88d6ae1859f6e41dda3c4f3080189c691614eb39/client/images/logo.png -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 16 | 17 | KlusterBud 18 | 19 | 20 | 21 |
22 | 23 | 27 | 28 | 29 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /client/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { createRoot } from 'react-dom/client'; 4 | import App from './app'; 5 | import MainContainer from './Containers/MainContainer'; 6 | 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render(); 10 | -------------------------------------------------------------------------------- /client/main.js: -------------------------------------------------------------------------------- 1 | // const queryHandler = function (string) { 2 | // const address = 'localhost:9090/api/v1/query?query=' + string; 3 | // console.log(address); 4 | // } 5 | document.addEventListener('DOMContentLoaded', () => { 6 | const submitButton = document.getElementById('submit'); 7 | submitButton.addEventListener('click', () => { 8 | const url = '/query/' + document.getElementById('queryString').value; 9 | console.log(url) 10 | //http://localhost:3000/query/up 11 | fetch(url) 12 | .then(response => response.json()) 13 | .then(data => { 14 | document.getElementById('info').innerText = JSON.stringify(data, null, 2); 15 | }) 16 | .catch(err => console.log('ERROR:', err)) 17 | }) 18 | }) 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "klusterbud", 3 | "version": "1.0.0", 4 | "description": "OSP Project", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "NODE_ENV=production nodemon server/index.js", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "NODE_ENV=development nodemon server/server.js & NODE_ENV=development webpack serve --open", 11 | "dev-server": "NODE_ENV=development webpack-dev-server --open --hot & NODE_ENV=development nodemon server/server.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/oslabs-beta/KlusterBud.git" 16 | }, 17 | "author": "Christopher Lu, Michael Wang, Asher Mitchell-Smith, Fredy Rubio", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/oslabs-beta/KlusterBud/issues" 21 | }, 22 | "homepage": "https://github.com/oslabs-beta/KlusterBud#readme", 23 | "devDependencies": { 24 | "babel-loader": "^9.1.3", 25 | "css-loader": "^6.8.1", 26 | "electron": "^27.0.3", 27 | "file-loader": "^6.2.0", 28 | "react-dom": "^18.2.0", 29 | "style-loader": "^3.3.3", 30 | "ts-loader": "^9.5.0", 31 | "typescript": "^5.2.2", 32 | "webpack": "^5.89.0" 33 | }, 34 | "dependencies": { 35 | "@babel/core": "^7.23.3", 36 | "@babel/preset-env": "^7.23.3", 37 | "@babel/preset-react": "^7.23.3", 38 | "axios": "^1.6.2", 39 | "babel-loader": "^9.1.3", 40 | "body-parser": "^1.20.2", 41 | "bootstrap": "^5.3.2", 42 | "cors": "^2.8.5", 43 | "dotenv": "^16.3.1", 44 | "express": "^4.18.2", 45 | "file": "^0.2.2", 46 | "html-webpack-plugin": "^5.5.3", 47 | "loader": "^2.1.1", 48 | "nodemon": "^3.0.1", 49 | "react": "^18.2.0", 50 | "react-bootstrap": "^2.9.1", 51 | "react-dom": "^18.2.0", 52 | "react-router-dom": "^6.20.0", 53 | "sass-loader": "^13.3.2", 54 | "webpack": "^5.89.0", 55 | "webpack-cli": "^5.1.4", 56 | "webpack-dev-server": "^4.15.1" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /server/controller/RSController.js: -------------------------------------------------------------------------------- 1 | const { response } = require("express"); 2 | 3 | const RSController = {}; 4 | 5 | RSController.fetch = (req, res, next) => { 6 | fetch('http://localhost:9090/api/v1/query?query=kube_deployment_spec_replicas') 7 | .then(response => response.json()) 8 | .then(data => { 9 | const replicaSets = []; 10 | // populate the replicaSets array with the name(stored under 'deployment') of every replica set 11 | data.data.result.forEach(element => { 12 | replicaSets.push(element.metric.deployment); 13 | }); 14 | console.log(replicaSets); 15 | // console.log(data.data.result); 16 | // Send only the data.data.result part to the client 17 | res.locals.replicaSets = replicaSets; 18 | return next(); 19 | }) 20 | .catch(err => {console.log(err)}) 21 | } 22 | 23 | module.exports = RSController; -------------------------------------------------------------------------------- /server/controller/queryController.js: -------------------------------------------------------------------------------- 1 | const queryController = {}; 2 | 3 | 4 | //get the cpu response time of all nodes in our cluster from the prometheus server 5 | queryController.fetch = (req, res, next) => { 6 | console.log('/QUERY ROUTE IS RUNNING', req.params.string) 7 | fetch('http://localhost:9090/api/v1/query?query=container_cpu_usage_seconds_total') 8 | .then(response => { 9 | return response.json()}) 10 | .then(data => { 11 | const items = {}; 12 | data.data.result.forEach(element => { 13 | items[element.metric.pod] = element.value; 14 | }); 15 | console.log(items); 16 | // console.log(data.data.result); 17 | // Send only the data.data.result part to the client 18 | res.locals.data = items; 19 | return next(); 20 | }) 21 | } 22 | 23 | //reduces information down to array of only the nodes in our chosen replica set, then returns an anomaly node OR 'no anomalies found yet' 24 | queryController.sort = (req, res, next) => { 25 | console.log(req.params.string) 26 | const RSPods = {}; 27 | Object.keys(res.locals.data).forEach(element => { 28 | if(element.includes(req.params.string)) { 29 | RSPods[element] = res.locals.data[element]; 30 | } 31 | }) 32 | res.locals[req.params.string] = RSPods; 33 | 34 | //function acceps an array of values, returns the median value 35 | function medianArr(arr){ 36 | arr.sort(function(a, b) { 37 | return a - b; 38 | }); 39 | 40 | const n = arr.length; 41 | let median; 42 | 43 | if (n % 2 === 0) { 44 | const midLeft = arr[n / 2 - 1]; 45 | const midRight = arr[n / 2]; 46 | median = (midLeft + midRight) / 2; 47 | } else { 48 | median = arr[Math.floor(n / 2)]; 49 | } 50 | 51 | return median; 52 | } 53 | //find the median of the cpu response times of our nodes 54 | const median = medianArr(Object.values(RSPods).map(ele => ele[1])) 55 | console.log(median) 56 | 57 | 58 | console.log(RSPods) 59 | //go through our pods, compare their cpu usage to the median 60 | for(const key in RSPods){ 61 | //if its > 2x the median we assign it to res.locals for our response 62 | if(RSPods[key][1] > (2*median) ){ 63 | res.locals.pod = key 64 | res.locals.medianSec = median 65 | res.locals.errorFound = true 66 | console.log(res.locals.pod) 67 | return next(); 68 | } 69 | } 70 | //if we get to this point, just send back 'no anomalies found yet' 71 | res.locals.pod = 'no anomalies found yet...' 72 | res.locals.medianSec = median 73 | res.locals.errorFound = false 74 | return next(); 75 | }; 76 | 77 | module.exports = queryController; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const queryController = require('./controller/queryController') 5 | const RSController = require('./controller/RSController') 6 | 7 | // const queryController = require('./controllers/queryController'); 8 | 9 | const PORT = 3000; 10 | const app = express(); 11 | 12 | 13 | 14 | app.listen(PORT, console.log(`Listening on PORT: ${PORT}`)); 15 | 16 | app.use(express.json()); 17 | app.use(express.urlencoded()) 18 | 19 | // app.use(bodyParser.urlencoded()); 20 | // app.use(bodyParser.text()); 21 | 22 | app.use('/', express.static(path.resolve(__dirname, '../dist'))) 23 | 24 | app.get('/', (req, res) => { 25 | return res.status(200).sendFile(path.resolve(__dirname, '../client/index.html')); 26 | }) 27 | //gets names of replica sets from prometheus server, sends them to front end in an array 28 | app.get('/loadRS', RSController.fetch, (req, res) => { 29 | res.status(200).json({result: res.locals.replicaSets}) 30 | }) 31 | 32 | app.get('/query/:string', queryController.fetch, queryController.sort, (req, res) => { 33 | res.status(200).json({ result: res.locals.pod, median: res.locals.medianSec, abnormalNodeFound: res.locals.errorFound }) 34 | }) 35 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 3 | const loader = require('sass-loader'); 4 | 5 | module.exports = { 6 | entry: './client/index.jsx', 7 | 8 | devServer: { 9 | host: 'localhost', 10 | port: 8080, 11 | compress: true, 12 | hot: true, 13 | static: { 14 | directory: path.resolve(__dirname, 'dist'), 15 | publicPath: '/', 16 | }, 17 | proxy: { 18 | '/': 'http://localhost:3000', 19 | }, 20 | }, 21 | 22 | output: { 23 | path: path.resolve(__dirname, 'dist'), 24 | filename: 'bundle.js', 25 | }, 26 | 27 | plugins: [ 28 | new HTMLWebpackPlugin({ 29 | template: './client/index.html', 30 | }) 31 | ], 32 | 33 | mode: process.env.NODE_ENV, 34 | 35 | module: { 36 | rules: [ 37 | { 38 | test: /\.jsx?$/, 39 | exclude: /node_modules/, 40 | use: { 41 | loader: 'babel-loader', 42 | options: { 43 | presets: ['@babel/preset-env', '@babel/preset-react'], 44 | }, 45 | }, 46 | }, 47 | { 48 | test: /\.css$/i, 49 | exclude: /node_modules/, 50 | use: ['style-loader', 'css-loader'], 51 | }, 52 | { 53 | test: /\.(png|jpe?g|gif)$/i, 54 | use: [ 55 | { 56 | loader: 'file-loader', 57 | }, 58 | ], 59 | }, 60 | ], 61 | }, 62 | resolve: { 63 | extensions: ['.js', '.jsx'], 64 | }, 65 | devtool: 'eval-source-map', 66 | }; 67 | --------------------------------------------------------------------------------