├── yaml ├── kubric-depl.yaml └── kubric-rbac.yaml ├── .gitignore ├── public ├── client │ ├── scss │ │ ├── _variables.scss │ │ └── application.scss │ ├── .DS_Store │ ├── assets │ │ ├── cat.jpeg │ │ ├── kubric.png │ │ └── kubric-colored-skeleton.png │ ├── actions │ │ ├── clusterActionCreators.js │ │ ├── actionTypes.js │ │ ├── logsActionCreator.js │ │ └── metricsActionCreators.js │ ├── reducers │ │ ├── ingressesReducer.js │ │ ├── deploymentsReducer.js │ │ ├── loginReducer.js │ │ ├── masterNodeReducer.js │ │ ├── metricsReducer.js │ │ ├── servicesReducer.js │ │ ├── nodesReducer.js │ │ ├── podsReducer.js │ │ └── logsReducer.js │ ├── Containers │ │ ├── LoginContainer.jsx │ │ ├── PodChartContainer.jsx │ │ ├── NodeChartContainer.jsx │ │ ├── ConfigContainer.jsx │ │ ├── PodsContainer.jsx │ │ ├── NodeXContainer.jsx │ │ ├── MasterNodeContainer.jsx │ │ ├── LogContainer.jsx │ │ ├── PersistQueryContainer.jsx │ │ ├── MetricsContainer.jsx │ │ └── LiveQueryContainer.jsx │ ├── Components │ │ ├── Home.jsx │ │ ├── NodeComponent.jsx │ │ ├── PodComponent.jsx │ │ ├── MasterNodeGraph.jsx │ │ ├── PodWriteToDiskComponent.jsx │ │ ├── PodMemoryComponent.jsx │ │ ├── PodLogsComponent.jsx │ │ ├── PodCpuComponent.jsx │ │ ├── LoginComponent.jsx │ │ ├── NodeMemoryComponent.jsx │ │ ├── NodeCpuComponent.jsx │ │ ├── NodeWriteToDiskComponent.jsx │ │ ├── NodeCpuSaturationComponent.jsx │ │ └── LogRowComponent.jsx │ ├── store.js │ ├── App.jsx │ └── Navigation │ │ └── Navigation.jsx ├── .DS_Store └── index.html ├── logGen-app ├── .gitignore ├── README.md ├── Dockerfile ├── package.json ├── logGen-app-depl.yaml ├── app │ └── server.js └── package-lock.json ├── .DS_Store ├── server ├── routes │ ├── signUpRouter.js │ ├── loginRouter.js │ ├── logsRouter.js │ ├── clusterRouter.js │ └── metricsRouter.js ├── model.js ├── controllers │ ├── loginController.js │ ├── clusterController.js │ ├── logsController.js │ └── metricsController.js └── server.js ├── values.yaml ├── src └── index.jsx ├── webpack.config.js ├── fluent-update.yaml ├── package.json ├── workflow.md └── README.md /yaml/kubric-depl.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /yaml/kubric-rbac.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /public/client/scss/_variables.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /logGen-app/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kubric/HEAD/.DS_Store -------------------------------------------------------------------------------- /logGen-app/README.md: -------------------------------------------------------------------------------- 1 | ## THIS IS A SAMPLE APP FOR LOGGING AT INTERVALS ## -------------------------------------------------------------------------------- /public/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kubric/HEAD/public/.DS_Store -------------------------------------------------------------------------------- /public/client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kubric/HEAD/public/client/.DS_Store -------------------------------------------------------------------------------- /public/client/assets/cat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kubric/HEAD/public/client/assets/cat.jpeg -------------------------------------------------------------------------------- /public/client/assets/kubric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kubric/HEAD/public/client/assets/kubric.png -------------------------------------------------------------------------------- /public/client/assets/kubric-colored-skeleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/kubric/HEAD/public/client/assets/kubric-colored-skeleton.png -------------------------------------------------------------------------------- /logGen-app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | COPY package*.json /usr/app/ 4 | COPY app/* /usr/app/ 5 | 6 | WORKDIR /usr/app 7 | 8 | RUN npm install 9 | CMD ["node","server.js"] -------------------------------------------------------------------------------- /public/client/actions/clusterActionCreators.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from './actionTypes.js'; 2 | 3 | export const renderCluster = boolean => { 4 | // console.log('render cluster action creator valid user value', boolean); 5 | return { 6 | type: actionTypes.RECEIVE_LOGIN, 7 | payload: boolean, 8 | }; 9 | }; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Kubric 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /server/routes/signUpRouter.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const loginController = require('../controllers/loginController'); 3 | const signUpRouter = Router(); 4 | 5 | signUpRouter.post('/', loginController.signUp, (req, res, error ) => { 6 | return res.status(200).send(res.locals.validUser); 7 | }); 8 | 9 | module.exports = signUpRouter; -------------------------------------------------------------------------------- /server/routes/loginRouter.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const loginController = require('../controllers/loginController'); 3 | const loginRouter = Router(); 4 | 5 | loginRouter.post('/', loginController.getLogin, (req, res, error ) => { 6 | return res.status(200).send(res.locals.validUser); 7 | }); 8 | 9 | 10 | module.exports = loginRouter; -------------------------------------------------------------------------------- /values.yaml: -------------------------------------------------------------------------------- 1 | esJavaOpts: "-Xmx128m -Xms128m" 2 | 3 | resources: 4 | requests: 5 | cpu: "100m" 6 | memory: "512M" 7 | limits: 8 | cpu: "1000m" 9 | memory: "512M" 10 | 11 | volumeClaimTemplate: 12 | accessModes: [ "ReadWriteOnce" ] 13 | storageClassName: "linode-block-storage" 14 | resources: 15 | requests: 16 | storage: 100M -------------------------------------------------------------------------------- /server/routes/logsRouter.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const {getAppLogs,getAppFields, getIndices} = require('../controllers/logsController'); 3 | const logsRouter = Router(); 4 | 5 | logsRouter.get('/app',getAppLogs,(req,res)=>{ 6 | res.status(200).json({appLogs:res.locals.appLogs}) 7 | }) 8 | logsRouter.get('/appFields',getIndices,getAppFields,(req,res)=>{ 9 | res.status(200).json(res.locals.appFields) 10 | }) 11 | 12 | module.exports = logsRouter; -------------------------------------------------------------------------------- /public/client/reducers/ingressesReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js' 2 | 3 | const initialState = { 4 | ingresses: [], 5 | } 6 | 7 | function ingressesReducer (state = initialState, action) { 8 | const { type, payload } = action; 9 | 10 | switch (type) { 11 | case actionTypes.RECEIVE_INGRESSES: 12 | // add logic here 13 | 14 | 15 | return { ...state, ingresses } 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export default ingressesReducer; -------------------------------------------------------------------------------- /public/client/reducers/deploymentsReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js' 2 | 3 | const initialState = { 4 | deployments: [], 5 | } 6 | 7 | function deploymentsReducer (state = initialState, action) { 8 | const { type, payload } = action; 9 | 10 | switch (type) { 11 | case actionTypes.RECEIVE_DEPLOYMENTS: 12 | // add logic here 13 | 14 | return { ...state, deployments }; 15 | default: 16 | return state; 17 | } 18 | 19 | } 20 | 21 | export default deploymentsReducer; -------------------------------------------------------------------------------- /logGen-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logGen-app", 3 | "version": "1.0.0", 4 | "description": "App for generating simple logs", 5 | "main": "server.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "node app/server.js" 10 | }, 11 | "author": "kubric", 12 | "license": "ISC", 13 | "dependencies": { 14 | "express": "^4.17.1", 15 | "node-fetch": "^3.0.0", 16 | "pino": "6.3.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/client/reducers/loginReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js' 2 | 3 | const initialState = { 4 | validUser : false, 5 | }; 6 | 7 | export default function loginReducer (state = initialState, action) { 8 | const {type, payload} = action; 9 | 10 | switch (type) { 11 | case actionTypes.RECEIVE_LOGIN: { 12 | const validUser = payload; 13 | return { 14 | ...state, 15 | validUser, 16 | }; 17 | } 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { render } from 'react-dom'; 4 | import App from '../public/client/App.jsx'; 5 | import store from '../public/client/store.js' 6 | import ReactDOM from "react-dom"; 7 | // import styles from scss/css so webpack can bundle styles 8 | import styles from '../public/client/scss/application.scss'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root'), 15 | ); 16 | 17 | -------------------------------------------------------------------------------- /logGen-app/logGen-app-depl.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: loggen-app 5 | labels: 6 | app: loggen-app 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: loggen-app 12 | template: 13 | metadata: 14 | labels: 15 | app: loggen-app 16 | spec: 17 | containers: 18 | - name: loggen-app 19 | image: jlhline/loggen-app:loggen-app 20 | imagePullPolicy: Always 21 | ports: 22 | - containerPort: 3000 23 | 24 | -------------------------------------------------------------------------------- /public/client/Containers/LoginContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LoginComponent from '../Components/LoginComponent.jsx'; 3 | import Grid from '@mui/material/Grid'; 4 | 5 | function LoginContainer(props){ 6 | return ( 7 | 8 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default LoginContainer; -------------------------------------------------------------------------------- /public/client/Components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Paper from '@mui/material/Paper'; 3 | import Box from '@mui/material/Box'; 4 | import kubricLogo from '../assets/kubric.png' 5 | function Home() { 6 | return ( 7 |
8 | 9 | 10 | 11 | 12 | 13 |
14 | ) 15 | } 16 | export default Home; 17 | -------------------------------------------------------------------------------- /server/routes/clusterRouter.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const clusterController = require('../controllers/clusterController'); 3 | const clusterRouter = Router(); 4 | 5 | clusterRouter.get( 6 | '/getLists', 7 | clusterController.getPods, 8 | clusterController.getServices, 9 | clusterController.getDeployments, 10 | //clusterController.getIngresses, (having an issue with this line idk why; getting HTTP error) 11 | clusterController.getNodes, 12 | //sending a compiled object with each middleware's data with an unique property name in a format of {component}List 13 | (req, res)=> res.status(200).json(res.locals.list) 14 | ); 15 | 16 | module.exports = clusterRouter; -------------------------------------------------------------------------------- /server/model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const myURI = 'mongodb+srv://kubric:kubric123@kubricdb.vqagz.mongodb.net/myFirstDatabase?retryWrites=true&w=majority'; 4 | 5 | mongoose.connect(myURI, { 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true, 8 | }) 9 | .then(() => console.log('Kubric mongoose connected')) 10 | .catch((err) => console.log(`new err ${err}`)) 11 | 12 | const userDB = new mongoose.Schema({ 13 | username : { 14 | type: String, 15 | required: true, 16 | unique: true, 17 | }, 18 | password : { 19 | type: String, 20 | required: true, 21 | unique: true, 22 | } 23 | }) 24 | 25 | module.exports = mongoose.model('UserDB', userDB); -------------------------------------------------------------------------------- /logGen-app/app/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import pino from 'pino'; 3 | import fetch from 'node-fetch'; 4 | 5 | const app = express(); 6 | const logger = pino({ 7 | 8 | level:'info', 9 | timestamp: () => `,"time":"${new Date().toISOString()}"` 10 | }); 11 | 12 | logger.info('Prepare for dog breeds') 13 | 14 | const dogBreeds = () =>{ 15 | fetch('https://api.thedogapi.com/v1/images/search',{ 16 | headers:{'Content-Type' : 'application/json'} 17 | }) 18 | .then(res=>res.json()) 19 | .then(res=>{ 20 | logger.info(res[0].breeds[0].name) 21 | }) 22 | .catch((error)=>{console.log("caught error:",error)}) 23 | } 24 | setInterval(dogBreeds,20000); 25 | 26 | app.listen(3000,function(){ 27 | logger.info("app listening on port 3000") 28 | }) -------------------------------------------------------------------------------- /public/client/reducers/masterNodeReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js' 2 | 3 | const initialState = { 4 | serverAPILatency: [], 5 | serverAPIsuccessReq: [], 6 | controllerAddCounter: [], 7 | etcdRequestRate: [], 8 | }; 9 | 10 | export default function masterNodeReducer (state = initialState, action) { 11 | const {type, payload} = action; 12 | 13 | switch(type){ 14 | case actionTypes.RECEIVE_MASTER_NODE: { 15 | const {serverAPILatency, serverAPIsuccessReq, controllerAddCounter, etcdRequestRate} = payload; 16 | return { 17 | ...state, 18 | serverAPILatency, 19 | serverAPIsuccessReq, 20 | controllerAddCounter, 21 | etcdRequestRate, 22 | }; 23 | } 24 | 25 | default: 26 | return state; 27 | }; 28 | }; -------------------------------------------------------------------------------- /public/client/Containers/PodChartContainer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Stack from '@mui/material/Stack'; 3 | import PodCpuComponent from '../Components/PodCpuComponent.jsx' 4 | import PodMemoryComponent from '../Components/PodMemoryComponent.jsx'; 5 | import PodWriteToDiskComponent from '../Components/PodWriteToDiskComponent.jsx'; 6 | import PodLogsComponent from '../Components/PodLogsComponent.jsx'; 7 | 8 | export default function PodChartContainer() { 9 | 10 | return ( 11 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /public/client/Containers/NodeChartContainer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Stack from '@mui/material/Stack'; 3 | import NodeCpuComponent from '../Components/NodeCpuComponent.jsx' 4 | import NodeMemoryComponent from '../Components/NodeMemoryComponent.jsx'; 5 | import NodeCpuSaturationComponent from '../Components/NodeCpuSaturationComponent.jsx'; 6 | import NodeWriteToDiskComponent from '../Components/NodeWriteToDiskComponent.jsx'; 7 | 8 | export default function PodChartContainer() { 9 | 10 | const charts = { 11 | 12 | } 13 | 14 | return ( 15 |
16 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | ); 28 | } -------------------------------------------------------------------------------- /public/client/Containers/ConfigContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function ConfigContainer(props){ 4 | return ( 5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | {/* should this be a drop down menu instead? */} 16 | 17 | 18 |
19 |
20 | ); 21 | } 22 | 23 | export default ConfigContainer; -------------------------------------------------------------------------------- /public/client/store.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, applyMiddleware, compose, createStore } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import podsReducer from './reducers/podsReducer'; 4 | import servicesReducer from './reducers/servicesReducer'; 5 | import logsReducer from './reducers/logsReducer'; 6 | import nodesReducer from './reducers/nodesReducer'; 7 | import masterNodeReducer from './reducers/masterNodeReducer'; 8 | import loginReducer from './reducers/loginReducer'; 9 | 10 | const rootReducer = combineReducers({ 11 | loginReducer, 12 | logsReducer, 13 | podsReducer, 14 | nodesReducer, 15 | servicesReducer, 16 | masterNodeReducer, 17 | // deploymentsReducer, 18 | // ingressesReducer, 19 | // metricsReducer, 20 | // ADD ANY NEW REDUCERS HERE 21 | }); 22 | 23 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 24 | 25 | const store = createStore( 26 | rootReducer, 27 | composeEnhancers(applyMiddleware(thunk)) 28 | ); 29 | 30 | export default store; -------------------------------------------------------------------------------- /server/routes/metricsRouter.js: -------------------------------------------------------------------------------- 1 | const { Router } = require('express'); 2 | const metricsController = require('../controllers/metricsController'); 3 | const metricsRouter = Router(); 4 | 5 | metricsRouter.get( 6 | '/', 7 | metricsController.getCPUSatByNodes, 8 | metricsController.getCPUByNodes, 9 | metricsController.getMemoryByNodes, 10 | metricsController.getWriteToDiskRateByNodes, 11 | // metricsController.getMemoryBarData, <-- this could be developed 12 | (req,res)=> res.status(200).json(res.locals.nodeMetrics) 13 | ); 14 | 15 | metricsRouter.get( 16 | '/getPodMetrics/:nodeName', 17 | metricsController.getCPUByPods, 18 | metricsController.getMemoryByPods, 19 | metricsController.getWriteToDiskRateByPods, 20 | metricsController.getLogsByPods, 21 | (req,res)=> res.status(200).json(res.locals.podMetrics) 22 | ); 23 | 24 | metricsRouter.get( 25 | '/getMasterNode', 26 | metricsController.getMasterNodeMetrics, 27 | (req,res)=> res.status(200).json(res.locals.masterNode) 28 | ); 29 | 30 | module.exports = metricsRouter; -------------------------------------------------------------------------------- /public/client/reducers/metricsReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js' 2 | 3 | const initialState = { 4 | defaultMetrics: [], 5 | podCpuMetrics: [], 6 | podMemoryMetrics: [], 7 | serverApiMetrics: [], 8 | } 9 | 10 | export default function metricsReducer (state = initialState, action) { 11 | const { type, payload } = action; 12 | switch (type) { 13 | 14 | case actionTypes.DEFAULT_METRICS_RECEIVED: { 15 | let defaultMetrics = payload; 16 | 17 | return { 18 | ...state, 19 | defaultMetrics, 20 | } 21 | } 22 | case actionTypes.PODS_CPU_METRICS_RECEIVED: { 23 | let podCpuMetrics = payload; 24 | 25 | return { 26 | ...state, 27 | podCpuMetrics, 28 | } 29 | } 30 | case actionTypes.PODS_MEMORY_METRICS_RECEIVED: { 31 | let podMemoryMetrics = payload; 32 | 33 | return { 34 | ...state, 35 | podMemoryMetrics, 36 | } 37 | } 38 | case actionTypes.SERVERAPI_METRICS_RECEIVED: { 39 | let serverApiMetrics = payload; 40 | 41 | return { 42 | ...state, 43 | serverApiMetrics, 44 | } 45 | 46 | } 47 | default: 48 | return state; 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /server/controllers/loginController.js: -------------------------------------------------------------------------------- 1 | const model = require('../model'); 2 | const bcrypt = require('bcrypt'); 3 | 4 | const loginController = {}; 5 | 6 | loginController.getLogin = async (req, res, next) => { 7 | res.locals.validUser = false; 8 | const user = req.body.username; 9 | const pw = req.body.password; 10 | 11 | const userInDB = await model.find({username : user}); 12 | 13 | if (userInDB.length) { 14 | bcrypt.compare(pw, userInDB[0].password, (err, data) => { 15 | try { 16 | if (err) { 17 | res.locals.validUser = false; 18 | } 19 | if (data) { 20 | res.locals.validUser = true; 21 | } 22 | next(); 23 | } 24 | catch(err) {return next(err)} 25 | }); 26 | } else { 27 | res.locals.validUser = false; 28 | next(); 29 | } 30 | }; 31 | 32 | loginController.signUp = async (req, res, next) => { 33 | res.locals.validUser = false; 34 | const user = req.body.username; 35 | const pw = req.body.password; 36 | 37 | bcrypt.hash(pw, 10, async (err, hash) => { 38 | const userInDB = await model.create({username: user, password: hash}); 39 | res.locals.validUser = true; 40 | next(); 41 | }); 42 | }; 43 | 44 | module.exports = loginController; 45 | 46 | -------------------------------------------------------------------------------- /public/client/actions/actionTypes.js: -------------------------------------------------------------------------------- 1 | //login 2 | export const RECEIVE_LOGIN = 'RECEIVE_LOGIN'; 3 | 4 | // 5 | export const RECEIVE_PODS = 'RECEIVE_PODS'; 6 | export const RECEIVE_SERVICES = 'RECEIVE_SERVICES'; 7 | export const RECEIVE_DEPLOYMENTS = 'RECEIVE_DEPLOYMENTS'; 8 | export const RECEIVE_INGRESSES = 'RECEIVE_INGRESSES'; 9 | export const DEFAULT_METRICS_RECEIVED = 'DEFAULT_METRICS_RECEIVED'; 10 | export const PODS_CPU_METRICS_RECEIVED = 'PODS_CPU_METRICS_RECEIVED'; 11 | export const PODS_MEMORY_METRICS_RECEIVED = 'PODS_MEMORY_METRICS_RECEIVED'; 12 | export const SERVERAPI_METRICS_RECEIVED = 'SERVERAPI_METRICS_RECEIVED'; 13 | 14 | // pod stuff 15 | export const RENDER_POD_METRICS = 'RENDER_POD_METRICS'; 16 | export const DISPLAY_POD_METRICS = 'DISPLAY_POD_METRICS'; 17 | export const RECEIVE_APP_LOGS = 'RECEIVE_APP_LOGS'; 18 | export const APP_LOGS_RECEIVED = 'APP_LOGS_RECEIVED'; 19 | export const RECEIVE_APP_LOG_FIELDS = 'RECEIVE_APP_LOG_FIELDS'; 20 | export const APP_LOG_FIELDS_RECEIVED = 'APP_LOG_FIELDS_RECEIVED'; 21 | export const SELECT_INDEX = 'SELECT_INDEX'; 22 | 23 | // node stuff 24 | export const RECEIVE_NODES = 'RECEIVE_NODES'; 25 | export const DISPLAY_NODE_METRICS = 'DISPLAY_NODE_METRICS'; 26 | export const RENDER_NODE_METRICS = 'RENDER_NODE_METRICS'; 27 | 28 | // master node stuff 29 | export const RECEIVE_MASTER_NODE = 'RECEIVE_MASTER_NODE'; -------------------------------------------------------------------------------- /public/client/reducers/servicesReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js' 2 | 3 | const initialState = { 4 | services: [], 5 | } 6 | 7 | function servicesReducer(state = initialState, action) { 8 | const { type, payload } = action; 9 | 10 | switch (type) { 11 | case actionTypes.RECEIVE_SERVICES: 12 | const { items } = payload.data; 13 | let services = []; 14 | items.forEach(service => { 15 | const { metadata, spec, status } = service; 16 | // if this isn't a kubernetes service, push it into services array 17 | if (metadata.name !== 'kubernetes' ) { 18 | services.push({ 19 | metadata: { 20 | creationTime: metadata.creationTimestamp, 21 | name: metadata.name, 22 | namespace: metadata.namespace, 23 | // where are the strings on the next two lines coming from? 24 | managedBy: metadata.labels['app.kubernetes.io/managed-by'], 25 | app: metadata.labels['k8s-app'], 26 | prometheus: metadata.labels.prometheus, 27 | uid: metadata.uid, 28 | }, 29 | spec: { ...spec }, 30 | status: { ...status }, 31 | }); 32 | } 33 | }); 34 | return { ...state, services }; 35 | 36 | default: 37 | return state; 38 | } 39 | } 40 | 41 | export default servicesReducer; -------------------------------------------------------------------------------- /public/client/Containers/PodsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import PodComponent from '../Components/PodComponent.jsx'; 3 | import { connect } from 'react-redux'; 4 | import * as actions from '../actions/metricsActionCreators.js' 5 | import { alpha } from '@mui/material'; 6 | import List from '@mui/material/List'; 7 | 8 | 9 | const mapStateToProps = state => { 10 | return { 11 | pods: state.podsReducer.pods, 12 | } 13 | } 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return { 17 | fetchPodMetrics: (nodeName) => dispatch(actions.fetchPodMetrics(nodeName)), 18 | } 19 | } 20 | 21 | function PodsContainer(props) { 22 | 23 | useEffect(() => { 24 | props.fetchPodMetrics(props.nodeName); 25 | }, []); 26 | 27 | const podsElement = []; 28 | let keyCount = 1; 29 | 30 | for (let pod in props.pods) { 31 | const { name, cpuValues, memoryValues, healthy, alive, displayMetrics } = props.pods[pod]; 32 | 33 | podsElement.push( 34 | 44 | ); 45 | keyCount ++; 46 | } 47 | 48 | return ( 49 |
50 | 51 | {podsElement} 52 | 53 |
54 | ); 55 | }; 56 | 57 | 58 | 59 | export default connect(mapStateToProps, mapDispatchToProps)(PodsContainer); -------------------------------------------------------------------------------- /public/client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import LoginContainer from './Containers/LoginContainer.jsx'; 4 | import LogContainer from './Containers/LogContainer.jsx'; 5 | import MetricsContainer from './Containers/MetricsContainer.jsx'; 6 | import CssBaseline from '@mui/material/CssBaseline'; 7 | import { createTheme,ThemeProvider } from '@mui/material/styles'; 8 | import Navigation from './Navigation/Navigation.jsx'; 9 | import Home from './Components/Home.jsx'; 10 | import {connect} from 'react-redux'; 11 | 12 | const mapStateToProps = (state) => { 13 | return { 14 | appLogs: state.logsReducer.appLogs, 15 | validUser: state.loginReducer.validUser, 16 | }; 17 | }; 18 | 19 | export class App extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | } 23 | 24 | render() { 25 | if (this.props.validUser === false) { 26 | return ( 27 |
28 | 29 |
30 | ); 31 | } 32 | if (this.props.validUser === true) { 33 | return ( 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | ) 47 | } 48 | } 49 | } 50 | 51 | export default connect(mapStateToProps, null)(App); 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /public/client/Containers/NodeXContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NodeComponent from '../Components/NodeComponent.jsx'; 3 | import { connect } from 'react-redux'; 4 | import * as actions from '../actions/metricsActionCreators.js' 5 | import List from '@mui/material/List'; 6 | import { alpha } from '@mui/material'; 7 | import { blueGrey } from '@mui/material/colors'; 8 | import Paper from '@mui/material/Paper'; 9 | 10 | const mapStateToProps = state => { 11 | return { 12 | nodes: state.nodesReducer.nodes, 13 | } 14 | } 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | renderNodeMetrics: () => dispatch(actions.renderNodeMetrics()), 19 | } 20 | } 21 | 22 | const NodesContainer = (props) => { 23 | const nodesElement = []; 24 | let keyCount = 1; 25 | 26 | for (let node in props.nodes) { 27 | const { name, cpuValues, memoryValues, healthy, alive, displayMetrics } = props.nodes[node]; 28 | 29 | nodesElement.push( 30 | 40 | ); 41 | keyCount ++; 42 | } 43 | 44 | return ( 45 |
46 | {/* */} 47 | 48 | {nodesElement} 49 | 50 | {/* */} 51 |
52 | ); 53 | }; 54 | 55 | 56 | export default connect(mapStateToProps, mapDispatchToProps)(NodesContainer); -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | const app = express(); 4 | const port = 3000; //can change 5 | const clusterRouter = require('./routes/clusterRouter'); 6 | const loginRouter = require('./routes/loginRouter'); 7 | const logsRouter = require('./routes/logsRouter'); 8 | const signUpRouter = require('./routes/signUpRouter'); 9 | const metricsRouter = require('./routes/metricsRouter'); 10 | const cors = require('cors'); 11 | 12 | // to deal with cors erros 13 | app.use(cors()); 14 | 15 | //to parse the incoming requests with JSON 16 | app.use(express.json()); 17 | app.use(express.urlencoded({ extended: true })); 18 | 19 | //render html for home and react routes 20 | app.get(['/home','/metrics','/logs'], 21 | (req, res)=> { 22 | res.status(200).sendFile(path.join(__dirname, '../public/index.html')) 23 | }); 24 | app.get('/', 25 | (req, res)=> { 26 | res.status(200).sendFile(path.join(__dirname, '../public/index.html')) 27 | }); 28 | 29 | //api routers 30 | app.use('/api/cluster', clusterRouter); 31 | app.use('/api/signup', signUpRouter); 32 | app.use('/api/login', loginRouter); 33 | app.use('/api/logs', logsRouter); 34 | app.use('/api/metrics', metricsRouter); 35 | 36 | 37 | //send all other end point to 404 not found 38 | app.use('*', (req, res) => res.sendStatus(404)); 39 | 40 | //global error handler 41 | app.use((err, req, res, next)=>{ 42 | const defaultErr = { 43 | log: 'unknown middleware error', 44 | status: 400, 45 | message: {err: 'error occurred'} 46 | }; 47 | const errorObj = { 48 | ...defaultErr, 49 | log: err.name + ' ' + err.message, 50 | message: {err: err.message} 51 | }; 52 | console.log(errorObj.log); 53 | return res.status(errorObj.status).json(errorObj.message); 54 | }); 55 | 56 | app.listen(port, ()=>console.log(`server at port ${port}`)); -------------------------------------------------------------------------------- /server/controllers/clusterController.js: -------------------------------------------------------------------------------- 1 | const k8s = require('@kubernetes/client-node'); 2 | const kc = new k8s.KubeConfig(); 3 | kc.loadFromDefault(); 4 | const k8sApi = kc.makeApiClient(k8s.CoreV1Api); 5 | const k8sApi2 = kc.makeApiClient(k8s.ExtensionsV1beta1Api); 6 | const k8sApi3 = kc.makeApiClient(k8s.AppsV1Api); 7 | 8 | const clusterController = {}; 9 | 10 | 11 | clusterController.getPods = (req, res, next) =>{ 12 | k8sApi.listNamespacedPod('default') 13 | .then(data=>{ 14 | //going to be the first middleware in get method chains, so that it creates a blank object that will store each request with an unique property name 15 | res.locals.list = {}; 16 | res.locals.list.podList = data.body; 17 | next(); 18 | }) 19 | .catch(err => next(err)); 20 | }; 21 | 22 | clusterController.getServices = (req, res, next) =>{ 23 | k8sApi.listNamespacedService('default') 24 | .then(data=>{ 25 | res.locals.list.serviceList = data.body; 26 | next(); 27 | }) 28 | .catch(err => next(err)); 29 | }; 30 | 31 | clusterController.getDeployments = (req, res, next) =>{ 32 | k8sApi3.listNamespacedDeployment('default') 33 | .then(data=>{ 34 | res.locals.list.deploymentList = data.body; 35 | next(); 36 | }) 37 | .catch(err => next(err)); 38 | }; 39 | 40 | clusterController.getIngresses = (req, res, next) =>{ 41 | console.log('getIngre') 42 | k8sApi2.listNamespacedIngress('default') 43 | .then(data=>{ 44 | res.locals.list.ingressList = data.body; 45 | next(); 46 | }) 47 | .catch(err => next(err)); 48 | }; 49 | 50 | clusterController.getNodes = (req, res, next) =>{ 51 | k8sApi.listNode('default') 52 | .then(data=>{ 53 | res.locals.list.nodeList = data.body; 54 | next(); 55 | }) 56 | .catch(err => next(err)); 57 | }; 58 | 59 | module.exports = clusterController; -------------------------------------------------------------------------------- /public/client/reducers/nodesReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js' 2 | 3 | const initialState = { 4 | nodes: {}, 5 | }; 6 | 7 | function nodesReducer (state = initialState, action) { 8 | const { type, payload } = action; 9 | 10 | switch (type) { 11 | case actionTypes.RECEIVE_NODES: { 12 | const { nodesCpu, nodesMemory, CPUSatValsNodes, writeToDiskNodes } = payload; 13 | const nodes = {}; 14 | 15 | nodesCpu.forEach ( metric => { 16 | if (!nodes[metric.metric.instance]) { 17 | nodes[metric.metric.instance] = { 18 | name: metric.metric.instance, 19 | cpuValues: metric.values, 20 | displayMetrics: true, 21 | healthy: true, 22 | alive: true, 23 | } 24 | } 25 | else { 26 | nodes[metric.metric.node].values = metric.values; 27 | } 28 | }) 29 | nodesMemory.forEach( metric => { 30 | nodes[metric.metric.instance].memoryValues = metric.values; 31 | }) 32 | CPUSatValsNodes.forEach( metric => { 33 | nodes[metric.metric.instance].CPUSatValsNodes = metric.values; 34 | }); 35 | writeToDiskNodes.forEach( metric => { 36 | nodes[metric.metric.instance].writeToDiskNodes = metric.values; 37 | }); 38 | 39 | return {...state, nodes}; 40 | } 41 | 42 | case actionTypes.DISPLAY_NODE_METRICS: 43 | const nodeName = payload; 44 | const nodesObj = JSON.parse(JSON.stringify(state.nodes)); 45 | const node = nodesObj[nodeName]; 46 | 47 | node.displayMetrics = node.displayMetrics ? false : true; 48 | 49 | nodesObj[nodeName] = node; 50 | 51 | return { 52 | ...state, 53 | nodes: nodesObj, 54 | } 55 | 56 | default: 57 | return state; 58 | } 59 | } 60 | 61 | export default nodesReducer; -------------------------------------------------------------------------------- /public/client/Containers/MasterNodeContainer.jsx: -------------------------------------------------------------------------------- 1 | import React,{useEffect, useState} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/metricsActionCreators.js'; 4 | import Stack from '@mui/material/Stack'; 5 | import MasterNodeGraph from '../Components/MasterNodeGraph.jsx'; 6 | 7 | const mapDispatchToProps = (dispatch) => { 8 | return { 9 | fetchMasterNodeMetrics: () => dispatch(actions.fetchMasterNodeMetrics()) 10 | } 11 | } 12 | 13 | const mapStateToProps = (state) => { 14 | return { 15 | masterNode: { 16 | serverAPILatency: state.masterNodeReducer.serverAPILatency, 17 | serverAPISuccessReq: state.masterNodeReducer.serverAPIsuccessReq, 18 | controllerAddCounter: state.masterNodeReducer.controllerAddCounter, 19 | etcdRequestRate: state.masterNodeReducer.etcdRequestRate, 20 | } 21 | } 22 | } 23 | 24 | function MasterNodeContainer(props) { 25 | 26 | useEffect(()=>{ 27 | props.fetchMasterNodeMetrics(); 28 | },[]) 29 | 30 | return ( 31 |
32 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | ); 45 | } 46 | 47 | export default connect(mapStateToProps, mapDispatchToProps)(MasterNodeContainer); -------------------------------------------------------------------------------- /public/client/Components/NodeComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/metricsActionCreators.js' 4 | import ListItem from '@mui/material/ListItem'; 5 | import ListItemButton from '@mui/material/ListItemButton'; 6 | import ListItemIcon from '@mui/material/ListItemIcon'; 7 | import ListItemText from '@mui/material/ListItemText'; 8 | import Checkbox from '@mui/material/Checkbox'; 9 | 10 | const mapStateToProps = state => { 11 | return { 12 | node: state.podsReducer.nodes 13 | } 14 | } 15 | 16 | const mapDispatchToProps = dispatch => { 17 | return { 18 | displayNodeMetrics: (nodeName) => dispatch(actions.displayNodeMetrics(nodeName)), 19 | } 20 | } 21 | 22 | function NodeComponent (props) { 23 | const [checked, setChecked] = React.useState([0]); 24 | 25 | const handleToggle = (nodeName) => () => { 26 | const currentIndex = checked.indexOf(nodeName); 27 | const newChecked = [...checked]; 28 | 29 | if (currentIndex === -1) { 30 | newChecked.push(nodeName); 31 | } else { 32 | newChecked.splice(currentIndex, 1); 33 | } 34 | 35 | setChecked(newChecked); 36 | props.displayNodeMetrics(nodeName) 37 | } 38 | 39 | return ( 40 |
41 | 47 | 48 | 49 | 56 | 57 | 58 | 59 | 60 |
61 | ); 62 | 63 | }; 64 | 65 | export default connect(mapStateToProps, mapDispatchToProps)(NodeComponent); -------------------------------------------------------------------------------- /public/client/reducers/podsReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js'; 2 | 3 | const initialState = { 4 | pods: {}, 5 | } 6 | 7 | function podsReducer(state = initialState, action) { 8 | const { type, payload } = action; 9 | 10 | switch (type) { 11 | case actionTypes.RECEIVE_PODS: 12 | const { cpuMetrics, memoryMetrics, writeToDiskPods,logMetrics } = payload 13 | let pods = {}; 14 | 15 | cpuMetrics.forEach( metric => { 16 | if (!pods[metric.metric.pod]) { 17 | pods[metric.metric.pod] = { 18 | name: metric.metric.pod, 19 | cpuValues: metric.values, 20 | displayMetrics: true, 21 | healthy: true, 22 | alive: true, 23 | } 24 | } 25 | else { 26 | pods[metric.metric.pod].cpuValues = metric.values; 27 | } 28 | }) 29 | 30 | memoryMetrics.forEach( metric => { 31 | pods[metric.metric.pod].memoryValues = metric.values; 32 | }) 33 | 34 | for(let pod in pods){ 35 | writeToDiskPods.forEach(metric => { 36 | if(pod === metric.metric.pod){ 37 | pods[pod].writeToDiskValues = metric.values; 38 | return; 39 | } 40 | }) 41 | if(!pods[pod].writeToDiskValues) pods[pod].writeToDiskValues = []; 42 | } 43 | for(let pod in pods){ 44 | logMetrics.forEach(metric => { 45 | if(pod === metric.metric.pod){ 46 | pods[pod].logMetrics = metric.values; 47 | return; 48 | } 49 | }) 50 | if(!pods[pod].logMetrics) pods[pod].logMetrics = []; 51 | } 52 | return {...state, pods}; 53 | 54 | case actionTypes.DISPLAY_POD_METRICS: 55 | const podName = payload; 56 | const podsObj = JSON.parse(JSON.stringify(state.pods)); 57 | const pod = podsObj[podName]; 58 | 59 | pod.displayMetrics = pod.displayMetrics ? false : true; 60 | podsObj[podName] = pod; 61 | 62 | return {...state,pods: podsObj}; 63 | 64 | default: 65 | return state; 66 | } 67 | } 68 | 69 | export default podsReducer; -------------------------------------------------------------------------------- /public/client/Components/PodComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/metricsActionCreators.js' 4 | import ListItem from '@mui/material/ListItem'; 5 | import ListItemButton from '@mui/material/ListItemButton'; 6 | import ListItemIcon from '@mui/material/ListItemIcon'; 7 | import ListItemText from '@mui/material/ListItemText'; 8 | import Checkbox from '@mui/material/Checkbox'; 9 | 10 | const mapStateToProps = state => { 11 | return { 12 | pod: state.podsReducer.pods 13 | } 14 | } 15 | 16 | const mapDispatchToProps = dispatch => { 17 | return { 18 | displayPodMetrics: (podName) => dispatch(actions.displayPodMetrics(podName)), 19 | } 20 | } 21 | 22 | function PodComponent (props) { 23 | const [checked, setChecked] = React.useState([0]); 24 | 25 | // changes state to display corresponding pod's metrics on graphs 26 | const handleToggle = (podName) => () => { 27 | const currentIndex = checked.indexOf(podName); 28 | const newChecked = [...checked]; 29 | 30 | if (currentIndex === -1) { 31 | newChecked.push(podName); 32 | } else { 33 | newChecked.splice(currentIndex, 1); 34 | } 35 | setChecked(newChecked); 36 | props.displayPodMetrics(podName) 37 | } 38 | 39 | return ( 40 |
41 | 47 | 48 | 49 | 56 | 57 | 58 | 59 | 60 |
61 | ); 62 | 63 | }; 64 | 65 | 66 | export default connect(mapStateToProps, mapDispatchToProps)(PodComponent); 67 | 68 | 69 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const path = require('path'); 4 | 5 | console.log(`Launching in ${process.env.NODE_ENV} mode`); 6 | 7 | module.exports = { 8 | 9 | entry: path.resolve(__dirname, './src/index.jsx'), 10 | mode: process.env.NODE_ENV, 11 | 12 | output: { 13 | path: path.join(__dirname, 'build'), 14 | filename: 'bundle.js', 15 | }, 16 | 17 | devServer: { 18 | static: { 19 | directory: path.join(__dirname, 'public'), 20 | }, 21 | compress: true, 22 | port: 8080, 23 | proxy: { 24 | '/': { 25 | target: 'http://localhost:3000', 26 | changeOrigin: true, 27 | }, 28 | }, 29 | }, 30 | 31 | resolve: { 32 | // Enable importing JS / JSX files without specifying their extension 33 | extensions: ['.js', '.jsx'], 34 | }, 35 | 36 | plugins: [ 37 | new HtmlWebpackPlugin({ 38 | template: path.join(__dirname, 'public', 'index.html'), 39 | }), 40 | new MiniCssExtractPlugin(), 41 | ], 42 | 43 | module: { 44 | rules: [ 45 | { 46 | test: /\.jsx?/, 47 | exclude: /(node_modules)/, 48 | use: { 49 | loader: 'babel-loader', 50 | options: { 51 | presets: ['@babel/preset-env', '@babel/preset-react'], 52 | }, 53 | }, 54 | }, 55 | 56 | { 57 | test: /\.s[ac]ss$/i, 58 | exclude: /(node_modules)/, 59 | // use: ['style-loader', 'css-loader', 'sass-loader'], 60 | use: [ 61 | MiniCssExtractPlugin.loader, 62 | { loader: 'css-loader', options: { url: false, sourceMap: true } }, 63 | { loader: 'sass-loader', options: { sourceMap: true } }, 64 | ], 65 | }, 66 | { 67 | test: /\.js$/, 68 | enforce: 'pre', 69 | use: ['source-map-loader'], 70 | }, 71 | { 72 | test: /\.(png|jpe?g|gif)$/i, 73 | use: [ 74 | { 75 | loader: 'file-loader', 76 | }, 77 | ], 78 | }, 79 | ], 80 | 81 | }, 82 | performance: { 83 | hints: false, 84 | maxEntrypointSize: 512000, 85 | maxAssetSize: 512000 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /public/client/actions/logsActionCreator.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as actionTypes from './actionTypes.js'; 3 | 4 | 5 | export const getAppLogs = (queryObj) => { 6 | return (dispatch) => { 7 | const {name,field,value,all} = queryObj; 8 | console.log("name",name) 9 | console.log("getAppLogsReached",JSON.stringify(queryObj)) 10 | const getAppLogsURL = 11 | `http://localhost:3000/api/logs/app? 12 | unimportant=1&name=${name}&field=${field}&value=${value}&all=${all}` 13 | // +`&start=${new Date(new Date().setDate(new Date().getDate() - 1) 14 | // ).toISOString()}&end=${new Date().toISOString()}&step=10m`; 15 | axios.get(getAppLogsURL) 16 | .then(response => { 17 | console.log('response from /api/logs/app', response.data.appLogs); 18 | dispatch(dispatchAppLogs(response.data.appLogs)); 19 | return response; 20 | }) 21 | .catch (err => console.log(`error in get app logs fetch: ${err}`)) 22 | } 23 | } 24 | export const getAppLogFields = () => { 25 | return (dispatch) => { 26 | const getAppLogFieldsURL = 'http://localhost:3000/api/logs/appFields'; 27 | axios.get(getAppLogFieldsURL) 28 | .then(response => { 29 | console.log('response from /api/logs/appFields', response.data); 30 | let fieldArray = []; 31 | let indexArray = []; 32 | for(let index in response.data){ 33 | indexArray.push(index); 34 | fieldArray.push(response.data[index]); 35 | } 36 | //console.log(response.data); 37 | dispatch(dispatchAppLogFields(fieldArray,indexArray)); 38 | return response; 39 | }) 40 | .catch (err => console.log(`error in get app fields fetch: ${err}`)) 41 | } 42 | } 43 | export const dispatchAppLogs = logs => { 44 | return { 45 | type: actionTypes.APP_LOGS_RECEIVED, 46 | payload: logs, 47 | } 48 | } 49 | export const selectIndex = index => { 50 | return { 51 | type: actionTypes.SELECT_INDEX, 52 | payload: index 53 | } 54 | } 55 | export const dispatchAppLogFields = (fields,indices) => { 56 | return { 57 | type: actionTypes.APP_LOG_FIELDS_RECEIVED, 58 | payload: {'fields':fields,'indices':indices} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /fluent-update.yaml: -------------------------------------------------------------------------------- 1 | kind: ConfigMap 2 | apiVersion: v1 3 | metadata: 4 | name: fluentd-forwarder-cm 5 | namespace: default 6 | labels: 7 | app.kubernetes.io/component: forwarder 8 | app.kubernetes.io/instance: fluentd 9 | app.kubernetes.io/managed-by: Helm 10 | app.kubernetes.io/name: fluentd 11 | helm.sh/chart: fluentd-1.3.0 12 | annotations: 13 | meta.helm.sh/release-name: fluentd 14 | meta.helm.sh/release-namespace: default 15 | data: 16 | fluentd.conf: | 17 | 18 | # Ignore fluentd own events 19 | 20 | @type null 21 | 22 | 23 | # HTTP input for the liveness and readiness probes 24 | 25 | @type http 26 | port 9880 27 | 28 | 29 | # Throw the healthcheck to the standard output instead of forwarding it 30 | 31 | @type null 32 | 33 | 34 | # Get the logs from the containers running in the node 35 | 36 | @type tail 37 | path /var/log/containers/*.log 38 | pos_file /opt/bitnami/fluentd/logs/buffers/fluentd-docker.pos 39 | tag kubernetes.* 40 | read_from_head true 41 | format json 42 | time_format %Y-%m-%dT%H:%M:%S.%NZ 43 | 44 | 45 | 46 | @type parser 47 | key_name log 48 | 49 | @type multi_format 50 | 51 | format json 52 | time_key time 53 | keep_time_key true 54 | 55 | 56 | 57 | 58 | # enrich with kubernetes metadata 59 | 60 | @type kubernetes_metadata 61 | 62 | 63 | 64 | 65 | @type elasticsearch 66 | include_tag_key true 67 | host "elasticsearch-master.default.svc.cluster.local" 68 | port "9200" 69 | index_name "loggen-app-logs" 70 | 71 | @type file 72 | path /opt/bitnami/fluentd/logs/buffers/loggen-app-logs.buffer 73 | flush_thread_count 2 74 | flush_interval 5s 75 | 76 | 77 | 78 | @type elasticsearch 79 | include_tag_key true 80 | host "elasticsearch-master.default.svc.cluster.local" 81 | port "9200" 82 | index_name "elasticsearch-logs" 83 | 84 | @type file 85 | path /opt/bitnami/fluentd/logs/buffers/elasticsearch-logs.buffer 86 | flush_thread_count 2 87 | flush_interval 5s 88 | 89 | 90 | -------------------------------------------------------------------------------- /public/client/Components/MasterNodeGraph.jsx: -------------------------------------------------------------------------------- 1 | import { propsToClassKey } from '@mui/styles'; 2 | import React, {useEffect, useState} from 'react'; 3 | import ZingChart from 'zingchart-react'; 4 | 5 | const MasterNodeGraph = ({name, data,yLabel}) => { 6 | const valuesToGraph = []; 7 | 8 | const getValues = (array) => { 9 | array.forEach(obj => { 10 | const graphValues =[]; 11 | const graphName = obj.metric.resource || obj.metric.group || obj.metric.operation || obj.metric.name; 12 | 13 | obj.values.forEach(dataPoint => { 14 | const date = new Date(dataPoint[0]*1000); 15 | const hours = date.getHours(); 16 | const minutes = date.getMinutes(); 17 | const seconds = date.getSeconds(); 18 | const time = `${hours}:${minutes}:${seconds}`; 19 | 20 | graphValues.push([time, parseFloat(dataPoint[1])]); 21 | }) 22 | valuesToGraph.push( 23 | { 24 | type: "line", 25 | decimals:3, 26 | text: graphName, 27 | values: graphValues, 28 | } 29 | ); 30 | }) 31 | } 32 | 33 | getValues(data); 34 | 35 | const graphConfig = { 36 | theme: 'dark', 37 | type: 'line', 38 | "globals": { 39 | "font-family": "Roboto", 40 | "border-radius" : 15, 41 | }, 42 | 43 | title: { 44 | text: `${name}`, 45 | "font-size": "15em", 46 | "alpha": 1, 47 | "adjust-layout": true, 48 | }, 49 | 50 | plot: { 51 | marker: { 52 | visible: false, 53 | }, 54 | animation: { 55 | effect: "ANIMATION_FADE_IN", 56 | speed:"50" 57 | }, 58 | decimals:3, 59 | tooltip: { 60 | text: "%vv at %kt from %t", 61 | decimals:3, 62 | } 63 | }, 64 | 65 | plotarea: { 66 | "margin": "dynamic", 67 | "margin-right": "30", 68 | 'width':'100%', 69 | 'height': '100%', 70 | }, 71 | 72 | scaleX: { 73 | item: { 74 | fontWeight: 'normal', 75 | }, 76 | label:{ 77 | text: "Time(60m)" 78 | } 79 | 80 | }, 81 | scaleY: { 82 | minValue:0, 83 | minorTicks: 9, 84 | label:{ 85 | text: yLabel 86 | }, 87 | item:{ 88 | fontWeight: 'normal', 89 | } 90 | }, 91 | 92 | crosshairX: { 93 | visible: false, 94 | }, 95 | 96 | series: valuesToGraph, 97 | } 98 | 99 | return ( 100 | 101 | ) 102 | } 103 | 104 | export default MasterNodeGraph; -------------------------------------------------------------------------------- /public/client/reducers/logsReducer.js: -------------------------------------------------------------------------------- 1 | import * as actionTypes from '../actions/actionTypes.js' 2 | 3 | 4 | const initialState = { 5 | appLogFields: [], 6 | appLogIndices: [], 7 | appLogs: [], 8 | selectedFields: [], 9 | selectedIndex:0 10 | } 11 | export default function metricsReducer (state = initialState, action) { 12 | const { type, payload } = action; 13 | switch (type) { 14 | case actionTypes.APP_LOG_FIELDS_RECEIVED: { 15 | let appLogFields = payload.fields; 16 | let appLogIndices = payload.indices; 17 | return { 18 | ...state, 19 | appLogFields, 20 | appLogIndices 21 | } 22 | } 23 | case actionTypes.APP_LOGS_RECEIVED: { 24 | console.log("hello for app log reducer case",payload) 25 | let idIterate = 0; 26 | let appLogs = payload.map((log)=>{ 27 | let filteredLog = {} 28 | filteredLog.id = log._id ? log._id : idIterate++; 29 | filteredLog.message = log._source.message ? log._source.message : ''; 30 | if(log._source.msg) filteredLog.message = log._source.msg; 31 | if(log._source.kubernetes.pod_name) filteredLog.podName = log._source.kubernetes.pod_name; 32 | if(log._source.level) filteredLog.level = log._source.level; 33 | if(log._source.kubernetes.host) filteredLog.host = log._source.kubernetes.host; 34 | filteredLog.timestamp = log._source.timestamp ? log._source.timestamp : ''; 35 | //if(log._source.@timestamp) filteredLog.timestamp = log._source.@timestamp; 36 | 37 | if(log._source.time) filteredLog.timestamp = log._source.time; 38 | if(filteredLog.timestamp) { 39 | const date = new Date(filteredLog.timestamp).toLocaleDateString('en-US',{ year: "numeric", month: "long", day: "numeric" } ) 40 | const time = new Date(filteredLog.timestamp).toLocaleTimeString('en-US', { hour12: false } ) 41 | filteredLog.timestamp = date.concat(" " + time); 42 | if(filteredLog.timestamp === 'Invalid Date Invalid Date') filteredLog.timestamp = '' 43 | } 44 | return filteredLog; 45 | }); 46 | console.log('appLogs', appLogs); 47 | return { 48 | ...state, 49 | appLogs, 50 | } 51 | } 52 | case actionTypes.SELECT_INDEX: { 53 | const index = payload; 54 | const selectFields = state.appLogFields.slice()[index]; 55 | console.log('selected index', index); 56 | return { 57 | ...state, 58 | selectedIndex:index, 59 | selectedFields:selectFields 60 | } 61 | } 62 | default: 63 | return state; 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /public/client/Components/PodWriteToDiskComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ZingChart from 'zingchart-react'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | pods: state.podsReducer.pods 8 | } 9 | } 10 | 11 | const PodWriteToDiskComponent = (props) => { 12 | 13 | const valuesToGraph = []; 14 | 15 | const getValues = (pods) => { 16 | for (let pod in pods) { 17 | const podValues = []; 18 | if (pods[pod].displayMetrics) { 19 | pods[pod].writeToDiskValues.forEach(dataPoint => { 20 | const date = new Date(dataPoint[0]*1000); 21 | const day = date.getDay(); 22 | const hours = date.getHours(); 23 | const minutes = date.getMinutes(); 24 | const seconds = date.getSeconds(); 25 | const time = `${day}:${hours}:${minutes}:${seconds}`; 26 | 27 | podValues.push([time, parseFloat(dataPoint[1])/10000]); 28 | }); 29 | 30 | valuesToGraph.push({ 31 | type: "line", 32 | decimals:6, 33 | text: pods[pod].name, 34 | values: podValues, 35 | }); 36 | } 37 | } 38 | } 39 | 40 | getValues(props.pods); 41 | 42 | const podWriteToDiskGraphData = { 43 | theme: 'dark', 44 | type: 'line', 45 | "globals": { 46 | "font-family": "Roboto", 47 | "border-radius" : 15, 48 | }, 49 | 50 | title: { 51 | text: 'Write to Disk Rate', 52 | "font-size": "15em", 53 | "alpha": 1, 54 | "adjust-layout": true, 55 | }, 56 | 57 | plot: { 58 | marker: { 59 | visible: false, 60 | }, 61 | animation: { 62 | effect: "ANIMATION_FADE_IN", 63 | speed:"200" 64 | }, 65 | decimals:6, 66 | tooltip: { 67 | text: "%vv at %kt time from %t", 68 | decimals:6, 69 | } 70 | }, 71 | 72 | plotarea: { 73 | "margin": "dynamic", 74 | "margin-right": "60", 75 | 'width':'100%', 76 | 'height': '100%', 77 | }, 78 | 79 | scaleX: { 80 | item: { 81 | fontWeight: 'normal', 82 | }, 83 | label:{ 84 | text: 'Time(1d)' 85 | }, 86 | }, 87 | scaleY: { 88 | minValue:0, 89 | minorTicks: 9, 90 | item:{ 91 | fontWeight: 'normal', 92 | }, 93 | label:{ 94 | text: props.yLabel 95 | }, 96 | }, 97 | 98 | crosshairX: { 99 | visible: false, 100 | }, 101 | 102 | series: valuesToGraph, 103 | } 104 | 105 | return ( 106 |
107 | Write to Disk rate 108 |
109 | ) 110 | } 111 | 112 | 113 | export default connect(mapStateToProps, null)(PodWriteToDiskComponent); -------------------------------------------------------------------------------- /public/client/Components/PodMemoryComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ZingChart from 'zingchart-react'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | pods: state.podsReducer.pods 8 | } 9 | } 10 | 11 | const PodMemoryComponent = (props) => { 12 | const { pods } = props; 13 | const valuesToGraph = []; 14 | 15 | const getValues = (pods) => { 16 | for (let pod in pods) { 17 | const podValues = []; 18 | if (pods[pod].displayMetrics) { 19 | pods[pod].memoryValues.forEach(dataPoint => { 20 | const date = new Date(dataPoint[0]*1000); 21 | const hours = date.getHours(); 22 | const minutes = date.getMinutes(); 23 | const seconds = date.getSeconds(); 24 | const time = `${hours}:${minutes}:${seconds}`; 25 | podValues.push([time, parseFloat(dataPoint[1])*0.000001]); 26 | }); 27 | valuesToGraph.push( 28 | { 29 | type: "line", 30 | decimals:3, 31 | text: pods[pod].name, 32 | values: podValues, 33 | } 34 | ); 35 | } 36 | } 37 | } 38 | 39 | getValues(pods); 40 | 41 | const podMemoryGraphData = { 42 | theme: 'dark', 43 | type: 'line', 44 | "globals": { 45 | "font-family": "Roboto", 46 | "border-radius" : 15, 47 | }, 48 | 49 | title: { 50 | text: 'Memory Usage', 51 | "font-size": "15em", 52 | "alpha": 1, 53 | "adjust-layout": true, 54 | }, 55 | 56 | plot: { 57 | marker: { 58 | visible: false, 59 | }, 60 | animation: { 61 | effect: "ANIMATION_FADE_IN", 62 | speed:"200" 63 | }, 64 | decimals:3, 65 | tooltip: { 66 | text: "%vv at %kt from %t", 67 | decimals:3, 68 | } 69 | }, 70 | 71 | plotarea: { 72 | "margin": "dynamic", 73 | "margin-right": "60", 74 | 'width':'100%', 75 | 'height': '100%', 76 | }, 77 | 78 | scaleX: { 79 | item: { 80 | fontWeight: 'normal', 81 | }, 82 | label:{ 83 | text: "Time(60m)" 84 | } 85 | 86 | }, 87 | scaleY: { 88 | minValue:0, 89 | minorTicks: 9, 90 | item:{ 91 | fontWeight: 'normal', 92 | }, 93 | 94 | label:{ 95 | text: props.yLabel 96 | }, 97 | }, 98 | 99 | crosshairX: { 100 | visible: false, 101 | }, 102 | 103 | series: valuesToGraph, 104 | } 105 | 106 | return ( 107 |
108 | Pod Zing Chart 109 |
110 | ) 111 | } 112 | 113 | export default connect(mapStateToProps, null)(PodMemoryComponent); -------------------------------------------------------------------------------- /public/client/Components/PodLogsComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ZingChart from 'zingchart-react'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | pods: state.podsReducer.pods 8 | } 9 | } 10 | 11 | const PodLogsComponent = (props) => { 12 | const { pods } = props; 13 | const valuesToGraph = []; 14 | 15 | const getValues = (pods) => { 16 | for (let pod in pods) { 17 | const podValues = []; 18 | if (pods[pod].displayMetrics) { 19 | pods[pod].logMetrics.forEach(dataPoint => { 20 | const date = new Date(dataPoint[0]*1000); 21 | const day = date.getDay(); 22 | const hours = date.getHours(); 23 | const minutes = date.getMinutes(); 24 | const seconds = date.getSeconds(); 25 | const time = `${day}:${hours}:${minutes}:${seconds}`; 26 | podValues.push([time, parseFloat(dataPoint[1])*0.000001]); 27 | }); 28 | valuesToGraph.push( 29 | { 30 | type: "line", 31 | decimals:3, 32 | text: pods[pod].name, 33 | values: podValues, 34 | } 35 | ); 36 | } 37 | } 38 | } 39 | 40 | getValues(pods); 41 | 42 | const podLogsGraphData = { 43 | theme: 'dark', 44 | type: 'line', 45 | "globals": { 46 | "font-family": "Roboto", 47 | "border-radius" : 15, 48 | }, 49 | 50 | title: { 51 | text: 'Log Memory', 52 | "font-size": "15em", 53 | "alpha": 1, 54 | "adjust-layout": true, 55 | }, 56 | 57 | plot: { 58 | marker: { 59 | visible: false, 60 | }, 61 | animation: { 62 | effect: "ANIMATION_FADE_IN", 63 | speed:"200" 64 | }, 65 | decimals:3, 66 | tooltip: { 67 | text: "%vv at %kt from %t", 68 | decimals:3, 69 | } 70 | }, 71 | 72 | plotarea: { 73 | "margin": "dynamic", 74 | "margin-right": "60", 75 | 'width':'100%', 76 | 'height': '100%', 77 | }, 78 | 79 | scaleX: { 80 | item: { 81 | fontWeight: 'normal', 82 | }, 83 | label:{ 84 | text: "Time(1d)" 85 | } 86 | 87 | }, 88 | scaleY: { 89 | minValue:0, 90 | minorTicks: 9, 91 | item:{ 92 | fontWeight: 'normal', 93 | }, 94 | 95 | label:{ 96 | text: props.yLabel 97 | }, 98 | }, 99 | 100 | crosshairX: { 101 | visible: false, 102 | }, 103 | 104 | series: valuesToGraph, 105 | } 106 | 107 | return ( 108 |
109 | Pod Zing Chart 110 |
111 | ) 112 | } 113 | 114 | export default connect(mapStateToProps, null)(PodLogsComponent); -------------------------------------------------------------------------------- /public/client/Components/PodCpuComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ZingChart from 'zingchart-react'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | pods: state.podsReducer.pods, 8 | } 9 | } 10 | 11 | const PodCpuComponent = (props) => { 12 | 13 | const { pods } = props; 14 | const valuesToGraph = []; 15 | 16 | // parses data and configures for graph 17 | const getValues = (pods) => { 18 | for (let pod in pods) { 19 | const podValues = []; 20 | 21 | if (pods[pod].displayMetrics) { 22 | pods[pod].cpuValues.forEach(dataPoint => { 23 | const date = new Date(dataPoint[0]*1000); 24 | const hours = date.getHours(); 25 | const minutes = date.getMinutes(); 26 | const seconds = date.getSeconds(); 27 | const time = `${hours}:${minutes}:${seconds}`; 28 | 29 | podValues.push([time, parseFloat(dataPoint[1])]); 30 | }); 31 | 32 | valuesToGraph.push( 33 | { 34 | type: "line", 35 | decimals:3, 36 | text: pods[pod].name, 37 | values: podValues, 38 | } 39 | ); 40 | 41 | } 42 | } 43 | } 44 | 45 | getValues(pods); 46 | 47 | const podCpuGraphData = { 48 | theme: 'dark', 49 | type: 'line', 50 | "globals": { 51 | "font-family": "Roboto", 52 | "border-radius" : 15, 53 | }, 54 | 55 | title: { 56 | text: 'CPU Usage Rate', 57 | "font-size": "15em", 58 | "alpha": 1, 59 | "adjust-layout": true, 60 | }, 61 | 62 | plot: { 63 | marker: { 64 | visible: false, 65 | }, 66 | animation: { 67 | effect: "ANIMATION_FADE_IN", 68 | speed:"200" 69 | }, 70 | decimals:3, 71 | tooltip: { 72 | text: "%vv at %kt time from %t", 73 | decimals:3, 74 | } 75 | }, 76 | 77 | plotarea: { 78 | "margin": "dynamic", 79 | "margin-right": "60", 80 | 'width':'100%', 81 | 'height': '100%', 82 | }, 83 | 84 | scaleX: { 85 | item: { 86 | fontWeight: 'normal', 87 | }, 88 | label:{ 89 | text: "Time(60m)" 90 | } 91 | 92 | }, 93 | scaleY: { 94 | minValue:0, 95 | minorTicks: 9, 96 | item:{ 97 | fontWeight: 'normal', 98 | }, 99 | label:{ 100 | text: props.yLabel 101 | }, 102 | }, 103 | 104 | crosshairX: { 105 | visible: false, 106 | }, 107 | 108 | series: valuesToGraph, 109 | } 110 | 111 | return ( 112 |
113 | 114 |
115 | ) 116 | } 117 | 118 | export default connect(mapStateToProps, null)(PodCpuComponent); -------------------------------------------------------------------------------- /public/client/Components/LoginComponent.jsx: -------------------------------------------------------------------------------- 1 | // import Button from '@' 2 | // import { importNamespaceSpecifier } from '@babel/types'; 3 | import React, {useState} from 'react'; 4 | import Box from '@mui/material/Box'; 5 | import Button from '@mui/material/Box'; 6 | import TextField from '@mui/material/TextField'; 7 | import Stack from '@mui/material/Stack'; 8 | import Grid from '@mui/material/Grid'; 9 | import Input from '@mui/material/Input'; 10 | import InputLabel from '@mui/material/InputLabel'; 11 | import FormControl from '@mui/material/FormControl'; 12 | import * as actions from '../actions/clusterActionCreators.js'; 13 | import {connect} from 'react-redux'; 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return { 17 | renderCluster: (boolean) => {dispatch(actions.renderCluster(boolean))} 18 | }; 19 | }; 20 | 21 | function LoginComponent(props) { 22 | const [username, setUsername] = useState(''); 23 | const [password, setPassword] = useState(''); 24 | 25 | const submitFormLogin = (e) => { 26 | fetch('/api/login', { 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json' 30 | }, 31 | body: JSON.stringify({username: username, password: password}), 32 | }) 33 | .then((res) => res.json()) 34 | .then((res) => { 35 | props.renderCluster(res); 36 | if (res === false) { 37 | alert('Username or password not found'); 38 | } 39 | }); 40 | }; 41 | 42 | const submitFormSignUp = (e) => { 43 | fetch('/api/signUp', { 44 | method: 'POST', 45 | headers: { 46 | 'Content-Type': 'application/json' 47 | }, 48 | body: JSON.stringify({username: username, password: password}), 49 | }) 50 | .then((res) => res.json()) 51 | .then((res) => { 52 | props.renderCluster(res); 53 | }); 54 | }; 55 | 56 | return ( 57 |
58 | 61 | 62 | :not(style)': { m: 2, width: '35ch'} 65 | }} 66 | > 67 | 68 | 69 |
70 | setUsername(e.target.value)} variant="outlined" required/> 71 | setPassword(e.target.value)} variant="outlined" required/> 72 | 73 |
74 | 75 | 76 | 77 | 78 |
79 |
80 | ); 81 | } 82 | 83 | export default connect(null, mapDispatchToProps)(LoginComponent); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubric", 3 | "version": "1.0.0", 4 | "description": "Kubernetes monitoring and logging tool", 5 | "main": "./src/index.jsx", 6 | "scripts": { 7 | "start": "cross-env NODE_ENV=production node server/server.js", 8 | "build": "cross-env NODE_ENV=production webpack", 9 | "dev": "concurrently \"cross-env NODE_ENV=development webpack serve --open\" \"cross-env NODE_ENV=development nodemon server/server.js\"", 10 | "test": " jest --runInBand --detectOpenHandles --verbose" 11 | }, 12 | "nodemonConfig": { 13 | "ignore": [ 14 | "build", 15 | "client" 16 | ] 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/oslabs-beta/kubric.git" 21 | }, 22 | "author": "laurabotel ", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/oslabs-beta/kubric/issues" 26 | }, 27 | "homepage": "https://github.com/oslabs-beta/kubric#readme", 28 | "dependencies": { 29 | "@elastic/elasticsearch": "^7.15.0", 30 | "@emotion/react": "^11.5.0", 31 | "@emotion/styled": "^11.3.0", 32 | "@kubernetes/client-node": "^0.15.1", 33 | "@mui/icons-material": "^5.0.4", 34 | "@mui/material": "^5.0.4", 35 | "@mui/styles": "^5.0.1", 36 | "@mui/x-data-grid": "^5.0.0-beta.4", 37 | "assert": "^2.0.0", 38 | "axios": "^0.23.0", 39 | "babel-jest": "^27.3.0", 40 | "bcrypt": "^5.0.1", 41 | "bcryptjs": "^2.4.3", 42 | "bootstrap": "^5.1.3", 43 | "browserfy": "^1.0.0", 44 | "cors": "^2.8.5", 45 | "d3": "^7.1.1", 46 | "dotenv": "^10.0.0", 47 | "express": "^4.17.1", 48 | "jest": "^27.3.0", 49 | "jsonwebtoken": "^8.5.1", 50 | "material-ui-image": "^3.3.2", 51 | "memory-cache": "^0.2.0", 52 | "mongodb": "^4.1.3", 53 | "mongoose": "^6.0.12", 54 | "prom-client": "^14.0.0", 55 | "prop-types": "^15.7.2", 56 | "randomstring": "^1.2.1", 57 | "react": "^17.0.2", 58 | "react-dom": "^17.0.2", 59 | "react-redux": "^7.2.5", 60 | "react-router-dom": "^5.3.0", 61 | "react-ticker": "^1.2.2", 62 | "redux": "^4.1.1", 63 | "redux-thunk": "^2.3.0", 64 | "zingchart": "^2.9.7", 65 | "zingchart-react": "^3.1.0" 66 | }, 67 | "devDependencies": { 68 | "@babel/core": "^7.15.8", 69 | "@babel/preset-env": "^7.15.8", 70 | "@babel/preset-react": "^7.14.5", 71 | "babel-core": "^6.26.3", 72 | "babel-loader": "^8.2.2", 73 | "concurrently": "^6.3.0", 74 | "cross-env": "^7.0.3", 75 | "css-loader": "^6.4.0", 76 | "eslint": "^8.0.1", 77 | "file-loader": "^6.2.0", 78 | "html-webpack-plugin": "^5.4.0", 79 | "http-proxy-middleware": "^2.0.1", 80 | "mini-css-extract-plugin": "^2.4.2", 81 | "node-sass": "^6.0.1", 82 | "nodemon": "^2.0.13", 83 | "redux-mock-store": "^1.5.4", 84 | "sass": "^1.43.2", 85 | "sass-loader": "^12.2.0", 86 | "source-map-loader": "^3.0.0", 87 | "style-loader": "^3.3.0", 88 | "webpack": "^5.58.2", 89 | "webpack-cli": "^4.9.1", 90 | "webpack-dev-server": "^4.3.1" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /public/client/Components/NodeMemoryComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ZingChart from 'zingchart-react'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | nodes: state.nodesReducer.nodes 8 | } 9 | } 10 | 11 | const NodeMemoryComponent = (props) => { 12 | const { nodes } = props; 13 | const valuesToGraph = []; 14 | 15 | const getValues = (nodes) => { 16 | for (let node in nodes) { 17 | const nodeValues = []; 18 | let nameShortened = nodes[node].name; 19 | nameShortened = nameShortened.slice(0,3) + "..." + nameShortened.slice(nameShortened.length-5,nameShortened.length) 20 | if (nodes[node].displayMetrics) { 21 | nodes[node].memoryValues.forEach(dataPoint => { 22 | const date = new Date(dataPoint[0]*1000); 23 | const hours = date.getHours(); 24 | const minutes = date.getMinutes(); 25 | const seconds = date.getSeconds(); 26 | const time = `${hours}:${minutes}:${seconds}`; 27 | 28 | nodeValues.push([time, parseFloat(dataPoint[1])]); 29 | }); 30 | valuesToGraph.push( 31 | { 32 | type: "line", 33 | decimals:3, 34 | text: nameShortened, 35 | values: nodeValues, 36 | } 37 | ); 38 | } 39 | } 40 | } 41 | 42 | getValues(nodes); 43 | 44 | const nodeMemoryGraphData = { 45 | theme: 'dark', 46 | type: 'line', 47 | "globals": { 48 | "font-family": "Roboto", 49 | "border-radius" : 15, 50 | }, 51 | 52 | title: { 53 | text: 'Memory Utilization', 54 | "font-size": "15em", 55 | "alpha": 1, 56 | "adjust-layout": true, 57 | }, 58 | 59 | plot: { 60 | marker: { 61 | visible: false, 62 | }, 63 | decimals:3, 64 | animation: { 65 | effect: "ANIMATION_FADE_IN", 66 | speed: "200" 67 | }, 68 | tooltip: { 69 | text: "%vv at %kt from %t", 70 | decimals:3, 71 | } 72 | }, 73 | 74 | plotarea: { 75 | "margin": "dynamic", 76 | "margin-right": "60", 77 | 'width':'100%', 78 | 'height': '100%', 79 | }, 80 | 81 | scaleX: { 82 | item: { 83 | fontWeight: 'normal', 84 | }, 85 | label:{ 86 | text: "Time(60m)" 87 | } 88 | 89 | }, 90 | scaleY: { 91 | label:{ 92 | text: "Per Node" 93 | }, 94 | format: "%v%", 95 | minValue:0, 96 | minorTicks: 9, 97 | item:{ 98 | fontWeight: 'normal', 99 | } 100 | }, 101 | 102 | crosshairX: { 103 | visible: false, 104 | }, 105 | 106 | series: valuesToGraph, 107 | } 108 | 109 | return ( 110 |
111 | Pod Zing Chart 112 |
113 | ) 114 | } 115 | 116 | export default connect(mapStateToProps, null)(NodeMemoryComponent); -------------------------------------------------------------------------------- /public/client/Components/NodeCpuComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ZingChart from 'zingchart-react'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | nodes: state.nodesReducer.nodes, 8 | } 9 | } 10 | 11 | const NodeCpuComponent = (props) => { 12 | 13 | const { nodes } = props; 14 | 15 | const valuesToGraph = []; 16 | 17 | const getValues = (nodes) => { 18 | for (let node in nodes) { 19 | const nodeValues = []; 20 | let nameShortened = nodes[node].name; 21 | nameShortened = nameShortened.slice(0,3) + "..." + nameShortened.slice(nameShortened.length-5,nameShortened.length) 22 | 23 | if (nodes[node].displayMetrics) { 24 | nodes[node].cpuValues.forEach(dataPoint => { 25 | const date = new Date(dataPoint[0]*1000); 26 | const hours = date.getHours(); 27 | const minutes = date.getMinutes(); 28 | const seconds = date.getSeconds(); 29 | const time = `${hours}:${minutes}:${seconds}`; 30 | 31 | nodeValues.push([time, parseFloat(dataPoint[1])]); 32 | 33 | }); 34 | valuesToGraph.push( 35 | { 36 | type: "line", 37 | decimals:3, 38 | text: nameShortened, 39 | values: nodeValues, 40 | min: 0, 41 | } 42 | ); 43 | } 44 | } 45 | } 46 | 47 | getValues(nodes); 48 | 49 | const nodeCpuGraphData = { 50 | theme: 'dark', 51 | type: 'line', 52 | "globals": { 53 | "font-family": "Roboto", 54 | "border-radius" : 15, 55 | }, 56 | 57 | title: { 58 | text: 'CPU Utilization', 59 | "font-size": "15em", 60 | "alpha": 1, 61 | "adjust-layout": true, 62 | }, 63 | 64 | plot: { 65 | marker: { 66 | visible: false, 67 | }, 68 | decimals:3, 69 | animation: { 70 | effect: "ANIMATION_FADE_IN", 71 | speed: "200" 72 | }, 73 | tooltip: { 74 | text: "%vv at %kt from %t", 75 | decimals:3, 76 | } 77 | }, 78 | 79 | plotarea: { 80 | "margin": "dynamic", 81 | "margin-right": "60", 82 | 'width':'100%', 83 | 'height': '100%', 84 | }, 85 | 86 | scaleX: { 87 | item: { 88 | fontWeight: 'normal', 89 | }, 90 | label:{ 91 | text: "Time(60m)" 92 | } 93 | 94 | }, 95 | scaleY: { 96 | label:{ 97 | text: "Per Node" 98 | }, 99 | minValue:0, 100 | minorTicks: 9, 101 | format: "%v%", 102 | item:{ 103 | fontWeight: 'normal', 104 | } 105 | }, 106 | 107 | crosshairX: { 108 | visible: false, 109 | }, 110 | 111 | series: valuesToGraph, 112 | } 113 | 114 | return ( 115 |
116 | 117 |
118 | ) 119 | } 120 | 121 | export default connect(mapStateToProps, null)(NodeCpuComponent); -------------------------------------------------------------------------------- /workflow.md: -------------------------------------------------------------------------------- 1 | # kubric 2 | 3 | ## **GIT WORKFLOWS** 4 | ### To start working on a *new* feature: 5 | 1. create a new issue with an appropriate name, add it to current project, and assign it to yourself (or appropriate person) 6 | 2. create a local feature branch with corresponding name, set its upstream to a branch with the same name in github 7 | 8 | ### **To merge dev branch into your local branch** 9 | *Do this whenever there has been an update to dev* **and BEFORE** *pushing your local changes on your feature branch to the remote branch...*: 10 | 1. make sure you're on your feature branch (confirm with `git branch`) 11 | 2. `git commit` your recent changes (**but don't push yet!**) 12 | 3. `git checkout dev` to switch to dev branch 13 | 4. `git pull origin dev` to pull down most recent changes to your local dev branch 14 | 5. `git checkout ` to switch back to your local feature branch 15 | 6. `git merge dev` to merge newest changes from dev into your local branch 16 | 7. `git push origin ` to update the remote feature branch to include your local changes and dev's changes 17 | 18 | ### **To merge a feature branch into dev** 19 | *Double check you have merged most current version of dev into your local feature branch *AND* you've pushed your local feature branch changes to the remote feature branch* 20 | 1. Go to the GitHub repo 21 | 2. If you recently `pushed` to your feature branch, there will be an alert at the top of the page that says: `" had recent pushes x minutes ago"` with a button that says `Compare & pull request`. Click the button. 22 | 3. Make sure `base: dev` and `compare: featurebranchname` 23 | 4. Add succinct commentary about what changes are included. 24 | 5. Click `Create Pull Request` 25 | 6. Let Scrum Master know that you submitted a PR that needs review. 26 | 27 | ## File Trees: 28 | 29 | //BACK END 30 | 31 | --install prometheus and kubernetes node.js clients libraries 32 | 33 | //ClusterRouter & Controller 34 | --Handles proxy fetched to kubernetes api server 35 | --returns podList, nodeList, serviceList, deploymentList, ingressList 36 | 37 | //Metrics Router & Controller 38 | --scrapes metrics from kubernetes client register 39 | 40 | //Logs Router & Controller 41 | --TBD 42 | 43 | //Server 44 | Handles above routes, and react router client side routing(ex:/structure renders the structure component) 45 | -switch routes in app.jsx 46 | 47 | //YAML Files 48 | --our deployment 49 | --rbac deployment 50 | --possible log deployment? 51 | 52 | //FRONT END 53 | --Make use of material/ui library for display(core,themes,icons, etc) rechart, zingchart 54 | --Using REDUX && hooks 55 | --actions 56 | --actionTypes 57 | --metricsActionCreator 58 | - scrape metrics from port 9090 (prometheus server) 59 | --clusterActionCreator 60 | - K8s cluster configuration from API server 61 | --logsActionCreator (TBD) 62 | --reducers 63 | --deploymentReducer 64 | --ingressReducer 65 | --metricsReducer 66 | --nodesReducer 67 | --podsReducer 68 | --servicesReducer 69 | ---logReducer (TBD) 70 | --store 71 | --combine our reducers 72 | 73 | -------------------------------------------------------------------------------- /public/client/Components/NodeWriteToDiskComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ZingChart from 'zingchart-react'; 3 | import { connect } from 'react-redux'; 4 | 5 | const mapStateToProps = state => { 6 | return { 7 | nodes: state.nodesReducer.nodes, 8 | } 9 | } 10 | 11 | const NodeWriteToDiskComponent = (props) => { 12 | const { nodes } = props; 13 | const valuesToGraph = []; 14 | 15 | const getValues = (nodes) => { 16 | for (let node in nodes) { 17 | const nodeValues = []; 18 | let nameShortened = nodes[node].name; 19 | nameShortened = nameShortened.slice(0,3) + "..." + nameShortened.slice(nameShortened.length-5,nameShortened.length) 20 | 21 | if (nodes[node].displayMetrics) { 22 | nodes[node].writeToDiskNodes.forEach(dataPoint => { 23 | const date = new Date(dataPoint[0]*1000); 24 | const day = date.getDay(); 25 | const hours = date.getHours(); 26 | const minutes = date.getMinutes(); 27 | const seconds = date.getSeconds(); 28 | const time = `${day}:${hours}:${minutes}:${seconds}`; 29 | 30 | nodeValues.push([time, parseFloat(dataPoint[1])*0.000001]); 31 | }); 32 | valuesToGraph.push( 33 | { 34 | type: "line", 35 | text: nameShortened, 36 | values: nodeValues, 37 | min: 0, 38 | } 39 | ); 40 | } 41 | } 42 | } 43 | 44 | getValues(nodes); 45 | 46 | const nodeWriteToDiskData = { 47 | theme: 'dark', 48 | type: 'line', 49 | "globals": { 50 | "font-family": "Roboto", 51 | "border-radius" : 15, 52 | }, 53 | 54 | title: { 55 | text: 'Write to Disk Rate', 56 | "font-size": "15em", 57 | "alpha": 1, 58 | "adjust-layout": true, 59 | }, 60 | 61 | plot: { 62 | marker: { 63 | visible: false, 64 | }, 65 | decimals:3, 66 | animation: { 67 | effect: "ANIMATION_FADE_IN" 68 | }, 69 | tooltip: { 70 | text: "%vv at %kt time from %t", 71 | decimals:3, 72 | } 73 | }, 74 | 75 | plotarea: { 76 | "margin": "dynamic", 77 | "margin-right": "60", 78 | 'width':'100%', 79 | 'height': '100%', 80 | }, 81 | 82 | scaleX: { 83 | item: { 84 | fontWeight: 'normal', 85 | }, 86 | label:{ 87 | text: "Time(1d)" 88 | } 89 | 90 | }, 91 | scaleY: { 92 | label:{ 93 | text: "Per Node (MBps)" 94 | }, 95 | format: "%v", 96 | minValue:0, 97 | minorTicks: 9, 98 | item:{ 99 | fontWeight: 'normal', 100 | } 101 | }, 102 | 103 | crosshairX: { 104 | visible: false, 105 | }, 106 | 107 | series: valuesToGraph, 108 | } 109 | 110 | return ( 111 |
112 | 113 |
114 | ) 115 | } 116 | 117 | export default connect(mapStateToProps, null)(NodeWriteToDiskComponent); -------------------------------------------------------------------------------- /public/client/Components/NodeCpuSaturationComponent.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'zingchart/es6'; 3 | import ZingChart from 'zingchart-react'; 4 | import { connect } from 'react-redux'; 5 | 6 | const mapStateToProps = state => { 7 | return { 8 | nodes: state.nodesReducer.nodes, 9 | } 10 | } 11 | 12 | const NodeCpuSaturationComponent = (props) => { 13 | 14 | const { nodes } = props; 15 | const valuesToGraph = []; 16 | const getValues = (nodes) => { 17 | for (let node in nodes) { 18 | const nodeValues = []; 19 | let nameShortened = nodes[node].name; 20 | nameShortened = nameShortened.slice(0,3) + "..." + nameShortened.slice(nameShortened.length-5,nameShortened.length) 21 | 22 | if (nodes[node].displayMetrics) { 23 | nodes[node].CPUSatValsNodes.forEach(dataPoint => { 24 | const date = new Date(dataPoint[0]*1000); 25 | const day = date.getDay(); 26 | const hours = date.getHours(); 27 | const minutes = date.getMinutes(); 28 | const seconds = date.getSeconds(); 29 | const time = `${day}:${hours}:${minutes}:${seconds}`; 30 | 31 | nodeValues.push([time, parseFloat(dataPoint[1])]); 32 | 33 | }); 34 | valuesToGraph.push( 35 | { 36 | type: "line", 37 | decimals:3, 38 | text: nameShortened, 39 | values: nodeValues, 40 | min: 0, 41 | } 42 | ); 43 | } 44 | } 45 | } 46 | 47 | getValues(nodes); 48 | 49 | const nodeCpuSaturationGraphData = { 50 | theme: 'dark', 51 | type: 'line', 52 | "globals": { 53 | "font-family": "Roboto", 54 | "border-radius" : 15, 55 | }, 56 | 57 | title: { 58 | text: 'CPU Saturation', 59 | "font-size": "15em", 60 | "alpha": 1, 61 | "adjust-layout": true, 62 | }, 63 | 64 | plot: { 65 | marker: { 66 | visible: false, 67 | }, 68 | decimals:3, 69 | animation: { 70 | effect: "ANIMATION_FADE_IN", 71 | speed: "200" 72 | }, 73 | tooltip: { 74 | text: "%vv at %kt from %t", 75 | decimals:3, 76 | } 77 | }, 78 | 79 | plotarea: { 80 | "margin": "dynamic", 81 | "margin-right": "60", 82 | 'width':'100%', 83 | 'height': '100%', 84 | }, 85 | 86 | scaleX: { 87 | item: { 88 | fontWeight: 'normal', 89 | }, 90 | label:{ 91 | text: "Time(1d)" 92 | } 93 | }, 94 | scaleY: { 95 | label:{ 96 | text: "Per Node" 97 | }, 98 | format: "%v%", 99 | minValue:0, 100 | minorTicks: 9, 101 | item:{ 102 | fontWeight: 'normal', 103 | } 104 | }, 105 | 106 | crosshairX: { 107 | visible: false, 108 | }, 109 | 110 | series: valuesToGraph, 111 | } 112 | 113 | return ( 114 |
115 | 116 |
117 | ) 118 | } 119 | 120 | export default connect(mapStateToProps, null)(NodeCpuSaturationComponent); -------------------------------------------------------------------------------- /public/client/actions/metricsActionCreators.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as actionTypes from './actionTypes.js'; 3 | 4 | export const fetchNodeMetrics = () => { 5 | return (dispatch) => { 6 | const nodesMetrcisUrl = 'http://localhost:3000/api/metrics' 7 | axios.get(nodesMetrcisUrl) 8 | .then( response => { 9 | dispatch(getNodes(response.data.CPUNodes, response.data.MemoryNodes, response.data.CPUSatValsNodes, response.data.WriteToDiskNodes)); 10 | }) 11 | .catch (err => console.log('error from inside fetchNodeMetrics')) 12 | } 13 | } 14 | 15 | export const fetchPodMetrics = (nodeName) => { 16 | return (dispatch) => { 17 | const nodesMetrcisUrl = `http://localhost:3000/api/metrics/getPodMetrics/${nodeName}` 18 | axios.get(nodesMetrcisUrl) 19 | .then( response => { 20 | dispatch(getPods(response.data.CPUPods, response.data.MemoryPods, response.data.WriteToDiskPods,response.data.LogsByPods)); 21 | }) 22 | .catch (err => console.log('error from inside fetchPodMetrics')) 23 | } 24 | } 25 | 26 | export const fetchMasterNodeMetrics = () => { 27 | return (dispatch) => { 28 | axios.get('/api/metrics/getMasterNode') 29 | .then(response => { 30 | dispatch(getMasterNode(response.data)); 31 | }) 32 | .catch(err => console.log('error from fetchMasterNodeMetrics')) 33 | }; 34 | }; 35 | 36 | export const getDefaultMetrics = metrics => { 37 | return { 38 | type: actionTypes.DEFAULT_METRICS_RECEIVED, 39 | payload: metrics, 40 | } 41 | } 42 | 43 | export const getPodCpuMetrics = metrics => { 44 | return { 45 | type: actionTypes.PODS_CPU_METRICS_RECEIVED, 46 | payload: metrics, 47 | } 48 | } 49 | 50 | export const getPodMemoryMetrics = metrics => { 51 | return { 52 | type: actionTypes.PODS_MEMORY_METRICS_RECEIVED, 53 | payload: metrics, 54 | } 55 | } 56 | 57 | export const getServerApiMetrics = metrics => { 58 | return { 59 | type: actionTypes.SERVERAPI_METRICS_RECEIVED, 60 | payload: metrics, 61 | } 62 | } 63 | 64 | export const renderPodMetrics = (podName, metrics) => { 65 | return { 66 | type: actionTypes.RENDER_POD_METRICS, 67 | payload: {podName, metrics} 68 | } 69 | } 70 | 71 | export const getPods = (cpuMetrics, memoryMetrics, writeToDiskPods,logMetrics) => { 72 | return { 73 | type: actionTypes.RECEIVE_PODS, 74 | payload: {cpuMetrics, memoryMetrics, writeToDiskPods,logMetrics}, 75 | } 76 | } 77 | 78 | export const displayPodMetrics = (podName) => { 79 | return { 80 | type: actionTypes.DISPLAY_POD_METRICS, 81 | payload: podName, 82 | } 83 | } 84 | 85 | export const getNodes = (nodesCpu, nodesMemory, CPUSatValsNodes, writeToDiskNodes) => { 86 | return { 87 | type: actionTypes.RECEIVE_NODES, 88 | payload: {nodesCpu, nodesMemory, CPUSatValsNodes, writeToDiskNodes} 89 | } 90 | } 91 | 92 | export const displayNodeMetrics = (nodeName) => { 93 | return { 94 | type: actionTypes.DISPLAY_NODE_METRICS, 95 | payload: nodeName, 96 | } 97 | } 98 | 99 | export const renderNodeMetrics = (nodeName, metrics) => { 100 | return { 101 | type: actionTypes.RENDER_NODE_METRICS, 102 | payload: {nodeName, metrics} 103 | } 104 | } 105 | 106 | export const getMasterNode = (metrics) => { 107 | return { 108 | type: actionTypes.RECEIVE_MASTER_NODE, 109 | payload: metrics, 110 | } 111 | } -------------------------------------------------------------------------------- /public/client/Containers/LogContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import LogRowComponent from '../Components/LogRowComponent.jsx' 4 | import * as actions from '../actions/logsActionCreator.js'; 5 | import PersistQueryContainer from './PersistQueryContainer.jsx'; 6 | import LiveQueryContainer from './LiveQueryContainer.jsx'; 7 | import Button from '@mui/material/Button' 8 | import { Container } from '@mui/material'; 9 | import { makeStyles } from '@mui/styles'; 10 | import Tabs from '@mui/material/Tabs'; 11 | import Tab from '@mui/material/Tab'; 12 | import Typography from '@mui/material/Typography'; 13 | import Box from '@mui/material/Box'; 14 | 15 | // TODO: edit the buildLogRows helper function to pass in appropriate properties 16 | 17 | const tabStyles = makeStyles({ 18 | flexContainer:{ 19 | width:"100%", 20 | display:"flex", 21 | flexDirection:"row", 22 | justifyContent:'space-evenly' 23 | }, 24 | scroller:{ 25 | width:"100%", 26 | display:"flex", 27 | flexDirection:"row", 28 | justifyContent:'space-evenly' 29 | } 30 | }) 31 | function TabPanel(props) { 32 | const { children, value, index, ...other } = props; 33 | return ( 34 | 48 | ); 49 | } 50 | function addProps(index) { 51 | return { 52 | id: `log-tab-${index}`, 53 | 'aria-controls': `log-tabpanel-${index}`, 54 | }; 55 | } 56 | const useStyles = makeStyles({ 57 | root:{ 58 | background: 'rgba(69,172,120,0.52)', 59 | border: 0, 60 | borderRadius: 4, 61 | boxShadow: '6px 2px 3px -1px rgba(0,0,0,0.75)', 62 | color: 'white', 63 | padding: '30px 30px 30px 30px', 64 | }, 65 | }) 66 | 67 | const LogContainer = (props) => { 68 | const containerClasses = useStyles(); 69 | const tabClasses = tabStyles(); 70 | const [value, setValue] = React.useState(0); 71 | 72 | const handleChange = (event, newValue) => { 73 | setValue(newValue); 74 | }; 75 | 76 | 77 | return( 78 |
79 | 83 | 84 | 88 | 89 | {/* */} 90 | 91 | 92 | 93 | 96 | 101 | 102 | 103 | 104 | {/* 105 | 106 | */} 107 | 108 | 109 | 110 | 111 |
112 | ) 113 | 114 | }; 115 | 116 | export default LogContainer; -------------------------------------------------------------------------------- /server/controllers/logsController.js: -------------------------------------------------------------------------------- 1 | //will require database model for logs 2 | const { Client } = require('@elastic/elasticsearch'); 3 | const client = new Client({ node: 'http://localhost:9200' }) 4 | 5 | const logsController = {}; 6 | //TO ADD:add time window property 7 | logsController.getAppLogs = (req, res, next) => { 8 | const {name,field,value,all} = req.query; 9 | console.log("from log controller",req.query) 10 | let matchObj = {} 11 | let queryObj = {} 12 | let termArray = []; 13 | if(value){ 14 | const tokenize = (sentence) =>{ 15 | sentence.split(' ').forEach((word)=>{ 16 | let termObj = {"term":{}} 17 | word=word.replace(/[\W_]+/g,' ').trim(); 18 | if(word.split(' ')[0]!==word) tokenize(word) 19 | else { 20 | termObj.term[field] = word.toLowerCase() 21 | termArray.push(termObj) 22 | }; 23 | }) 24 | } 25 | tokenize(value); 26 | } 27 | matchObj.bool = {}; 28 | matchObj.bool.should = termArray; 29 | matchObj.bool.minimum_should_match = termArray.length; 30 | console.log("matchObj",JSON.stringify(matchObj)); 31 | 32 | if(all==='true') queryObj.match_all = {}; 33 | else queryObj = matchObj; 34 | console.log("queryObj in back",JSON.stringify(queryObj)) 35 | 36 | client.search({ 37 | index: name, 38 | size: 50, 39 | body: { 40 | // sort : [ 41 | // { "time" : {"order" : "desc", "format": "strict_date_optional_time_nanos"}}, 42 | // ], 43 | query: queryObj } 44 | },(err, result) => { 45 | if (err) { 46 | console.log("error",err) 47 | next({'error':err}) 48 | } 49 | else { 50 | res.locals.appLogs = result.body.hits.hits; 51 | next(); 52 | } 53 | }) 54 | } 55 | 56 | logsController.getIndices = (req,res,next) => { 57 | client.cat.indices({ 58 | format: 'json' 59 | },(err,result)=>{ 60 | if (err) next({'error':err}) 61 | else { 62 | const indices = [] 63 | result.body.forEach((indexObj)=>{ 64 | if(indexObj.index!=='.geoip_databases') indices.push(indexObj.index); 65 | }) 66 | res.locals.appIndices = indices; 67 | next(); 68 | } 69 | }) 70 | } 71 | 72 | logsController.getAppFields = (req,res,next) => { 73 | client.indices.getMapping({ 74 | index: res.locals.appIndices 75 | },(err,result)=>{ 76 | if (err) { 77 | console.log("error",err) 78 | next({'error':err}) 79 | } 80 | else { 81 | const fieldsObj = {} 82 | for(entry in result.body){ 83 | fieldsObj[entry] = flattenLogFields(result.body[entry].mappings.properties) 84 | } 85 | res.locals.appFields = fieldsObj; 86 | next(); 87 | } 88 | }) 89 | } 90 | const flattenLogFields = (fields) => { 91 | const fieldKeys = []; 92 | const nested = (input) => { 93 | for(key in input){ 94 | if(input[key].properties){ 95 | const keys = Object.keys(input[key].properties) 96 | const nestedKeyStrings = keys.map((elem)=>{ 97 | return `${key}.${elem}` 98 | }) 99 | fieldKeys.push(nestedKeyStrings); 100 | } 101 | else(fieldKeys.push(key)); 102 | } 103 | } 104 | nested(fields); 105 | return fieldKeys.flat(); 106 | } 107 | // curl -X GET "localhost:9200/loggen-logs/_analyze?pretty" -H 'Content-Type: application/json' -d' 108 | // { 109 | // "field": "message", 110 | // "text": "loaded module [ingest-common]" 111 | // } 112 | // ' 113 | 114 | module.exports = logsController; -------------------------------------------------------------------------------- /public/client/Containers/PersistQueryContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/logsActionCreator.js'; 4 | import Autocomplete from '@mui/material/Autocomplete'; 5 | import TextField from '@mui/material/TextField'; 6 | import Button from '@mui/material/Button'; 7 | import { makeStyles } from '@mui/styles'; 8 | 9 | const mapStateToProps = (state) => { 10 | return { 11 | appLogFields: state.logsReducer.appLogFields, 12 | appLogIndices: state.logsReducer.appLogIndices, 13 | selectedIndex: state.logsReducer.selectedIndex, 14 | selectedFields: state.logsReducer.selectedFields, 15 | } 16 | } 17 | 18 | const mapDispatchToProps = (dispatch) => { 19 | return { 20 | getAppLogFields: () => dispatch(actions.getAppLogFields()), 21 | getAppLogs: (obj) => dispatch(actions.getAppLogs(obj)), 22 | selectIndex: (index) => dispatch(actions.selectIndex(index)) 23 | } 24 | } 25 | const inputStyles = makeStyles({ 26 | input: { 27 | height: 40 28 | } 29 | }) 30 | 31 | const PersistQueryContainer = (props) => { 32 | //depending on the current tab, rendered components will be different 33 | const classes = inputStyles(); 34 | useEffect(() => { 35 | props.getAppLogFields() 36 | }, []); 37 | const [query, setQuery] = useState({'name':'','field':'','value':'', 'all':true}) 38 | let queryBoolean = false; 39 | const checkQuery = () =>{ 40 | if(query.name && query.field) queryBoolean = true; 41 | } 42 | const handleIndex = (event,value)=>{ 43 | props.selectIndex([...props.appLogIndices].indexOf(value)) 44 | }; 45 | const search = () =>{ 46 | if(queryBoolean) props.getAppLogs(query) 47 | } 48 | 49 | return ( 50 |
53 | } 60 | onChange={(event,value)=>{ 61 | setQuery({...query,name:value,all:true}) 62 | handleIndex(event,value) 63 | checkQuery(); 64 | }} 65 | /> 66 | { 75 | setQuery({...query,field:value,all:true}) 76 | checkQuery(); 77 | }} 78 | renderInput={(params) => } 79 | /> 80 | { 90 | console.log("string input?",event.target.value); 91 | setQuery({...query,value:event.target.value,all:false}) 92 | checkQuery(); 93 | }} 94 | 95 | /> 96 | 105 |
106 | ); 107 | } 108 | 109 | 110 | 111 | 112 | 113 | export default connect(mapStateToProps, mapDispatchToProps)(PersistQueryContainer); -------------------------------------------------------------------------------- /public/client/Navigation/Navigation.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withRouter } from 'react-router'; 3 | import AppBar from '@mui/material/AppBar'; 4 | import Box from '@mui/material/Box'; 5 | import Button from '@mui/material/Button'; 6 | import Drawer from '@mui/material/Drawer'; 7 | import List from '@mui/material/List'; 8 | import ListItem from '@mui/material/ListItem'; 9 | import Toolbar from '@mui/material/Toolbar'; 10 | import IconButton from '@mui/material/IconButton'; 11 | import MenuIcon from '@mui/icons-material/Menu'; 12 | import ListItemIcon from '@mui/material/ListItemIcon'; 13 | import ListItemText from '@mui/material/ListItemText'; 14 | import Divider from '@mui/material/Divider'; 15 | import MenuItem from '@mui/material/MenuItem'; 16 | import Menu from '@mui/material/Menu'; 17 | import { makeStyles } from '@mui/styles'; 18 | import HomeIcon from '@mui/icons-material/Home'; 19 | import TimelineIcon from '@mui/icons-material/Timeline'; 20 | import LibraryBooksIcon from '@mui/icons-material/LibraryBooks'; 21 | 22 | import Image from 'material-ui-image' 23 | //import img from '../assets/cat.jpeg' 24 | 25 | 26 | const Navigation = ({history}) => { 27 | //const [anchorEl, setAnchorEl] = React.useState(null); 28 | const [state, setState] = React.useState({ 29 | right: false, 30 | }); 31 | const toggleDrawer = (anchor, open) => (event) => { 32 | if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) { 33 | return; 34 | } 35 | setState({ ...state, [anchor]: open }); 36 | }; 37 | const handleMenu = (event) => { 38 | setAnchorEl(event.currentTarget); 39 | }; 40 | 41 | const handleClose = (event) => { 42 | setAnchorEl(null); 43 | }; 44 | const handleClick = (event) => { 45 | //setAnchorEl(null); 46 | history.push(`/${event.currentTarget.innerText.toLowerCase()}`); 47 | }; 48 | 49 | const list = (anchor) => ( 50 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | 83 | return ( 84 | 85 | 86 | 87 |
88 | {/*
*/} 89 | {['right'].map((anchor) => ( 90 | 91 | 92 | 97 | {list(anchor)} 98 | 99 | 100 | ))} 101 |
102 | 103 | 104 | 105 | 106 | 107 | ); 108 | } 109 | export default withRouter(Navigation); -------------------------------------------------------------------------------- /public/client/Components/LogRowComponent.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { DataGrid } from '@mui/x-data-grid'; 4 | import { makeStyles } from '@mui/styles'; 5 | import { MailRounded } from '@mui/icons-material'; 6 | import { minHeight } from '@mui/system'; 7 | 8 | const mapStateToProps = (state) => { 9 | return { 10 | appLogs: state.logsReducer.appLogs, 11 | } 12 | } 13 | const columns = [ 14 | {field: 'message', headerName: 'message',minWidth:150, headerAlign:'left', flex:1}, 15 | {field: 'timestamp', headerName: 'timestamp', minWidth:100,headerAlign:'left', flex:1}, 16 | {field: 'podName', headerName: 'pod name', minWidth:150,headerAlign:'left', flex:1}, 17 | {field: 'host', headerName: 'host',headerAlign:'left',minWidth:150,flex:1}, 18 | {field: 'level', headerName: 'level',headerAlign:'left',minWidth:50,flex:.5}, 19 | {field: 'id', headerName: 'id', headerAlign:'left',minWidth:100,flex:1}, 20 | ]; 21 | 22 | const useStyles = makeStyles({ 23 | root:{ 24 | '& .MuiDataGrid-renderingZone': { 25 | maxHeight: 'none !important', 26 | maxWidth:'100% !important', 27 | margin: '0px !important', 28 | padding: '0px !important', 29 | }, 30 | '& .MuiDataGrid-cell': { 31 | lineHeight: 'unset !important', 32 | overflowWrap: 'break-word !important', 33 | wordWrap: 'break-word !important', 34 | maxHeight: 'none !important', 35 | whiteSpace: 'normal', 36 | margin: '0px 0px 0px 0px!important', 37 | padding: '0px 8px 0px 8px !important' 38 | }, 39 | 40 | '& .MuiDataGrid-columnsContainer': { 41 | backgroundColor: 'whitesmoke', 42 | borderRadius: 4, 43 | margin: '0px 0px 20px 0px !important', 44 | }, 45 | '& .MuiDataGrid-columnHeader': { 46 | 47 | alignItems: 'flex-start !important', 48 | margin: '0px !important', 49 | padding: '0px !important', 50 | }, 51 | '& .MuiDataGrid-columnHeaderTitleContainer': { 52 | alignItems: 'flex-start !important', 53 | justifyContent: 'flex-start !important', 54 | color: 'grey !important', 55 | margin: '0px !important', 56 | padding: '0px 0px 0px 10px !important', 57 | }, 58 | '& .MuiDataGrid-columnHeaderDraggableContainer': { 59 | alignItems: 'flex-start !important', 60 | margin: '0px !important', 61 | padding: '0px !important', 62 | }, 63 | 64 | '& .MuiDataGrid-row': { 65 | maxHeight: 'none !important', 66 | margin: '0px !important', 67 | padding: '0px !important', 68 | width:'100% !important', 69 | 70 | }, 71 | '& .MuiDataGrid-window': { 72 | borderRadius: 4, 73 | margin: '1px 0px 0px 0px !important', 74 | overflowX: 'hidden !important', 75 | }, 76 | '& .MuiDataGrid-viewport': { 77 | maxWidth: '100% !important', 78 | minWidth: '100% !important', 79 | 80 | }, 81 | '& .MuiDataGrid-dataContainer': { 82 | margin: '0px !important', 83 | padding: '0px !important', 84 | }, 85 | background: 'rgba(69,172,120,0.52)', 86 | border: 0, 87 | borderRadius: 4, 88 | //boxShadow: '0 3px 5px 2px rgba(255, 105, 135, .3)', 89 | color: 'white', 90 | height: '100%', 91 | width: '100%', 92 | padding: '0 30px', 93 | minHeight:480, 94 | overflowY:'auto', 95 | }, 96 | footerContainer: { 97 | height: "1px !important", 98 | borderRadius: 4, 99 | background: 'rgba(69,172,120,0.52)', 100 | } 101 | }) 102 | 103 | 104 | const LogRowComponent = (props) => { 105 | const classes = useStyles(); 106 | 107 | 108 | return ( 109 | 110 |
111 | 118 |
119 | ); 120 | } 121 | 122 | export default connect(mapStateToProps, null)(LogRowComponent); 123 | -------------------------------------------------------------------------------- /public/client/Containers/MetricsContainer.jsx: -------------------------------------------------------------------------------- 1 | import React,{useEffect} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/metricsActionCreators.js'; 4 | import NodeXContainer from './NodeXContainer.jsx'; 5 | import NodeChartContainer from './NodeChartContainer.jsx'; 6 | import PodChartContainer from './PodChartContainer.jsx'; 7 | import PodsContainer from './PodsContainer.jsx'; 8 | import MasterNodeContainer from './MasterNodeContainer.jsx'; 9 | import { Container } from '@mui/material'; 10 | import { makeStyles } from '@mui/styles'; 11 | import Tabs from '@mui/material/Tabs'; 12 | import Tab from '@mui/material/Tab'; 13 | import Typography from '@mui/material/Typography'; 14 | import Box from '@mui/material/Box'; 15 | 16 | const mapStateToProps = state => { 17 | return { 18 | nodes: state.nodesReducer.nodes, 19 | } 20 | } 21 | 22 | const mapDispatchToProps = dispatch => { 23 | return { 24 | fetchNodeMetrics: () => dispatch(actions.fetchNodeMetrics()), 25 | } 26 | } 27 | 28 | const useStyles = makeStyles({ 29 | root:{ 30 | background: 'rgba(69,172,120,0.52)', 31 | border: 0, 32 | borderRadius: 4, 33 | boxShadow: '6px 2px 3px -1px rgba(0,0,0,0.75)', 34 | color: 'white', 35 | padding: '30px 30px 30px 30px', 36 | }, 37 | }) 38 | 39 | const tabStyles = makeStyles({ 40 | flexContainer:{ 41 | width:"100%", 42 | display:"flex", 43 | flexDirection:"row", 44 | justifyContent:'space-evenly' 45 | }, 46 | scroller:{ 47 | width:"100%", 48 | display:"flex", 49 | flexDirection:"row", 50 | justifyContent:'space-evenly' 51 | } 52 | }) 53 | 54 | function TabPanel(props) { 55 | const { children, value, index, ...other } = props; 56 | return ( 57 | 71 | ); 72 | } 73 | 74 | function addProps(index) { 75 | return { 76 | id: `cluster-tab-${index}`, 77 | 'aria-controls': `cluster-tabpanel-${index}`, 78 | }; 79 | } 80 | 81 | function MetricsContainer(props) { 82 | const containerClasses = useStyles(); 83 | const tabClasses = tabStyles(); 84 | const [value, setValue] = React.useState(0); 85 | 86 | const handleChange = (event, newValue) => { 87 | setValue(newValue); 88 | }; 89 | 90 | useEffect(() => { 91 | props.fetchNodeMetrics(); 92 | }, []); 93 | 94 | const tabPanels = []; 95 | const tabs = []; 96 | let tabNum = 2; 97 | 98 | for (let node in props.nodes) { 99 | tabs.push(); 100 | tabPanels.push( 101 | 105 | 106 | 107 | ); 108 | tabNum += 1; 109 | } 110 | 111 | return ( 112 |
113 | 119 | 120 | 121 | 122 | 123 | {tabs} 124 | 125 | 126 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | {/* Tab Panel for each node */} 142 | {tabPanels} 143 | 144 | 145 |
146 | ) 147 | } 148 | 149 | export default connect(mapStateToProps, mapDispatchToProps)(MetricsContainer); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubric 2 | 3 | Kubric aims to provide a clean dashboard that displays important worker node and pod metrics. Kubric provides insight into the master node, the gatekeeper of communication to the cluster and responsible for orchestrating all the heavy lifting to corresponding worker nodes 4 | 5 | Additionally, Kubric persists logs and allows developers to query persisted logs even if a pod has been evicted and replaced. Developers need not worry about logs dying with pods or about log rotation policies because logs are persistently stored and queryable through Kubric 6 | 7 | * **Query Persistent Log Storage by Index Name, Field, Value** 8 | 9 |

10 | 11 |

12 | 13 | * **Toggle Visualization to View Relative Performance** 14 | 15 |

16 | 17 |

18 | 19 | * **Tab between Overview, Master and Worker Nodes Views** 20 | 21 |

22 | 23 |

24 | 25 | ## Set Up Prerequisites 26 | 27 | Here are links to the technology we used to implement our application. Be sure to have kubernetes installed before beginning setup 28 | 29 | * **Kubernetes** 30 | * To run commands against your cluster, make sure you have [Kubectl](https://kubernetes.io/docs/tasks/tools/) installed for your operating system 31 | 32 | * **Helm** 33 | * [Helm charts](https://helm.sh/docs/intro/install/) are a great resource to download interdependent YAML configuration files for complicated setup 34 | 35 | * **Fluentd** 36 | * [Fluentd](https://github.com/bitnami/charts/tree/master/bitnami/fluentd) is our log forwarding agent of choice 37 | 38 | * **Elasticsearch** 39 | * [Elasticsearch](https://github.com/elastic/helm-charts/tree/master/elasticsearch) is what we use for provisioning remote storage 40 | 41 | * **Prometheus** 42 | * [Prometheus](https://prometheus-operator.dev/docs/prologue/quick-start/) is the standard for metrics pipeline monitoring 43 | 44 | * **Linode LKE** 45 | * [Linode LKE](https://www.linode.com/) is the remote storage provider we chose, future support for GKE and EKS is in the works 46 | 47 | ## Set Up 48 | 49 | 1. Begin by cloning this repo 50 | 51 | 2. Create an account and provision three worker nodes with at least 8GB of RAM and 160GB of storage 52 | 53 | 3. Upon successful provisioning, download your Kubeconfig yaml file 54 | 55 | From the command line run:
56 | MacOS/Linux:
57 | ``` 58 | export KUBECONFIG=/path/to/config.yaml 59 | ``` 60 | 61 | Windows: 62 | ``` 63 | create folder C:\Users\username\.kube 64 | 65 | rename cluster-config.yaml to config 66 | 67 | put config file in .kube folder 68 | ``` 69 | 70 | You should now be connected to your remote cluster and able to run kubectl commands against it 71 | 72 | 4. To deploy our sample log generator app to the cluster: 73 | 74 | ``` 75 | kubectl apply -f logGen-app/logGen-app-depl.yaml 76 | ``` 77 | 78 | If you have not installed Helm do so now 79 | 80 | 5. Install Elasticsearch: 81 | 82 | ``` 83 | helm repo add elastic https://Helm.elastic.co 84 | helm install elasticsearch elastic/elasticsearch -f values.yaml 85 | ``` 86 | 87 | 6. Install Fluentd: 88 | 89 | ``` 90 | helm repo add bitnami https://charts.bitnami.com/bitnami 91 | helm install fluentd bitnami/fluentd 92 | ``` 93 | 94 | 7. Apply our log forwarding config file: 95 | 96 | ``` 97 | kubectl apply -f fluent-update.yaml 98 | kubectl rollout restart daemonset/fluentd 99 | ``` 100 | 101 | 8. Install Prometheus: 102 | 103 | To deploy prometheus to this cluster, follow the above link's quick start guide sections 104 | 105 | 8. To open ports for app access: 106 | 107 | ``` 108 | kubectl --n monitoring port-forward svc/prometheus-k8s 9090 109 | kubectl port-forward service/elasticsearch-master 9200 110 | ``` 111 | 112 |
113 | 114 | That's it! 115 | Make sure to npm install and for now npm run dev, navigate to localhost:8080 to log in and view cluster info. 116 | 117 | To add new applications to filter logs from, simply add another match statement to the fluentd config file and follow the syntax provided. Then kubectl rollout the update 118 | 119 | 120 | ## What's Next? 121 | 122 | **Kubric is still under the development**, further features and optimizations to be implemented! Stay tuned for more updates! 123 | Feel free to contact us for any comments, concerns, suggestions! 124 | 125 | ## Contributers 126 | 127 | * Laura Botel : [Github](https://github.com/laurabotel) | [LinkedIn](https://www.linkedin.com/in/laurabotel/) 128 | * Luke Cho : [Github](https://github.com/luke-h-cho) | [LinkedIn](https://www.linkedin.com/in/luke-h-cho/) 129 | * James Cross : [Github](https://github.com/James-P-Cross) | [LinkedIn](www.linkedin.com/in/james-p-cross1) 130 | * John Haberstroh : [Github](https://github.com/jlhline) | [LinkedIn](https://www.linkedin.com/in/john-haberstroh-9436ab117/) 131 | 132 | 133 | -------------------------------------------------------------------------------- /public/client/scss/application.scss: -------------------------------------------------------------------------------- 1 | @import "_variables"; 2 | @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); 3 | 4 | html { 5 | font-family: 'Roboto', sans-serif; 6 | color: whitesmoke; 7 | // background-color: #fff; 8 | // background-image: 9 | // linear-gradient(90deg, transparent 79px, #abced4 79px, #abced4 81px, transparent 81px), 10 | // linear-gradient(#eee .1em, transparent .1em); 11 | // background-size: 100% 1.2em; 12 | } 13 | 14 | body { 15 | background: rgba(69, 172, 120, 0.52); 16 | 17 | } 18 | 19 | div { 20 | 21 | margin-top: .1em; 22 | margin-bottom: .1em; 23 | padding-top: .1em; 24 | padding-bottom: .1em; 25 | } 26 | 27 | input { 28 | margin: .25em; 29 | } 30 | 31 | button { 32 | margin: .25em; 33 | } 34 | 35 | #loginButtons { 36 | color: black 37 | } 38 | #backgroundContainer { 39 | 40 | } 41 | 42 | #loginPage { 43 | display:flex; 44 | flex-direction: column; 45 | } 46 | 47 | #loginPage h1 { 48 | color: black; 49 | } 50 | 51 | #user-pw { 52 | display: flex; 53 | flex-direction: column; 54 | } 55 | 56 | .MuiFormControl-root MuiTextField-root css-1u3bzj6-MuiFormControl-root-MuiTextField-root { 57 | align-content: center; 58 | align-items: center 59 | } 60 | 61 | #username { 62 | display: flex; 63 | align-items: center; 64 | align-content: center; 65 | } 66 | 67 | #login-container { 68 | // background-color: #F1FAEE; 69 | padding: .5em; 70 | } 71 | #metricsContainer { 72 | // background-color: #F1FAEE; 73 | padding: .5em; 74 | width: 85%; 75 | height: 100%; 76 | } 77 | 78 | #loginButtons { 79 | color: rgb(66, 65, 65); 80 | } 81 | 82 | #loginButtonIn { 83 | // background-color: rgb(180, 98, 114); 84 | border-style: solid; 85 | border-width: 2px; 86 | border-radius: 10px; 87 | border-color: rgb(241, 181, 226); 88 | font-size: 20px; 89 | } 90 | 91 | #loginButtonUp { 92 | border-style: solid; 93 | border-radius: 10px; 94 | border-width: 2px; 95 | border-color: rgb(181, 205, 241); 96 | font-size: 20px; 97 | } 98 | 99 | #appBarBox{ 100 | // background-color: #F1FAEE; 101 | margin:auto; 102 | align-items:center; 103 | justify-content: center; 104 | padding: .5em; 105 | width: 85%; 106 | height: 100%; 107 | border-radius: 10, 108 | } 109 | #backgroundBox{ 110 | // background-color: #F1FAEE; 111 | margin:auto; 112 | align-items:center; 113 | justify-content: center; 114 | padding: .5em; 115 | width: 85%; 116 | height: 100%; 117 | border-radius: 10; 118 | } 119 | #config-container { 120 | // background-color: #89B0AE; 121 | padding: .5em; 122 | } 123 | 124 | 125 | // #pods-container { 126 | // // background-color: #457B9D; 127 | // border-color: #457B9D; 128 | // background-color: #457B9D; 129 | // border-width: .25em; 130 | // border-style: solid; 131 | // padding: 5.5em 3.5em 3.5em; 132 | // display: flex; 133 | // flex-wrap: wrap; 134 | // // align-items: center; 135 | // justify-content: center; 136 | // align-items: center; 137 | // height: 300px; 138 | // width: 300px; 139 | // clip-path: polygon(50% 0%, 90% 20%, 100% 60%, 75% 100%, 25% 100%, 0% 60%, 10% 20%); 140 | // } 141 | 142 | // .pod-component { 143 | // background-color: rgb(49, 113, 49); 144 | // border-radius: 2.5%; 145 | // color: whitesmoke; 146 | // font-weight: bold; 147 | // text-align: center; 148 | 149 | // font-size: .75vh; 150 | // vertical-align: middle; 151 | // height: 5em; 152 | // width: 7.5em; 153 | // margin: .5em; 154 | // padding: .1em; 155 | // // clip-path: polygon(50% 0%, 90% 20%, 100% 60%, 75% 100%, 25% 100%, 0% 60%, 10% 20%); 156 | // } 157 | 158 | // #query-container { 159 | // // background-color: #A8DADC; 160 | // padding: .5em; 161 | // } 162 | 163 | #logContainer { 164 | // background-color: #A8DADC; 165 | padding: .5em; 166 | width: 85%; 167 | height: 100%; 168 | 169 | } 170 | 171 | #loginLogo h1 { 172 | text-align: center; 173 | } 174 | 175 | #loginPage #loginButtons { 176 | justify-content: center; 177 | } 178 | 179 | #loginPage #loginButtonIn { 180 | cursor: pointer; 181 | font-weight: 600; 182 | } 183 | #loginPage #loginButtonUp { 184 | cursor: pointer; 185 | font-weight: 600; 186 | } 187 | 188 | #loginPage { 189 | // display: flex; 190 | // direction: row; 191 | border: 2px solid grey; 192 | border-radius: 10px; 193 | width: auto; 194 | background-color:#fffafa; 195 | align-items: center; 196 | } 197 | 198 | .logDate { 199 | grid-area: date; 200 | } 201 | .logType { 202 | grid-area: type; 203 | } 204 | 205 | #password { 206 | -webkit-text-security: disc; 207 | // text-security: disc; 208 | } 209 | .podName { 210 | grid-area: pod; 211 | } 212 | .logMessage { 213 | grid-area: message; 214 | } 215 | 216 | #loginLogo { 217 | // background-color: red; 218 | align-items: center; 219 | // width: 100%; 220 | } 221 | 222 | .logRowComponent { 223 | background-color: #FAF9F9; 224 | color: black; 225 | margin: .5em; 226 | padding: .25em; 227 | display: grid; 228 | grid-template: 'date type type pod pod message message message' 229 | } 230 | 231 | .primary-btn { 232 | background-color: #1D3557; 233 | color: #F1FAEE 234 | } 235 | .secondary-btn { 236 | background-color: #FAF9F9; 237 | 238 | } 239 | 240 | .counter { 241 | background-color: #576e8f; 242 | } 243 | 244 | .gauge { 245 | background-color:cadetblue; 246 | } 247 | 248 | .histogram { 249 | background-color: rgb(180, 98, 114); 250 | } 251 | // #zingchart-react-0{ 252 | // height: 500px; 253 | // } 254 | .chart { 255 | display: flex; 256 | flex-direction: row; 257 | justify-content: space-evenly; 258 | width: 100%; 259 | height: 100%; 260 | min-height: 200px; 261 | margin: 0px; 262 | padding: 0px; 263 | } -------------------------------------------------------------------------------- /public/client/Containers/LiveQueryContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import { connect } from 'react-redux'; 3 | import * as actions from '../actions/logsActionCreator.js'; 4 | import Autocomplete from '@mui/material/Autocomplete'; 5 | import TextField from '@mui/material/TextField'; 6 | import Button from '@mui/material/Button'; 7 | import { makeStyles } from '@mui/styles'; 8 | 9 | const mapStateToProps = (state) => { 10 | return { 11 | appLogFields: state.logsReducer.appLogFields, 12 | selectedIndex: state.logsReducer.selectedIndex 13 | } 14 | } 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | 18 | return { 19 | getAppLogFields: () => dispatch(actions.getAppLogFields()), 20 | getAppLogs: (obj) => dispatch(actions.getAppLogs(obj)), 21 | selectIndex: (index) => dispatch(actions.selectIndex(index)) 22 | } 23 | } 24 | 25 | //sample data;; store it with property named label; 26 | const testQueryIndex = [ 27 | {label: "loggen-app"}, 28 | {label: "fluent-d1231"} 29 | ] 30 | 31 | const testQueryField = [ 32 | {label: "type"}, 33 | {label: "podName"} 34 | ] 35 | 36 | 37 | // const QueryContainer = ({isPersist}) => { 38 | // const display = []; 39 | //depending on the current tab, rendered components will be different 40 | // if(isPersist){ 41 | 42 | // } 43 | 44 | const LiveQueryContainer = (props) => { 45 | 46 | 47 | return ( 48 |
49 | } 55 | /> 56 | } 62 | /> 63 | 70 | 77 |
78 | ); 79 | } 80 | // class QueryContainer extends React.Component { 81 | // constructor(props) { 82 | // super(props); 83 | // this.queryObj = {'all':false,'value':'','field':'','name':this.props.appLogFields[this.props.selectedIndex]} 84 | // this.sendQuery = this.sendQuery.bind(this); 85 | // this.addValue = this.addValue.bind(this); 86 | // this.selectField = this.selectField.bind(this); 87 | // this.selectIndex = this.selectIndex.bind(this); 88 | // this.indices = []; 89 | // this.fields = []; 90 | // this.getFields = this.getFields.bind(this); 91 | // } 92 | // getFields(){ 93 | // console.log("hello from query container") 94 | // this.props.getAppLogFields(); 95 | // console.log('appLogFields in cdm',this.props.appLogFields); 96 | // if(this.props.appLogFields.length){ 97 | // let indexCounter = -1 98 | // this.indices = this.props.appLogFields.map((obj)=>{ 99 | // indexCounter++; 100 | // return 101 | // }) 102 | // console.log("indices",this.indices) 103 | // let fieldsCounter = -1; 104 | // this.fields = this.props.appLogFields[this.props.selectedIndex].fields.map((obj)=>{ 105 | // fieldsCounter++; 106 | // return 107 | // }) 108 | // console.log("fields",this.fields) 109 | // } 110 | // } 111 | // componentDidMount() { 112 | // //setInterval(this.getFields,1000) 113 | 114 | // // console.log('props after fetched in Component did mount', this.props); 115 | // } 116 | // sendQuery(){ 117 | // if(this.queryObj.value) this.props.getAppLogs(); 118 | // else { 119 | // this.queryObj.all = true; 120 | // this.props.getAppLogs(this.queryObj); 121 | // } 122 | // } 123 | // selectIndex(e){ 124 | // console.log("index",e.value) 125 | // this.props.selectIndex(e.value); 126 | // } 127 | // selectField(e){ 128 | // console.log("field",e.value) 129 | // this.queryObj.field = e.value; 130 | // } 131 | // addValue(e){ 132 | // console.log("value",e.value) 133 | // this.queryObj.value = e.value; 134 | // } 135 | 136 | 137 | 138 | // render(){ 139 | 140 | // return ( 141 | //
142 | // } 152 | // /> 153 | // } 160 | // /> 161 | // 162 | // 163 | //
164 | // ); 165 | // } 166 | 167 | // }; 168 | 169 | /* 170 | 171 | { } 174 | 175 | 176 | 179 | 180 | 181 | // 182 | // 183 | // 184 | // filter menu component 185 | */ 186 | 187 | 188 | 189 | 190 | export default connect(mapStateToProps, mapDispatchToProps)(LiveQueryContainer); -------------------------------------------------------------------------------- /server/controllers/metricsController.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const metricsController = {}; 3 | 4 | //to predefine and provide current, previous and step to get arrays of values to display data maybe make it customizable? 5 | //current date 6 | const endDate = new Date(); 7 | //one hour as initial for start 8 | let startSet = 1; 9 | let dayStartSet = 24; 10 | //an hour before current 11 | const startDate = new Date(endDate.getTime()-startSet*3600000); 12 | const dayStartDate = new Date(endDate.getTime()-dayStartSet*3600000); 13 | //step initial 14 | let step = '1m'; 15 | let dayStep = '24m'; 16 | 17 | 18 | //top 4 relevant metrics by each node in the cluster 19 | //CPU saturation % by the node 20 | metricsController.getCPUSatByNodes = (req, res, next) => { 21 | res.locals.nodeMetrics = {}; 22 | axios.get(`http://localhost:9090/api/v1/query_range?query=(sum(node_load15)%20by%20(instance)%20/%20count(node_cpu_seconds_total%7Bmode="system"%7D)%20by%20(instance))*100&start=${dayStartDate.toISOString()}&end=${endDate.toISOString()}&step=${dayStep}`) 23 | .then(data => { 24 | res.locals.nodeMetrics.CPUSatValsNodes = data.data.data.result; 25 | next(); 26 | }) 27 | .catch(err=>next(err)); 28 | }; 29 | 30 | //CPU utilization % by the node 31 | metricsController.getCPUByNodes = (req, res, next) => { 32 | axios.get(`http://localhost:9090/api/v1/query_range?query=100%20-%20(avg%20by%20(instance)%20(irate(node_cpu_seconds_total{mode=%22idle%22}[60m]))%20*%20100)&start=${startDate.toISOString()}&end=${endDate.toISOString()}&step=${step}`) 33 | .then(data => { 34 | res.locals.nodeMetrics.CPUNodes = data.data.data.result; 35 | next(); 36 | }) 37 | .catch(err=>next(err)); 38 | }; 39 | 40 | //Memory utilization % by the node 41 | metricsController.getMemoryByNodes = (req, res, next) => { 42 | axios.get(`http://localhost:9090/api/v1/query_range?query=sum((1-(node_memory_MemAvailable_bytes/node_memory_MemTotal_bytes))*100)%20by%20(instance)&start=${startDate.toISOString()}&end=${endDate.toISOString()}&step=${step}`) 43 | .then(data => { 44 | res.locals.nodeMetrics.MemoryNodes = data.data.data.result; 45 | next(); 46 | }) 47 | .catch(err=>next(err)); 48 | }; 49 | 50 | //WriteToDisk rate by the node 51 | metricsController.getWriteToDiskRateByNodes = (req, res, next) => { 52 | axios.get(`http://localhost:9090/api/v1/query_range?query=sum(rate(node_disk_written_bytes_total[60m]))by(instance)&start=${dayStartDate.toISOString()}&end=${endDate.toISOString()}&step=${dayStep}`) 53 | .then(data => { 54 | res.locals.nodeMetrics.WriteToDiskNodes = data.data.data.result; 55 | next(); 56 | }) 57 | .catch(err=>next(err)); 58 | } 59 | 60 | //For Horizontal memory bar graph (to be developed) to present the memory allocation/availablity in the cluster 61 | metricsController.getMemoryBarData = (req, res, next) => { 62 | axios.get(`http://localhost:9090/api/v1/query?query=sum(cluster:namespace:pod_memory:active:kube_pod_container_resource_limits) by (node) / sum(machine_memory_bytes) by (node)`) 63 | .then(data => { 64 | res.locals.nodeMetrics.MemoryBarGraph = data.data.data.result; 65 | next(); 66 | }) 67 | .catch(err=>next(err)); 68 | } 69 | 70 | //pod metrics: node's name will be added as reqeust parameter, it will pull relevent pod metrics inside the node 71 | 72 | //cpu usage rate by pod inside one node 73 | metricsController.getCPUByPods = (req, res, next) => { 74 | res.locals.podMetrics = {}; 75 | const node = req.params.nodeName; 76 | axios.get(`http://localhost:9090/api/v1/query_range?query=sum(rate(container_cpu_usage_seconds_total{node="${node}",pod!=%22POD%22,%20pod!=%22%22}[60m]))%20by%20(pod)&start=${startDate.toISOString()}&end=${endDate.toISOString()}&step=${step}`) 77 | .then(data => { 78 | res.locals.podMetrics.CPUPods = data.data.data.result; 79 | next(); 80 | }) 81 | .catch(err=>next(err)); 82 | }; 83 | 84 | //memory usuage by pod inside one node 85 | metricsController.getMemoryByPods = (req, res, next) => { 86 | const node = req.params.nodeName; 87 | axios.get(`http://localhost:9090/api/v1/query_range?query=sum(container_memory_working_set_bytes{node="${node}",pod!=%22POD%22,%20pod!=%22%22})%20by%20(pod)&start=${startDate.toISOString()}&end=${endDate.toISOString()}&step=${step}`) 88 | .then(data => { 89 | res.locals.podMetrics.MemoryPods = data.data.data.result; 90 | next(); 91 | }) 92 | .catch(err=>next(err)); 93 | }; 94 | 95 | //disk write rate by pod inside one node 96 | metricsController.getWriteToDiskRateByPods = (req, res, next) => { 97 | const node = req.params.nodeName; 98 | axios.get(`http://localhost:9090/api/v1/query_range?query=sum(rate(container_fs_writes_bytes_total{node="${node}",pod!=%22POD%22,%20pod!=%22%22}[60m]))%20by%20(pod)&start=${dayStartDate.toISOString()}&end=${endDate.toISOString()}&step=${dayStep}`) 99 | .then(data => { 100 | res.locals.podMetrics.WriteToDiskPods = data.data.data.result; 101 | next(); 102 | }) 103 | .catch(err=>next(err)); 104 | } 105 | 106 | //kubelet logs by pod inside one node 107 | metricsController.getLogsByPods = (req, res, next) => { 108 | const node = req.params.nodeName; 109 | axios.get(`http://localhost:9090/api/v1/query_range?query=sum(kubelet_container_log_filesystem_used_bytes{node="${node}",pod!=%22POD%22,%20pod!=%22%22})%20by%20(pod)&start=${dayStartDate.toISOString()}&end=${endDate.toISOString()}&step=${dayStep}`) 110 | .then(data => { 111 | res.locals.podMetrics.LogsByPods = data.data.data.result; 112 | next(); 113 | }) 114 | .catch(err=>next(err)); 115 | } 116 | 117 | //use promise all to resolve multiple axios requests to pull relevant control plane/master node components 118 | metricsController.getMasterNodeMetrics = (req, res, next) => { 119 | const urls = { 120 | serverAPILatency: `http://localhost:9090/api/v1/query_range?query=sum(cluster_quantile:apiserver_request_duration_seconds:histogram_quantile{resource!="",quantile="0.9"}) by (resource)&start=${startDate.toISOString()}&end=${endDate.toISOString()}&step=${step}`, 121 | serverAPIsuccessReq: `http://localhost:9090/api/v1/query_range?query=sum(rate(apiserver_request_total{job="apiserver",code=~"2..",group!=""}[60m])) by (group)&start=${startDate.toISOString()}&end=${endDate.toISOString()}&step=${step}`, 122 | controllerAddCounter: `http://localhost:9090/api/v1/query_range?query=rate(workqueue_adds_total[60m])&start=${startDate.toISOString()}&end=${endDate.toISOString()}&step=${step}`, 123 | etcdRequestRate: `http://localhost:9090/api/v1/query_range?query=sum(rate(etcd_request_duration_seconds_sum[60m]))by(operation)&start=${startDate.toISOString()}&end=${endDate.toISOString()}&step=${step}`, 124 | } 125 | 126 | const promises = []; 127 | 128 | for(let url in urls){ 129 | promises.push(axios.get(urls[url])); 130 | }; 131 | 132 | Promise.all(promises) 133 | .then(results => { 134 | let index = 0; 135 | for(let url in urls){ 136 | urls[url] = results[index].data.data.result; 137 | index++; 138 | } 139 | res.locals.masterNode = urls; 140 | next(); 141 | }) 142 | .catch(err=>next(err)); 143 | }; 144 | 145 | module.exports = metricsController; 146 | 147 | -------------------------------------------------------------------------------- /logGen-app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logGen-app", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "logGen-app", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "express": "^4.17.1", 13 | "node-fetch": "^3.0.0", 14 | "pino": "6.3.1" 15 | } 16 | }, 17 | "node_modules/accepts": { 18 | "version": "1.3.7", 19 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 20 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 21 | "dependencies": { 22 | "mime-types": "~2.1.24", 23 | "negotiator": "0.6.2" 24 | }, 25 | "engines": { 26 | "node": ">= 0.6" 27 | } 28 | }, 29 | "node_modules/array-flatten": { 30 | "version": "1.1.1", 31 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 32 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 33 | }, 34 | "node_modules/atomic-sleep": { 35 | "version": "1.0.0", 36 | "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 37 | "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", 38 | "engines": { 39 | "node": ">=8.0.0" 40 | } 41 | }, 42 | "node_modules/body-parser": { 43 | "version": "1.19.0", 44 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 45 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 46 | "dependencies": { 47 | "bytes": "3.1.0", 48 | "content-type": "~1.0.4", 49 | "debug": "2.6.9", 50 | "depd": "~1.1.2", 51 | "http-errors": "1.7.2", 52 | "iconv-lite": "0.4.24", 53 | "on-finished": "~2.3.0", 54 | "qs": "6.7.0", 55 | "raw-body": "2.4.0", 56 | "type-is": "~1.6.17" 57 | }, 58 | "engines": { 59 | "node": ">= 0.8" 60 | } 61 | }, 62 | "node_modules/bytes": { 63 | "version": "3.1.0", 64 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 65 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", 66 | "engines": { 67 | "node": ">= 0.8" 68 | } 69 | }, 70 | "node_modules/content-disposition": { 71 | "version": "0.5.3", 72 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 73 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 74 | "dependencies": { 75 | "safe-buffer": "5.1.2" 76 | }, 77 | "engines": { 78 | "node": ">= 0.6" 79 | } 80 | }, 81 | "node_modules/content-type": { 82 | "version": "1.0.4", 83 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 84 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", 85 | "engines": { 86 | "node": ">= 0.6" 87 | } 88 | }, 89 | "node_modules/cookie": { 90 | "version": "0.4.0", 91 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 92 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", 93 | "engines": { 94 | "node": ">= 0.6" 95 | } 96 | }, 97 | "node_modules/cookie-signature": { 98 | "version": "1.0.6", 99 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 100 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 101 | }, 102 | "node_modules/data-uri-to-buffer": { 103 | "version": "3.0.1", 104 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", 105 | "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", 106 | "engines": { 107 | "node": ">= 6" 108 | } 109 | }, 110 | "node_modules/debug": { 111 | "version": "2.6.9", 112 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 113 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 114 | "dependencies": { 115 | "ms": "2.0.0" 116 | } 117 | }, 118 | "node_modules/depd": { 119 | "version": "1.1.2", 120 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 121 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", 122 | "engines": { 123 | "node": ">= 0.6" 124 | } 125 | }, 126 | "node_modules/destroy": { 127 | "version": "1.0.4", 128 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 129 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 130 | }, 131 | "node_modules/ee-first": { 132 | "version": "1.1.1", 133 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 134 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 135 | }, 136 | "node_modules/encodeurl": { 137 | "version": "1.0.2", 138 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 139 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", 140 | "engines": { 141 | "node": ">= 0.8" 142 | } 143 | }, 144 | "node_modules/escape-html": { 145 | "version": "1.0.3", 146 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 147 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 148 | }, 149 | "node_modules/etag": { 150 | "version": "1.8.1", 151 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 152 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", 153 | "engines": { 154 | "node": ">= 0.6" 155 | } 156 | }, 157 | "node_modules/express": { 158 | "version": "4.17.1", 159 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 160 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 161 | "dependencies": { 162 | "accepts": "~1.3.7", 163 | "array-flatten": "1.1.1", 164 | "body-parser": "1.19.0", 165 | "content-disposition": "0.5.3", 166 | "content-type": "~1.0.4", 167 | "cookie": "0.4.0", 168 | "cookie-signature": "1.0.6", 169 | "debug": "2.6.9", 170 | "depd": "~1.1.2", 171 | "encodeurl": "~1.0.2", 172 | "escape-html": "~1.0.3", 173 | "etag": "~1.8.1", 174 | "finalhandler": "~1.1.2", 175 | "fresh": "0.5.2", 176 | "merge-descriptors": "1.0.1", 177 | "methods": "~1.1.2", 178 | "on-finished": "~2.3.0", 179 | "parseurl": "~1.3.3", 180 | "path-to-regexp": "0.1.7", 181 | "proxy-addr": "~2.0.5", 182 | "qs": "6.7.0", 183 | "range-parser": "~1.2.1", 184 | "safe-buffer": "5.1.2", 185 | "send": "0.17.1", 186 | "serve-static": "1.14.1", 187 | "setprototypeof": "1.1.1", 188 | "statuses": "~1.5.0", 189 | "type-is": "~1.6.18", 190 | "utils-merge": "1.0.1", 191 | "vary": "~1.1.2" 192 | }, 193 | "engines": { 194 | "node": ">= 0.10.0" 195 | } 196 | }, 197 | "node_modules/fast-redact": { 198 | "version": "2.1.0", 199 | "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-2.1.0.tgz", 200 | "integrity": "sha512-0LkHpTLyadJavq9sRzzyqIoMZemWli77K2/MGOkafrR64B9ItrvZ9aT+jluvNDsv0YEHjSNhlMBtbokuoqii4A==", 201 | "engines": { 202 | "node": ">=6" 203 | } 204 | }, 205 | "node_modules/fast-safe-stringify": { 206 | "version": "2.1.1", 207 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", 208 | "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" 209 | }, 210 | "node_modules/fetch-blob": { 211 | "version": "3.1.2", 212 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.2.tgz", 213 | "integrity": "sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==", 214 | "funding": [ 215 | { 216 | "type": "github", 217 | "url": "https://github.com/sponsors/jimmywarting" 218 | }, 219 | { 220 | "type": "paypal", 221 | "url": "https://paypal.me/jimmywarting" 222 | } 223 | ], 224 | "dependencies": { 225 | "web-streams-polyfill": "^3.0.3" 226 | }, 227 | "engines": { 228 | "node": "^12.20 || >= 14.13" 229 | } 230 | }, 231 | "node_modules/finalhandler": { 232 | "version": "1.1.2", 233 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 234 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 235 | "dependencies": { 236 | "debug": "2.6.9", 237 | "encodeurl": "~1.0.2", 238 | "escape-html": "~1.0.3", 239 | "on-finished": "~2.3.0", 240 | "parseurl": "~1.3.3", 241 | "statuses": "~1.5.0", 242 | "unpipe": "~1.0.0" 243 | }, 244 | "engines": { 245 | "node": ">= 0.8" 246 | } 247 | }, 248 | "node_modules/flatstr": { 249 | "version": "1.0.12", 250 | "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", 251 | "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" 252 | }, 253 | "node_modules/forwarded": { 254 | "version": "0.2.0", 255 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 256 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 257 | "engines": { 258 | "node": ">= 0.6" 259 | } 260 | }, 261 | "node_modules/fresh": { 262 | "version": "0.5.2", 263 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 264 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", 265 | "engines": { 266 | "node": ">= 0.6" 267 | } 268 | }, 269 | "node_modules/http-errors": { 270 | "version": "1.7.2", 271 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 272 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 273 | "dependencies": { 274 | "depd": "~1.1.2", 275 | "inherits": "2.0.3", 276 | "setprototypeof": "1.1.1", 277 | "statuses": ">= 1.5.0 < 2", 278 | "toidentifier": "1.0.0" 279 | }, 280 | "engines": { 281 | "node": ">= 0.6" 282 | } 283 | }, 284 | "node_modules/iconv-lite": { 285 | "version": "0.4.24", 286 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 287 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 288 | "dependencies": { 289 | "safer-buffer": ">= 2.1.2 < 3" 290 | }, 291 | "engines": { 292 | "node": ">=0.10.0" 293 | } 294 | }, 295 | "node_modules/inherits": { 296 | "version": "2.0.3", 297 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 298 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 299 | }, 300 | "node_modules/ipaddr.js": { 301 | "version": "1.9.1", 302 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 303 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 304 | "engines": { 305 | "node": ">= 0.10" 306 | } 307 | }, 308 | "node_modules/media-typer": { 309 | "version": "0.3.0", 310 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 311 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", 312 | "engines": { 313 | "node": ">= 0.6" 314 | } 315 | }, 316 | "node_modules/merge-descriptors": { 317 | "version": "1.0.1", 318 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 319 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 320 | }, 321 | "node_modules/methods": { 322 | "version": "1.1.2", 323 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 324 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", 325 | "engines": { 326 | "node": ">= 0.6" 327 | } 328 | }, 329 | "node_modules/mime": { 330 | "version": "1.6.0", 331 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 332 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 333 | "bin": { 334 | "mime": "cli.js" 335 | }, 336 | "engines": { 337 | "node": ">=4" 338 | } 339 | }, 340 | "node_modules/mime-db": { 341 | "version": "1.50.0", 342 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", 343 | "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==", 344 | "engines": { 345 | "node": ">= 0.6" 346 | } 347 | }, 348 | "node_modules/mime-types": { 349 | "version": "2.1.33", 350 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", 351 | "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", 352 | "dependencies": { 353 | "mime-db": "1.50.0" 354 | }, 355 | "engines": { 356 | "node": ">= 0.6" 357 | } 358 | }, 359 | "node_modules/ms": { 360 | "version": "2.0.0", 361 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 362 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 363 | }, 364 | "node_modules/negotiator": { 365 | "version": "0.6.2", 366 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 367 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", 368 | "engines": { 369 | "node": ">= 0.6" 370 | } 371 | }, 372 | "node_modules/node-fetch": { 373 | "version": "3.0.0", 374 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", 375 | "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==", 376 | "dependencies": { 377 | "data-uri-to-buffer": "^3.0.1", 378 | "fetch-blob": "^3.1.2" 379 | }, 380 | "engines": { 381 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 382 | }, 383 | "funding": { 384 | "type": "opencollective", 385 | "url": "https://opencollective.com/node-fetch" 386 | } 387 | }, 388 | "node_modules/on-finished": { 389 | "version": "2.3.0", 390 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 391 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 392 | "dependencies": { 393 | "ee-first": "1.1.1" 394 | }, 395 | "engines": { 396 | "node": ">= 0.8" 397 | } 398 | }, 399 | "node_modules/parseurl": { 400 | "version": "1.3.3", 401 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 402 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 403 | "engines": { 404 | "node": ">= 0.8" 405 | } 406 | }, 407 | "node_modules/path-to-regexp": { 408 | "version": "0.1.7", 409 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 410 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 411 | }, 412 | "node_modules/pino": { 413 | "version": "6.3.1", 414 | "resolved": "https://registry.npmjs.org/pino/-/pino-6.3.1.tgz", 415 | "integrity": "sha512-RgT010a5FfnxJ2AwB0TqcEuM+gNsnd08PZnCob98JSTLldLF0GMFJ/Z1VE/rdl5yJCqcoLwftmZSwSFY4/Hc2g==", 416 | "dependencies": { 417 | "fast-redact": "^2.0.0", 418 | "fast-safe-stringify": "^2.0.7", 419 | "flatstr": "^1.0.12", 420 | "pino-std-serializers": "^2.4.2", 421 | "quick-format-unescaped": "^4.0.1", 422 | "sonic-boom": "^1.0.0" 423 | }, 424 | "bin": { 425 | "pino": "bin.js" 426 | } 427 | }, 428 | "node_modules/pino-std-serializers": { 429 | "version": "2.5.0", 430 | "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-2.5.0.tgz", 431 | "integrity": "sha512-wXqbqSrIhE58TdrxxlfLwU9eDhrzppQDvGhBEr1gYbzzM4KKo3Y63gSjiDXRKLVS2UOXdPNR2v+KnQgNrs+xUg==" 432 | }, 433 | "node_modules/proxy-addr": { 434 | "version": "2.0.7", 435 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 436 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 437 | "dependencies": { 438 | "forwarded": "0.2.0", 439 | "ipaddr.js": "1.9.1" 440 | }, 441 | "engines": { 442 | "node": ">= 0.10" 443 | } 444 | }, 445 | "node_modules/qs": { 446 | "version": "6.7.0", 447 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 448 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", 449 | "engines": { 450 | "node": ">=0.6" 451 | } 452 | }, 453 | "node_modules/quick-format-unescaped": { 454 | "version": "4.0.4", 455 | "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 456 | "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" 457 | }, 458 | "node_modules/range-parser": { 459 | "version": "1.2.1", 460 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 461 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 462 | "engines": { 463 | "node": ">= 0.6" 464 | } 465 | }, 466 | "node_modules/raw-body": { 467 | "version": "2.4.0", 468 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 469 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 470 | "dependencies": { 471 | "bytes": "3.1.0", 472 | "http-errors": "1.7.2", 473 | "iconv-lite": "0.4.24", 474 | "unpipe": "1.0.0" 475 | }, 476 | "engines": { 477 | "node": ">= 0.8" 478 | } 479 | }, 480 | "node_modules/safe-buffer": { 481 | "version": "5.1.2", 482 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 483 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 484 | }, 485 | "node_modules/safer-buffer": { 486 | "version": "2.1.2", 487 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 488 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 489 | }, 490 | "node_modules/send": { 491 | "version": "0.17.1", 492 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 493 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 494 | "dependencies": { 495 | "debug": "2.6.9", 496 | "depd": "~1.1.2", 497 | "destroy": "~1.0.4", 498 | "encodeurl": "~1.0.2", 499 | "escape-html": "~1.0.3", 500 | "etag": "~1.8.1", 501 | "fresh": "0.5.2", 502 | "http-errors": "~1.7.2", 503 | "mime": "1.6.0", 504 | "ms": "2.1.1", 505 | "on-finished": "~2.3.0", 506 | "range-parser": "~1.2.1", 507 | "statuses": "~1.5.0" 508 | }, 509 | "engines": { 510 | "node": ">= 0.8.0" 511 | } 512 | }, 513 | "node_modules/send/node_modules/ms": { 514 | "version": "2.1.1", 515 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 516 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 517 | }, 518 | "node_modules/serve-static": { 519 | "version": "1.14.1", 520 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 521 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 522 | "dependencies": { 523 | "encodeurl": "~1.0.2", 524 | "escape-html": "~1.0.3", 525 | "parseurl": "~1.3.3", 526 | "send": "0.17.1" 527 | }, 528 | "engines": { 529 | "node": ">= 0.8.0" 530 | } 531 | }, 532 | "node_modules/setprototypeof": { 533 | "version": "1.1.1", 534 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 535 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 536 | }, 537 | "node_modules/sonic-boom": { 538 | "version": "1.4.1", 539 | "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", 540 | "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", 541 | "dependencies": { 542 | "atomic-sleep": "^1.0.0", 543 | "flatstr": "^1.0.12" 544 | } 545 | }, 546 | "node_modules/statuses": { 547 | "version": "1.5.0", 548 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 549 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", 550 | "engines": { 551 | "node": ">= 0.6" 552 | } 553 | }, 554 | "node_modules/toidentifier": { 555 | "version": "1.0.0", 556 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 557 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", 558 | "engines": { 559 | "node": ">=0.6" 560 | } 561 | }, 562 | "node_modules/type-is": { 563 | "version": "1.6.18", 564 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 565 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 566 | "dependencies": { 567 | "media-typer": "0.3.0", 568 | "mime-types": "~2.1.24" 569 | }, 570 | "engines": { 571 | "node": ">= 0.6" 572 | } 573 | }, 574 | "node_modules/unpipe": { 575 | "version": "1.0.0", 576 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 577 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", 578 | "engines": { 579 | "node": ">= 0.8" 580 | } 581 | }, 582 | "node_modules/utils-merge": { 583 | "version": "1.0.1", 584 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 585 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", 586 | "engines": { 587 | "node": ">= 0.4.0" 588 | } 589 | }, 590 | "node_modules/vary": { 591 | "version": "1.1.2", 592 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 593 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", 594 | "engines": { 595 | "node": ">= 0.8" 596 | } 597 | }, 598 | "node_modules/web-streams-polyfill": { 599 | "version": "3.1.1", 600 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz", 601 | "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==", 602 | "engines": { 603 | "node": ">= 8" 604 | } 605 | } 606 | }, 607 | "dependencies": { 608 | "accepts": { 609 | "version": "1.3.7", 610 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 611 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 612 | "requires": { 613 | "mime-types": "~2.1.24", 614 | "negotiator": "0.6.2" 615 | } 616 | }, 617 | "array-flatten": { 618 | "version": "1.1.1", 619 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 620 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 621 | }, 622 | "atomic-sleep": { 623 | "version": "1.0.0", 624 | "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", 625 | "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" 626 | }, 627 | "body-parser": { 628 | "version": "1.19.0", 629 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 630 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 631 | "requires": { 632 | "bytes": "3.1.0", 633 | "content-type": "~1.0.4", 634 | "debug": "2.6.9", 635 | "depd": "~1.1.2", 636 | "http-errors": "1.7.2", 637 | "iconv-lite": "0.4.24", 638 | "on-finished": "~2.3.0", 639 | "qs": "6.7.0", 640 | "raw-body": "2.4.0", 641 | "type-is": "~1.6.17" 642 | } 643 | }, 644 | "bytes": { 645 | "version": "3.1.0", 646 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 647 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 648 | }, 649 | "content-disposition": { 650 | "version": "0.5.3", 651 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 652 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 653 | "requires": { 654 | "safe-buffer": "5.1.2" 655 | } 656 | }, 657 | "content-type": { 658 | "version": "1.0.4", 659 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 660 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 661 | }, 662 | "cookie": { 663 | "version": "0.4.0", 664 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 665 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 666 | }, 667 | "cookie-signature": { 668 | "version": "1.0.6", 669 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 670 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 671 | }, 672 | "data-uri-to-buffer": { 673 | "version": "3.0.1", 674 | "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", 675 | "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" 676 | }, 677 | "debug": { 678 | "version": "2.6.9", 679 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 680 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 681 | "requires": { 682 | "ms": "2.0.0" 683 | } 684 | }, 685 | "depd": { 686 | "version": "1.1.2", 687 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 688 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 689 | }, 690 | "destroy": { 691 | "version": "1.0.4", 692 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 693 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 694 | }, 695 | "ee-first": { 696 | "version": "1.1.1", 697 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 698 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 699 | }, 700 | "encodeurl": { 701 | "version": "1.0.2", 702 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 703 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 704 | }, 705 | "escape-html": { 706 | "version": "1.0.3", 707 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 708 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 709 | }, 710 | "etag": { 711 | "version": "1.8.1", 712 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 713 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 714 | }, 715 | "express": { 716 | "version": "4.17.1", 717 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 718 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 719 | "requires": { 720 | "accepts": "~1.3.7", 721 | "array-flatten": "1.1.1", 722 | "body-parser": "1.19.0", 723 | "content-disposition": "0.5.3", 724 | "content-type": "~1.0.4", 725 | "cookie": "0.4.0", 726 | "cookie-signature": "1.0.6", 727 | "debug": "2.6.9", 728 | "depd": "~1.1.2", 729 | "encodeurl": "~1.0.2", 730 | "escape-html": "~1.0.3", 731 | "etag": "~1.8.1", 732 | "finalhandler": "~1.1.2", 733 | "fresh": "0.5.2", 734 | "merge-descriptors": "1.0.1", 735 | "methods": "~1.1.2", 736 | "on-finished": "~2.3.0", 737 | "parseurl": "~1.3.3", 738 | "path-to-regexp": "0.1.7", 739 | "proxy-addr": "~2.0.5", 740 | "qs": "6.7.0", 741 | "range-parser": "~1.2.1", 742 | "safe-buffer": "5.1.2", 743 | "send": "0.17.1", 744 | "serve-static": "1.14.1", 745 | "setprototypeof": "1.1.1", 746 | "statuses": "~1.5.0", 747 | "type-is": "~1.6.18", 748 | "utils-merge": "1.0.1", 749 | "vary": "~1.1.2" 750 | } 751 | }, 752 | "fast-redact": { 753 | "version": "2.1.0", 754 | "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-2.1.0.tgz", 755 | "integrity": "sha512-0LkHpTLyadJavq9sRzzyqIoMZemWli77K2/MGOkafrR64B9ItrvZ9aT+jluvNDsv0YEHjSNhlMBtbokuoqii4A==" 756 | }, 757 | "fast-safe-stringify": { 758 | "version": "2.1.1", 759 | "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", 760 | "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" 761 | }, 762 | "fetch-blob": { 763 | "version": "3.1.2", 764 | "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.2.tgz", 765 | "integrity": "sha512-hunJbvy/6OLjCD0uuhLdp0mMPzP/yd2ssd1t2FCJsaA7wkWhpbp9xfuNVpv7Ll4jFhzp6T4LAupSiV9uOeg0VQ==", 766 | "requires": { 767 | "web-streams-polyfill": "^3.0.3" 768 | } 769 | }, 770 | "finalhandler": { 771 | "version": "1.1.2", 772 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 773 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 774 | "requires": { 775 | "debug": "2.6.9", 776 | "encodeurl": "~1.0.2", 777 | "escape-html": "~1.0.3", 778 | "on-finished": "~2.3.0", 779 | "parseurl": "~1.3.3", 780 | "statuses": "~1.5.0", 781 | "unpipe": "~1.0.0" 782 | } 783 | }, 784 | "flatstr": { 785 | "version": "1.0.12", 786 | "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", 787 | "integrity": "sha512-4zPxDyhCyiN2wIAtSLI6gc82/EjqZc1onI4Mz/l0pWrAlsSfYH/2ZIcU+e3oA2wDwbzIWNKwa23F8rh6+DRWkw==" 788 | }, 789 | "forwarded": { 790 | "version": "0.2.0", 791 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 792 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 793 | }, 794 | "fresh": { 795 | "version": "0.5.2", 796 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 797 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 798 | }, 799 | "http-errors": { 800 | "version": "1.7.2", 801 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 802 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 803 | "requires": { 804 | "depd": "~1.1.2", 805 | "inherits": "2.0.3", 806 | "setprototypeof": "1.1.1", 807 | "statuses": ">= 1.5.0 < 2", 808 | "toidentifier": "1.0.0" 809 | } 810 | }, 811 | "iconv-lite": { 812 | "version": "0.4.24", 813 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 814 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 815 | "requires": { 816 | "safer-buffer": ">= 2.1.2 < 3" 817 | } 818 | }, 819 | "inherits": { 820 | "version": "2.0.3", 821 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 822 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 823 | }, 824 | "ipaddr.js": { 825 | "version": "1.9.1", 826 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 827 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 828 | }, 829 | "media-typer": { 830 | "version": "0.3.0", 831 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 832 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 833 | }, 834 | "merge-descriptors": { 835 | "version": "1.0.1", 836 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 837 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 838 | }, 839 | "methods": { 840 | "version": "1.1.2", 841 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 842 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 843 | }, 844 | "mime": { 845 | "version": "1.6.0", 846 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 847 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 848 | }, 849 | "mime-db": { 850 | "version": "1.50.0", 851 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", 852 | "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" 853 | }, 854 | "mime-types": { 855 | "version": "2.1.33", 856 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", 857 | "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", 858 | "requires": { 859 | "mime-db": "1.50.0" 860 | } 861 | }, 862 | "ms": { 863 | "version": "2.0.0", 864 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 865 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 866 | }, 867 | "negotiator": { 868 | "version": "0.6.2", 869 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 870 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 871 | }, 872 | "node-fetch": { 873 | "version": "3.0.0", 874 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0.tgz", 875 | "integrity": "sha512-bKMI+C7/T/SPU1lKnbQbwxptpCrG9ashG+VkytmXCPZyuM9jB6VU+hY0oi4lC8LxTtAeWdckNCTa3nrGsAdA3Q==", 876 | "requires": { 877 | "data-uri-to-buffer": "^3.0.1", 878 | "fetch-blob": "^3.1.2" 879 | } 880 | }, 881 | "on-finished": { 882 | "version": "2.3.0", 883 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 884 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 885 | "requires": { 886 | "ee-first": "1.1.1" 887 | } 888 | }, 889 | "parseurl": { 890 | "version": "1.3.3", 891 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 892 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 893 | }, 894 | "path-to-regexp": { 895 | "version": "0.1.7", 896 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 897 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 898 | }, 899 | "pino": { 900 | "version": "6.3.1", 901 | "resolved": "https://registry.npmjs.org/pino/-/pino-6.3.1.tgz", 902 | "integrity": "sha512-RgT010a5FfnxJ2AwB0TqcEuM+gNsnd08PZnCob98JSTLldLF0GMFJ/Z1VE/rdl5yJCqcoLwftmZSwSFY4/Hc2g==", 903 | "requires": { 904 | "fast-redact": "^2.0.0", 905 | "fast-safe-stringify": "^2.0.7", 906 | "flatstr": "^1.0.12", 907 | "pino-std-serializers": "^2.4.2", 908 | "quick-format-unescaped": "^4.0.1", 909 | "sonic-boom": "^1.0.0" 910 | } 911 | }, 912 | "pino-std-serializers": { 913 | "version": "2.5.0", 914 | "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-2.5.0.tgz", 915 | "integrity": "sha512-wXqbqSrIhE58TdrxxlfLwU9eDhrzppQDvGhBEr1gYbzzM4KKo3Y63gSjiDXRKLVS2UOXdPNR2v+KnQgNrs+xUg==" 916 | }, 917 | "proxy-addr": { 918 | "version": "2.0.7", 919 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 920 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 921 | "requires": { 922 | "forwarded": "0.2.0", 923 | "ipaddr.js": "1.9.1" 924 | } 925 | }, 926 | "qs": { 927 | "version": "6.7.0", 928 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 929 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 930 | }, 931 | "quick-format-unescaped": { 932 | "version": "4.0.4", 933 | "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", 934 | "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" 935 | }, 936 | "range-parser": { 937 | "version": "1.2.1", 938 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 939 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 940 | }, 941 | "raw-body": { 942 | "version": "2.4.0", 943 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 944 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 945 | "requires": { 946 | "bytes": "3.1.0", 947 | "http-errors": "1.7.2", 948 | "iconv-lite": "0.4.24", 949 | "unpipe": "1.0.0" 950 | } 951 | }, 952 | "safe-buffer": { 953 | "version": "5.1.2", 954 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 955 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 956 | }, 957 | "safer-buffer": { 958 | "version": "2.1.2", 959 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 960 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 961 | }, 962 | "send": { 963 | "version": "0.17.1", 964 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 965 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 966 | "requires": { 967 | "debug": "2.6.9", 968 | "depd": "~1.1.2", 969 | "destroy": "~1.0.4", 970 | "encodeurl": "~1.0.2", 971 | "escape-html": "~1.0.3", 972 | "etag": "~1.8.1", 973 | "fresh": "0.5.2", 974 | "http-errors": "~1.7.2", 975 | "mime": "1.6.0", 976 | "ms": "2.1.1", 977 | "on-finished": "~2.3.0", 978 | "range-parser": "~1.2.1", 979 | "statuses": "~1.5.0" 980 | }, 981 | "dependencies": { 982 | "ms": { 983 | "version": "2.1.1", 984 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 985 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 986 | } 987 | } 988 | }, 989 | "serve-static": { 990 | "version": "1.14.1", 991 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 992 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 993 | "requires": { 994 | "encodeurl": "~1.0.2", 995 | "escape-html": "~1.0.3", 996 | "parseurl": "~1.3.3", 997 | "send": "0.17.1" 998 | } 999 | }, 1000 | "setprototypeof": { 1001 | "version": "1.1.1", 1002 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 1003 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 1004 | }, 1005 | "sonic-boom": { 1006 | "version": "1.4.1", 1007 | "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", 1008 | "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", 1009 | "requires": { 1010 | "atomic-sleep": "^1.0.0", 1011 | "flatstr": "^1.0.12" 1012 | } 1013 | }, 1014 | "statuses": { 1015 | "version": "1.5.0", 1016 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 1017 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 1018 | }, 1019 | "toidentifier": { 1020 | "version": "1.0.0", 1021 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 1022 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 1023 | }, 1024 | "type-is": { 1025 | "version": "1.6.18", 1026 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 1027 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 1028 | "requires": { 1029 | "media-typer": "0.3.0", 1030 | "mime-types": "~2.1.24" 1031 | } 1032 | }, 1033 | "unpipe": { 1034 | "version": "1.0.0", 1035 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1036 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 1037 | }, 1038 | "utils-merge": { 1039 | "version": "1.0.1", 1040 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1041 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 1042 | }, 1043 | "vary": { 1044 | "version": "1.1.2", 1045 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1046 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 1047 | }, 1048 | "web-streams-polyfill": { 1049 | "version": "3.1.1", 1050 | "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.1.1.tgz", 1051 | "integrity": "sha512-Czi3fG883e96T4DLEPRvufrF2ydhOOW1+1a6c3gNjH2aIh50DNFBdfwh2AKoOf1rXvpvavAoA11Qdq9+BKjE0Q==" 1052 | } 1053 | } 1054 | } 1055 | --------------------------------------------------------------------------------