├── .gitignore ├── dashboard ├── .gitignore ├── __mocks__ │ └── fileMock.js ├── setupJest.js ├── server │ ├── graphQL │ │ ├── schemaDirectives.js │ │ ├── dataSources.js │ │ ├── schemas.js │ │ ├── typeDefs.js │ │ ├── resolvers.js │ │ └── dataSource.js │ ├── routes │ │ ├── alertsRouter.js │ │ ├── prometheusRouter.js │ │ └── metricsRouter.js │ ├── controllers │ │ ├── alertsController.js │ │ ├── portController.js │ │ └── metricController.js │ ├── alertmanager.yaml │ └── server.js ├── client │ ├── assets │ │ ├── loading.gif │ │ ├── periscopeLogo.png │ │ ├── periscope_logo_transparent.png │ │ ├── periscopeTextLogoTransparent.png │ │ ├── colors.js │ │ └── dateConverter.js │ ├── index.html │ ├── index.js │ ├── container │ │ ├── LandingContainer.jsx │ │ ├── NodeContainer.jsx │ │ └── PodContainer.jsx │ ├── components │ │ ├── Header.jsx │ │ ├── MemoryTooltip.js │ │ ├── PodMemoryTooltip.js │ │ ├── PodCpuToolTip.js │ │ ├── TimeSeriesTooltip.js │ │ ├── PodMemorySeriesTooltip.js │ │ ├── PodInfoTableSetup.jsx │ │ ├── ClusterInfo.jsx │ │ ├── table.jsx │ │ ├── PodTable.jsx │ │ ├── Memory.jsx │ │ ├── PodMemoryCurrentComponent.jsx │ │ ├── CPU.jsx │ │ ├── PodCPU.jsx │ │ ├── PodMemorySeriesComponent.jsx │ │ ├── DiskUsage.jsx │ │ └── PodInfoRows.jsx │ ├── App.jsx │ └── style.css ├── build │ ├── 39bf65959351bb4a8db9dd5262d4440a.png │ ├── 7d8919a21df05a5b6f992ace69ce803f.png │ ├── f5005f30c27a2d2e548c0d3e273efbe2.gif │ └── bundle.js.LICENSE.txt ├── babel.config.js ├── webpack.config.js ├── __tests__ │ └── ReactTesting.jsx └── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /dashboard/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.export = ''; -------------------------------------------------------------------------------- /dashboard/setupJest.js: -------------------------------------------------------------------------------- 1 | require('jest-fetch-mock').enableMocks() 2 | -------------------------------------------------------------------------------- /dashboard/server/graphQL/schemaDirectives.js: -------------------------------------------------------------------------------- 1 | //for additional schema directives. 2 | 3 | module.exports = () => {}; 4 | -------------------------------------------------------------------------------- /dashboard/client/assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Periscope/HEAD/dashboard/client/assets/loading.gif -------------------------------------------------------------------------------- /dashboard/client/assets/periscopeLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Periscope/HEAD/dashboard/client/assets/periscopeLogo.png -------------------------------------------------------------------------------- /dashboard/build/39bf65959351bb4a8db9dd5262d4440a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Periscope/HEAD/dashboard/build/39bf65959351bb4a8db9dd5262d4440a.png -------------------------------------------------------------------------------- /dashboard/build/7d8919a21df05a5b6f992ace69ce803f.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Periscope/HEAD/dashboard/build/7d8919a21df05a5b6f992ace69ce803f.png -------------------------------------------------------------------------------- /dashboard/build/f5005f30c27a2d2e548c0d3e273efbe2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Periscope/HEAD/dashboard/build/f5005f30c27a2d2e548c0d3e273efbe2.gif -------------------------------------------------------------------------------- /dashboard/client/assets/periscope_logo_transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Periscope/HEAD/dashboard/client/assets/periscope_logo_transparent.png -------------------------------------------------------------------------------- /dashboard/client/assets/periscopeTextLogoTransparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Periscope/HEAD/dashboard/client/assets/periscopeTextLogoTransparent.png -------------------------------------------------------------------------------- /dashboard/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', {targets: {node: 'current'}}], 4 | ['@babel/preset-react', {targets: {node: 'current'}}] 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /dashboard/server/graphQL/dataSources.js: -------------------------------------------------------------------------------- 1 | const PrometheusAPI = require('./dataSource.js'); 2 | 3 | const memory = { 4 | isPrometheusUp: false, 5 | } 6 | 7 | module.exports = () => { 8 | return { 9 | prometheusAPI: new PrometheusAPI(memory), 10 | }; 11 | }; -------------------------------------------------------------------------------- /dashboard/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Dashboard 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /dashboard/client/assets/colors.js: -------------------------------------------------------------------------------- 1 | const lineColors = [ 2 | '#43f8f6', 3 | '#f8b243', 4 | '#ecf843', 5 | '#7ef843', 6 | '#f84343', 7 | '#e56e24', 8 | '#3C9D4E', 9 | '#7031AC', 10 | '#C94D6D', 11 | '#E4BF58', 12 | '#4174C9', 13 | '#990066', 14 | '#3d1c02', 15 | '#ffccda', 16 | '#ffaabb', 17 | '#d1edee', 18 | '#d3dde4', 19 | '#456789', 20 | '#f2850d', 21 | '#f6ecde', 22 | '#ffbcc5', 23 | '#bcddb3', 24 | '#bfaf92', 25 | '#f3e9d9', 26 | '#88ffcc', 27 | '#00b89f', 28 | '#05a3ad', 29 | '#1150af', 30 | '#231f20', 31 | ]; 32 | 33 | export default lineColors; -------------------------------------------------------------------------------- /dashboard/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import App from './App.jsx'; 5 | import style from './style.css'; 6 | // import { 7 | // ApolloClient, 8 | // InMemoryCache, 9 | // ApolloProvider, 10 | // useQuery, 11 | // gql, 12 | // } from '@apollo/client'; 13 | 14 | // const client = new ApolloClient({ 15 | // uri: '/graphql' 16 | // }) 17 | 18 | render( 19 | 20 | 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ); 26 | -------------------------------------------------------------------------------- /dashboard/server/routes/alertsRouter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description: WIP --> Router for alert manager 4 | * ***************************************************************************** 5 | */ 6 | 7 | const express = require('express'); 8 | const alertsRouter = express.Router(); 9 | const alertsController = require('../controllers/alertsController'); 10 | 11 | alertsRouter.post('/', alertsController.createAlert, (req, res) => { 12 | res.status(200).send('Alert Created!'); 13 | }); 14 | 15 | module.exports = alertsRouter; -------------------------------------------------------------------------------- /dashboard/server/routes/prometheusRouter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description: WIP --> Router for port forwarding Prometheus server 4 | * ***************************************************************************** 5 | */ 6 | 7 | 8 | const express = require('express'); 9 | const prometheusRouter = express.Router(); 10 | const portController = require('../controllers/portController'); 11 | 12 | prometheusRouter.get('/', portController.portForward, (req, res) => { 13 | res.status(200).send(res.locals.promUp); 14 | }); 15 | 16 | module.exports = prometheusRouter; -------------------------------------------------------------------------------- /dashboard/server/graphQL/schemas.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ****************************************************************************************** 3 | * @description: Transfers all the info for GraphQL queries to Apollo server 4 | * ****************************************************************************************** 5 | */ 6 | 7 | 8 | const typeDefs = require('./typeDefs'); 9 | const resolvers = require('./resolvers'); 10 | const dataSources = require('./dataSources.js'); 11 | const schemaDirectives = require('./schemaDirectives.js'); 12 | 13 | module.exports = { 14 | typeDefs, 15 | resolvers, 16 | dataSources, 17 | cors: { credentials: true }, 18 | introspection: true, 19 | }; 20 | -------------------------------------------------------------------------------- /dashboard/client/container/LandingContainer.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Landing page container 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | import Logo from '../assets/periscopeTextLogoTransparent.png'; 10 | 11 | const LandingContainer = () => { 12 | 13 | return ( 14 |
15 | 16 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | export default LandingContainer; 23 | -------------------------------------------------------------------------------- /dashboard/client/assets/dateConverter.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | const unixToDateTime = (unixTime) => { 6 | 7 | const months = { 8 | 0: 'Jan', 9 | 1: 'Feb', 10 | 2: 'Mar', 11 | 3: 'Apr', 12 | 4: 'May', 13 | 5: 'Jun', 14 | 6: 'Jul', 15 | 7: 'Aug', 16 | 8: 'Sept', 17 | 9: 'Oct', 18 | 10: 'Nov', 19 | 11: 'Dec' 20 | } 21 | 22 | const unixDate = new Date(unixTime); 23 | let month = unixDate.getMonth(); 24 | if (months[month]) month = months[month]; 25 | let date = unixDate.getDate(); 26 | let hour = unixDate.getHours(); 27 | let min = unixDate.getMinutes(); 28 | if (min < 10) min = `0${min}`; 29 | return `${month} ${date} ${hour}:${min}`; 30 | }; 31 | 32 | export default unixToDateTime; -------------------------------------------------------------------------------- /dashboard/server/routes/metricsRouter.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ********************************************************************************** 3 | * @description: WIP --> Controller for RestAPI pulls for Node metrics (not in use) 4 | * ********************************************************************************** 5 | */ 6 | 7 | 8 | const express = require('express'); 9 | const metricsRouter = express.Router(); 10 | const metricController = require('../controllers/metricController'); 11 | 12 | metricsRouter.get('/', 13 | metricController.getNodeCPU, 14 | metricController.getTotalDisk, 15 | metricController.getFreeDisk, 16 | metricController.getNodeMemory, 17 | metricController.getClusterInfo, (req, res) => { 18 | res.status(200).json(res.locals); 19 | }); 20 | 21 | module.exports = metricsRouter; 22 | -------------------------------------------------------------------------------- /dashboard/client/components/Header.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Header for our dashboard 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import { Link } from 'react-router-dom'; 9 | import Logo from '../assets/periscope_logo_transparent.png' 10 | 11 | const Header = () => { 12 | 13 | return ( 14 |
15 | 16 |
17 | 18 | 19 |
20 |
21 | ) 22 | } 23 | 24 | export default Header; 25 | 26 | 27 | -------------------------------------------------------------------------------- /dashboard/client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, useEffect } from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 3 | import LandingContainer from './container/LandingContainer.jsx'; 4 | import NodeContainer from './container/NodeContainer.jsx'; 5 | import PodContainer from './container/PodContainer.jsx'; 6 | import Header from './components/Header.jsx'; 7 | 8 | const App = () => { 9 | // useEffect with prometheus port forwarding 10 | useEffect(() => { 11 | fetch('/prometheus').then((data) => { 12 | console.log('connected to prometheus'); 13 | }); 14 | }, []); 15 | 16 | return ( 17 | 18 |
19 | 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /dashboard/client/components/MemoryTooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Custom tooltip that provides hover information 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import colors from '../assets/colors'; 9 | 10 | const style = { 11 | backgroundColor: '#1F1B24', 12 | opacity: '0.9', 13 | border: 'none', 14 | borderRadius: '5px', 15 | padding: '5px', 16 | color: 'gray', 17 | textAlign: 'left', 18 | fontSize: '14px', 19 | } 20 | 21 | const MemoryTooltip = props => { 22 | const { active, payload, label } = props; 23 | if (!active || !payload) return null; 24 | 25 | return ( 26 |
27 |

28 | {payload[0].value}% 29 |

30 |
31 | ) 32 | } 33 | 34 | export default MemoryTooltip; -------------------------------------------------------------------------------- /dashboard/client/components/PodMemoryTooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Custom tooltip for hover information 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import colors from '../assets/colors'; 9 | 10 | const style = { 11 | backgroundColor: '#1F1B24', 12 | opacity: '0.9', 13 | border: 'none', 14 | borderRadius: '5px', 15 | padding: '5px', 16 | color: 'gray', 17 | textAlign: 'left', 18 | fontSize: '14px', 19 | } 20 | 21 | const PodMemoryTooltip = props => { 22 | const { active, payload, label } = props; 23 | if (!active || !payload) return null; 24 | 25 | 26 | return ( 27 |
28 |

29 | {payload[0].payload.name}: {payload[0].value}MB 30 |

31 |
32 | ) 33 | } 34 | 35 | export default PodMemoryTooltip; -------------------------------------------------------------------------------- /dashboard/server/controllers/alertsController.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description: WIP --> Controller for alert manager 4 | * ***************************************************************************** 5 | */ 6 | 7 | 8 | const fetch = require('node-fetch'); 9 | 10 | const alertsController = {}; 11 | 12 | alertsController.createAlert = async () => { 13 | 14 | // get type of alert from frontend 15 | 16 | const url = 'http://localhost:9093/api/v2/alerts/rules'; 17 | 18 | fetch(url, { 19 | method: 'POST', 20 | headers: 'Content-Type: application/json', 21 | body: { 22 | "rules": [ 23 | { 24 | "alert": "Test Alert", 25 | "expr": "up == 1", 26 | "for": "1m", 27 | } 28 | ] 29 | } 30 | }); 31 | 32 | 33 | 34 | {/* 35 | // if statement to create the alert to send 36 | 37 | 38 | 39 | 40 | // send appropriate alert type to alertmanager api */} 41 | 42 | 43 | }; 44 | 45 | modules.export = alertsController; 46 | -------------------------------------------------------------------------------- /dashboard/server/alertmanager.yaml: -------------------------------------------------------------------------------- 1 | # /* 2 | # * ****************************************************************************************** 3 | # * @description: Custom YAML file for AlertManager (WIP) 4 | # * ****************************************************************************************** 5 | # */ 6 | 7 | global: 8 | resolve_timeout: 5m 9 | http_config: 10 | follow_redirects: true 11 | smtp_hello: localhost 12 | smtp_require_tls: true 13 | pagerduty_url: https://events.pagerduty.com/v2/enqueue 14 | opsgenie_api_url: https://api.opsgenie.com/ 15 | wechat_api_url: https://qyapi.weixin.qq.com/cgi-bin/ 16 | victorops_api_url: https://alert.victorops.com/integrations/generic/20131114/alert/ 17 | route: 18 | receiver: 'null' 19 | group_by: 20 | - job 21 | continue: false 22 | routes: 23 | - receiver: 'null' 24 | match: 25 | alertname: Watchdog 26 | continue: false 27 | group_wait: 30s 28 | group_interval: 5m 29 | repeat_interval: 12h 30 | 31 | receivers: 32 | - name: 'User' 33 | email_configs: 34 | - to: 'periscopeoslab@gmail.com' 35 | -------------------------------------------------------------------------------- /dashboard/client/components/PodCpuToolTip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Custom tooltip that provides hover information 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import colors from '../assets/colors'; 9 | 10 | const style = { 11 | backgroundColor: '#1F1B24', 12 | opacity: '0.9', 13 | border: 'none', 14 | borderRadius: '5px', 15 | padding: '5px', 16 | color: 'gray', 17 | textAlign: 'left', 18 | fontSize: '14px', 19 | } 20 | 21 | const PodCpuToolTip = props => { 22 | const { active, payload, label } = props; 23 | if (!active || !payload) return null; 24 | 25 | const podEntries = []; 26 | for (let i = 0; i < payload.length; i++) { 27 | podEntries.push(

28 | {payload[i].name}: {payload[i].value}% 29 |

); 30 | } 31 | 32 | return ( 33 |
34 |

35 | {payload[0].payload.time} 36 |

37 | {podEntries} 38 |
39 | ) 40 | } 41 | 42 | export default PodCpuToolTip; 43 | -------------------------------------------------------------------------------- /dashboard/client/components/TimeSeriesTooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Custom tooltip for hover information 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import colors from '../assets/colors'; 9 | 10 | const style = { 11 | backgroundColor: '#1F1B24', 12 | opacity: '0.9', 13 | border: 'none', 14 | borderRadius: '5px', 15 | padding: '5px', 16 | color: 'gray', 17 | textAlign: 'left', 18 | fontSize: '14px', 19 | } 20 | 21 | const TimeSeriesTooltip = props => { 22 | const { active, payload, label } = props; 23 | if (!active || !payload) return null; 24 | 25 | const nodeEntries = []; 26 | for (let i = 0; i < payload.length; i++) { 27 | nodeEntries.push(

28 | node{i + 1}: {payload[0].payload[`node${i + 1}`]}% 29 |

); 30 | } 31 | 32 | return ( 33 |
34 |

35 | {payload[0].payload.time} 36 |

37 | {nodeEntries} 38 |
39 | ) 40 | } 41 | 42 | export default TimeSeriesTooltip; -------------------------------------------------------------------------------- /dashboard/client/components/PodMemorySeriesTooltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description: Custom tooltip for hover information 4 | * ***************************************************************************** 5 | */ 6 | 7 | 8 | import React from 'react'; 9 | import colors from '../assets/colors'; 10 | 11 | const style = { 12 | backgroundColor: '#1F1B24', 13 | opacity: '0.9', 14 | border: 'none', 15 | borderRadius: '5px', 16 | padding: '5px', 17 | color: 'gray', 18 | textAlign: 'left', 19 | fontSize: '14px', 20 | } 21 | 22 | const PodMemorySeriesTooltip = props => { 23 | const { active, payload, label } = props; 24 | if (!active || !payload) return null; 25 | 26 | const podEntries = []; 27 | for (let i = 0; i < payload.length; i++) { 28 | podEntries.push(

29 | {payload[i].name}: {payload[i].value}MB 30 |

); 31 | } 32 | 33 | return ( 34 |
35 |

36 | {payload[0].payload.time} 37 |

38 | {podEntries} 39 |
40 | ) 41 | } 42 | 43 | export default PodMemorySeriesTooltip; -------------------------------------------------------------------------------- /dashboard/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './client/index.js', 5 | output: { 6 | path: path.resolve(__dirname, 'build'), 7 | filename: 'bundle.js', 8 | }, 9 | mode: process.env.NODE_ENV, 10 | module: { 11 | rules: [ 12 | { 13 | test: /.(js|jsx)$/, 14 | exclude: /node_modules/, 15 | loader: 'babel-loader', 16 | options: { 17 | presets: ['@babel/preset-env', '@babel/preset-react'], 18 | plugins: [ 19 | '@babel/plugin-transform-runtime', 20 | '@babel/transform-async-to-generator', 21 | ], 22 | }, 23 | }, 24 | { 25 | test: /css$/, 26 | use: ['style-loader', 'css-loader'], 27 | }, 28 | { 29 | test: /\.(png|jpe?g|gif)$/i, 30 | use: [ 31 | { 32 | loader: 'file-loader', 33 | }, 34 | ], 35 | }, 36 | ], 37 | }, 38 | devServer: { 39 | publicPath: '/build', 40 | host: 'localhost', 41 | port: 8080, 42 | contentBase: path.resolve(__dirname, './client'), 43 | proxy: { 44 | '/': { 45 | target: 'http://localhost:3000', 46 | secure: false, 47 | }, 48 | }, 49 | }, 50 | resolve: { 51 | extensions: ['.js', '.jsx'], 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /dashboard/server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const path = require('path'); 4 | const prometheusRouter = require('./routes/prometheusRouter'); 5 | const metricsRouter = require('./routes/metricsRouter'); 6 | const PORT = 3000; 7 | const { ApolloServer } = require('apollo-server-express'); 8 | const schema = require('./graphQL/schemas.js'); 9 | 10 | 11 | let apollo = null; 12 | async function startApolloServer(schema) { 13 | const app = express(); 14 | app.use(express.json()); 15 | app.use(express.urlencoded({ extended: true })); 16 | app.use('/build', express.static(path.resolve(__dirname, '../build'))); 17 | 18 | app.use('/prometheus', prometheusRouter); 19 | 20 | apollo = new ApolloServer(schema); 21 | await apollo.start(); 22 | apollo.applyMiddleware({ app}); 23 | console.log('apollo server listening'); 24 | app.get('/', (req, res) => { 25 | return res 26 | .status(200) 27 | .sendFile(path.resolve(__dirname, '../client/index.html')); 28 | }); 29 | 30 | app.use('/*', (req, res) => { 31 | return res.sendFile(path.resolve(__dirname, '../client/index.html'));} 32 | ); 33 | 34 | app.listen(PORT, () => { 35 | console.log(`Server listening on port: ${PORT}...`); 36 | console.log(`Server gql path is ${apollo.graphqlPath}`); 37 | }); 38 | 39 | } 40 | 41 | startApolloServer(schema); 42 | 43 | -------------------------------------------------------------------------------- /dashboard/server/graphQL/typeDefs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ****************************************************************************************** 3 | * @description: GraphQL schemas 4 | * ****************************************************************************************** 5 | */ 6 | 7 | const { gql } = require('apollo-server-express'); 8 | 9 | module.exports = gql` 10 | type ClusterInfo { 11 | data: ClusterInfoDataObject 12 | } 13 | type ClusterInfoDataObject { 14 | result: [ClusterInfoNode] 15 | } 16 | type ClusterInfoMetric { 17 | internal_ip: String 18 | node: String 19 | } 20 | type ClusterInfoNode { 21 | metric: ClusterInfoMetric 22 | value: [String] 23 | } 24 | 25 | type NodeInfo { 26 | data: NodeDataObject 27 | } 28 | type NodeDataObject { 29 | result: [Node] 30 | } 31 | type Node { 32 | metric: Instance 33 | value: [String] 34 | values: [[String]] 35 | } 36 | type Instance { 37 | instance: String 38 | pod: String 39 | } 40 | 41 | type PodMetaData { 42 | data: PodDataObject 43 | } 44 | type PodDataObject { 45 | result: [PodMetric] 46 | } 47 | type PodMetric { 48 | metric: PodInfo 49 | } 50 | type PodInfo { 51 | node: String 52 | pod: String 53 | pod_ip: String 54 | } 55 | 56 | type Query { 57 | getClusterInfo: ClusterInfo 58 | getNodeCpu(startTime: String, endTime: String, step: String): NodeInfo 59 | getNodeMemory: NodeInfo 60 | getTotalDiskSpace: NodeInfo 61 | getFreeDiskSpace(startTime: String, endTime: String, step: String): NodeInfo 62 | getPodCpu(startTime: String, endTime: String, step: String): NodeInfo 63 | getPodMemorySeries( startTime: String, endTime: String, step: String): NodeInfo 64 | getPodMemoryCurrent: NodeInfo 65 | getPodInfo: PodMetaData 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /dashboard/client/components/PodInfoTableSetup.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Table to display pod info 4 | * ***************************************************************************** 5 | */ 6 | 7 | 8 | import React from 'react'; 9 | import PodTable from './PodTable.jsx'; 10 | import { useTable } from 'react-table'; 11 | import {MDBTable} from 'mdbreact' 12 | 13 | 14 | 15 | const PodInfoTableSetup = ({ podNums, newClick, clickedArray }) => { 16 | 17 | if (podNums) { 18 | const podNames = Object.keys(podNums); 19 | const pods = []; 20 | // create an object for each pod in podnums with relevant data; store objects in pods array to pass as data to table 21 | for (let i = 0; i < podNames.length; i++) { 22 | const newObj = {}; 23 | const podName = podNames[i]; 24 | newObj['podName'] = podName; 25 | newObj['podNumber'] = podNums[podName].number; 26 | newObj['internal_ip'] = podNums[podName].pod_ip; 27 | newObj['node'] = podNums[podName].node; 28 | pods.push(newObj); 29 | } 30 | 31 | // columns for pod table 32 | const columns = [ 33 | { 34 | Header: 'Pod#', 35 | accessor: 'podNumber', 36 | }, 37 | { 38 | Header: 'Pod Name', 39 | accessor: 'podName', 40 | }, 41 | { 42 | Header: 'Pod Ip', 43 | accessor: 'internal_ip', 44 | }, 45 | { 46 | Header: 'Node', 47 | accessor: 'node', 48 | }, 49 | ]; 50 | 51 | return ( 52 |
53 | 54 | 55 | 56 |
57 | ); 58 | } 59 | }; 60 | 61 | export default PodInfoTableSetup; 62 | -------------------------------------------------------------------------------- /dashboard/server/graphQL/resolvers.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ****************************************************************************************** 3 | * @description: Apollo GraphQL resolvers for all our Nodes & Pods queries 4 | * ****************************************************************************************** 5 | */ 6 | 7 | const resolvers = { 8 | Query: { 9 | getClusterInfo: async (parent, args, { dataSources }, info) => { 10 | return dataSources.prometheusAPI.getClusterInfo(); 11 | }, 12 | getNodeCpu: async (parent, { startTime, endTime, step }, { dataSources }, info) => { 13 | return dataSources.prometheusAPI.getNodeCpu(startTime, endTime, step); 14 | }, 15 | getNodeMemory: async (parent, args, { dataSources }, info) => { 16 | return dataSources.prometheusAPI.getNodeMemory() 17 | }, 18 | getTotalDiskSpace: async (parent, args, { dataSources }, info) => { 19 | return dataSources.prometheusAPI.getTotalDiskSpace(); 20 | }, 21 | getFreeDiskSpace: async (parent, {startTime, endTime, step}, { dataSources }, info) => { 22 | return dataSources.prometheusAPI.getFreeDiskSpace(startTime, endTime, step); 23 | }, 24 | getPodCpu: async (parent, {startTime, endTime, step}, { dataSources }, info) => { 25 | return dataSources.prometheusAPI.getPodCpu(startTime, endTime, step); 26 | }, 27 | getPodMemorySeries: async (parent, {startTime, endTime, step}, { dataSources }, info) => { 28 | return dataSources.prometheusAPI.getPodMemorySeries(startTime, endTime, step); 29 | }, 30 | getPodMemoryCurrent: async (parent, args, { dataSources }, info) => { 31 | return dataSources.prometheusAPI.getPodMemoryCurrent(); 32 | }, 33 | getPodInfo: async (parent, args, { dataSources }, info) => { 34 | return dataSources.prometheusAPI.getPodInfo(); 35 | } 36 | }, 37 | }; 38 | 39 | module.exports = resolvers; -------------------------------------------------------------------------------- /dashboard/client/components/ClusterInfo.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Component that renders Node cluster info 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import Table from './table.jsx'; 9 | import { useTable } from 'react-table'; 10 | 11 | const ClusterInfo = ({ clusterInfo }) => { 12 | if (clusterInfo.data) { 13 | const clusterInfoArr = clusterInfo.data.result; 14 | const nodes = []; 15 | 16 | //loops through each node and saves the node information in an object then pushes to an array. This data will be passed to react-table. 17 | for (let i = 0; i < clusterInfoArr.length; i++) { 18 | const nodeName = clusterInfoArr[i].metric.node; 19 | const nodeNumber = 'node' + (i + 1); 20 | const internal_ip = clusterInfoArr[i].metric.internal_ip; 21 | const time = new Date(clusterInfoArr[i].value[0] * 1000).toLocaleString(); 22 | const newObj = {}; 23 | newObj['nodeName'] = nodeName; 24 | newObj['nodeNumber'] = nodeNumber; 25 | newObj['internal_ip'] = internal_ip; 26 | nodes.push(newObj); 27 | } 28 | 29 | //creates header titles for react-table and binds them to key in node object. 30 | const columns = [ 31 | { 32 | Header: 'Node Number', 33 | accessor: 'nodeNumber', 34 | }, 35 | { 36 | Header: 'Node Name', 37 | accessor: 'nodeName', 38 | }, 39 | { 40 | Header: 'Internal Ip', 41 | accessor: 'internal_ip', 42 | }, 43 | ]; 44 | 45 | 46 | return ( 47 |
48 |

Cluster Info

49 |
50 | 51 | 52 | 53 | ); 54 | } 55 | }; 56 | 57 | export default ClusterInfo; 58 | -------------------------------------------------------------------------------- /dashboard/server/controllers/portController.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ****************************************************************************************** 3 | * @description: Controller that activates our Kubernetes portforwarding 4 | * ****************************************************************************************** 5 | */ 6 | 7 | const fetch = require('node-fetch'); 8 | const { spawn } = require('child_process'); 9 | const portController = {}; 10 | 11 | 12 | //boolean that determines if the port is open 13 | let isPromUp = false; 14 | let isAlertUp = false; 15 | 16 | 17 | portController.portForward = async (req, res, next) => { 18 | try { 19 | //sets up port forwarding on prometheus server so we can grab data 20 | const process = await spawn('kubectl', ['--namespace=default', 'port-forward', 'prometheus-prometheus-kube-prometheus-prometheus-0', '9090']); 21 | 22 | // sets up portforwarding for alert manager 23 | const process2 = spawn('kubectl', [ 24 | '--namespace=default', 25 | 'port-forward', 26 | 'services/alertmanager-operated', 27 | '9093', 28 | ]) 29 | 30 | await process.stdout.on('data', data => { 31 | console.log(`stdout: ${data}`); 32 | isPromUp = true; 33 | res.locals.promUp = isPromUp; 34 | console.log('res.locals.promUP: ', res.locals.promUp); 35 | }); 36 | 37 | await process.stderr.on('data', (data) => { 38 | console.log(`stderr: ${data}`); 39 | }); 40 | 41 | await process.on('close', (code) => { 42 | console.log(`child process exited with code ${code}`); 43 | if (code === 1) {isPromUp = true; 44 | res.locals.promUp = isPromUp; 45 | console.log('child process res.locals.promUp: ', res.locals.promUp) 46 | } 47 | }); 48 | 49 | console.log('returning next') 50 | return next(); 51 | } catch (err) { 52 | console.log(err); 53 | } 54 | }; 55 | 56 | module.exports = portController; -------------------------------------------------------------------------------- /dashboard/client/components/table.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description React table for Node cluster info 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import { useTable } from 'react-table'; 9 | import lineColors from '../assets/colors'; 10 | 11 | export default function Table({ columns, data }) { 12 | const { 13 | getTableProps, // table props from react-table 14 | getTableBodyProps, // table body props from react-table 15 | headerGroups, // headerGroups, if your table has groupings 16 | rows, // rows for the table based on the data passed 17 | prepareRow, // Prepare the row (this function needs to be called for each row before getting the row props) 18 | } = useTable({ 19 | columns, 20 | data, 21 | }); 22 | 23 | 24 | return ( 25 |
26 |
27 | 28 | {headerGroups.map((headerGroup) => ( 29 | 30 | {headerGroup.headers.map((column) => ( 31 | 32 | ))} 33 | 34 | ))} 35 | 36 | 37 | {rows.map((row, index) => { 38 | prepareRow(row); 39 | return ( 40 | 41 | {row.cells.map((cell, i) => { 42 | return ( 43 | 48 | ); 49 | })} 50 | 51 | ); 52 | })} 53 | 54 |
{column.render('Header')}
46 | {cell.render('Cell')} 47 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /dashboard/__tests__/ReactTesting.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { render, screen, cleanup, waitForElementToBeRemoved, waitFor } from '@testing-library/react'; 6 | import React from 'react'; 7 | import userEvent from '@testing-library/user-event' 8 | import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; 9 | import App from '../client/App'; 10 | import LandingContainer from '../client/container/landingContainer'; 11 | import NodeContainer from '../client/container/NodeContainer'; 12 | import PodContainer from '../client/container/PodContainer'; 13 | import fetch from 'node-fetch'; 14 | import '@testing-library/jest-dom'; 15 | 16 | 17 | 18 | describe('Home Page & Navbar tests', () => { 19 | 20 | beforeEach (() => render( )); 21 | 22 | test('Header links to node page', () => { 23 | const dashboard = screen.getByRole('button', { name: 'Node Dashboard' }); 24 | userEvent.click(dashboard); 25 | const image = document.querySelector("img") 26 | expect(image.id).toBe('logo'); 27 | }); 28 | 29 | test('Header links to pod page', () => { 30 | const dashboard = screen.getByRole('button', { name: 'Pod Dashboard' }); 31 | userEvent.click(dashboard); 32 | const memoryTitle = screen.getByText('Pod Memory Usage'); 33 | expect(memoryTitle.textContent).toEqual('Pod Memory Usage'); 34 | }); 35 | 36 | 37 | }); 38 | 39 | 40 | describe('Node Container', () => { 41 | 42 | beforeEach (() => render()); 43 | 44 | test('renders Node loading component', () => { 45 | const loading = screen.getByRole('img'); 46 | expect(loading.id).toEqual('loading'); 47 | }); 48 | 49 | }); 50 | 51 | describe('Pod Container', () => { 52 | 53 | beforeEach (() => render()); 54 | 55 | 56 | test('renders Pod container', () => { 57 | const cpu = screen.getByText('CPU Usage'); 58 | expect(cpu.textContent).toBe('CPU Usage'); 59 | }); 60 | 61 | test('has list of pods', () => { 62 | const pods = document.querySelector('.pod-table'); 63 | expect(pods).toBeTruthy(); 64 | }); 65 | 66 | }); 67 | 68 | 69 | -------------------------------------------------------------------------------- /dashboard/client/components/PodTable.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Populates table with pod information 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React from 'react'; 8 | import { useTable } from 'react-table'; 9 | import lineColors from '../assets/colors'; 10 | 11 | export default function PodTable({ columns, data, newClick, clickedArray }) { 12 | const { 13 | getTableProps, // table props from react-table 14 | getTableBodyProps, // table body props from react-table 15 | headerGroups, // headerGroups, if your table has groupings 16 | rows, // rows for the table based on the data passed 17 | prepareRow, // Prepare the row (this function needs to be called for each row before getting the row props) 18 | } = useTable({ 19 | columns, 20 | data, 21 | }); 22 | 23 | // toggle color of row on click 24 | function changeColor(id) { 25 | const row = document.getElementById(id); 26 | if (row.style.color === 'orange') { 27 | row.style.color = "gray"; 28 | } else row.style.color = 'orange'; 29 | } 30 | 31 | return ( 32 |
33 | 34 | 35 | {headerGroups.map((headerGroup) => ( 36 | 37 | {headerGroup.headers.map((column) => ( 38 | 39 | ))} 40 | 41 | ))} 42 | 43 | 44 | {rows.map((row, index) => { 45 | prepareRow(row); 46 | return ( 47 | changeColor(index)}> 52 | {row.cells.map((cell, i) => { 53 | return ( 54 | 62 | ); 63 | })} 64 | 65 | ); 66 | })} 67 | 68 |
{column.render('Header')}
{ 57 | newClick(row.original.podName); 58 | }} 59 | {...cell.getCellProps()}> 60 | {cell.render('Cell')} 61 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /dashboard/server/graphQL/dataSource.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ****************************************************************************************** 3 | * @description: Apollo GraphQL queries for Nodes 4 | * ****************************************************************************************** 5 | */ 6 | 7 | const { spawn } = require('child_process'); 8 | const { RESTDataSource } = require('apollo-datasource-rest'); 9 | const camelCaseKeys = require('camelcase-keys'); 10 | const API_URL = 'http://localhost:9090/api/v1/'; 11 | 12 | class PrometheusAPI extends RESTDataSource { 13 | constructor({ isPrometheusUp }) { 14 | super(); 15 | this.baseURL = API_URL; 16 | } 17 | 18 | async getClusterInfo() { 19 | let query = 'query?query=kube_node_info'; 20 | const data = await this.get(query); 21 | return data; 22 | } 23 | 24 | async getNodeCpu( startTime, endTime, step ) { 25 | let query = `query_range?query=sum(rate(container_cpu_usage_seconds_total{image!=%22%22}[${step}]))by(instance)&start=${startTime}&end=${endTime}&step=${step}`; 26 | const data = await this.get(query); 27 | return data; 28 | } 29 | 30 | async getNodeMemory() { 31 | let query = 'query?query=sum(container_memory_usage_bytes)by(instance)%20/%20sum(container_spec_memory_limit_bytes)%20by%20(instance)' 32 | const data = await this.get(query); 33 | return data; 34 | } 35 | 36 | async getTotalDiskSpace() { 37 | let query = 'query?query=sum(node_filesystem_size_bytes)by(instance)' 38 | const data = await this.get(query); 39 | return data; 40 | } 41 | 42 | async getFreeDiskSpace( startTime, endTime, step ) { 43 | let query = `query_range?query=sum(node_filesystem_free_bytes)by(instance)&start=${startTime}&end=${endTime}&step=${step}`; 44 | const data = await this.get(query); 45 | return data; 46 | } 47 | 48 | async getPodCpu( startTime, endTime, step ) { 49 | let query = `query_range?query=sum(rate(container_cpu_usage_seconds_total[${step}]))by(pod)&start=${startTime}&end=${endTime}&step=${step}`; 50 | const data = await this.get(query); 51 | return data; 52 | } 53 | 54 | async getPodMemorySeries( startTime, endTime, step ) { 55 | let query = `query_range?query=sum(rate(container_memory_usage_bytes[${step}]))by(pod)&start=${startTime}&end=${endTime}&step=${step}`; 56 | const data = await this.get(query); 57 | return data; 58 | } 59 | 60 | async getPodMemoryCurrent() { 61 | let query = `query?query=sum(container_memory_usage_bytes)by(pod)`; 62 | const data = await this.get(query); 63 | return data; 64 | } 65 | 66 | async getPodInfo() { 67 | let query = `query?query=kube_pod_info`; 68 | const data = await this.get(query); 69 | return data; 70 | } 71 | } 72 | 73 | module.exports = PrometheusAPI; 74 | -------------------------------------------------------------------------------- /dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "periscope-dashboard", 3 | "version": "1.0.0", 4 | "description": "The Periscope dashboard monitors Kubernetes clusters", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "NODE_ENV=production node server/server.js", 9 | "build": "NODE_ENV=production webpack", 10 | "dev": "NODE_ENV=development nodemon server/server.js & NODE_ENV=development webpack server --open" 11 | }, 12 | "jest": { 13 | "automock": false, 14 | "setupFiles": [ 15 | "./setupJest.js" 16 | ], 17 | "transform": { 18 | "^.+\\.(js|jsx)$": "babel-jest" 19 | }, 20 | "moduleNameMapper": { 21 | "\\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/__mocks__/fileMock.js", 22 | "\\.(css|less)$": "/__mocks__/fileMock.js" 23 | } 24 | }, 25 | "author": "", 26 | "license": "ISC", 27 | "dependencies": { 28 | "@apollo/client": "^3.4.10", 29 | "apollo-datasource-rest": "^3.2.0", 30 | "apollo-server-core": "^3.3.0", 31 | "apollo-server-express": "^3.3.0", 32 | "axios": "^0.21.1", 33 | "camelcase-keys": "^7.0.0", 34 | "express": "^4.17.1", 35 | "graphiql": "^1.4.2", 36 | "graphql": "^15.5.2", 37 | "graphql-tag": "^2.12.5", 38 | "jest-fetch-mock": "^3.0.3", 39 | "mdbreact": "^5.1.0", 40 | "node-fetch": "^2.6.1", 41 | "pg-promise": "^10.11.0", 42 | "prop-types": "^15.7.2", 43 | "react": "^17.0.2", 44 | "react-dom": "^17.0.2", 45 | "react-redux": "^7.2.4", 46 | "react-router": "^5.2.0", 47 | "react-router-dom": "^5.2.0", 48 | "react-table": "^7.7.0", 49 | "recharts": "^2.1.0", 50 | "redux": "^4.1.0", 51 | "redux-devtools-extension": "^2.13.9", 52 | "redux-thunk": "^2.3.0" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.14.8", 56 | "@babel/plugin-proposal-class-properties": "^7.14.5", 57 | "@babel/plugin-transform-async-to-generator": "^7.14.5", 58 | "@babel/plugin-transform-runtime": "^7.15.0", 59 | "@babel/preset-env": "^7.14.8", 60 | "@babel/preset-react": "^7.14.5", 61 | "@testing-library/dom": "^8.2.0", 62 | "@testing-library/jest-dom": "^5.14.1", 63 | "@testing-library/react": "^12.0.0", 64 | "@testing-library/user-event": "^13.2.1", 65 | "babel-core": "^6.26.3", 66 | "babel-loader": "^8.2.2", 67 | "babel-preset-env": "^1.7.0", 68 | "css-loader": "^6.2.0", 69 | "file-loader": "^6.2.0", 70 | "jest": "^27.1.1", 71 | "msw": "^0.35.0", 72 | "nodemon": "^2.0.12", 73 | "react-hot-loader": "^4.13.0", 74 | "sockjs": "^0.3.21", 75 | "style-loader": "^3.2.1", 76 | "supertest": "^6.1.6", 77 | "webpack": "^5.47.0", 78 | "webpack-cli": "^4.7.2", 79 | "webpack-dev-server": "^3.11.2" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /dashboard/client/components/Memory.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Component that renders Node memory usage 4 | * ***************************************************************************** 5 | */ 6 | 7 | 8 | import React, { useState, useEffect } from 'react'; 9 | import { 10 | BarChart, 11 | Bar, 12 | Cell, 13 | XAxis, 14 | YAxis, 15 | CartesianGrid, 16 | Tooltip, 17 | Legend, 18 | ResponsiveContainer, 19 | } from 'recharts'; 20 | import MemoryTooltip from './MemoryTooltip'; 21 | import colors from '../assets/colors'; 22 | 23 | 24 | const Memory = ({ nodeMemory, nodeNums }) => { 25 | const resultArr = []; 26 | const [result, setResult] = useState([]); 27 | const [render, setRender] = useState(false); 28 | if (nodeMemory.data) { 29 | const nodes = nodeMemory.data.result; 30 | nodes.forEach((node, i) => { 31 | // match length of instance to length of ip addresses in reference node list 32 | const len = nodeNums[0].length; 33 | const internal_ip = node.metric.instance.slice(0, len); 34 | // find position of node in reference list 35 | const position = nodeNums.findIndex((ip) => ip === internal_ip); 36 | const dataPoint = {}; 37 | 38 | 39 | // builds a datapoint that has the correct node # & the % memory used data 40 | dataPoint.node = 'node' + (position + 1); 41 | dataPoint.ip = internal_ip; 42 | dataPoint.percentageMemoryUsed = (parseFloat(node.value[1])*100).toFixed(2); 43 | resultArr[position] = dataPoint; 44 | }); 45 | //prevents recharts.js from causing infinite loop of re-renderes 46 | if (render === false) { 47 | setResult(resultArr); 48 | setRender(true); 49 | } 50 | } 51 | 52 | return ( 53 |
54 |

Memory Usage

55 |
56 | 67 | 68 | {render && } 69 | { 70 | return `${tick}%`; 71 | }} 72 | /> 73 | 74 | 75 | {resultArr.map((entry, index) => ( 76 | 77 | ))} 78 | 79 | 80 |
81 |
82 | ); 83 | }; 84 | 85 | export default Memory; 86 | 87 | -------------------------------------------------------------------------------- /dashboard/server/controllers/metricController.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ****************************************************************************************** 3 | * @description: Controller for node metrics using REST API (not in use, switched to GraphQL) 4 | * ****************************************************************************************** 5 | */ 6 | 7 | 8 | 9 | const fetch = require('node-fetch'); 10 | 11 | const metricController = {}; 12 | 13 | const currentDate = Math.floor(Date.now() / 1000); 14 | const startDate = currentDate - 21600; 15 | 16 | metricController.getTotalDisk = async (req, res, next) => { 17 | // get totalbytes==> disk usage will be (total-free) / total 18 | const totalQuery = `http://localhost:9090/api/v1/query?query=sum(node_filesystem_size_bytes)by(instance)`; 19 | 20 | try { 21 | const response = await fetch(totalQuery); 22 | res.locals.totalDisk = await response.json(); 23 | 24 | return next(); 25 | } catch (err) { 26 | return next(err); 27 | } 28 | }; 29 | 30 | metricController.getFreeDisk = async (req, res, next) => { 31 | // get the free bytes: time series 32 | const freeQuery = `http://localhost:9090/api/v1/query_range?query=sum(node_filesystem_free_bytes)by(instance)&start=${startDate}&end=${currentDate}&step=1m`; 33 | 34 | // try/catch block to get free disk data bytes 35 | try { 36 | const response = await fetch(freeQuery); 37 | res.locals.freeDisk = await response.json(); 38 | 39 | return next(); 40 | } catch (err) { 41 | return next(err); 42 | } 43 | }; 44 | 45 | metricController.getNodeCPU = async (req, res, next) => { 46 | const query = `http://localhost:9090/api/v1/query_range?query=sum(rate(container_cpu_usage_seconds_total{image!=%22%22}[1m]))by(instance)&start=${startDate}&end=${currentDate}&step=1m`; 47 | 48 | try { 49 | const response = await fetch(query); 50 | res.locals.nodeCPU = await response.json(); 51 | return next(); 52 | } catch (err) { 53 | return next(err); 54 | } 55 | }; 56 | 57 | metricController.getNodeMemory = async (req, res, next) => { 58 | const query = `http://localhost:9090/api/v1/query?query=sum(container_memory_usage_bytes)by(instance)%20/%20sum(container_spec_memory_limit_bytes)%20by%20(instance)`; 59 | 60 | try { 61 | const response = await fetch(query); 62 | res.locals.nodeMemory = await response.json(); 63 | 64 | return next(); 65 | } catch (err) { 66 | return next(err); 67 | } 68 | }; 69 | 70 | metricController.getClusterInfo = async (req, res, next) => { 71 | const query = `http://localhost:9090/api/v1/query?query=kube_node_info`; 72 | 73 | try { 74 | const response = await fetch(query); 75 | res.locals.clusterInfo = await response.json(); 76 | 77 | return next(); 78 | } catch (err) { 79 | return next(err); 80 | } 81 | }; 82 | 83 | module.exports = metricController; 84 | -------------------------------------------------------------------------------- /dashboard/client/components/PodMemoryCurrentComponent.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Chart component to render current memory usage of all pods in cluster 4 | * ***************************************************************************** 5 | */ 6 | 7 | 8 | import React, { useState, useEffect } from 'react'; 9 | import { 10 | BarChart, 11 | Bar, 12 | Cell, 13 | XAxis, 14 | YAxis, 15 | CartesianGrid, 16 | Tooltip, 17 | Legend, 18 | ResponsiveContainer, 19 | } from 'recharts'; 20 | import PodMemoryTooltip from './PodMemoryTooltip'; 21 | import colors from '../assets/colors'; 22 | 23 | const PodMemoryCurrentComponent = ({ podMemoryCurrent, podNums, clickedArray }) => { 24 | const [result, setResult] = useState([]); // data to pass to the chart 25 | const [render, setRender] = useState(false); // render state to allow recharts animation but prevent constant re-rendering 26 | let sortedData = [] 27 | 28 | // check if current memory data has been received from query AND if podNums list contains pods 29 | if(podMemoryCurrent.data && Object.keys(podNums).length > 0) { 30 | 31 | const data = []; 32 | const podArray = podMemoryCurrent.data.result; 33 | for (let i = 0; i < podArray.length; i++) { 34 | const pod = {}; // create objects for each pod with relevant data from current memory query and pod number from podNums 35 | const podName = podArray[i].metric.pod; 36 | const newPodNumber = podNums[podName]; 37 | if(newPodNumber) { // if pod exists in podNums (doesn't have a null node), assign values and push to data array 38 | pod.name = newPodNumber.name; 39 | pod.value = +((+(podArray[i].value[1]) / 1000000).toFixed(2)) ; 40 | pod.number = newPodNumber.number 41 | data.push(pod) 42 | } 43 | } 44 | 45 | sortedData = data.sort((a,b)=>(a.number > b.number) ? 1 : -1); // sort data array by pod number 46 | 47 | if (render === false) { 48 | setResult(sortedData); // set results with sorted data array 49 | setRender(true); 50 | } 51 | } 52 | 53 | return ( 54 |
55 |

Pod Memory Usage

56 |
57 | 68 | 69 | {render && } 70 | 71 | 72 | 73 | 74 |
75 |
76 | ); 77 | }; 78 | 79 | export default PodMemoryCurrentComponent; 80 | 81 | -------------------------------------------------------------------------------- /dashboard/client/components/CPU.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Component that renders Node CPU chart 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React, { useState } from 'react'; 8 | import { 9 | LineChart, 10 | Line, 11 | XAxis, 12 | YAxis, 13 | CartesianGrid, 14 | Tooltip, 15 | Legend, 16 | ResponsiveContainer, 17 | } from 'recharts'; 18 | import TimeSeriesTooltip from './TimeSeriesTooltip'; 19 | import colors from '../assets/colors'; 20 | 21 | const CPU = (props) => { 22 | const resultArr = []; 23 | const lines = []; 24 | const [results, setResults] = useState([]); 25 | const [render, setRender] = useState(false); 26 | if (props.cpu.data) { 27 | const nodes = props.cpu.data.result; 28 | const nodeNums = props.nodeNums; 29 | 30 | // establishes a for loop based on length of first node 31 | nodes[0].values.forEach((x, i) => { 32 | const dataPoint = {}; 33 | let current = new Date(x[0] * 1000); 34 | dataPoint.time = current.toLocaleString(); 35 | 36 | for (let j = 0; j < nodes.length; j++) { 37 | // match length of instance to length of ip addresses in our reference node list 38 | const len = nodeNums[0].length; 39 | const internal_ip = nodes[j].metric.instance.slice(0, len); 40 | // find position of node in reference list 41 | const position = nodeNums.findIndex((ip) => ip === internal_ip); 42 | //create a datapoint with the correct node# (from reference list) and the relevant value 43 | dataPoint[`node${position + 1}`] = +(parseFloat(nodes[j].values[i][1])*100).toFixed( 44 | 2); 45 | } 46 | resultArr.push(dataPoint); 47 | }); 48 | if (render === false) { 49 | setResults(resultArr); 50 | setRender(true); 51 | } 52 | 53 | //create line for CPU data for each node. 54 | for (let i = 0; i < nodes.length; i++) { 55 | lines.push( 56 | 63 | ); 64 | } 65 | }; 66 | 67 | 68 | return ( 69 |
70 |

CPU Usage

71 | 72 | 83 | 84 | 89 | { 90 | return `${tick}%`; 91 | }} 92 | /> 93 | 94 | 98 | {lines} 99 | 100 |
101 | ); 102 | }; 103 | 104 | export default CPU; 105 | -------------------------------------------------------------------------------- /dashboard/client/components/PodCPU.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Component that renders Pod CPU usage chart 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React, { useState } from 'react'; 8 | import { 9 | LineChart, 10 | Line, 11 | XAxis, 12 | YAxis, 13 | CartesianGrid, 14 | Tooltip, 15 | Legend, 16 | ResponsiveContainer, 17 | } from 'recharts'; 18 | import PodCpuToolTip from './PodCpuToolTip'; 19 | import colors from '../assets/colors'; 20 | 21 | const PodCPU = ({ clickedArray, timeWindow, step }) => { 22 | const [results, setResults] = useState([]); 23 | const [render, setRender] = useState(false); 24 | const [clickedLength, setClickedLength] = useState(0); 25 | const [timeWindowChange, setTimeWindowChange] = useState(timeWindow); 26 | const [stepChange, setStepChange] = useState(step); 27 | const lines = []; 28 | const resultArray = []; 29 | 30 | // clickedArray is the user selected list of pods, passed down from Pod Container 31 | 32 | if (clickedLength !== clickedArray.length) { // use clickedLength to trigger re-render when new clicked array is received 33 | if (clickedArray.length === 0) setClickedLength(0); 34 | setRender(false); 35 | } 36 | 37 | if (clickedArray.length > 0) { 38 | // create datapoint objects for each point in time series with values for first pod in clicked array 39 | clickedArray[0].cpuValues.forEach((x, i) => { 40 | const dataPoint = {}; 41 | let time = new Date(x[0] * 1000); 42 | dataPoint.time = time.toLocaleString(); 43 | 44 | // add values for other pods 45 | for (let j = 0; j < clickedArray.length; j++) { 46 | dataPoint[clickedArray[j].name] = +( 47 | parseFloat(clickedArray[j].cpuValues[i][1]) * 100 48 | ).toFixed(2); 49 | } 50 | resultArray.push(dataPoint); // results array contains datapoint objects 51 | }); 52 | 53 | if (render === false) { 54 | setResults(resultArray); 55 | setClickedLength(clickedArray.length); 56 | setRender(true); 57 | } 58 | 59 | for (let i = 0; i < clickedArray.length; i++) { 60 | lines.push( 61 | 68 | ); 69 | } 70 | } 71 | 72 | return ( 73 |
74 |

CPU Usage

75 | 76 | 86 | 87 | 92 | { 95 | return `${tick}%`; 96 | }} 97 | /> 98 | 99 | 100 | {lines} 101 | 102 |
103 | ); 104 | }; 105 | 106 | export default PodCPU; 107 | -------------------------------------------------------------------------------- /dashboard/client/components/PodMemorySeriesComponent.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Linechart component to render time-series data of memory usage of selected pods 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React, { useState } from 'react'; 8 | import { 9 | LineChart, 10 | Line, 11 | XAxis, 12 | YAxis, 13 | CartesianGrid, 14 | Tooltip, 15 | Legend, 16 | ResponsiveContainer, 17 | } from 'recharts'; 18 | import PodMemorySeriesTooltip from './PodMemorySeriesTooltip'; 19 | import colors from '../assets/colors'; 20 | 21 | const PodMemorySeriesComponent = ({ clickedArray }) => { 22 | const [results, setResults] = useState([]); // data to pass to chart component 23 | const [render, setRender] = useState(false); // render to track recharts animation without constant re-rendering 24 | const [clickedLength, setClickedLength] = useState(0); 25 | const lines = []; 26 | const resultArray = []; 27 | 28 | if (clickedLength !== clickedArray.length) { // if the length of the clickedarray changes allow re-render with new clickedarray 29 | if (clickedArray.length === 0) setClickedLength(0); 30 | setRender(false); 31 | } 32 | 33 | 34 | 35 | if (clickedArray.length > 0) { 36 | clickedArray[0].memorySeriesValues.forEach((x, i) => { 37 | const dataPoint = {}; // create datapoint object for each time/memory value of the first pod in clickedarray 38 | let time = new Date(x[0] * 1000); 39 | dataPoint.time = time.toLocaleString(); 40 | 41 | for (let j = 0; j < clickedArray.length; j++) { // add values for other pods in the clickedarray 42 | dataPoint[clickedArray[j].name] = +( 43 | parseFloat(clickedArray[j].memorySeriesValues[i][1]) / 1000000 44 | ).toFixed(4); 45 | } 46 | resultArray.push(dataPoint); // push each datapoint to the resultarray 47 | }); 48 | 49 | if (render === false) { 50 | setResults(resultArray); // set results with resultarray 51 | setClickedLength(clickedArray.length); // update clickedlength state to current clickedarray length 52 | setRender(true); 53 | } 54 | 55 | for (let i = 0; i < clickedArray.length; i++) { 56 | lines.push( 57 | 64 | ); 65 | } 66 | } 67 | 68 | return ( 69 |
70 |

Memory Usage

71 | 72 | 82 | 83 | 88 | { 91 | return `${tick}MB`; 92 | }} 93 | /> 94 | 95 | 96 | {lines} 97 | 98 |
99 | ); 100 | }; 101 | 102 | export default PodMemorySeriesComponent; 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Periscope 2 | 3 |

4 | Periscope logo 5 |

6 | 7 |

Periscope Dashboard

8 | 9 |
10 | 11 | [![Status](https://img.shields.io/badge/status-active-success.svg)]() 12 | 13 |
14 | 15 | --- 16 | 17 |

Periscope is the dashboard solution for monitoring and tracking your Kubernetes pods & nodes. 18 |
19 |
20 | Visit us at getperiscopedashboard.com 21 |

22 | 23 | 24 | 25 | ## 📝 Table of Contents 26 | - [About](#about) 27 | - [Built Using](#built_using) 28 | - [Demo](#demo) 29 | - [Getting Started](#getting_started) 30 | - [Prerequisites](#prerequisites) 31 | - [Authors](#authors) 32 | - [Coming Soon](#coming_soon) 33 | 34 | ## 🧐 About 35 |

Periscope is the dashboard solution for monitoring and tracking your Kubernetes pods & nodes. 

36 | 37 |

Periscope integrates with a Prometheus server and then displays the core metrics that any engineer needs to understand the state and health of their cluster. 38 | Engineers can see CPU, disk usage and memory usage across their cluster.

39 | 40 |

The dashboard makes it easy to see troubling trends thereby providing developers with the information needed to make changes.

41 | 42 | ### ⛏️ Built Using 43 | - [Kubernetes](https://www.kubernetes.dev/) 44 | - [Prometheus|PromQL](https://prometheus.io/) 45 | - [React](https://reactjs.org) 46 | - [NodeJS|Express](https://expressjs.com/) 47 | - [Apollo GraphQL](https://www.apollographql.com/) 48 | - [React Router](https://reactrouter.com/) 49 | - [Locust](https://locust.io/) 50 | - [Recharts](https://recharts.org/en-US/) 51 | - [React Table](https://react-table.tanstack.com/) 52 | - [Webpack](https://webpack.js.org/) 53 | - [React Testing Library](https://testing-library.com/docs/react-testing-library/) 54 | - [Jest](https://jestjs.io/) 55 | 56 | 57 | 58 | ## 🎥 Demo 59 | 60 | 61 | 62 | 63 | 64 | 65 | ## 🏁 Getting Started 66 | Start by forking and cloning this repo. 67 | 68 | ### Prerequisites 69 | - Install the [kubectl](https://kubernetes.io/docs/tasks/tools) command line tools. 70 | - Host your Kubernetes cluster on a service like [GKE](https://cloud.google.com/kubernetes-engine) or [EKS](https://aws.amazon.com/eks/) or use [MiniKube](https://minikube.sigs.k8s.io/docs/start). 71 | - Install [the Prometheus server](https://prometheus-operator.dev/docs/prologue/quick-start/) in order to generate your metrics 72 | - Save your Prometheus server on the default namespace 73 | - Then build and run the dashboard! 74 | 75 | ## ✍️ Authors 76 | - Adda Kridler: [Github](https://github.com/addakridler) | [LinkedIn](https://www.linkedin.com/in/adda-kridler-23028887/) 77 | - Junie Hou: [Github](https://github.com/selilac) | [LinkedIn](https://www.linkedin.com/in/juniehou/) 78 | - Ronke Oyekunle: [Github](https://github.com/ronke11) | [LinkedIn](https://www.linkedin.com/in/royekunle) 79 | - Shawn Convery: [Github](https://github.com/smconvery) | [LinkedIn](https://www.linkedin.com/in/shawn-convery-459b79167/) 80 | 81 | ## 🎉 Coming Soon! 82 | - Set email / slack alerts for major changes in metrics 83 | - Ability to enter your own PromQL query  84 | - Log History 85 | -------------------------------------------------------------------------------- /dashboard/client/container/NodeContainer.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Node dashboard page container 4 | * ***************************************************************************** 5 | */ 6 | 7 | import React, { useState, useEffect } from 'react'; 8 | import Memory from '../components/Memory.jsx'; 9 | import CPU from '../components/CPU.jsx'; 10 | import ClusterInfo from '../components/ClusterInfo.jsx'; 11 | import DiskUsage from '../components/DiskUsage.jsx'; 12 | import loading from '../assets/loading.gif'; 13 | 14 | const mainContainerGraphQL = () => { 15 | const [cpu, setCPU] = useState({}); 16 | const [totalDisk, setTotalDisk] = useState({}); 17 | const [freeDisk, setFreeDisk] = useState({}); 18 | const [nodeMemory, setNodeMemory] = useState({}); 19 | const [clusterInfo, setClusterInfo] = useState({}); 20 | const [isLoading, setIsLoading] = useState(true); 21 | const [nodeNums, setNodeNums] = useState([]); 22 | const [called, setCalled] = useState(false); 23 | 24 | const sixHours = 21600; 25 | const endTime = Math.floor(Date.now() / 1000);; 26 | const startTime = endTime - sixHours; 27 | 28 | 29 | const step = '5m'; 30 | 31 | //graphQL query 32 | const query = `{ 33 | getFreeDiskSpace(startTime: "${startTime}", endTime: "${endTime}", step: "${step}") { 34 | data { 35 | result { 36 | metric { 37 | instance 38 | } 39 | values 40 | } 41 | } 42 | } 43 | getNodeCpu(startTime: "${startTime}", endTime: "${endTime}", step: "${step}") { 44 | data { 45 | result { 46 | metric { 47 | instance 48 | } 49 | values 50 | } 51 | } 52 | } 53 | getClusterInfo { 54 | data { 55 | result { 56 | metric { 57 | internal_ip 58 | node 59 | } 60 | value 61 | } 62 | } 63 | } 64 | getNodeMemory { 65 | data { 66 | result { 67 | metric { 68 | instance 69 | } 70 | value 71 | } 72 | } 73 | } 74 | getTotalDiskSpace { 75 | data { 76 | result { 77 | metric { 78 | instance 79 | } 80 | value 81 | } 82 | } 83 | } 84 | }`; 85 | 86 | //fetch request based on graphQL query 87 | useEffect(() => { 88 | fetch('/graphql', { 89 | method: 'POST', 90 | headers: { 91 | 'Content-Type': 'application/json', 92 | }, 93 | body: JSON.stringify({ 94 | query, 95 | }), 96 | }) 97 | .then((res) => res.json()) 98 | .then((res) => { 99 | const data = res.data; 100 | setCPU(data.getNodeCpu); 101 | setTotalDisk(data.getTotalDiskSpace); 102 | setFreeDisk(data.getFreeDiskSpace); 103 | setNodeMemory(data.getNodeMemory); 104 | setClusterInfo(data.getClusterInfo); 105 | setIsLoading(false); 106 | }); 107 | }, []); 108 | 109 | // if data is loaded and data states are set, but called state is false 110 | if (!isLoading && !called) { 111 | const result = []; 112 | for (let i = 1; i <= clusterInfo.data.result.length; i++) { 113 | // create nodes 1 through x based on internal Ip addresses 114 | result.push(clusterInfo.data.result[i - 1].metric.internal_ip); 115 | } 116 | setNodeNums(result); 117 | setCalled(true); 118 | } 119 | 120 | //displays a loading gif if data pull isn't complete yet 121 | return isLoading ? ( 122 | 123 | ) : ( 124 |
125 |
126 | 127 |
128 |
129 | 130 |
131 |
132 | 133 |
134 |
135 | 136 |
137 |
138 | ); 139 | }; 140 | 141 | export default mainContainerGraphQL; 142 | -------------------------------------------------------------------------------- /dashboard/client/components/DiskUsage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import lineColors from '../assets/colors'; 3 | /* 4 | * ***************************************************************************** 5 | * @description Component that renders Node CPU chart 6 | * ***************************************************************************** 7 | */ 8 | 9 | import TimeSeriesTooltip from './TimeSeriesTooltip'; 10 | import { 11 | LineChart, 12 | Line, 13 | XAxis, 14 | YAxis, 15 | CartesianGrid, 16 | Tooltip, 17 | Legend, 18 | ResponsiveContainer, 19 | } from 'recharts'; 20 | 21 | const DiskUsage = (props) => { 22 | 23 | // nodes object ==> name of node: total diskSpace 24 | const nodes = {}; 25 | const data = []; 26 | const lines = []; 27 | const [diskUsage, setDiskUsage] = useState([]); 28 | const [render, setRender] = useState(false); 29 | if (props.free.data) { 30 | const total = props.total.data?.result; 31 | const free = props.free.data?.result; 32 | const nodes = {}; 33 | const nodeNums = props.nodeNums; 34 | // loop through freediskspace and get the times 35 | 36 | // loops through totalDiskSpace query and pushes the name of node and total diskspace of node into an object 37 | for (let i = 0; i < total.length; i++) { 38 | // push each node #: diskSpace 39 | 40 | // match length of instance to length of ip addresses in node list 41 | const len = nodeNums[0].length; 42 | const internal_ip = total[i].metric.instance.slice(0, len); 43 | // // find position of node in reference list 44 | const position = nodeNums.findIndex((ip) => ip === internal_ip); 45 | 46 | nodes[`node${position + 1}`] = total[i].value[1]; 47 | } 48 | 49 | // loops through FreeDiskSpace and sends time and value @ time to new object 50 | for (let i = 0; i < free.length; i++) { 51 | const values = free[i].values; 52 | //find correct nodeNum 53 | // match length of instance to length of ip addresses in node list 54 | const len = nodeNums[0].length; 55 | const internal_ip = free[i].metric.instance.slice(0, len); 56 | // // find position of node in reference list 57 | const position = nodeNums.findIndex((ip) => ip === internal_ip); 58 | 59 | // grab all the times from the first index of the array 60 | if (i === 0) { 61 | for (let j = 0; j < values.length; j++) { 62 | const time = new Date(values[j][0] * 1000).toLocaleString(); 63 | data.push({ time: time }); 64 | } 65 | } 66 | // put the node # & it's value in each time object 67 | for (let k = 0; k < data.length; k++) { 68 | // (total size - value at each time) / total size 69 | const totalDisk = nodes[`node${position + 1}`]; 70 | const freeDiskSpace = values[k][1]; 71 | data[k][`node${position + 1}`] = (((totalDisk - freeDiskSpace) / totalDisk)*100).toFixed(2); 72 | } 73 | } 74 | //prevents recharts.js from creating infinite loop with re-renders. 75 | if (render === false) { 76 | setDiskUsage(data); 77 | setRender(true); 78 | } 79 | 80 | //adds a line in the graph for each node with total disk usage over time. 81 | 82 | for (let i = 0; i < total.length; i++) { 83 | lines.push( 84 | 91 | ); 92 | } 93 | } 94 | return ( 95 |
96 |

Disk Usage

97 | 109 | 110 | 111 | 116 | { 117 | return `${tick}%`; 118 | }} 119 | /> 120 | 121 | 125 | {lines} 126 | 127 |
128 | ); 129 | }; 130 | 131 | export default DiskUsage; 132 | -------------------------------------------------------------------------------- /dashboard/client/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, 3 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | background-color: #121212; 5 | color: gray; 6 | margin: 0px; 7 | } 8 | 9 | button { 10 | background-color: #0e4f8c; 11 | border: none; 12 | border-radius: 5px; 13 | color: whitesmoke; 14 | padding: 5px 10px; 15 | margin: 5px; 16 | font-weight: bold; 17 | } 18 | 19 | h2 { 20 | text-align: center; 21 | } 22 | 23 | .landing { 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: center; 27 | align-items: center; 28 | height: 100vh; 29 | } 30 | 31 | .header { 32 | display: flex; 33 | justify-content: space-between; 34 | align-items: center; 35 | height: 30px; 36 | margin: 0px; 37 | padding: 10px; 38 | } 39 | 40 | .main-container { 41 | display: flex; 42 | justify-content: center; 43 | flex-wrap: wrap; 44 | } 45 | 46 | .components { 47 | border-color: #1f1b24; 48 | margin: 3px; 49 | border-radius: 10px; 50 | background-color: #1f1b24; 51 | text-align: center; 52 | } 53 | 54 | .dropdowns { 55 | display: flex; 56 | justify-content: flex-end; 57 | margin-bottom: 5px; 58 | margin-right: 30px; 59 | } 60 | 61 | .dropdowns button { 62 | background-color: #ff8505; 63 | border-radius: 0px; 64 | margin-bottom: 0px; 65 | } 66 | 67 | .dropdown-menu { 68 | /* border-radius: 0px 0px 5px 5px; */ 69 | background-color: gray; 70 | opacity: 0.9; 71 | position: absolute; 72 | z-index: 2; 73 | margin: 0px 5px 5px; 74 | } 75 | 76 | .dropdown-item { 77 | font-weight: bold; 78 | text-decoration: none; 79 | color: black; 80 | } 81 | 82 | .dropdown-div { 83 | padding: 5px; 84 | color: black; 85 | font-weight: bold; 86 | } 87 | 88 | .dropdown-div:hover { 89 | background-color: whitesmoke; 90 | } 91 | .dropdown-div:active { 92 | color: #ff8505; 93 | } 94 | 95 | #timeMenu { 96 | width: 95.33px; 97 | } 98 | 99 | #stepMenu { 100 | width: 48.89px; 101 | } 102 | 103 | #podInfo { 104 | display: flex; 105 | flex-direction: column; 106 | justify-content: center; 107 | } 108 | 109 | .pod-info-rows { 110 | display: flex; 111 | flex-direction: column; 112 | justify-content: center; 113 | width: 750px; 114 | } 115 | 116 | .pod-container { 117 | display: flex; 118 | justify-content: center; 119 | flex-wrap: wrap; 120 | margin-top: 40px; 121 | } 122 | 123 | .chart-container { 124 | padding-bottom: 20px; 125 | } 126 | .table { 127 | display: flex; 128 | justify-content: center; 129 | padding-bottom: 20px; 130 | } 131 | 132 | td { 133 | margin: 0; 134 | padding-bottom: 10px; 135 | padding-left: 4px; 136 | padding-right: 4px; 137 | padding-top: 10px; 138 | /* border-bottom: 0.5px solid rgb(100, 100, 117, 0.5); */ 139 | } 140 | 141 | tr:nth-of-type(odd) { 142 | background-color: #25212c; 143 | } 144 | tr > th { 145 | background-color: #1f1b24; 146 | padding-bottom: 10px; 147 | } 148 | .table-row { 149 | cursor: pointer; 150 | } 151 | .table-row:hover { 152 | color: #0e4f8c; 153 | } 154 | 155 | .column1 { 156 | width: 300px; 157 | text-overflow: scroll; 158 | overflow: hidden; 159 | } 160 | .column3 { 161 | width: 300px; 162 | text-overflow: scroll; 163 | overflow: hidden; 164 | } 165 | 166 | .table { 167 | margin: 0px 15px; 168 | } 169 | .pod-table td { 170 | padding: 5px; 171 | font-size: 12px; 172 | } 173 | .pod-table th { 174 | font-size: 12px; 175 | padding: 5px; 176 | } 177 | 178 | .recharts-wrapper { 179 | margin: 0 auto; 180 | } 181 | 182 | #CPU { 183 | width: 750px; 184 | } 185 | 186 | #disk-usage { 187 | width: 750px; 188 | } 189 | 190 | #clusterInfo { 191 | width: 500px; 192 | } 193 | #memory { 194 | width: 500px; 195 | } 196 | #logo { 197 | width: 75px; 198 | margin-right: -35px; 199 | margin-left: -5px; 200 | } 201 | 202 | #podMemory { 203 | padding-right: 25px; 204 | } 205 | #podCpu { 206 | padding-right: 25px; 207 | } 208 | 209 | #loading { 210 | display: block; 211 | margin-top: 100px; 212 | margin-left: auto; 213 | margin-right: auto; 214 | width: 50%; 215 | } 216 | 217 | @media screen and (max-width: 1280px) { 218 | .main-container { 219 | flex-direction: column; 220 | align-items: center; 221 | } 222 | #clusterInfo { 223 | width: 750px; 224 | order: 2; 225 | } 226 | #memory { 227 | width: 750px; 228 | order: 1; 229 | } 230 | #podInfo { 231 | order: 0; 232 | } 233 | #podCpu { 234 | width: 750px; 235 | order: 1; 236 | } 237 | #podMemory { 238 | width: 750px; 239 | order: 2; 240 | } 241 | #podMemoryBars { 242 | order: 3; 243 | } 244 | 245 | 246 | 247 | } 248 | -------------------------------------------------------------------------------- /dashboard/client/components/PodInfoRows.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Information and selection of individual pods to be displayed in PodInfoTable; 4 | functions to select time and step range for time-series queries 5 | * ***************************************************************************** 6 | */ 7 | 8 | 9 | import React, { useState } from 'react'; 10 | import PodInfoTableSetup from './PodInfoTableSetup.jsx'; 11 | 12 | const PodInfoRows = ({ 13 | clickedArray, 14 | setClickedArray, 15 | podNums, 16 | setStep, 17 | setTimeWindow, 18 | }) => { 19 | const [isTimeOpen, setIsTimeOpen] = useState(false); // state for time range dropdown 20 | const [isStepOpen, setIsStepOpen] = useState(false); // state for step range dropdown 21 | 22 | // on click function to select pods to add to clickedArray in podContainer 23 | const newClick = (arg) => { 24 | let found = false; 25 | const newClickedArray = clickedArray.slice(); // copy of current clickedArray to update 26 | for (let i = 0; i < newClickedArray.length; i++) { 27 | if (newClickedArray[i].podName === arg) { // if selected pod is already in clickedArray, remove it 28 | newClickedArray.splice(i, 1); 29 | setClickedArray(newClickedArray); 30 | found = true; 31 | break; 32 | } 33 | } 34 | if (!found) { // if selected pod is not in clickedArray, add it 35 | newClickedArray.push(podNums[arg]); 36 | setClickedArray(newClickedArray); 37 | } 38 | }; 39 | 40 | // changes all colors of pods back to gray (unselects) once the time-range or time-step is changed 41 | function changeColorsBack() { 42 | const rows = document.querySelectorAll('.table-row'); 43 | for (const row of rows) { 44 | if (row.style.color === 'orange') row.style.color = "gray"; 45 | } 46 | } 47 | 48 | //time range variables for time range selection 49 | const oneHour = 3600; 50 | const sixHours = 21600; 51 | const twelveHours = 43200; 52 | const oneDay = 86400; 53 | const threeDays = 259200; 54 | 55 | const times = [oneHour, sixHours, twelveHours, oneDay, threeDays]; 56 | const timeStrs = ['1hr', '6hrs', '12hrs', '1day', '3days']; 57 | const timeButtons = []; 58 | times.forEach((time, i) => { 59 | //create dropwdown items to select time range 60 | timeButtons.push( 61 |
{ 65 | setTimeWindow(time); 66 | setClickedArray([]); 67 | setIsTimeOpen(false); 68 | changeColorsBack(); 69 | }}> 70 | {timeStrs[i]} 71 |
72 | ); 73 | }); 74 | 75 | const steps = ['5m', '15m', '30m', '1h']; // step range variables in array 76 | const stepButtons = []; 77 | steps.forEach((step, i) => { 78 | // create dropdown items to select step range 79 | stepButtons.push( 80 |
{ 84 | setStep(step); 85 | setClickedArray([]); 86 | setIsStepOpen(false); 87 | changeColorsBack(); 88 | }}> 89 | {step} 90 |
91 | ); 92 | }); 93 | 94 | // functions to toggle time and step dropdowns 95 | const toggleStep = () => { 96 | if (isTimeOpen) setIsTimeOpen(false); 97 | isStepOpen ? setIsStepOpen(false) : setIsStepOpen(true); 98 | }; 99 | 100 | const toggleTime = () => { 101 | if (isStepOpen) setIsStepOpen(false); 102 | isTimeOpen ? setIsTimeOpen(false) : setIsTimeOpen(true); 103 | }; 104 | 105 | 106 | 107 | return ( 108 |
109 |
110 |
111 | 114 | {isTimeOpen && ( 115 |
116 | {timeButtons} 117 |
118 | )} 119 |
120 |
121 | 124 | {isStepOpen && ( 125 |
126 | {stepButtons} 127 |
128 | )} 129 |
130 |
131 | 139 |
140 |
141 |
142 | 147 |
148 |
149 | ); 150 | }; 151 | 152 | export default PodInfoRows; 153 | -------------------------------------------------------------------------------- /dashboard/client/container/PodContainer.jsx: -------------------------------------------------------------------------------- 1 | /* 2 | * ***************************************************************************** 3 | * @description Pod dashboard page container 4 | * ***************************************************************************** 5 | */ 6 | 7 | 8 | import React, { useState, useEffect } from 'react'; 9 | import PodMemoryCurrentComponent from '../components/PodMemoryCurrentComponent.jsx'; 10 | import PodCPU from '../components/PodCPU.jsx'; 11 | import PodInfoRows from '../components/PodInfoRows.jsx'; 12 | import PodMemorySeriesComponent from '../components/PodMemorySeriesComponent.jsx'; 13 | 14 | const PodContainer = () => { 15 | const [podCpu, setPodCpu] = useState({}); 16 | const [podMemorySeries, setPodMemorySeries] = useState({}); 17 | const [podMemoryCurrent, setPodMemoryCurrent] = useState({}); 18 | const [podInfo, setPodInfo] = useState({}); 19 | const [isLoading, setIsLoading] = useState(true); 20 | const [podNums, setPodNums] = useState({}); 21 | const [called, setCalled] = useState(false); 22 | const [clickedArray, setClickedArray] = useState([]); 23 | const [timeWindow, setTimeWindow] = useState(21600); 24 | const [step, setStep] = useState('5m'); 25 | 26 | // time variables for promQL query range 27 | const endTime = Math.floor(Date.now() / 1000); 28 | const startTime = endTime - timeWindow; 29 | 30 | // query to graphql server 31 | const query = `{ 32 | getPodCpu(startTime: "${startTime}", endTime: "${endTime}", step: "${step}") { 33 | data { 34 | result { 35 | metric { 36 | pod 37 | } 38 | values 39 | } 40 | } 41 | } 42 | getPodMemorySeries(startTime: "${startTime}", endTime: "${endTime}", step: "${step}") { 43 | data { 44 | result { 45 | metric { 46 | pod 47 | } 48 | values 49 | } 50 | } 51 | } 52 | getPodMemoryCurrent { 53 | data { 54 | result { 55 | metric { 56 | pod 57 | } 58 | value 59 | } 60 | } 61 | } 62 | getPodInfo { 63 | data { 64 | result { 65 | metric { 66 | node 67 | pod 68 | pod_ip 69 | } 70 | } 71 | } 72 | } 73 | }`; 74 | 75 | // fetch to graphql backend, set state with resulting data 76 | useEffect(() => { 77 | fetch('/graphql', { 78 | method: 'POST', 79 | headers: { 80 | 'Content-Type': 'application/json', 81 | }, 82 | body: JSON.stringify({ 83 | query, 84 | }), 85 | }) 86 | .then((res) => res.json()) 87 | .then((res) => { 88 | const data = res.data; 89 | setPodCpu(data.getPodCpu); 90 | setPodMemorySeries(data.getPodMemorySeries); 91 | setPodMemoryCurrent(data.getPodMemoryCurrent); 92 | setPodInfo(data.getPodInfo); 93 | setIsLoading(false); 94 | setCalled(false); // reset called to false for updating with fresh data 95 | }); 96 | }, [timeWindow, step]); 97 | 98 | // if data is loaded and data states are set, but called state is false 99 | if (!isLoading && !called) { 100 | const podInfoNumbers = {}; // empty object to store pod info with names 101 | let counter = 1; // counter to keep track of non-null pods 102 | 103 | for (let i = 0; i < podInfo.data.result.length; i++) { 104 | // create pods 1 through x based on pod names 105 | let pod = podInfo.data.result[i].metric; 106 | if (pod.node) { // skip pods with null nodes 107 | podInfoNumbers[pod.pod] = { // pod object with data 108 | node: pod.node, 109 | pod_ip: pod.pod_ip, 110 | name: `pod${counter}`, 111 | number: counter, 112 | podName: pod.pod 113 | }; 114 | counter++; // counter to keep track of number of valid pods (no null nodes) 115 | } 116 | } 117 | 118 | for (let i = 0; i < podCpu.data.result.length; i++) { 119 | // update individual pod objects with cpu values and memory values 120 | let cpuPod = podCpu.data.result[i].metric.pod; 121 | if (podInfoNumbers[cpuPod]) podInfoNumbers[cpuPod].cpuValues = podCpu.data.result[i].values; 122 | let memPod = podMemorySeries.data.result[i].metric.pod; 123 | if (podInfoNumbers[memPod]) podInfoNumbers[memPod].memorySeriesValues = podMemorySeries.data.result[i].values; 124 | } 125 | 126 | setPodNums(podInfoNumbers); 127 | setCalled(true); 128 | } 129 | 130 | 131 | return ( 132 |
133 |
134 | 135 |
136 |
137 | 138 |
139 |
140 | 141 |
142 |
143 | 144 |
145 |
146 | ) 147 | }; 148 | 149 | export default PodContainer; 150 | -------------------------------------------------------------------------------- /dashboard/build/bundle.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! 14 | Copyright (c) 2018 Jed Watson. 15 | Licensed under the MIT License (MIT), see 16 | http://jedwatson.github.io/classnames 17 | */ 18 | 19 | /*! Conditions:: INITIAL */ 20 | 21 | /*! Production:: $accept : expression $end */ 22 | 23 | /*! Production:: css_value : ANGLE */ 24 | 25 | /*! Production:: css_value : CHS */ 26 | 27 | /*! Production:: css_value : EMS */ 28 | 29 | /*! Production:: css_value : EXS */ 30 | 31 | /*! Production:: css_value : FREQ */ 32 | 33 | /*! Production:: css_value : LENGTH */ 34 | 35 | /*! Production:: css_value : PERCENTAGE */ 36 | 37 | /*! Production:: css_value : REMS */ 38 | 39 | /*! Production:: css_value : RES */ 40 | 41 | /*! Production:: css_value : SUB css_value */ 42 | 43 | /*! Production:: css_value : TIME */ 44 | 45 | /*! Production:: css_value : VHS */ 46 | 47 | /*! Production:: css_value : VMAXS */ 48 | 49 | /*! Production:: css_value : VMINS */ 50 | 51 | /*! Production:: css_value : VWS */ 52 | 53 | /*! Production:: css_variable : CSS_VAR LPAREN CSS_CPROP COMMA math_expression RPAREN */ 54 | 55 | /*! Production:: css_variable : CSS_VAR LPAREN CSS_CPROP RPAREN */ 56 | 57 | /*! Production:: expression : math_expression EOF */ 58 | 59 | /*! Production:: math_expression : LPAREN math_expression RPAREN */ 60 | 61 | /*! Production:: math_expression : NESTED_CALC LPAREN math_expression RPAREN */ 62 | 63 | /*! Production:: math_expression : SUB PREFIX SUB NESTED_CALC LPAREN math_expression RPAREN */ 64 | 65 | /*! Production:: math_expression : css_value */ 66 | 67 | /*! Production:: math_expression : css_variable */ 68 | 69 | /*! Production:: math_expression : math_expression ADD math_expression */ 70 | 71 | /*! Production:: math_expression : math_expression DIV math_expression */ 72 | 73 | /*! Production:: math_expression : math_expression MUL math_expression */ 74 | 75 | /*! Production:: math_expression : math_expression SUB math_expression */ 76 | 77 | /*! Production:: math_expression : value */ 78 | 79 | /*! Production:: value : NUMBER */ 80 | 81 | /*! Production:: value : SUB NUMBER */ 82 | 83 | /*! Rule:: $ */ 84 | 85 | /*! Rule:: (--[0-9a-z-A-Z-]*) */ 86 | 87 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)% */ 88 | 89 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)Hz\b */ 90 | 91 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)\b */ 92 | 93 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ch\b */ 94 | 95 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)cm\b */ 96 | 97 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)deg\b */ 98 | 99 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dpcm\b */ 100 | 101 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dpi\b */ 102 | 103 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dppx\b */ 104 | 105 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)em\b */ 106 | 107 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ex\b */ 108 | 109 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)grad\b */ 110 | 111 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)in\b */ 112 | 113 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)kHz\b */ 114 | 115 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)mm\b */ 116 | 117 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ms\b */ 118 | 119 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)pc\b */ 120 | 121 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)pt\b */ 122 | 123 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)px\b */ 124 | 125 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)rad\b */ 126 | 127 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)rem\b */ 128 | 129 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)s\b */ 130 | 131 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)turn\b */ 132 | 133 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vh\b */ 134 | 135 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vmax\b */ 136 | 137 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vmin\b */ 138 | 139 | /*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vw\b */ 140 | 141 | /*! Rule:: ([a-z]+) */ 142 | 143 | /*! Rule:: (calc) */ 144 | 145 | /*! Rule:: (var) */ 146 | 147 | /*! Rule:: , */ 148 | 149 | /*! Rule:: - */ 150 | 151 | /*! Rule:: \( */ 152 | 153 | /*! Rule:: \) */ 154 | 155 | /*! Rule:: \* */ 156 | 157 | /*! Rule:: \+ */ 158 | 159 | /*! Rule:: \/ */ 160 | 161 | /*! Rule:: \s+ */ 162 | 163 | /*! decimal.js-light v2.5.1 https://github.com/MikeMcl/decimal.js-light/LICENCE */ 164 | 165 | /** @license React v0.20.2 166 | * scheduler.production.min.js 167 | * 168 | * Copyright (c) Facebook, Inc. and its affiliates. 169 | * 170 | * This source code is licensed under the MIT license found in the 171 | * LICENSE file in the root directory of this source tree. 172 | */ 173 | 174 | /** @license React v16.13.1 175 | * react-is.production.min.js 176 | * 177 | * Copyright (c) Facebook, Inc. and its affiliates. 178 | * 179 | * This source code is licensed under the MIT license found in the 180 | * LICENSE file in the root directory of this source tree. 181 | */ 182 | 183 | /** @license React v17.0.1 184 | * react-dom.production.min.js 185 | * 186 | * Copyright (c) Facebook, Inc. and its affiliates. 187 | * 188 | * This source code is licensed under the MIT license found in the 189 | * LICENSE file in the root directory of this source tree. 190 | */ 191 | 192 | /** @license React v17.0.1 193 | * react.production.min.js 194 | * 195 | * Copyright (c) Facebook, Inc. and its affiliates. 196 | * 197 | * This source code is licensed under the MIT license found in the 198 | * LICENSE file in the root directory of this source tree. 199 | */ 200 | 201 | /** @license React v17.0.2 202 | * react-dom.production.min.js 203 | * 204 | * Copyright (c) Facebook, Inc. and its affiliates. 205 | * 206 | * This source code is licensed under the MIT license found in the 207 | * LICENSE file in the root directory of this source tree. 208 | */ 209 | 210 | /** @license React v17.0.2 211 | * react.production.min.js 212 | * 213 | * Copyright (c) Facebook, Inc. and its affiliates. 214 | * 215 | * This source code is licensed under the MIT license found in the 216 | * LICENSE file in the root directory of this source tree. 217 | */ 218 | 219 | //! moment.js 220 | 221 | //! moment.js locale configuration 222 | --------------------------------------------------------------------------------