├── .gitignore ├── .vscode └── settings.json ├── ErrorElement.tsx ├── LICENSE ├── README.md ├── SetupREADME.md ├── client ├── App.tsx ├── Components │ ├── AlertBar.tsx │ ├── Dashboard │ │ ├── AddMetric.tsx │ │ ├── Dashboard.tsx │ │ ├── MetricDisplay.tsx │ │ └── MetricDisplayPreview.tsx │ ├── HPA Dashboard │ │ ├── DoubleLineGraph.tsx │ │ ├── HPADashboard.tsx │ │ ├── IndivDLG.tsx │ │ ├── TableDisplay.tsx │ │ └── TableRow.tsx │ └── Visualizer │ │ ├── AlertFlag.tsx │ │ ├── ControlPlane.tsx │ │ ├── Deployments.tsx │ │ ├── Dropdown.tsx │ │ ├── Namespaces.tsx │ │ ├── Nodes.tsx │ │ ├── Pods.tsx │ │ └── Services.tsx ├── Routes │ ├── ClusterView.tsx │ ├── Home.tsx │ └── SetUp.tsx ├── __tests__ │ ├── App.test.tsx │ ├── pages │ │ └── Home.test.tsx │ ├── setup.ts │ └── test-utils.tsx ├── assets │ ├── Anago-Members │ │ ├── Rylie.jpg │ │ ├── Steve.jpeg │ │ ├── alexandra.png │ │ └── halia2.jpg │ ├── gifs │ │ ├── .DS_Store │ │ ├── AddMetric.gif │ │ ├── AlertVid.gif │ │ ├── ClusterVid.gif │ │ ├── HPAVid.gif │ │ └── MonitorVid.gif │ ├── images │ │ ├── AnagoCrashed.png │ │ ├── abg3.png │ │ ├── alert.png │ │ ├── anago.png │ │ ├── control-plane.png │ │ ├── control.png │ │ ├── deployment.png │ │ ├── github.png │ │ ├── github2.png │ │ ├── linkedInLogo.webp │ │ ├── namespace.png │ │ ├── node.png │ │ ├── pod.png │ │ ├── service.png │ │ ├── shipwreck.png │ │ └── trash-can.png │ ├── style.scss │ └── vite-env.d.ts ├── context │ ├── functions.ts │ ├── loaders.ts │ └── stateStore.tsx └── index.tsx ├── dev ├── Grafana │ ├── clusterConfig.yaml │ ├── grafana.ini │ ├── grafana.ini(default) │ └── values.yaml ├── alertManagerInfo.md ├── eksctlREADME.md ├── grafanaDashboard.tsx ├── image-1.png ├── image-2.png ├── image.png ├── minikubeREADME.md ├── pithy │ ├── .dockerignore │ ├── .gitignore │ ├── Dockerfile │ ├── package-lock.json │ ├── package.json │ ├── server.js │ ├── src │ │ ├── index.html │ │ ├── index.js │ │ └── style.css │ └── yaml │ │ ├── aws-pithy-service.yaml │ │ ├── aws-pithy.yaml │ │ ├── pithy-hpa.yaml │ │ ├── pithy-service.yaml │ │ └── pithy.yaml ├── prometheusREADME.md └── promqlQueriesREADME.md ├── dist ├── assets │ ├── index-3e5a1e55.js │ └── index-d526a0c5.css ├── index.html └── vite.svg ├── index.html ├── package-lock.json ├── package.json ├── server ├── controllers │ ├── configController.ts │ ├── helperFuncs.ts │ ├── k8sApiController.ts │ ├── promApiController.ts │ └── userDataController.ts ├── models │ ├── defaultUserData.ts │ ├── demoData.json │ ├── queryBuilder.ts │ ├── userData.json │ └── userDataClass.ts ├── routers │ ├── configRouter.ts │ ├── dataRouter.ts │ ├── k8sRouter.ts │ └── userRouter.ts └── server.ts ├── tsconfig.json ├── tsconfig.node.json ├── types.ts ├── user-config.ts └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /pithy/node_modules 3 | 4 | /client/assets/images/.DS_Store 5 | .DS_Store 6 | client/assets/.DS_Store 7 | 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "testing.automaticallyOpenPeekView": "never" 3 | } 4 | -------------------------------------------------------------------------------- /ErrorElement.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteError } from 'react-router-dom'; 2 | import React from 'react'; 3 | 4 | const ErrorElement = () => { 5 | let error = useRouteError(); 6 | console.error(error); 7 | return ( 8 |
9 |
10 | 11 |
12 |
13 | 14 |

Oops! Page Not Found.

15 |
16 |
17 | ); 18 | }; 19 | 20 | export default ErrorElement; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anago 2 | 3 | ![Anago-Banner](client/assets/images/abg3.png) 4 | 5 | Anago is a tool to help developers monitor and visualize their Kubernetes clusters. Team Anago is equipped for monitoring Kubernetes clusters hosted both on local servers and on cloud-based platforms. Anago provides out-of-the-box support for Prometheus and Prometheus’ Alertmanager for scraping metrics and alerts and integrates with Chart.JS for the dashboard graphs. 6 | 7 | The name Anago, originates from Greek, meaning to lead to a higher place, to uplift, and to take to sea. In this spirit, Anago serves as a perfect compass to help you manage your containerized application deployments on the Kubernetes platform. 8 | 9 | Let’s dive into how the features of Anago’s platform can simplify and streamline managing your Kubernetes clusters. 10 | 11 |
12 | 13 | [![Kubernetes](https://img.shields.io/badge/kubernetes-326ce5.svg?&style=for-the-badge&logo=kubernetes&logoColor=white)](https://kubernetes.io/) [![Docker](https://img.shields.io/badge/Docker-2CA5E0?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com/) [![AWS](https://img.shields.io/badge/Amazon_AWS-FF9900?style=for-the-badge&logo=amazonaws&logoColor=white)](https://aws.amazon.com/) [![Prometheus](https://img.shields.io/badge/Prometheus-000000?style=for-the-badge&logo=prometheus&labelColor=000000)](https://prometheus.io/) [![Chart.js](https://img.shields.io/badge/Chart%20js-FF6384?style=for-the-badge&logo=chartdotjs&logoColor=white)](https://www.chartjs.org/) [![Helm](https://img.shields.io/badge/Helm-0F1689?style=for-the-badge&logo=Helm&labelColor=0F1689)](https://helm.sh/) [![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E)](https://vitejs.dev/) [![VitesT](https://img.shields.io/badge/Vitest-86b91a?style=for-the-badge&logo=vitest&logoColor=edd532)](https://vitest.dev/) [![ts-node](https://img.shields.io/badge/ts--node-3178C6?style=for-the-badge&logo=ts-node&logoColor=white)](https://www.npmjs.com/package/ts-node) [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?style=for-the-badge&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![JavaScript](https://img.shields.io/badge/JavaScript-323330?style=for-the-badge&logo=javascript&logoColor=F7DF1E)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) [![React](https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB)](https://react.dev/) [![ReactRouter](https://img.shields.io/badge/React_Router-CA4245?style=for-the-badge&logo=react-router&logoColor=white)](https://reactrouter.com/en/main) [![Express](https://img.shields.io/badge/Express%20js-000000?style=for-the-badge&logo=express&logoColor=white)](https://expressjs.com/) [![Node.js](https://img.shields.io/badge/Node%20js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://nodejs.org/en) [![HTML5](https://img.shields.io/badge/HTML5-E34F26?style=for-the-badge&logo=html5&logoColor=white)](https://developer.mozilla.org/en-US/docs/Glossary/HTML5) [![NPM](https://img.shields.io/badge/npm-CB3837?style=for-the-badge&logo=npm&logoColor=white)](https://www.npmjs.com/) [![Eslint](https://img.shields.io/badge/ESLint-4B3263?style=for-the-badge&logo=eslint&logoColor=white)](https://eslint.org/) 14 | 15 |
16 | 17 | ## Table of Contents 18 | 19 | 1. [Anago](#Anago) 20 | 2. [Features](#features) 21 | 3. [Getting Started with Anago](#Getting-Started-with-Anago) 22 | 4. [Contributing](#Contributing) 23 | 5. [Progress](#Progress) 24 | 6. [Scripts](#Scripts) 25 | 7. [Our Team](#our-team) 26 | 8. [License](#license) 27 | 28 | ## Features 29 | 30 | Anago is a Kubernetes cluster monitoring and visualization tool providing relevant, richly detailed insights into the health of your application deployments with the following useful features: 31 | 32 | 1. **Real-time Data Monitoring**: Anago seamlessly integrates with your cloud-based and/or locally hosted Kubernetes clusters to allow for a real-time analysis of cluster performance. 33 | ![data-monitoring](/client/assets/gifs/MonitorVid.gif) 34 | 2. **Customizable Metrics**: Anago harnesses the power of Prometheus to scrape time-series data from your Kubernetes clusters. We offer customized query-building to configure your dashboard to display cluster metrics and data insights relevant to you. We provide the tools for your cluster management, and you decide which direction you want to take them. 35 | ![data-monitoring](/client/assets/gifs/AddMetric.gif) 36 | 3. **Alerts**: Anago displays relevant real-time alerts relating to cluster health concerns, allowing you to address issues with your deployments proactively to combat downtime in production. 37 | ![data-monitoring](/client/assets/gifs/AlertVid.gif) 38 | 4. **Horizontal Pod Autoscaler Monitoring**: Anago allows you to monitor actively deployed Horizontal Pod Autoscalers in real time or connect to your test application to see isolated test results before the rollout of new features or deployment configurations. Anago stores logs of notable historical timestamps for you to reference to not miss meaningful warning signs and/or longer-term trends, diminishing the abstraction of your HPA usage by providing insights into areas lacking efficiency alongside any bottlenecks that can slow your deployed applications and also drive up the cost of your clusters astronomically. Anago will act as an additional set of eyes and devise tailored recommendations on how to optimize your current HPA configurations. 39 | ![data-monitoring](/client/assets/gifs/HPAVid.gif) 40 | 5. **Cluster Visualization**: Anago provides flexible and detailed visualization of your Kubernetes clusters on our ClusterView dashboard. See your clusters in their entirety with all of their nested components, or filter to display specific clusters, nodes, and namespaces that are relevant to your needs. ClusterView provides extremely detailed insights into each component in your cluster with real-time data from Prometheus and the Kubernetes API. Alerts are displayed visually so you can see which parts of your cluster require your attention. 41 | ![data-monitoring](/client/assets/gifs/ClusterVid.gif) 42 | 6. **Platform-agnostic**: Anago supports clusters hosted on cloud-computing platforms and local servers. We are flexible and adaptable for your scale and configuration needs and provide tools to help you integrate your clusters with our technology seamlessly. Anago is for everyone. 43 | 44 | ## Getting Started with Anago 45 | 46 | Let’s walk through how to get your cluster [setup with Anago](/SetupREADME.md) 47 | 48 | ## Contributing 49 | 50 | Contributions are part of the foundation of the Open Source Community. They create a space for developers to share, collaborate, learn, and inspire! Any contributions you choose to make are greatly appreciated. 51 | 52 | If you wish to contribute, please follow these guidelines: 53 | 54 | 1. Fork and clone the repository 55 | 2. Branch off of the dev branch to create your own feature branch 56 | - The Feature branch name should start with feat, fix, bug, docs, test, wip, or merge (e.g. feat/database) 57 | - see the [Scripts](##Scripts) section below for additional details 58 | 3. Commit your changes (git commit -m '(feat/bugfix/style/etc.): [commit message here]') 59 | - Please review [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) standards for more information 60 | 4. Once your new feature is built out, you can submit a pull request to dev 61 | 62 | ## Progress 63 | 64 | | Feature | Status | 65 | | ------------------------------------------------ | ------ | 66 | | HPA Recommendations | ⏳ | 67 | | Integrate CI/CD pipeline | ⏳ | 68 | | Automated Monitoring Tool Deployment & Config | ⏳ | 69 | | Increase Test Coverage | 🙏🏻 | 70 | | Historical data page | 🙏🏻 | 71 | | Ability to export metric data in-app | 🙏🏻 | 72 | | OAuth2.0 with cloud-hosting providers (i.e. AWS) | 🙏🏻 | 73 | | Stress-Test page | 🙏🏻 | 74 | 75 | - ✅ = Ready to use 76 | - ⏳ = In progress 77 | - 🙏🏻 = Looking for contributors 78 | 79 | ## Scripts 80 | 81 | Below are descriptions of each npm script: 82 | 83 | - `npm run dev`: Starts the development server using Nodemon 84 | - `npm run dev:front`: Starts the development for the frontend using Vite 85 | - `npm run dev:back`: Starts the development server for the backend using Nodemon 86 | - `npm run build`: Starts the build mode 87 | - `npm run lint`: Applies standardized linting 88 | - `npm run preview`: Runs a vite preview 89 | - `npm vitest`: Runs tests with vitest 90 | 91 | ## Our Team 92 | 93 | 94 | 95 | 103 | 111 | 119 | 127 | 128 | 129 |
96 | “Alexandra” 97 |
98 | Alexandra Ashcraft 99 |
100 | 101 | 102 |
104 | Halia 105 |
106 | Halia Haynes 107 |
108 | 109 | 110 |
112 | Rylie 113 |
114 | Rylie Pereira 115 |
116 | 117 | 118 |
120 | Steve 121 |
122 | Steve Schlepphorst 123 |
124 | 125 | 126 |
130 | 131 | ## License 132 | 133 | By contributing, you agree that your contributions will be licensed under Anago's MIT License. 134 | -------------------------------------------------------------------------------- /SetupREADME.md: -------------------------------------------------------------------------------- 1 | # Setting up Anago 2 | 3 | Welcome to Anago. The first section of this set-up document walks through installing and configuring Anago for an existing cluster with Prometheus available. The rest of the document describes the basic steps of deploying an EKS cluster from scratch, intended for a development or DevOps team newly approaching K8s. 4 | 5 | # Setup for Anago: 6 | 7 | 1. Clone the Anago repository locally and install required modules: 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | 2. Modify the [user-config.ts](/user-config.ts) file to point to access your Prometheus instance and Alertmanager. For example, forward these services to the default ports (9090 and 9093): 14 | 15 | ```bash 16 | kubectl port-forward svc/[service-name] -n [namespace] 9090 17 | kubectl port-forward svc/[service-name] -n [namespace] 9093 18 | ``` 19 | 20 | 3. Launch Anago from the command line: 21 | 22 | ```bash 23 | npm run dev 24 | ``` 25 | 26 | Navigate to the local access point for Vite (by default, [http://locahost:5173](http://locahost:5173)), and you should see Anago! 27 | Several core metrics are populated by default (or if `NEW_USER=true` in user-config.ts). 28 | 29 | All set? click [here](/README.md) to return to the main README.md 30 | 31 | # New to Kubernetes? 32 | 33 | Teams without an active cluster will have a longer path for initially setting up Kubernetes and then Anago. The following step-by-step guide will move through one use case for deploying and monitoring a simple cluster using the eksctl tool to configure an EKS cluster on EC2 instances with Prometheus. At the end of these steps, you should be ready to use Anago (described above). Depending on your needs, only some of these steps may be necessary. 34 | 35 | NOTE: to deploy K8s cluster locally, consider using [minikube](https://minikube.sigs.k8s.io/docs/) 36 | 37 | ## Table of Contents 38 | 39 | 1. [Prerequisites](#Prerequisites) 40 | 2. [AWS Login and Authentication](#AWS-Login-and-Authentication) 41 | 3. [eksctl Setup](#eksctl-Setup) 42 | 4. [Loading the image to ECR](#Loading-the-image-to-ECR) 43 | 5. [Deployment + Services](#Deployment-+-Services) 44 | 6. [Making the Metrics Available](#Making-the-Metrics-Available) 45 | 7. [Prometheus Setup](#Prometheus-Setup-using-Helm) 46 | 8. [AlertManager Setup](#AlertManager-Setup) 47 | 48 | ## Prerequisites 49 | 50 | 1. For the following setup, the following installations and setups are required: 51 | - [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 52 | - [eksctl](https://docs.aws.amazon.com/eks/latest/userguide/eksctl.html) 53 | - [kubectl](https://kubernetes.io/docs/tasks/tools/) 54 | - [Docker Desktop](https://www.docker.com/products/docker-desktop/) 55 | 1. Fork the Anago repository to your personal GitHub account. 56 | 1. Clone the forked repository to your local computer. 57 | 1. In the Anago folder of your local repository, install all the dependencies 58 | 59 | ```bash 60 | npm install 61 | ``` 62 | 63 | DISCLAIMER: this Anago setup describes a cloudhosted deployment 64 | 65 | ## AWS Login and Authentication 66 | 67 | 1. Log in to your personal AWS account as an IAM user 68 | 2. From the command line, run 69 | 70 | ```bash 71 | aws configure 72 | ``` 73 | 74 | and when prompted, enter your AWS Access Key ID and AWS Secret Access Key 3. Entering your credentials automatically creates the secret file: ~/.aws/config. You are then able to adjust that information as desired. 75 | 76 | NOTE: Find the hidden file on your local machine by typing 'command-shift-g' within your Finder. 77 | 78 | ## eksctl Setup 79 | 80 | 1. Create a cluster with your desired cluster name, region, node type, and node count 81 | 82 | ```bash 83 | eksctl create cluster --name [test-cluster] --region [us-east-2] --node-type [t2.micro] --nodes [2] 84 | ``` 85 | 86 | NOTE: eksctl will add a config file in ~/.kube that directs local kubectl commands to your EKS instance. 87 | 88 | 2. To access this EKS instance elsewhere: 89 | 90 | ```bash 91 | aws eks update-kubeconfig --region [us-east-2] --name [test-cluster] 92 | ``` 93 | 94 | ## Loading the image to ECR 95 | 96 | 1. Create an image repo in ECR, if there isn’t one 97 | 2. the software folder should have a Dockerfile 98 | 3. From ECR’s image repo (must be in the right region), click “View Push Commands” 99 | 4. Run these exact commands in order in order to hand amazon auth info to docker (expires after 12 hours), build and tag a docker image, and push it up to amazon 100 | 101 | NOTE: If using an M1 Mac, you MUST modify the docker build command in order to correct the build platform: 102 | 103 | ```bash 104 | docker buildx build --platform linux/amd64 -t [image] . 105 | ``` 106 | 107 | ## Deployment + Services 108 | 109 | 1. Use kubectl to deploy deployment(+pods) and services as usual (see KubernetesDeployment doc), using the ECR image link. For example: 110 | 111 | ```bash 112 | kubectl apply -f [yaml file] 113 | ``` 114 | 115 | OR 116 | 117 | ```bash 118 | kubectl create deployment [Depl-name] --image=[image] 119 | ``` 120 | 121 | 2. To surface the Service's external IP, run this command: 122 | 123 | ```bash 124 | kubectl get svc 125 | ``` 126 | 127 | ## Making the Metrics Available 128 | 129 | 1. Connect to your desired EKS cluster: 130 | 131 | ```bash 132 | aws eks --region region update-kubeconfig --name [cluster-name] 133 | ``` 134 | 135 | 2. Run the configuration to deploy the metrics server 136 | 137 | ```bash 138 | kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml 139 | ``` 140 | 141 | 3. Check to see that the Metrics Pod is up and running and that the /metrics endpoint is exposed 142 | 143 | ```bash 144 | kubectl get pods -n kube-system 145 | ``` 146 | 147 | ## Prometheus Setup (using Helm) 148 | 149 | 1. Add the prometheus-community Helm repo and update Helm: 150 | 151 | ```bash 152 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts 153 | ``` 154 | 155 | ```bash 156 | helm repo update 157 | ``` 158 | 159 | 2. Install the prometheus-community Helm repo with a desired release name in a created namespace. Also, 160 | install the alert manager. For example: 161 | 162 | ```bash 163 | helm install [release-name] prometheus-community/kube-prometheus-stack --namespace [namespace] --create-namespace --set alertmanager.persistentVolume.storageClass="gp2",server.persistentVolume.storageClass="gp2" 164 | ``` 165 | 166 | NOTE: after the Helm chart has been installed, you should see this: 167 | 168 | > NAME: [release-name] LAST DEPLOYED: [date] NAMESPACE: [namespace] default 169 | > STATUS: deployed REVISION: 1 NOTES: kube-prometheus-stack has beeninstalled. 170 | > Check its status by running: kubectl --namespace default get pods -l 171 | > "release=[namespace]" Refer to 172 | > https://github.com/prometheus-operator/kube-prometheus for instructions on how 173 | > to create and configure Alertmanager and Prometheus instances using the 174 | > Operator. 175 | 176 | 3. use kubectl to see what is installed in the cluster: 177 | 178 | ```bash 179 | kubectl get pod -n [namespace] 180 | ``` 181 | 182 | NOTE: you should see all nodes including the prometheus stack operator, the alert manager, and grafana 183 | 184 | 4. use kubectl to see all services: 185 | 186 | ```bash 187 | kubectl get services -n [namespace] 188 | ``` 189 | 190 | 5. to access your prometheus instance, use the kubectl port-forward to forward a local port into the Cluster with the service name. Example: 191 | 192 | ```bash 193 | kubectl port-forward svc/[service-name] -n [namespace] 9090 194 | ``` 195 | 196 | 6. navigate to http://localhost:9090 in your browser to access the Prometheus web UI. 197 | 198 | NOTE: Click Status, then Targets to see a list of scrape targets configured by Helm. 199 | 200 | NOTE: if you'd like more general information regarding Prometheus, [click here](./dev/prometheusREADME.md) 201 | 202 | ## AlertManager Setup 203 | 204 | NOTE: the prometheus-community/kube-prometheus-stack includes configuration files for AlertManager. 205 | 206 | 1. to access the alertManager UI, use the kubectl port-forward to forward a local port into the Cluster with the service name. Example: 207 | 208 | ```bash 209 | kubectl port-forward svc/[service-name] -n monitoring 9093 210 | ``` 211 | 212 | 2. navigate to http://localhost:9093 in your browser to access the AlertManager web UI. 213 | 214 | NOTE: You can see the active alerts configured by Helm. if more customization regarding alerts is needed, the configuration files may be adjusted and applied. 215 | 216 | NOTE: if you'd like more information regarding AlertManager, [click here](./dev/alertManagerInfo.md) 217 | -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Route, 3 | createBrowserRouter, 4 | createRoutesFromElements, 5 | RouterProvider, 6 | } from 'react-router-dom'; 7 | 8 | //import routes 9 | import Home from './Routes/Home'; 10 | import HPADashboard from './Components/HPA Dashboard/HPADashboard'; 11 | import Dashboard from './Components/Dashboard/Dashboard'; 12 | import ClusterView from './Routes/ClusterView'; 13 | import SetUp from './Routes/SetUp'; 14 | import ErrorElement from '../ErrorElement'; 15 | 16 | //import loaders 17 | import * as loaders from './context/loaders'; 18 | import React from 'react'; 19 | 20 | //create router to pass into router provider component returned from app. createBrowserRouter recommended for all latest React Router web projects. 21 | const router = createBrowserRouter( 22 | createRoutesFromElements( 23 | } 26 | loader={loaders.userLoader} 27 | id='home' 28 | errorElement={} 29 | > 30 | } 33 | errorElement={} 34 | /> 35 | } 38 | errorElement={} 39 | /> 40 | } 43 | loader={loaders.clusterLoader} 44 | id='cluster' 45 | errorElement={} 46 | /> 47 | } errorElement={} /> 48 | 49 | ) 50 | ); 51 | 52 | //provide router to application 53 | const App = () => { 54 | return ; 55 | }; 56 | 57 | export default App; 58 | -------------------------------------------------------------------------------- /client/Components/Dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Modal } from 'react-responsive-modal'; 3 | import MetricDisplay from './MetricDisplay'; 4 | import { UserData } from '../../../types'; 5 | import { useRouteLoaderData, useParams, Outlet } from 'react-router-dom'; 6 | import AddMetric from './AddMetric'; 7 | import AlertBar from '../AlertBar'; 8 | 9 | const Dashboard = () => { 10 | const userData = useRouteLoaderData('home') as UserData; 11 | const { id } = useParams(); 12 | 13 | const [lastUpdate, setLastUpdate] = useState(); 14 | const [addMetricModal, setAddMetricModal] = useState(false); 15 | // edit mode to allow deleting metrics 16 | const [editMode, setEditMode] = useState(false); 17 | 18 | useEffect(() => { 19 | setLastUpdate(new Date()); 20 | }, []); 21 | 22 | //pithy rendering example 23 | const refresh = () => setLastUpdate(new Date()); 24 | const pithy = () => { 25 | try { 26 | fetch('/api/pithy') 27 | .then((res) => res.json()) 28 | .then((res) => { 29 | setTimeout(() => pithy(), 700); 30 | }); 31 | } catch (err) { 32 | console.log(`failed to fetch pithy: ${err}`); 33 | } 34 | }; 35 | 36 | function saveMetricsAndReload() { 37 | setEditMode(false); 38 | setAddMetricModal(false); 39 | window.location.reload(); 40 | return; 41 | } 42 | 43 | const metricIds: string[] = userData.dashboards[0].metrics; 44 | return ( 45 |
46 | 47 |
48 | {id && ( 49 | <> 50 |

51 | {userData.dashboards[id].dashboardName} 52 |

53 |
54 | 55 | 58 | 59 | {/* 60 | 63 | */} 64 | 65 | 68 | 69 | {!editMode && ( 70 | 71 | 74 | 75 | )} 76 | {editMode && ( 77 | 78 | 81 | 82 | )} 83 |
84 |
85 | {metricIds.map((metricId) => ( 86 | 91 | ))} 92 | 93 |
94 |
95 |

Last updated: {`${lastUpdate}`}

96 |
97 | 98 | )} 99 |
100 | { 103 | saveMetricsAndReload(); 104 | }} 105 | > 106 | 110 | 111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default Dashboard; 118 | -------------------------------------------------------------------------------- /client/Components/Dashboard/MetricDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { UserData } from '../../../types'; 3 | import { Modal } from 'react-responsive-modal'; 4 | import { ScopeType } from '../../../types'; 5 | import { 6 | Chart as ChartJS, 7 | CategoryScale, 8 | LinearScale, 9 | PointElement, 10 | LineElement, 11 | Title, 12 | Tooltip, 13 | Legend, 14 | Colors, 15 | } from 'chart.js'; 16 | import { Line, Chart } from 'react-chartjs-2'; 17 | import { useRouteLoaderData } from 'react-router-dom'; 18 | import React from 'react'; 19 | ChartJS.register( 20 | CategoryScale, 21 | LinearScale, 22 | PointElement, 23 | LineElement, 24 | Colors, 25 | Title, 26 | Tooltip, 27 | Legend 28 | ); 29 | 30 | const MetricDisplay = ({ metricId, editMode }) => { 31 | // current userData 32 | const userData = useRouteLoaderData('home') as UserData; 33 | //state to handle modal and handling fetched data router 34 | const [open, setOpen]: any = useState(false); 35 | const [metricData, setMetricData]: any = useState({}); 36 | //button to delete metrics from userData 37 | const [trashCanClicked, setTrashCanClicked] = useState(false); 38 | 39 | // display options for metrics 40 | const options: any = { 41 | plugins: { 42 | legend: { 43 | display: false, 44 | }, 45 | }, 46 | interaction: { 47 | intersect: false, 48 | mode: 'nearest', 49 | }, 50 | }; 51 | // Most axes should always start at 0, but some may not -- uncomment next line and add items that may not need 0-basis for plotting 52 | // if (![].includes(userData.metrics[metricId].lookupType)) 53 | Object.assign(options, { scales: { y: { beginAtZero: true } } }); 54 | // Some axes should go up to ~100 55 | if ([4, 7].includes(userData.metrics[metricId].lookupType)) 56 | Object.assign(options, { 57 | scales: { y: { suggestedMax: 100, suggestedMin: 0 } }, 58 | }); 59 | 60 | //fetching data from Prometheus 61 | function fetchFromProm() { 62 | fetch(`/api/data/metrics/${metricId}`, { 63 | method: 'GET', 64 | }) 65 | .then((data) => data.json()) 66 | .then((data) => { 67 | setMetricData(data); 68 | }) 69 | .catch((err) => console.log(err)); 70 | } 71 | 72 | // send delete request to backend 73 | async function deleteMetric() { 74 | try { 75 | const response = await fetch(`/api/user/metrics/${metricId}`, { 76 | method: 'DELETE', 77 | headers: { 78 | 'Content-Type': 'application/json', 79 | }, 80 | }); 81 | if (!response.ok) { 82 | throw new Error('failed to delete metric from user data'); 83 | } else { 84 | setTrashCanClicked(true); 85 | } 86 | } catch (err) { 87 | console.log(err); 88 | } 89 | } 90 | 91 | useEffect( 92 | () => { 93 | //initial fetch request 94 | fetchFromProm(); 95 | // auto refresh section: 96 | let intervalTime: number; 97 | // if the metric is a range metric and has a stepSize property, refresh by the stepSize property 98 | if ( 99 | userData.metrics[metricId].scopeType === ScopeType.Range && 100 | userData.metrics[metricId].queryOptions.hasOwnProperty('stepSize') 101 | ) { 102 | intervalTime = userData.metrics[metricId].queryOptions.stepSize; 103 | } else if ( 104 | userData.metrics[metricId].scopeType === ScopeType.Instant && 105 | userData.metrics[metricId].queryOptions.hasOwnProperty('refresh') 106 | ) { 107 | // if it is not a range metric and has a refresh property, refresh by the refresh property 108 | intervalTime = userData.metrics[metricId].queryOptions.refresh; 109 | } else { 110 | intervalTime = 300; 111 | } 112 | 113 | // set interval to update data based on intervalTime in ms 114 | const interval: NodeJS.Timer = setInterval( 115 | fetchFromProm, 116 | intervalTime * 1000 117 | ); 118 | // clear interval so it only runs the setInterval when component is mounted 119 | return () => clearInterval(interval); 120 | }, 121 | [] 122 | ); 123 | 124 | //modal handler functions 125 | const openModal = () => setOpen(true); 126 | const closeModal = () => setOpen(false); 127 | 128 | return ( 129 |
130 |
135 |

136 | {userData.metrics[metricId].metricName}{' '} 137 | {editMode && !trashCanClicked && ( 138 | 145 | )} 146 |

147 | {metricData.hasOwnProperty('labels') && ( 148 | 149 | )} 150 |
151 | 152 |

153 | {userData.metrics[metricId].metricName} 154 |

155 | {metricData.hasOwnProperty('labels') && ( 156 | 163 | )} 164 |
165 |
166 |
167 |
168 | ); 169 | }; 170 | 171 | export default MetricDisplay; 172 | -------------------------------------------------------------------------------- /client/Components/Dashboard/MetricDisplayPreview.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Modal } from 'react-responsive-modal'; 3 | import { 4 | Chart as ChartJS, 5 | CategoryScale, 6 | LinearScale, 7 | PointElement, 8 | LineElement, 9 | Title, 10 | Tooltip, 11 | Legend, 12 | Colors, 13 | } from 'chart.js'; 14 | import { Line, Chart } from 'react-chartjs-2'; 15 | import { useRouteLoaderData } from 'react-router-dom'; 16 | ChartJS.register( 17 | CategoryScale, 18 | LinearScale, 19 | PointElement, 20 | LineElement, 21 | Colors, 22 | Title, 23 | Tooltip, 24 | Legend 25 | ); 26 | 27 | // 28 | // Largely a copy of Metric Display, but takes fetched data instead of metricId 29 | // 30 | 31 | const MetricDisplay = ({ lookupType, metricData }) => { 32 | //state to handle modal and handling fetched data router 33 | const [open, setOpen]: any = useState(false); 34 | 35 | // display options for metrics 36 | const options: any = { 37 | plugins: { 38 | legend: { 39 | display: false, 40 | }, 41 | }, 42 | interaction: { 43 | intersect: false, 44 | mode: 'nearest', 45 | }, 46 | }; 47 | 48 | // Most axes should always start at 0, but some may not -- uncomment next line and add items that may not need 0-basis for plotting 49 | // if (![].includes(lookupType)) 50 | Object.assign(options, { scales: { y: { beginAtZero: true } } }); 51 | // Some axes should go up to ~100 52 | if ([4, 7].includes(lookupType)) 53 | Object.assign(options, { 54 | scales: { y: { suggestedMax: 100, suggestedMin: 0 } }, 55 | }); 56 | 57 | //modal handler functions 58 | const openModal = () => setOpen(true); 59 | const closeModal = () => setOpen(false); 60 | 61 | return ( 62 |
63 | 64 |
65 | {/* {metricId && } */} 66 | 67 |

Query Preview

68 | 79 |
80 |
81 |
82 | ); 83 | }; 84 | 85 | export default MetricDisplay; 86 | -------------------------------------------------------------------------------- /client/Components/HPA Dashboard/DoubleLineGraph.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { UserData } from '../../../types'; 3 | import { Modal } from 'react-responsive-modal'; 4 | import { 5 | Chart as ChartJS, 6 | CategoryScale, 7 | LinearScale, 8 | PointElement, 9 | LineElement, 10 | Title, 11 | Tooltip, 12 | Legend, 13 | Colors, 14 | } from 'chart.js'; 15 | import { Line, Chart } from 'react-chartjs-2'; 16 | import { useRouteLoaderData } from 'react-router-dom'; 17 | ChartJS.register( 18 | CategoryScale, 19 | LinearScale, 20 | PointElement, 21 | LineElement, 22 | Colors, 23 | Title, 24 | Tooltip, 25 | Legend, 26 | ); 27 | import IndivDLG from './IndivDLG'; 28 | 29 | const DoubleLineGraph = ({ metricIds }) => { 30 | const userData = useRouteLoaderData('home') as UserData; 31 | const queriesById = { 32 | 'HTTP Requests Total': metricIds[0], 33 | 'Number of Pods': metricIds[1], 34 | }; 35 | 36 | //state to handle modal and handling fetched data router 37 | const [metricData, setMetricData] = useState({}); 38 | const [fetchCount, setFetchCount] = useState(0); 39 | const [graphData, setGraphData] = useState(undefined); 40 | 41 | interface graphShape { 42 | labels: any[]; 43 | datasets: any[]; 44 | pointStyle: boolean; 45 | } 46 | 47 | const shapeData = () => { 48 | // check if all metrics have been successfully fetched 49 | // if not, do not display graph 50 | if ( 51 | Array.isArray(metricData[queriesById['HTTP Requests Total']]) || 52 | Array.isArray(metricData[queriesById['Number of Pods']]) 53 | ) { 54 | return; 55 | } 56 | const cacheByHPA = {}; 57 | // transform the data into the shape required for Chartjs multi axis line chart 58 | const finalShape: graphShape | any = { 59 | labels: metricData[queriesById['HTTP Requests Total']].labels, 60 | datasets: [], 61 | }; 62 | console.log('metricData', metricData); 63 | // filter data to display a graph for each deployed hpa 64 | metricData[queriesById['Number of Pods']].datasets.forEach(obj => { 65 | cacheByHPA[ 66 | obj.label.slice(obj.label.indexOf(`"`) + 1, obj.label.indexOf(`-`)) 67 | ] = JSON.parse(JSON.stringify(finalShape)); 68 | 69 | cacheByHPA[ 70 | obj.label.slice(obj.label.indexOf(`"`) + 1, obj.label.indexOf(`-`)) 71 | ].datasets.push({ 72 | label: obj.label, 73 | data: obj.data, 74 | pointStyle: false, 75 | borderColor: 'rgb(53, 162, 235)', 76 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 77 | yAxisID: 'y1', 78 | }); 79 | }); 80 | 81 | // assign HTTP Requests Total, organized by endpoints, to the graph 82 | metricData[queriesById['HTTP Requests Total']].datasets.forEach(obj => { 83 | let r = Math.random() * 100 + 127; 84 | if ( 85 | cacheByHPA.hasOwnProperty(obj.label.slice(9, obj.label.indexOf(`-`))) 86 | ) { 87 | cacheByHPA[obj.label.slice(9, obj.label.indexOf(`-`))].datasets.push({ 88 | label: obj.label, 89 | data: obj.data, 90 | pointStyle: false, 91 | borderColor: `rgb(${r}, 99, 132)`, 92 | backgroundColor: `rgba(${r}, 99, 132, 0.5)`, 93 | yAxisID: 'y', 94 | }); 95 | } 96 | }); 97 | // only display graphs that have metrics for both Pod Count and HTTP Requests 98 | Object.keys(cacheByHPA).forEach(key => { 99 | if (cacheByHPA[key].datasets.length < 2) { 100 | delete cacheByHPA[key]; 101 | } 102 | }); 103 | setGraphData(cacheByHPA); 104 | }; 105 | 106 | // fetch total number of pods and total HTTP request metrics 107 | const getPodsAndRequests = async () => { 108 | const currMetricCache: any = metricData; 109 | let count = 0; 110 | metricIds.forEach(id => { 111 | fetch(`/api/data/metrics/${id}`, { 112 | method: 'GET', 113 | }) 114 | .then(data => data.json()) 115 | .then(data => { 116 | currMetricCache[id] = data; 117 | setMetricData(currMetricCache); 118 | count += 1; 119 | setFetchCount(count); 120 | }) 121 | .catch(err => console.log('Error fetching from Prometheus: ', err)); 122 | }); 123 | }; 124 | 125 | //fetching data from Prometheus 126 | useEffect(() => { 127 | getPodsAndRequests(); 128 | }, []); 129 | 130 | // create double line graph once both metrics have been retrieved 131 | useEffect(() => { 132 | if (fetchCount === 2) { 133 | shapeData(); 134 | } 135 | }, [fetchCount]); 136 | 137 | return ( 138 |
139 | {/* display HTTP Requests vs Pod Count if graphData is defined */} 140 | {graphData ? ( 141 | Object.keys(JSON.parse(JSON.stringify(graphData))).map(hpa => { 142 | let title = hpa.slice(0, 1).toUpperCase() + hpa.slice(1); 143 | return ; 144 | }) 145 | ) : ( 146 |
147 |

Error displaying graph(s):

148 |

149 | Metrics can not be retrieved for either the Total HTTP Requests or 150 | Total Number of Pods. 151 |

152 |

153 | Please ensure your metric endpoints are exposed and your Ingress is 154 | set up. 155 |

156 |
157 | )} 158 |
159 | ); 160 | }; 161 | 162 | export default DoubleLineGraph; 163 | -------------------------------------------------------------------------------- /client/Components/HPA Dashboard/HPADashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { UserData } from '../../../types'; 3 | import { Modal } from 'react-responsive-modal'; 4 | import { 5 | Chart as ChartJS, 6 | CategoryScale, 7 | LinearScale, 8 | PointElement, 9 | LineElement, 10 | Title, 11 | Tooltip, 12 | Legend, 13 | Colors, 14 | } from 'chart.js'; 15 | import { Line, Chart } from 'react-chartjs-2'; 16 | import { useRouteLoaderData } from 'react-router-dom'; 17 | import TableDisplay from './TableDisplay'; 18 | import DoubleLineGraph from './DoubleLineGraph'; 19 | import AlertBar from '../AlertBar'; 20 | ChartJS.register( 21 | CategoryScale, 22 | LinearScale, 23 | PointElement, 24 | LineElement, 25 | Colors, 26 | Title, 27 | Tooltip, 28 | Legend, 29 | ); 30 | 31 | const HPADisplay = () => { 32 | const [tableData, setTableData]: any = useState(new Map()); 33 | const [fetchCount, setFetchCount] = useState(0); 34 | 35 | const userData = useRouteLoaderData('home') as UserData; 36 | const metricIds: string[] = userData.dashboards[1].metrics; 37 | 38 | // split metrics into their appropriate grouping components 39 | const table: string[] = []; 40 | const log = metricIds[metricIds.length - 1]; 41 | const doubleLineGraph: string[] = []; 42 | metricIds.slice(0, metricIds.length - 1).forEach(id => { 43 | userData.metrics[id].graphType === 0 44 | ? table.push(id) 45 | : doubleLineGraph.push(id); 46 | }); 47 | 48 | // fetch hpa table specific metrics to display 49 | const getTableData = async () => { 50 | // perserve the order of the metric results after fetching 51 | const tableOrder = tableData; 52 | let count = 0; 53 | table.forEach(async id => { 54 | tableOrder.set(id, null); 55 | 56 | fetch(`/api/data/metrics/${id}`, { 57 | method: 'GET', 58 | headers: { 'Content-Type': 'application/json' }, 59 | }) 60 | .then((data: Response): any => { 61 | return data.json(); 62 | }) 63 | .then(data => { 64 | // if (data[0] === 'No metrics meet the scope of the query') { 65 | // return console.log('No metrics meet the scope of the query'); 66 | // } 67 | tableOrder.set(id, data); 68 | setTableData(tableOrder); 69 | count += 1; 70 | setFetchCount(count); 71 | }) 72 | .catch(err => 73 | console.log('Error fetching metrics from Prometheus:', err), 74 | ); 75 | }); 76 | }; 77 | 78 | // fetch hpa table data on mount 79 | useEffect(() => { 80 | getTableData(); 81 | }, []); 82 | 83 | return ( 84 |
85 | 86 |

HPA Monitoring & Testing Suite

87 | {fetchCount === 7 && ( 88 | 93 | )} 94 | 95 |
96 | ); 97 | }; 98 | 99 | export default HPADisplay; 100 | -------------------------------------------------------------------------------- /client/Components/HPA Dashboard/IndivDLG.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { UserData } from '../../../types'; 3 | import { Modal } from 'react-responsive-modal'; 4 | import { 5 | Chart as ChartJS, 6 | CategoryScale, 7 | LinearScale, 8 | PointElement, 9 | LineElement, 10 | Title, 11 | Tooltip, 12 | Legend, 13 | Colors, 14 | } from 'chart.js'; 15 | import { Line, Chart } from 'react-chartjs-2'; 16 | import { useRouteLoaderData } from 'react-router-dom'; 17 | ChartJS.register( 18 | CategoryScale, 19 | LinearScale, 20 | PointElement, 21 | LineElement, 22 | Colors, 23 | Title, 24 | Tooltip, 25 | Legend, 26 | ); 27 | 28 | const IndivDLG = ({ graphData, graphTitle }) => { 29 | const [open, setOpen]: any = useState(false); 30 | // customize Chatjs graph options 31 | const options: any = { 32 | responsive: true, 33 | interaction: { 34 | mode: 'nearest', 35 | intersect: false, 36 | }, 37 | stacked: false, 38 | plugins: { 39 | legend: { 40 | display: false, 41 | }, 42 | title: { 43 | display: true, 44 | text: 'HTTP Request vs Pod Count', 45 | }, 46 | }, 47 | scales: { 48 | y: { 49 | type: 'linear' as const, 50 | display: true, 51 | position: 'left' as const, 52 | }, 53 | y1: { 54 | type: 'linear' as const, 55 | display: true, 56 | position: 'right' as const, 57 | grid: { 58 | drawOnChartArea: false, 59 | }, 60 | }, 61 | }, 62 | }; 63 | const optionsWithLegend = JSON.parse(JSON.stringify(options)); 64 | optionsWithLegend.plugins.legend.display = true; 65 | 66 | //modal handler functions 67 | const openModal = () => setOpen(true); 68 | const closeModal = () => setOpen(false); 69 | useEffect(() => { 70 | graphData.datasets = graphData.datasets.map(el => { 71 | el.pointStyle = false; 72 | return el; 73 | }); 74 | }, []); 75 | 76 | return ( 77 |
78 |

{graphTitle}

79 | {graphData.hasOwnProperty('labels') && ( 80 | 81 | )} 82 |
83 | 84 | 85 |

{graphTitle}

86 | {graphData.hasOwnProperty('labels') && ( 87 | 88 | )} 89 |
90 |
91 |
92 | ); 93 | }; 94 | 95 | export default IndivDLG; 96 | -------------------------------------------------------------------------------- /client/Components/HPA Dashboard/TableDisplay.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | ReactComponentElement, 3 | ReactNode, 4 | useEffect, 5 | useState, 6 | } from 'react'; 7 | import { UserData } from '../../../types'; 8 | import Box from '@mui/material/Box'; 9 | import Collapse from '@mui/material/Collapse'; 10 | import IconButton from '@mui/material/IconButton'; 11 | import Table from '@mui/material/Table'; 12 | import TableBody from '@mui/material/TableBody'; 13 | import TableCell from '@mui/material/TableCell'; 14 | import TableContainer from '@mui/material/TableContainer'; 15 | import TableHead from '@mui/material/TableHead'; 16 | import TableRow from '@mui/material/TableRow'; 17 | import Typography from '@mui/material/Typography'; 18 | import Paper from '@mui/material/Paper'; 19 | import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; 20 | import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; 21 | import { promResResultElements } from '../../../types'; 22 | import Row from './TableRow'; 23 | import { cleanTime } from '../../context/functions'; 24 | 25 | interface RowsObj { 26 | [key: string]: any[]; 27 | } 28 | 29 | const TableDisplay = ({ tableData, logId, graphIds }) => { 30 | const [filteredTableData, setfilteredTableData]: any[] = useState([]); 31 | 32 | const getHPAUtilization = async () => { 33 | fetch(`/api/data/metrics/${logId}`, { 34 | method: 'POST', 35 | headers: { 'Content-Type': 'application/json' }, 36 | body: JSON.stringify({ 37 | displayType: 'log', 38 | }), 39 | }) 40 | .then(data => data.json()) 41 | .then(data => { 42 | filterHPAUtilization(data); 43 | }) 44 | .catch(err => console.log('Error retrieving HPA Utilization data', err)); 45 | }; 46 | 47 | const filterHPAUtilization = (data: any[]) => { 48 | // initialize cache to store an array of metric values by their associated hpa 49 | const cache = {}; 50 | // if not metrics met the scope of the query 51 | if (data[0] === 'No metrics meet the scope of the query') { 52 | return filterTableData(cache); 53 | } else { 54 | data.forEach(metricObj => { 55 | cache[metricObj.metric.horizontalpodautoscaler] = []; 56 | if (!Array.isArray(metricObj.values[0][0])) { 57 | metricObj.values.forEach(arr => { 58 | cache[metricObj.metric.horizontalpodautoscaler].push([ 59 | cleanTime(arr[0]), 60 | arr[1], 61 | ]); 62 | }); 63 | } 64 | // if the values array from prometheus is >99, then it will return nested arrays of the values array (each holds the metrics for every 99 set of values) 65 | // in this case, only display the most recent 99 metrics 66 | else { 67 | metricObj.values[0].forEach(arr => { 68 | cache[metricObj.metric.horizontalpodautoscaler].push([ 69 | arr[0], 70 | arr[1], 71 | ]); 72 | }); 73 | } 74 | }); 75 | return filterTableData(cache); 76 | } 77 | }; 78 | 79 | const rows: RowsObj = {}; 80 | const filterTableData = cache => { 81 | // initialize object to store hpa's as keys and all associated metrics as values to eliminate potential errors due to lack of order preservation (individual scraped metrics may not be in order by hpa) and obtain constant lookup time 82 | const tableIterator = tableData.values(); 83 | 84 | // initialize a key for each active hpa, representing a row in the table 85 | tableIterator.next().value.forEach(metricObj => { 86 | rows[metricObj.metric.horizontalpodautoscaler] = []; 87 | }); 88 | // filter metrics into specific hpa row and associated table columns 89 | let column = 1; 90 | while (column < 7) { 91 | tableIterator.next().value.forEach(metricObj => { 92 | rows[metricObj.metric.horizontalpodautoscaler].push(metricObj.value[1]); 93 | }); 94 | column += 1; 95 | } 96 | setfilteredTableData( 97 | Object.keys(rows).map(hpa => { 98 | return ( 99 | 108 | ); 109 | }), 110 | ); 111 | }; 112 | 113 | useEffect(() => { 114 | getHPAUtilization(); 115 | }, []); 116 | 117 | return ( 118 |
119 | 120 | 121 | 122 | 123 | 124 | HPA 125 | Target Status % 126 | Target Spec % 127 | Min Pods 128 | Max Pods 129 | Current Replicas 130 | Desired Replicas 131 | 132 | 133 | {filteredTableData} 134 |
135 |
136 |
137 | ); 138 | }; 139 | 140 | export default TableDisplay; 141 | -------------------------------------------------------------------------------- /client/Components/HPA Dashboard/TableRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { UserData } from '../../../types'; 3 | import Box from '@mui/material/Box'; 4 | import Collapse from '@mui/material/Collapse'; 5 | import IconButton from '@mui/material/IconButton'; 6 | import Table from '@mui/material/Table'; 7 | import TableBody from '@mui/material/TableBody'; 8 | import TableCell from '@mui/material/TableCell'; 9 | import TableContainer from '@mui/material/TableContainer'; 10 | import TableHead from '@mui/material/TableHead'; 11 | import TableRow from '@mui/material/TableRow'; 12 | import Typography from '@mui/material/Typography'; 13 | import Paper from '@mui/material/Paper'; 14 | import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; 15 | import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; 16 | 17 | const Row = (props: { hpa: string; row: (string | number)[]; log: any }) => { 18 | const { hpa, row, log } = props; 19 | const [open, setOpen] = React.useState(false); 20 | 21 | const filterLogHPA = () => { 22 | if (typeof log === 'string') { 23 | return ( 24 | 25 | 26 | {log} 27 | 28 | 29 | ); 30 | } else { 31 | const removeDuplicateTimestamps: any[] = []; 32 | const finalLog: any[][] = []; 33 | log.forEach(arr => { 34 | if (!removeDuplicateTimestamps.includes(arr[0])) { 35 | removeDuplicateTimestamps.push(arr[0]); 36 | finalLog.push(arr); 37 | } 38 | }); 39 | return finalLog.map(arr => ( 40 | 41 | 42 | {arr[0]} 43 | 44 | {arr[1]} 45 | 46 | )); 47 | } 48 | }; 49 | 50 | return ( 51 | <> 52 | *': { borderBottom: 'unset' } }}> 53 | 54 | setOpen(!open)}> 58 | {open ? : } 59 | 60 | 61 | {hpa} 62 | {row[0]} 63 | {row[1]} 64 | {row[2]} 65 | {row[3]} 66 | {row[4]} 67 | {row[5]} 68 | 69 | 70 | 71 | 72 | 73 | 74 | {`HPA Utlization >= 80%`} 75 | 76 | 77 | 78 | 79 | Timestamp 80 | Utilization % 81 | 82 | 83 | {filterLogHPA()} 84 |
85 |
86 |
87 |
88 |
89 | 90 | ); 91 | }; 92 | 93 | export default Row; 94 | -------------------------------------------------------------------------------- /client/Components/Visualizer/AlertFlag.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const AlertFlag = () => { 4 | return ( 5 |
6 | alert-icon 11 |
12 | ); 13 | }; 14 | 15 | export default AlertFlag; 16 | -------------------------------------------------------------------------------- /client/Components/Visualizer/ControlPlane.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from 'react-responsive-modal'; 2 | import React, { useState } from 'react'; 3 | 4 | const ControlPlane = () => { 5 | const [open, setOpen] = useState(false); 6 | const openModal = () => setOpen(true); 7 | const closeModal = () => setOpen(false); 8 | return ( 9 |
10 | 16 | 17 |
18 | 19 |
20 |

21 | Your Current Cluster is hosted on Amazon Web Services' Elastic 22 | Kubernetes Service (EKS) 23 |

24 |
25 |
26 |
27 |
28 | ); 29 | }; 30 | 31 | export default ControlPlane; 32 | -------------------------------------------------------------------------------- /client/Components/Visualizer/Deployments.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from 'react-responsive-modal'; 2 | import React, { useState } from 'react'; 3 | import { cleanName } from '../../context/functions'; 4 | 5 | const Deployments = ({ 6 | name, 7 | replicas, 8 | creationTimestamp, 9 | labels, 10 | namespace, 11 | id, 12 | }) => { 13 | const [open, setOpen] = useState(false); 14 | 15 | //modal handler functions 16 | const openModal = () => setOpen(true); 17 | const closeModal = () => setOpen(false); 18 | 19 | let deploymentLabels: any = []; 20 | //create an array of 'labels' keys to be mapped over in the return statement 21 | if (labels) { 22 | deploymentLabels = Object.keys(labels); 23 | } 24 | return ( 25 |
26 |
27 | 33 |
{cleanName(name)}
34 |
35 | {labels && ( 36 |
37 | 38 |
39 |

Deployment Information:

40 |
41 |

Deployment Name:

42 |

{name}

43 |
44 |
45 |

Creation Timestamp:

46 |

{creationTimestamp}

47 |
48 |
49 |

Replicas:

50 |

{replicas}

51 |
52 |
53 |

Namespace

54 |

{namespace}

55 |
56 | {deploymentLabels.map(label => { 57 | if ( 58 | label === 'app' || 59 | label === 'k8s-app' || 60 | label === 'app.kubernetes.io/name' 61 | ) { 62 | return ( 63 |
64 |

App:

65 |

66 | {labels['app'] || 67 | labels['k8s-app'] || 68 | labels['app.kubernetes.io/name']} 69 |

70 |
71 | ); 72 | } 73 | })} 74 | {deploymentLabels.map(label => { 75 | { 76 | if (label === 'app.kubernetes.io/managed-by') { 77 | return ( 78 |
79 |

Managed By:

80 |

{labels['app.kubernetes.io/managed-by']}

81 |
82 | ); 83 | } 84 | } 85 | })} 86 | {deploymentLabels.map(label => { 87 | if (label === 'chart' || label === 'helm.sh/chart') { 88 | return ( 89 |
90 |

Chart:

91 |

{labels.chart || labels['helm.sh/chart']}

92 |
93 | ); 94 | } 95 | })} 96 |
97 |
98 |
99 | )} 100 |
101 | ); 102 | }; 103 | 104 | export default Deployments; 105 | -------------------------------------------------------------------------------- /client/Components/Visualizer/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef, useEffect, useContext } from 'react'; 2 | import { useRouteLoaderData } from 'react-router-dom'; 3 | import { StoreContext } from '../../context/stateStore'; 4 | 5 | export function Dropdown() { 6 | const clusterData: any = useRouteLoaderData('cluster'); 7 | const { selectedStates, setSelectedStates }: any = useContext(StoreContext); 8 | const [isDropdownDisplayed, setIsDropdownDisplayed] = useState(false); 9 | 10 | //set the default 'selected states' to include all nodes and namespaces returned from K8s API so the page initially loads with content 11 | const defaultStates = () => { 12 | let obj = {}; 13 | clusterData.nodes.map((node) => { 14 | obj[node.name] = false; 15 | }); 16 | clusterData.namespaces.map((namespace) => { 17 | obj[namespace.name] = false; 18 | }); 19 | return obj; 20 | }; 21 | 22 | //determine the number of nodes and namespaces that are selected from the dropdown and the total number of nodes and namespaces available. this creates a label for the dropdown bar that shows the proportion of available items are currently viewed 23 | const numSelected = Object.values(selectedStates).filter(Boolean).length; 24 | const numNodes = Object.keys(selectedStates).filter( 25 | (item) => item.charAt(0) === 'i' && selectedStates[item] === true 26 | ).length; 27 | const numNamespaces = Object.keys(selectedStates).filter( 28 | (item) => item.charAt(0) !== 'i' && selectedStates[item] === true 29 | ).length; 30 | const totalNodes = clusterData.nodes.length; 31 | const totalNamespaces = clusterData.namespaces.length; 32 | 33 | //handler function to close the dropdown if clicked outside of the dropdown window 34 | const dropdownRef = useRef(null); 35 | const onClick = (e) => { 36 | if (e.target !== dropdownRef.current) { 37 | setIsDropdownDisplayed(false); 38 | } 39 | }; 40 | 41 | //upon component mounting, set the default selected states to the recieved API data, add an event listener to handle closing the dropdown with clicks outside of reference. remove event listener when component unmounts 42 | useEffect(() => { 43 | const base = defaultStates(); 44 | setSelectedStates(base); 45 | document.addEventListener('click', onClick); 46 | 47 | return () => { 48 | document.removeEventListener('click', onClick); 49 | }; 50 | }, []); 51 | 52 | return ( 53 |
54 | 65 | {isDropdownDisplayed && ( 66 |
e.stopPropagation()} 70 | > 71 |

Nodes:

72 | {clusterData.nodes.map((node) => { 73 | return ( 74 |
75 |
78 | 80 | setSelectedStates({ 81 | ...selectedStates, 82 | [node.name]: e.target.checked, 83 | }) 84 | } 85 | checked={selectedStates[node.name]} 86 | type='checkbox' 87 | /> 88 | 89 |
90 |
91 | ); 92 | })} 93 |

Namespaces:

94 | {clusterData.namespaces.map((namespace) => { 95 | return ( 96 |
97 |
100 | 102 | setSelectedStates({ 103 | ...selectedStates, 104 | [namespace.name]: e.target.checked, 105 | }) 106 | } 107 | checked={selectedStates[namespace.name]} 108 | type='checkbox' 109 | /> 110 | 111 |
112 |
113 | ); 114 | })} 115 |
116 | )} 117 |
118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /client/Components/Visualizer/Namespaces.tsx: -------------------------------------------------------------------------------- 1 | import Deployments from './Deployments'; 2 | import Pods from './Pods'; 3 | import Services from './Services'; 4 | import Modal from 'react-responsive-modal'; 5 | import { useRouteLoaderData } from 'react-router-dom'; 6 | import React, { useContext, useState, useEffect } from 'react'; 7 | import { StoreContext } from '../../context/stateStore'; 8 | import { cleanTime, handleAlerts } from '../../context/functions'; 9 | import { Pod, Service, Deployment, CleanAlert } from '../../../types'; 10 | import AlertFlag from './AlertFlag'; 11 | 12 | const Namespaces = ({ id, name, creationTimestamp, phase, nodeName }) => { 13 | const clusterData: any = useRouteLoaderData('cluster'); 14 | const { selectedStates, displayedAlerts }: any = useContext(StoreContext); 15 | const [namespaceAlerts, setNamespaceAlerts]: any = useState([]); 16 | const pods: Pod[] = clusterData.pods; 17 | const services: Service[] = clusterData.services; 18 | const deployments: Deployment[] = clusterData.deployments; 19 | const [open, setOpen] = useState(false); 20 | const openModal = () => setOpen(true); 21 | const closeModal = () => setOpen(false); 22 | 23 | //clean up alert data for relevant information and assign relevant alerts to namespace state 24 | useEffect(() => { 25 | const alerts: any = displayedAlerts; 26 | alerts.forEach((alert: any) => { 27 | if (alert['affectedNamespace'] && !namespaceAlerts[alert]) { 28 | setNamespaceAlerts([alert, ...namespaceAlerts]); 29 | } 30 | }); 31 | }, []); 32 | 33 | //determine the number of namespaces selected in the dropdown menu by filtering the selectedStates stateful array 34 | const numNamespaces = Object.keys(selectedStates).filter( 35 | (item) => item.charAt(0) !== 'i' && selectedStates[item] === true 36 | ).length; 37 | 38 | //if the number of namespaces selected is zero (default, every namespace is displayed, or if the current namespace name is selected, render the namespace and its child components, or else render null) 39 | //if the number of namespaces selected is zero (default, every namespace is displayed, or if the current namespace name is selected, render the namespace and its child components, or else render null) 40 | return numNamespaces === 0 || selectedStates[name] ? ( 41 |
42 | {namespaceAlerts.length > 0 && 43 | namespaceAlerts.map((alert) => { 44 | if (alert['affectedNamespace'] === name) { 45 | return ; 46 | } 47 | })} 48 | 54 |
55 |
56 |

{`${name[0].toUpperCase().concat(name.slice(1))}`}

57 |

60 | Status: {phase} 61 |

62 |

{'Created: ' + cleanTime(creationTimestamp)}

63 |
64 |
65 | 66 |

Namespace Information:

67 |
68 | {namespaceAlerts.length > 0 && 69 | namespaceAlerts.map((alert) => { 70 | if (alert['affectedNamespace'] === name) { 71 | return ( 72 |
77 |

Alert Information:

78 |
79 |

Alert Name:

80 |

{alert.name}

81 |
82 |
83 |

Description:

84 |

{alert.description}

85 |
86 |
87 |

Summary:

88 |

{alert.summary}

89 |
90 |
91 |

Severity:

92 |

{alert.severity}

93 |
94 |
95 |

Start Time:

96 |

{alert.startTime}

97 |
98 |
99 |

Last Updated:

100 |

{alert.lastUpdated}

101 |
102 |
103 | ); 104 | } 105 | })} 106 |
107 |
108 |
109 |
110 |
111 | {clusterData && 112 | deployments.map((deployment) => 113 | deployment.namespace === name ? ( 114 | 123 | ) : null 124 | )} 125 |
126 |
127 | {pods.map((pod) => { 128 | if (pod.namespace === name && pod.nodeName === nodeName) 129 | return ( 130 | 145 | ); 146 | })} 147 |
148 |
149 | {clusterData && 150 | services.map((service) => 151 | service.namespace === name ? ( 152 | 162 | ) : null 163 | )} 164 |
165 |
166 |
167 |
168 | ) : null; 169 | }; 170 | 171 | export default Namespaces; 172 | -------------------------------------------------------------------------------- /client/Components/Visualizer/Nodes.tsx: -------------------------------------------------------------------------------- 1 | import Namespaces from './Namespaces'; 2 | import { useRouteLoaderData } from 'react-router-dom'; 3 | import { Modal } from 'react-responsive-modal'; 4 | import React, { useContext, useState, useEffect } from 'react'; 5 | import { StoreContext } from '../../context/stateStore'; 6 | import MetricDisplayPreview from '../Dashboard/MetricDisplayPreview'; 7 | import { LookupType } from '../../../types'; 8 | 9 | const Nodes = ({ 10 | name, 11 | creationTimestamp, 12 | labels, 13 | id, 14 | providerID, 15 | status, 16 | nodeName, 17 | }) => { 18 | const clusterData: any = useRouteLoaderData('cluster'); 19 | const namespaces: any = clusterData.namespaces; 20 | const [open, setOpen]: any = useState(false); 21 | const [metricData1, setMetricData1]: any = useState({}); 22 | const [metricData2, setMetricData2]: any = useState({}); 23 | const { selectedStates, displayedAlerts }: any = useContext(StoreContext); 24 | 25 | //modal handler functions 26 | const openModal = () => { 27 | const nodeIp: string = name.slice(3).split('.')[0].replaceAll('-', '.'); 28 | const nodeMetric1 = { 29 | name: '', 30 | lookupType: 4, 31 | duration: 259200, 32 | stepSize: 3600, 33 | scopeType: 0, 34 | context: 'Node', 35 | contextChoice: nodeIp, 36 | }; 37 | const nodeMetric2 = { 38 | name: '', 39 | lookupType: 7, 40 | duration: 259200, 41 | stepSize: 3600, 42 | scopeType: 0, 43 | context: 'Node', 44 | contextChoice: nodeIp, 45 | }; 46 | try { 47 | fetch('/api/data/metric', { 48 | method: 'POST', 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | body: JSON.stringify(nodeMetric1), 53 | }) 54 | .then((res) => res.json()) 55 | .then((res) => { 56 | // Should verify query validity as part of this process 57 | setMetricData1(res.metricData); 58 | }); 59 | } catch (err) { 60 | console.log('Error receiving metric preview: ', err); 61 | } 62 | try { 63 | fetch('/api/data/metric', { 64 | method: 'POST', 65 | headers: { 66 | 'Content-Type': 'application/json', 67 | }, 68 | body: JSON.stringify(nodeMetric2), 69 | }) 70 | .then((res) => res.json()) 71 | .then((res) => { 72 | // Should verify query validity as part of this process 73 | setMetricData2(res.metricData); 74 | }); 75 | } catch (err) { 76 | console.log('Error receiving metric preview: ', err); 77 | } 78 | 79 | setOpen(true); 80 | }; 81 | const closeModal = () => setOpen(false); 82 | 83 | //determines how many nodes have been selected in dropdown by mapping over selected states stateful array 84 | const numNodes = Object.keys(selectedStates).filter( 85 | (item) => item.charAt(0) === 'i' && selectedStates[item] === true 86 | ).length; 87 | 88 | //if no nodes are selected (default, all nodes will display) or the node's name is selected, display the node and its child components. else, display null. 89 | return numNodes === 0 || selectedStates[name] ? ( 90 |
91 |
92 | 98 |
99 |
100 |
101 | 102 |

Node Information:

103 |
104 |
105 |

Node Name:

106 |

{name}

107 |
108 |
109 |

Creation Timestamp:

110 |

{creationTimestamp}

111 |
112 |
113 |

Provider & Region:

114 |

{providerID}

115 |

{labels['topology.kubernetes.io/zone']}

116 |
117 |
118 |

Instance Type:

119 |

{labels['node.kubernetes.io/instance-type']}

120 |
121 |
122 |

Node Group:

123 |

{labels['alpha.eksctl.io/nodegroup-name']}

124 |
125 |
126 |

Architecture:

127 |

{labels['kubernetes.io/arch']}

128 |
129 |
130 | 131 |

Addresses:

132 | 133 | {clusterData && 134 | status.addresses.map((address) => { 135 | return ( 136 | 139 | ); 140 | })} 141 | 142 | 143 | {clusterData && 144 | status.addresses.map((address) => { 145 | return ( 146 | 149 | ); 150 | })} 151 | 152 |
137 | {address.type} 138 |
147 | {address.address} 148 |
153 |
154 |
155 | 156 |

Capacity:

157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 |
Number of Pods:Total Memory Capacity:Available Memory:Total CPU Capacity:Available CPU:Attachable Volumes:
{status.capacity.pods}{status.capacity.memory}{status.allocatable.memory}{status.capacity.cpu}{status.allocatable.cpu}{status.capacity['attachable-volumes-aws-ebs']}
174 |
175 |
176 | 177 |

Conditions:

178 | 179 | {clusterData && 180 | status.conditions.map((condition) => { 181 | return ; 182 | })} 183 | 184 | 185 | 186 | {clusterData && 187 | status.conditions.map((condition) => { 188 | return ( 189 | 190 | ); 191 | })} 192 | 193 | 194 | {clusterData && 195 | status.conditions.map((condition) => { 196 | return ( 197 | 203 | ); 204 | })} 205 | 206 | 207 | {clusterData && 208 | status.conditions.map((condition) => { 209 | return ( 210 | 216 | ); 217 | })} 218 | 219 |
{condition.type}
{condition.message}
200 | {'Last Heartbeat Time: ' + 201 | condition.lastHeartbeatTime} 202 |
213 | {'Last Transition Time: ' + 214 | condition.lastTransitionTime} 215 |
220 |
221 |
222 |
223 | {metricData1.hasOwnProperty('labels') && ( 224 |
225 |

Available Memory in Node

226 | 230 |
231 | )} 232 |
233 |
234 | {metricData2.hasOwnProperty('labels') && ( 235 |
236 |

Available Disk in Node

237 | 241 |
242 | )} 243 |
244 |
245 |
246 |
247 |
248 |
249 | {clusterData && 250 | namespaces.map((namespace) => ( 251 | 259 | ))} 260 |
261 |
262 |
263 | ) : null; 264 | }; 265 | 266 | export default Nodes; 267 | -------------------------------------------------------------------------------- /client/Components/Visualizer/Pods.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from 'react-responsive-modal'; 2 | import React, { useState, useContext, useEffect } from 'react'; 3 | import { cleanName, handleAlerts } from '../../context/functions'; 4 | import { StoreContext } from '../../context/stateStore'; 5 | import { CleanAlert } from '../../../types'; 6 | import AlertFlag from './AlertFlag'; 7 | 8 | const Pods = ({ 9 | conditions, 10 | containerStatuses, 11 | containers, 12 | creationTimestamp, 13 | labels, 14 | name, 15 | namespace, 16 | nodeName, 17 | phase, 18 | podIP, 19 | serviceAccount, 20 | id, 21 | }) => { 22 | const [open, setOpen]: any = useState(false); 23 | const { selectedStates, displayedAlerts }: any = useContext(StoreContext); 24 | const [podAlerts, setPodAlerts]: any = useState([]); 25 | 26 | //modal handler functions 27 | const openModal = () => setOpen(true); 28 | const closeModal = () => setOpen(false); 29 | 30 | //clean up alert data for relevant information and assign relevant alerts to pod state 31 | useEffect(() => { 32 | const alerts = displayedAlerts; 33 | alerts.forEach((alert: any) => { 34 | if (alert['affectedPod'] && !podAlerts[alert]) { 35 | setPodAlerts([alert, ...podAlerts]); 36 | } 37 | }); 38 | }, []); 39 | 40 | return ( 41 |
42 | {/* {podAlerts.length > 0 && 43 | podAlerts.map(alert => { 44 | if (alert['affectedPod'] === name) { 45 | return ; 46 | } 47 | })} */} 48 | 54 |
{cleanName(name)}
55 | 56 |
57 | 58 |

Pod Information:

59 |
60 | {podAlerts.length > 0 && 61 | podAlerts.map((alert) => { 62 | if (alert['affectedPod'].includes(name)) { 63 | return ( 64 |
69 |

Alert Information:

70 |
71 |

Alert Name:

72 |

{alert.name}

73 |
74 |
75 |

Description:

76 |

{alert.description}

77 |
78 |
79 |

Summary:

80 |

{alert.summary}

81 |
82 |
83 |

Severity:

84 |

{alert.severity}

85 |
86 |
87 |

Start Time:

88 |

{alert.startTime}

89 |
90 |
91 |

Last Updated:

92 |

{alert.lastUpdated}

93 |
94 |
95 | ); 96 | } 97 | })} 98 |
99 |

Pod Name:

100 |

{name}

101 |
102 |
103 |

Pod Status:

104 |

{phase}

105 |
106 |
107 |

Running Application:

108 |

{labels.app}

109 |
110 |
111 |

Namespace:

112 |

{namespace}

113 |
114 |
115 |

Creation Timestamp:

116 |

{creationTimestamp}

117 |
118 |
119 |

Pod IP:

120 |

{podIP}

121 |
122 |
123 |

Service Account:

124 |

{serviceAccount}

125 |
126 |
127 |

Node:

128 |

{nodeName}

129 |
130 |
131 | 132 |

Conditions:

133 | 134 | 135 | {conditions.map((condition) => { 136 | return ( 137 | 140 | ); 141 | })} 142 | 143 | 144 | {conditions.map((condition) => { 145 | return ( 146 | 150 | ); 151 | })} 152 | 153 | 154 | {conditions.map((condition) => { 155 | return ( 156 | 159 | ); 160 | })} 161 | 162 |
138 | {`${condition.type} Status: ${condition.status}`} 139 |
147 | {'Last Transition Time: ' + 148 | condition.lastTransitionTime} 149 |
157 | {'Last Probe Time: ' + condition.lastProbeTime} 158 |
163 |
164 |

Container Information:

165 | {containers.map((container) => { 166 | return ( 167 |
168 |
169 |

Container Name:

170 |

{container.name}

171 |
172 | 173 | {containerStatuses.map((status) => { 174 | if (status.name === container.name) { 175 | return ( 176 |
177 |
178 |

Image:

179 |

{status.image}

180 |
181 |
182 |

Status:

183 |

{Object.keys(status.state)}

184 |
185 |
186 |

Restart Count:

187 |

{status.restartCount}

188 |
189 |
190 | ); 191 | } 192 | })} 193 | 194 |
195 | 196 |

Ports:

197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | {container.ports ? ( 205 | container.ports.map((port) => { 206 | return ( 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | ); 217 | }) 218 | ) : ( 219 | 220 | )} 221 |
Name:Container Port:Host Port:Protocol:
{port.name}{port.containerPort}{port.hostPort}{port.protocol}
{null}
222 |
223 |
224 | 225 |

Volume Mounts:

226 | 227 | 228 | 229 | 230 | {container.volumeMounts.map((element) => { 231 | return ( 232 | 233 | 234 | 235 | 236 | ); 237 | })} 238 |
Name:Path:
{element.name}{element.mountPath}
239 |
240 |
241 | ); 242 | })} 243 |
244 |
245 |
246 |
247 | ); 248 | }; 249 | 250 | export default Pods; 251 | -------------------------------------------------------------------------------- /client/Components/Visualizer/Services.tsx: -------------------------------------------------------------------------------- 1 | import { Modal } from 'react-responsive-modal'; 2 | import React, { useState } from 'react'; 3 | import { cleanName } from '../../context/functions.js'; 4 | const Services = ({ 5 | name, 6 | loadBalancer, 7 | creationTimestamp, 8 | labels, 9 | namespace, 10 | ports, 11 | id, 12 | }) => { 13 | const [open, setOpen]: any = useState(false); 14 | 15 | //modal handler functions 16 | const openModal = () => setOpen(true); 17 | const closeModal = () => setOpen(false); 18 | 19 | return ( 20 |
21 | 27 |
{cleanName(name)}
28 | 29 |
30 | 31 |

Service Information:

32 |
33 |
34 |

Service Name:

35 |

{name}

36 |
37 |
38 |

Creation Timestamp:

39 |

{creationTimestamp}

40 |
41 |
42 | 43 |

Ports:

44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {ports.map((port) => { 52 | return ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | })} 64 |
Name:Port:Target Port:Protocol:
{port.name}{port.port}{port.targetPort}{port.protocol}
65 |
66 |
67 |

Namespace:

68 |

{namespace}

69 |
70 |
71 |
72 |
73 |
74 | ); 75 | }; 76 | 77 | export default Services; 78 | -------------------------------------------------------------------------------- /client/Routes/ClusterView.tsx: -------------------------------------------------------------------------------- 1 | import { useRouteLoaderData, Outlet } from 'react-router-dom'; 2 | import { StoreContext } from '../context/stateStore'; 3 | import { useContext } from 'react'; 4 | import { Dropdown } from '../Components/Visualizer/Dropdown'; 5 | import { Node } from '../../types'; 6 | import Nodes from '../Components/Visualizer/Nodes'; 7 | import ControlPlane from '../Components/Visualizer/ControlPlane'; 8 | import React from 'react'; 9 | import AlertBar from '../Components/AlertBar'; 10 | 11 | const ClusterView = () => { 12 | const clusterData: any = useRouteLoaderData('cluster'); 13 | const { setClusterData }: any = useContext(StoreContext); 14 | setClusterData(clusterData); 15 | const nodes: Node[] = clusterData.nodes; 16 | 17 | return ( 18 |
19 | 20 |
21 | {clusterData && } 22 | 23 | {clusterData && 24 | nodes.map((node) => ( 25 | 35 | ))} 36 |
37 |
38 | ); 39 | }; 40 | 41 | export default ClusterView; 42 | -------------------------------------------------------------------------------- /client/Routes/Home.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | NavLink, 3 | Outlet, 4 | useRouteLoaderData, 5 | useNavigate, 6 | } from 'react-router-dom'; 7 | import { UserData } from '../../types'; 8 | import React, { useContext, useEffect } from 'react'; 9 | import { StoreContext } from '../context/stateStore'; 10 | 11 | export default function Home() { 12 | const userData = useRouteLoaderData('home') as UserData; 13 | const navigate = useNavigate(); 14 | const { setHasFetchedUserData, setCurrentDashboard, setClusterData }: any = 15 | useContext(StoreContext); 16 | 17 | const dashboards = userData.dashboards; 18 | //set default dashboard and route to that dashboard 19 | useEffect(() => { 20 | setHasFetchedUserData(true); 21 | if (window.location.pathname === '/') return navigate('0'); 22 | }, []); 23 | 24 | // Fetch Cluster Data 25 | useEffect(() => { 26 | fetch('api/k8s/cluster') 27 | .then(data => data.json()) 28 | .then(data => { 29 | setClusterData(data); 30 | }) 31 | .catch(err => console.log(err)); 32 | }, []); 33 | 34 | return ( 35 |
36 |
37 |
38 | logo 43 |

Anago

44 |
45 | 46 | 60 |
61 | 62 |
63 | 64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /client/Routes/SetUp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SetUp = () => { 4 | return ( 5 |
6 |
7 |

Welcome to Anago!

8 |

9 | An extensive tutorial for DevOps teams new to Kubernetes can be found 10 | at our GitHub. If 11 | you have an existing cluster with Prometheus available, this set-up 12 | walks through the swift installation and configuration of Anago. 13 |

14 |
15 |
16 |
17 |

Follow these steps exactly:

18 |

19 | 1. Clone the{' '} 20 | Anago respository{' '} 21 | locally and install required modules: 22 |

23 |

npm install

24 |
25 |
26 |

27 | 2. Modify the{' '} 28 | 29 | user-config.ts 30 | {' '} 31 | file to point to access your Prometheus instance and Alertmanager. 32 | For example, forward these services to the default ports (9090 and 33 | 9093): 34 |

35 |

36 | kubectl port-forward svc/[service-name] -n [namespace] 9090 37 |

38 |

39 | kubectl port-forward svc/[service-name] -n [namespace] 9093 40 |

41 |
42 |
43 |

3. Launch Anago from the command line:

44 |

npm run dev

45 |
46 |
47 |
48 |

49 | Navigate to the local access point for Vite (by default,{' '} 50 | http://locahost:5173), and you should see a default 51 | Anago dashboard populated with a suite of common metrics! 52 |

53 |

54 | {' '} 55 | 56 | All set?{' '} 57 | 58 | Explore your new dashboard and gain insight in your Kubernetes 59 | cluster! 60 | 61 | 62 |

63 |
64 |
65 | ); 66 | }; 67 | 68 | export default SetUp; 69 | -------------------------------------------------------------------------------- /client/__tests__/App.test.tsx: -------------------------------------------------------------------------------- 1 | import App from '../App'; 2 | import Dashboard from '../Components/Dashboard/Dashboard'; 3 | import ClusterView from '../Routes/ClusterView'; 4 | import Home from '../Routes/Home'; 5 | import SetUp from '../Routes/SetUp'; 6 | import { render, screen } from './test-utils'; 7 | import * as loaders from '../context/loaders'; 8 | import { 9 | RouterProvider, 10 | createMemoryRouter, 11 | MemoryRouter, 12 | Route, 13 | createRoutesFromElements, 14 | } from 'react-router-dom'; 15 | import React from 'react'; 16 | import { vi, expect } from 'vitest'; 17 | 18 | const mockRoutes = [ 19 | { 20 | path: '/', 21 | element: , 22 | loader: vi.fn(), 23 | id: 'home', 24 | children: [ 25 | { 26 | path: ':id', 27 | element: , 28 | }, 29 | { 30 | path: 'clusterview', 31 | element: , 32 | loader: vi.fn(), 33 | id: 'cluster', 34 | }, 35 | { 36 | path: 'setup', 37 | element: , 38 | }, 39 | ], 40 | }, 41 | ]; 42 | 43 | describe('App Component Test', () => { 44 | test('renders without crashing', () => { 45 | const { container } = render(); 46 | expect(container).toBeInTheDocument(); 47 | }); 48 | }); 49 | 50 | // describe('App is providing routing capability to child components', async () => { 51 | // const router = createMemoryRouter(mockRoutes, { 52 | // initialEntries: ['/'], 53 | // initialIndex: 0, 54 | // }); 55 | // render(); 56 | // expect(() => screen.queryByText('Kubernetes Dashboard')); 57 | // }); 58 | 59 | // describe('creating routes from elements', () => { 60 | // it('creates a route config of nested Typescript components', () => { 61 | // expect( 62 | // createRoutesFromElements( 63 | // } loader={loaders.userLoader} id='home'> 64 | // } /> 65 | // } 68 | // loader={loaders.clusterLoader} 69 | // id='clusterview' 70 | // /> 71 | // } /> 72 | // , 73 | // ), 74 | // ).toMatchInlineSnapshot(` 75 | // [ 76 | // { 77 | // "Component": Home, 78 | // "ErrorBoundary": undefined, 79 | // "action": undefined, 80 | // "caseSensitive": undefined, 81 | // "children": [ 82 | // { 83 | // "Component": , 84 | // "ErrorBoundary": undefined, 85 | // "action": undefined, 86 | // "caseSensitive": undefined, 87 | // "element": 88 | // "errorElement": undefined, 89 | // "handle": undefined, 90 | // "hasErrorBoundary": false, 91 | // "id": undefined, 92 | // "index": undefined, 93 | // "lazy": undefined, 94 | // "loader": undefined, 95 | // "path": ":id", 96 | // "shouldRevalidate": undefined, 97 | // }, 98 | // { 99 | // "Component": undefined, 100 | // "ErrorBoundary": undefined, 101 | // "action": undefined, 102 | // "caseSensitive": undefined, 103 | // "element": , 104 | // "errorElement": undefined, 105 | // "handle": undefined, 106 | // "hasErrorBoundary": false, 107 | // "id": "home", 108 | // "index": undefined, 109 | // "lazy": undefined, 110 | // "loader": loader.clusterLoader, 111 | // "path": "clusterview", 112 | // "shouldRevalidate": undefined, 113 | // }, 114 | // { 115 | // "Component": undefined, 116 | // "ErrorBoundary": undefined, 117 | // "action": undefined, 118 | // "caseSensitive": undefined, 119 | // "element": , 120 | // "errorElement": undefined, 121 | // "handle": undefined, 122 | // "hasErrorBoundary": false, 123 | // "id": "0-2", 124 | // "index": undefined, 125 | // "lazy": undefined, 126 | // "loader": undefined, 127 | // "path": "/setup", 128 | // "shouldRevalidate": undefined, 129 | // }, 130 | // ], 131 | // "element": 132 | // "errorElement": undefined, 133 | // "handle": undefined, 134 | // "hasErrorBoundary": false, 135 | // "id": home, 136 | // "index": undefined, 137 | // "lazy": undefined, 138 | // "loader": loaders.userLoader 139 | // "path": "/", 140 | // "shouldRevalidate": undefined, 141 | // }, 142 | // ] 143 | // `); 144 | // }); 145 | 146 | 147 | // it('throws when the index route has children', () => { 148 | // expect(() => { 149 | // createRoutesFromChildren( 150 | // 151 | // {/* @ts-expect-error */} 152 | // 153 | // 154 | // 155 | // , 156 | // ); 157 | // }).toThrow('An index route cannot have child routes.'); 158 | // }); 159 | 160 | // it('supports react fragments for automatic ID generation', () => { 161 | // expect( 162 | // createRoutesFromElements( 163 | // } loader={loaders.userLoader}> 164 | // } /> 165 | // } 168 | // loader={loaders.clusterLoader} 169 | // id='clusterview' 170 | // /> 171 | // } /> 172 | // , 173 | // ), 174 | // ).toEqual([ 175 | // { 176 | // path: '/', 177 | // element: , 178 | // hasErrorBoundary: false, 179 | // children: [ 180 | // { 181 | // path: ':id', 182 | // element: , 183 | // hasErrorBoundary: false, 184 | // }, 185 | // { 186 | // id: 'clusterview', 187 | // path: 'clusterview', 188 | // element: , 189 | // hasErrorBoundary: false, 190 | 191 | // }, 192 | // { 193 | // path: 'setup', 194 | // element: , 195 | // hasErrorBoundary: false, 196 | // }, 197 | // ], 198 | // }, 199 | // ]); 200 | // }); 201 | // }); 202 | 203 | // vi.mock('react-router-dom', () => ({ 204 | // createBrowserRouter: vi.fn(), 205 | // createRoutesFromElements: vi.fn(), 206 | // useLoaderData: vi.fn(), 207 | // useRouteLoaderData: vi.fn(), 208 | // useNavigate: vi.fn(), 209 | // })); 210 | 211 | // const mockRouter = { 212 | // navigate: vi.fn(), 213 | // getCurrentRoute: vi.fn(), 214 | // useLoaderData: vi.fn(), 215 | // useRouteLoaderData: vi.fn(), 216 | // }; 217 | 218 | // mockRouter.setup(() => { 219 | // createRoutesFromElements.$mock(() => mockRoutes); 220 | // }); 221 | 222 | // const TestingComponent = () => { 223 | // const { hasFetchedUserData, currentDashboard }: any = 224 | // useContext(StoreContext); 225 | // return ( 226 | //
227 | //

{currentDashboard.toString()}

228 | //

{hasFetchedUserData.toString()}

229 | //
230 | // ); 231 | // }; 232 | // const hasFetched = { 233 | // currentDashboard: 'default', 234 | // }; 235 | // const hasNotFetched = { 236 | // currentDashboard: undefined, 237 | // }; 238 | 239 | // describe('Testing Store Provider Context', () => { 240 | // let providerProps; 241 | // beforeEach(()=>{ 242 | // (providerProps={ 243 | // dashboard: 'default', 244 | // hasFetchedUserData: true, 245 | // }) 246 | // }) 247 | 248 | // test('provides expected State object to child elements', ()=>{ 249 | // render( , {providerProps}); 250 | // expect(screen.queryByTestId('dashboard')).toEqual('default'); 251 | // expect(screen.queryByTestId('hasFetched')).toEqual('true'); 252 | 253 | // }) 254 | // }) 255 | 256 | // describe('', () => { 257 | // test('provides expected States object to child elements', () => { 258 | // [ 259 | // { 260 | // scenario: 'has fetched', 261 | // currentDashboard: hasFetched, 262 | // expectedDashboard: 'default', 263 | // expectedFetch: 'true', 264 | // }, 265 | // { 266 | // scenario: 'has not fetched', 267 | // currentDashboard: hasNotFetched, 268 | // expectedDashboard: undefined, 269 | // expectedFetch: 'false', 270 | // }, 271 | // ].forEach(({ scenario, currentDashboard, expectedDashboard, expectedFetch }) => { 272 | // test(scenario, () => { 273 | // const { getByTestId } = render( 274 | // 275 | // 276 | // , 277 | // ); 278 | // }); 279 | // expect(screen.getByTestId('dashboard')).toEqual(expectedDashboard); 280 | 281 | // expect(screen.queryByTestId('hasFetched')).toEqual(expectedFetch); 282 | 283 | // }); 284 | // }); 285 | // }); 286 | 287 | // // describe('') 288 | -------------------------------------------------------------------------------- /client/__tests__/pages/Home.test.tsx: -------------------------------------------------------------------------------- 1 | // import { getByTestId, render, screen, userEvent } from '../test-utils'; 2 | // import Home from '../../Routes/Home'; 3 | // import { 4 | // BrowserRouter, 5 | // createBrowserRouter, 6 | // MemoryRouter, 7 | // MemoryRouterProps, 8 | // } from 'react-router-dom'; 9 | // import TestRenderer from 'react-test-renderer'; 10 | 11 | 12 | 13 | // describe('Home ', () => { 14 | // test('checks if 5 is 5', () => { 15 | // const num = 5; 16 | // expect(num).toEqual(5); 17 | // }) 18 | // test('checks if home component renders without errors', ()=>{ 19 | // render( 20 | 21 | // 22 | 23 | 24 | // ) 25 | // }) 26 | // test('checks if page redirects to default dashboard upon page load', ()=>{ 27 | // render() 28 | // }) 29 | // }); 30 | 31 | 32 | 33 | // // describe('Home.tsx', () => { 34 | // // it('redirects to default dashboard upon page load', () => { 35 | // // const Home = TestRenderer.create().toJSON(); 36 | // // render(, { wrapper: BrowserRouter }); 37 | // // expect(screen.getByText('Kubernetes Dashboard')).toBeInTheDocument(); 38 | // // }); 39 | // // it('changes the url to default dashboard URL', () => { 40 | // // render(, { wrapper: BrowserRouter }); 41 | // // expect(global.window.location.href).toContain('/0'); 42 | // // }); 43 | // // }); 44 | 45 | 46 | // describe('Home.tsx', ()=>{ 47 | // it('test the children inside Home component', ()=>{ 48 | // const home = TestRenderer.create().toJSON(); 49 | // console.log(home); 50 | 51 | 52 | // }) 53 | // }) -------------------------------------------------------------------------------- /client/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | 7 | 8 | // export const mockRoute = vi.fn(); 9 | 10 | // vi.mock('react-router-dom', async ()=>{ 11 | // const router = await vi.importActual('../../App.tsx'); 12 | 13 | 14 | 15 | // }) -------------------------------------------------------------------------------- /client/__tests__/test-utils.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/export */ 2 | import { cleanup, render } from '@testing-library/react'; 3 | import { afterEach } from 'vitest'; 4 | import { StoreContext } from '../context/stateStore'; 5 | import { ReactElement } from 'react'; 6 | 7 | afterEach(() => { 8 | cleanup(); 9 | }); 10 | 11 | type providerProps = any | undefined; 12 | type renderOptions = {} | undefined; 13 | interface customRenderProps { 14 | ui: ReactElement; 15 | providerProps?: providerProps; 16 | renderOptions?: renderOptions; 17 | } 18 | 19 | const customRender = (ui: ReactElement, providerProps?: providerProps, renderOptions?: renderOptions) => { 20 | return render( 21 | // wrap provider(s) here if needed 22 | {ui}, 23 | 24 | renderOptions, 25 | ); 26 | }; 27 | 28 | export * from '@testing-library/react'; 29 | export { default as userEvent } from '@testing-library/user-event'; 30 | // override render export 31 | export { customRender as render }; 32 | -------------------------------------------------------------------------------- /client/assets/Anago-Members/Rylie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/Anago-Members/Rylie.jpg -------------------------------------------------------------------------------- /client/assets/Anago-Members/Steve.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/Anago-Members/Steve.jpeg -------------------------------------------------------------------------------- /client/assets/Anago-Members/alexandra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/Anago-Members/alexandra.png -------------------------------------------------------------------------------- /client/assets/Anago-Members/halia2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/Anago-Members/halia2.jpg -------------------------------------------------------------------------------- /client/assets/gifs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/gifs/.DS_Store -------------------------------------------------------------------------------- /client/assets/gifs/AddMetric.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/gifs/AddMetric.gif -------------------------------------------------------------------------------- /client/assets/gifs/AlertVid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/gifs/AlertVid.gif -------------------------------------------------------------------------------- /client/assets/gifs/ClusterVid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/gifs/ClusterVid.gif -------------------------------------------------------------------------------- /client/assets/gifs/HPAVid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/gifs/HPAVid.gif -------------------------------------------------------------------------------- /client/assets/gifs/MonitorVid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/gifs/MonitorVid.gif -------------------------------------------------------------------------------- /client/assets/images/AnagoCrashed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/AnagoCrashed.png -------------------------------------------------------------------------------- /client/assets/images/abg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/abg3.png -------------------------------------------------------------------------------- /client/assets/images/alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/alert.png -------------------------------------------------------------------------------- /client/assets/images/anago.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/anago.png -------------------------------------------------------------------------------- /client/assets/images/control-plane.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/control-plane.png -------------------------------------------------------------------------------- /client/assets/images/control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/control.png -------------------------------------------------------------------------------- /client/assets/images/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/deployment.png -------------------------------------------------------------------------------- /client/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/github.png -------------------------------------------------------------------------------- /client/assets/images/github2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/github2.png -------------------------------------------------------------------------------- /client/assets/images/linkedInLogo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/linkedInLogo.webp -------------------------------------------------------------------------------- /client/assets/images/namespace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/namespace.png -------------------------------------------------------------------------------- /client/assets/images/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/node.png -------------------------------------------------------------------------------- /client/assets/images/pod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/pod.png -------------------------------------------------------------------------------- /client/assets/images/service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/service.png -------------------------------------------------------------------------------- /client/assets/images/shipwreck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/shipwreck.png -------------------------------------------------------------------------------- /client/assets/images/trash-can.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/anago/cc8ca8c79f5fd551e1dd4dda517c94c103843277/client/assets/images/trash-can.png -------------------------------------------------------------------------------- /client/assets/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /client/context/functions.ts: -------------------------------------------------------------------------------- 1 | import { CleanAlert } from '../../types'; 2 | 3 | //clean times for improved readability 4 | export function cleanTime(date: string) { 5 | const newDate = new Date(date); 6 | const dateStr = newDate.toDateString().split(' ').slice(1, 3).join(' '); 7 | const time = newDate.toLocaleTimeString(); 8 | return dateStr + ' ' + time; 9 | } 10 | 11 | //clean component names for improved visuals and readability 12 | export function cleanName(name: string) { 13 | let split = name.split('-'); 14 | if (split.length > 3) { 15 | split = split.slice(2); 16 | } 17 | return split.join('-'); 18 | } 19 | 20 | //handle alert data processing for FE 21 | export function handleAlerts(statefulAlerts: []) { 22 | const current = [...statefulAlerts].map((alert) => { 23 | const obj = {}; 24 | obj['name'] = alert['labels']['alertname']; 25 | obj['description'] = alert['annotations']['description']; 26 | obj['summary'] = alert['annotations']['summary']; 27 | obj['severity'] = alert['labels']['severity']; 28 | if (alert['labels']['pod']) obj['affectedPod'] = alert['labels']['pod']; 29 | if (alert['labels']['namespace']) 30 | obj['affectedNamespace'] = alert['labels']['namespace']; 31 | obj['startsAt'] = alert['startsAt']; 32 | obj['startTime'] = cleanTime(alert['startsAt']); 33 | obj['lastUpdated'] = cleanTime(alert['updatedAt']); 34 | return obj; 35 | }); 36 | return current; 37 | } 38 | -------------------------------------------------------------------------------- /client/context/loaders.ts: -------------------------------------------------------------------------------- 1 | //fetch initial userData upon page load and pass to dashboard and metric display components 2 | export const userLoader = async () => { 3 | try { 4 | const res = await fetch('/api/user', { 5 | method: 'GET', 6 | }); 7 | return res.json(); 8 | } catch (err) { 9 | console.error('user loader error: ', err); 10 | } 11 | }; 12 | 13 | //fetch cluster information from kubernetes API and pass to visualizer components 14 | export const clusterLoader = async () => { 15 | try { 16 | const res = await fetch('api/k8s/cluster'); 17 | return res.json(); 18 | } catch (err) { 19 | console.error('cluster loader error: ', err); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /client/context/stateStore.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export const StoreContext = React.createContext({}); 4 | 5 | //creating the type for the children components passed into the state store function 6 | export type Props = { 7 | children: React.ReactNode; 8 | }; 9 | 10 | //declaring the parameter type Props to assign children to a ReactNode type 11 | function StoreProvider({ children }: Props) { 12 | 13 | const [currentMetrics, setCurrentMetrics] = useState([]); 14 | const [currentDashboard, setCurrentDashboard] = useState([]); 15 | const [hasFetchedUserData, setHasFetchedUserData] = useState(false); 16 | //manages selected items in the dropdown 17 | const [selectedStates, setSelectedStates] = useState({}); 18 | //manages currently displayed alerts 19 | const [displayedAlerts, setDisplayedAlerts] = useState([]); 20 | const [clusterData, setClusterData] = useState({}); 21 | 22 | const States = { 23 | currentDashboard: currentDashboard, 24 | setCurrentDashboard: setCurrentDashboard, 25 | hasFetchedUserData: hasFetchedUserData, 26 | setHasFetchedUserData: setHasFetchedUserData, 27 | currentMetrics: currentMetrics, 28 | setCurrentMetrics: setCurrentMetrics, 29 | selectedStates: selectedStates, 30 | setSelectedStates: setSelectedStates, 31 | displayedAlerts: displayedAlerts, 32 | setDisplayedAlerts: setDisplayedAlerts, 33 | clusterData: clusterData, 34 | setClusterData: setClusterData, 35 | }; 36 | 37 | return ( 38 | {children} 39 | ); 40 | }; 41 | export default StoreProvider; -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App'; 4 | import StoreProvider from './context/stateStore'; 5 | import './assets/style.scss'; 6 | 7 | ReactDOM.createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /dev/Grafana/clusterConfig.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: eksctl.io/v1alpha5 2 | kind: ClusterConfig 3 | 4 | metadata: 5 | name: my-cluster 6 | region: eu-west-1 7 | 8 | nodeGroups: 9 | - name: group1 10 | instanceType: 11 | desiredCapacity: 5 12 | volumeSize: 50 13 | ssh: 14 | allow: true 15 | - name: group2 16 | instanceType: 17 | desiredCapacity: 2 18 | volumeSize: 100 19 | ssh: 20 | publicKeyPath: ~/.ssh/other.pub 21 | 22 | 23 | //$ eksctl create cluster -f config.yaml 24 | -------------------------------------------------------------------------------- /dev/Grafana/grafana.ini: -------------------------------------------------------------------------------- 1 | [server] 2 | allow_embedding = true 3 | [security] 4 | allow_embedding = true 5 | [plugins] 6 | plugins = boomtheme-panel 7 | enable_alpha = true 8 | [feature_toggles] 9 | publicDashboards = true -------------------------------------------------------------------------------- /dev/Grafana/values.yaml: -------------------------------------------------------------------------------- 1 | prometheus: 2 | prometheusSpec: 3 | remoteWrite: 4 | - url: '' 5 | basicAuth: 6 | username: 7 | name: kubepromsecret 8 | key: username 9 | password: 10 | name: kubepromsecret 11 | key: password 12 | replicaExternalLabelName: '__replica__' 13 | externalLabels: { cluster: 'tester' } 14 | -------------------------------------------------------------------------------- /dev/alertManagerInfo.md: -------------------------------------------------------------------------------- 1 | # Guide to Using AlertManager: 2 | 3 | ## Contents: 4 | 1. [Introduction](#introduction) 5 | 2. [How Alert Manager Works](#how-alertmanager-works) 6 | 3. [General Setup and Configuration](#General-Setup-and-Configuration) 7 | 4. [Managing Alerts](#Managing-Alerts) 8 | 5. [Conclusion](#conclusion) 9 | 6. [Sources](#sources) 10 | 11 | ## Introduction 12 | The AlertManager is a vital component of the Prometheus ecosystem, serving as the hub for managing alerts sent by Prometheus or other monitoring systems. It equips developers with potential threats to the cluster health, allowing them to proactively evaluate and solve alerts, minimizing downtime while maximizing reliability. 13 | 14 | ## How AlertManager Works: 15 | The AlertManager handles alerts sent by a client application, in our case the Prometheus server. 16 | 17 | Alerting can be separated into two main parts: 18 | 1. **Alerting Rules**: Defined within Prometheus configuration files, these rules instruct the Prometheus server on when to send alerts to an Alertmanager. 19 | 2. **AlertManager Management**: The Alertmanager then manages these recieved alerts, grouping them as defined by the configuration file and determining the method for alerting team members (i.e. Email, or Slack) 20 | 21 | ## General Setup and Configuration: 22 | to optimally utilize AlertManager, the following general steps for setup are involved: 23 | 1. **Configure with Command Line Tools**: AlertManager can be configured using command line tools, allowing for specification relating to the setup. 24 | 2. **Integration with Prometheus**: Configure the Prometheus server configuration files to align with AlertManager. This ensures the alerts from the Prometheus server are property directed to AlertManager 25 | 3. **Defining Alert Rules**: Alerting rules must be defined within Prometheus to create meaningful and helpful alerts. These rules dictate when an alert will trigger. 26 | 4. **Defining Alert Mechanism**: AlertManager allows for notications through popular platforms like Slack and Email. The alert mechanism allows for alerts to properly be delivered to the intended team. 27 | 28 | ![Alt text](https://miro.medium.com/v2/resize%3Afit%3A1120/format%3Awebp/1%2AwWz5vwHcBeTATvBFKGqRkA.png) 29 | 30 | 31 | ## Managing Alerts 32 | Once the alerts get to the AlertManager, AlertManager is able to handle alerts through the following features: 33 | 1. **Silencing**: This feature allows for developers to mute alerts for a specified amount of time. This is helpful when dealing with known issues. 34 | 2. **Inhibition**: AlertManager intelligently inhibits certain alerts if other correlated alerts have been triggered. This helps to limit the duplication of related alerts. 35 | 3. **Grouping**: AlertManager groups similar alerts into a single notification, providing invaluable clarity and prevents teams from being overwhelmed with similar alerts. 36 | 4. **Sending Notifications**: Ultimately, the alerts are only beneficial if they reach the correct teams or individuals. The notification system ensures this core functionality. 37 | 38 | ## Conclusion 39 | AlertManager allows for efficient alert management to allow developers to swifly and proactively respond to potential issues. 40 | 41 | 42 | ### Sources: 43 | - [Prometheus Alerting Rules](https://prometheus.io/docs/prometheus/latest/configuration/alerting_rules/) 44 | - [Setting Up Prometheus with AlertManager](https://itnext.io/prometheus-with-alertmanager-f2a1f7efabd6) 45 | - [AlertManager Kubernetes Guide](https://devopscube.com/alert-manager-kubernetes-guide/) 46 | 47 | Click [Here](../SetupREADME.md) to Return to Main SetUp Page 48 | 49 | -------------------------------------------------------------------------------- /dev/eksctlREADME.md: -------------------------------------------------------------------------------- 1 | # EKS Setup 2 | 3 | ## Pre-reqs 4 | 5 | - AWS CLI 6 | - Minikube/kubectl 7 | - eksctl 8 | - docker 9 | 10 | ## Log in and Authenticate 11 | 12 | 1. Log in to aws.amazon.com as IAM user with Account ID: 180202761917, user: 13 | **\***, pwd: **\***! 14 | 2. From command line, run ‘aws configure’ and enter AuthID and AuthSecret 15 | AuthID: **\*** AuthSecret: **\*** 16 | 3. Entering your credentials automatically creates the secret file: 17 | ~/.aws/config. Find the file on your local machine by typing 18 | 'command-shift-g' within your Finder. You have to change the secret file to 19 | the following: 20 | 21 | > [default] region = us-east-2 22 | > 23 | > [profile USERNAME] region = us-east-2 output = json 24 | > 25 | > [profile S3FullAccess] role_arn = arn:aws:iam::AWS_ACCOUNT_ID:role/Admin-Kub 26 | > source_profile = USERNAME region = us-east-2 27 | 28 | Note: (Notes on the importance of the region, etc.) 29 | 30 | ## eksctl setup 31 | 32 | 1. `eksctl create cluster --name [test-cluster] --region us-east-2 --node-type [t2.micro] --nodes [2]` 33 | 2. eksctl will add a config file in ~/.kube that directs local kubectl commands 34 | to your EKS instance. 35 | 3. To access elsewhere, run this code: 36 | `aws eks update-kubeconfig --region us-east-2 --name [test-cluster]` 37 | 38 | ## Loading the image to ECR 39 | 40 | 1. Create an image repo in ECR, if there isn’t one 41 | 2. the software folder should have a Dockerfile 42 | 3. From ECR’s image repo (must be in the right region), click “View Push 43 | Commands” 44 | 4. Run these exact commands in order in order to hand amazon auth info to docker 45 | (expires after 12 hours), build and tag a docker image, and push it up to 46 | amazon WHAT EXACT COMMANDS?! 47 | 5. NOTE: If using an M1 Mac, you MUST modify the docker build command in order 48 | to correct the build platform: 49 | `docker buildx build --platform linux/amd64 -t [pithy] .` 50 | 51 | ## Deployment + Services 52 | 53 | 1. Use kubectl to deploy deployment(+pods) and services as usual (see Kubernetes 54 | Deployment doc), using the ECR image link. For example: 55 | `kubectl apply -f aws-pithy.yaml` (modify aws-pithy image uri) -or- 56 | `kubectl create deployment [Depl-name] --image=[AMAZON_ACCOUNT_ID.dkr.ecr.us-east-2.amazonaws.com/pithy]` 57 | 2. `kubectl get svc` should surface the Service’s external IP, now accessible 58 | 59 | ## Make the Metrics available 60 | 61 | 1. Connect to your desired EKS cluster 62 | `aws eks --region region update-kubeconfig --name cluster_name` 63 | 2. Run the configuration to deploy the metrics server 64 | `kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml` 65 | 3. Check to see that the Metrics Pod is up and running and that the /metrics 66 | endpoint is exposed `kubectl get pods -n kube-system` 67 | 68 | ## Prometheus + grafana (using Helm) 69 | 70 | 1. Add the prometheus-community Helm repo and update Helm: 71 | `helm repo add prometheus-community https://prometheus-community.github.io/helm-charts` 72 | `helm repo update` 73 | 2. Install the prometheus-community Helm repo with a desired release 74 | name(i.e.'prometheus') in a created namespace(i.e. 'monitoring'). Also, 75 | install the alert manager. For example: 76 | `helm install prometheus prometheus-community/kube-prometheus-stack --namespace monitoring --create-namespace --set alertmanager.persistentVolume.storageClass="gp2",server.persistentVolume.storageClass="gp2"` 77 | 78 | NOTE: after the Helm chart has been installed, you should see this: 79 | 80 | > NAME: [release-name] LAST DEPLOYED: [date] NAMESPACE: [namespace] default 81 | > STATUS: deployed REVISION: 1 NOTES: kube-prometheus-stack has beeninstalled. 82 | > Check its status by running: kubectl --namespace default get pods -l 83 | > "release=[namespace]" Refer to 84 | > https://github.com/prometheus-operator/kube-prometheus for instructions on how 85 | > to create and configure Alertmanager and Prometheus instances using the 86 | > Operator. 87 | 88 | 3. use kubectl to see what is installed in the cluster: 89 | `kubectl get pod -n [namespace]` NOTE: you should see all nodes including the 90 | prometheus stack operator, the alert manager, and grafana 91 | 4. use kubectl to see all services: `kubectl get services -n [namespace]` 92 | 5. to access your prometheus instance, use the kubectl port-forward to forward a 93 | local port into the Cluster with the service name. Example (with a service 94 | name): 95 | `kubectl port-forward svc/prometheus-kube-prometheus-prometheus -n [namespace] 9090` 96 | 6. navigate to http://localhost:9090 in your browser. NOTE: you should see the Prometheus web UI. Click Status, then Targets to see a list of preconfigured scrape targets. 97 | 98 | ## setting up AlertManager 99 | 100 | NOTE: the prometheus-community/kube-prometheus-stack includes configuration files for AlertManager. 101 | 102 | 1. to access the alertManager UI, use the kubectl port-forward to forward a 103 | local port into the Cluster with the service name. Example (with a service 104 | name): 105 | `kubectl port-forward svc/prometheus-kube-prometheus-alertmanager -n monitoring 9093` 106 | 2. navigate to http://localhost:9093 in your browser. NOTE: you should see the 107 | AlertManager web UI. You can see the active alerts. 108 | 109 | NOTE: if more customization regarding alerts is needed, the configuration files may be adjusted and applied. 110 | 111 | ## sending metrics to grafana cloud (NOTE: this is to use the Grafana web UI) 112 | 113 | Prereqs: Set up a grafana cloud account NOTES: To find your remote write 114 | endpoint, username, and password, navigate to your stack in the Cloud portal, 115 | and click Details next to the Prometheus panel. 116 | 117 | 1. use kubectl to create a Kubernetes Secret (called kubepromsecret) to store 118 | your Grafana Cloud Prometheus username and password 119 | `kubectl create secret generic kubepromsecret --from-literal=username=[username]} --from-literal=password=[API_TOKEN] -n monitoring` 120 | 2. in VS code, create a values.yaml file or update the current one and add this 121 | snippet to define a new Prometheus' remote_write configuration to the 122 | Kube-Prometheus release: 123 | > prometheus: prometheusSpec: remoteWrite: 124 | > 125 | > - url: "" basicAuth: 126 | > username: name: kubepromsecret key: username password: name: 127 | > kubepromsecret key: password 128 | > 129 | > replicaExternalLabelName: "**replica**" externalLabels: {cluster: 130 | > "[clustername]"} 131 | NOTE: this snippet will set the remote_write url and use the username and password from the Kubernetes Secret 132 | 3. save and close the file 133 | 4. apply the yaml file in the anago folder with the corrent file path: 134 | `helm upgrade -f dev/values.yaml [release_name] prometheus-community/kube-prometheus-stack -n [namespace]` 135 | NOTE: you can get a list of installed releases with 136 | `helm list -n [namespace]` 137 | 5. after applying those changes, use port-forward to navigate to the prometheus 138 | UI using the correct service: 139 | `kubectl port-forward svc/prometheus-kube-prometheus-prometheus -n [namespace] 9090` 140 | 6. Navigate to http://localhost:9090 in your browser, and then click Status and 141 | Configuration. Verify that the remote_write block you appended above has 142 | propagated to your running Prometheus instance. 143 | 7. Log in to your managed Grafana instance to begin querying your Cluster data. 144 | You can use the Billing/Usage dashboard to inspect incoming data rates in 145 | the last five minutes to confirm the flow of data to Grafana Cloud. 146 | 147 | ## import dashboards 148 | -------------------------------------------------------------------------------- /dev/grafanaDashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | //configuration file that has the values stored for users 4 | // import config from './config'; l 5 | 6 | class SampleDashboard extends React.Component { 7 | render() { 8 | return ( 9 | <> 10 | {/** different dashboards for different organizations 11 | * depending upon the parameters their panels value changes 12 | */} 13 | {/* {config.roles.user1_org1 === user.role.id && ( 14 |